@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,49 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import type { TimelineEventsResult, TimelineSource } from '../../api/timelineSource'
4
+
5
+ // The list reads its source from context; hand it a fixed empty result so the
6
+ // test is about the note row alone.
7
+ const eventsResult: TimelineEventsResult = { data: [], isLoading: false, isFetching: false, isError: false, refetch: () => {} }
8
+ const fakeSource: TimelineSource = {
9
+ capabilities: { mode: 'local', ringLimit: 10_000 },
10
+ useEvents: () => eventsResult,
11
+ }
12
+ vi.mock('../../context/TimelineSource', () => ({ useTimelineSource: () => fakeSource }))
13
+
14
+ import { TimelineRetentionNoteProvider } from '../../context/TimelineCustomization'
15
+ import { TimelineList } from './TimelineList'
16
+
17
+ describe('TimelineList history-limit row', () => {
18
+ const props = {
19
+ namespaces: [],
20
+ showDeleted: false,
21
+ onShowDeletedChange: () => {},
22
+ showRoutine: false,
23
+ onShowRoutineChange: () => {},
24
+ search: '',
25
+ onSearchChange: () => {},
26
+ activityFilter: [],
27
+ onActivityFilterChange: () => {},
28
+ kindFilter: [],
29
+ onKindFilterChange: () => {},
30
+ }
31
+ const withNote = (node: React.ReactNode) => (
32
+ <TimelineRetentionNoteProvider value={<a href="/plans">Host note</a>}>{node}</TimelineRetentionNoteProvider>
33
+ )
34
+
35
+ it('shows the host note in its own row', () => {
36
+ const html = renderToStaticMarkup(withNote(<TimelineList {...props} />))
37
+ expect(html).toContain('data-testid="timeline-retention-note"')
38
+ expect(html).toContain('<a href="/plans">Host note</a>')
39
+ })
40
+
41
+ it('renders no row without a host note (standalone Radar)', () => {
42
+ expect(renderToStaticMarkup(<TimelineList {...props} />)).not.toContain('timeline-retention-note')
43
+ })
44
+
45
+ it('leaves the note to the view when a scrubber owns the range', () => {
46
+ const html = renderToStaticMarkup(withNote(<TimelineList {...props} selectionWindow={{ fromMs: 0, toMs: 1 }} />))
47
+ expect(html).not.toContain('timeline-retention-note')
48
+ })
49
+ })
@@ -0,0 +1,20 @@
1
+ import { Clock } from 'lucide-react'
2
+ import { useTimelineRetentionNote } from '../../context/TimelineCustomization'
3
+
4
+ // The host's history-limit note as a full-width row above the timeline,
5
+ // beside the other history-completeness rows (truncation, stale live
6
+ // updates). Informational, not a warning: nothing is broken, the server
7
+ // serves this much. Renders nothing when the host has no note.
8
+ export function TimelineRetentionBanner() {
9
+ const note = useTimelineRetentionNote()
10
+ if (!note) return null
11
+ return (
12
+ <div
13
+ className="flex flex-wrap items-center gap-x-1.5 gap-y-1 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary"
14
+ data-testid="timeline-retention-note"
15
+ >
16
+ <Clock className="h-3.5 w-3.5 shrink-0" />
17
+ {note}
18
+ </div>
19
+ )
20
+ }
@@ -1,7 +1,7 @@
1
1
  import { useState, useMemo, useRef, useCallback, useEffect } from 'react'
2
2
  import { useNavigate, useSearchParams } from 'react-router-dom'
3
3
  import type { ReactNode } from 'react'
4
- import { Network, AlertTriangle, RefreshCw, Boxes, X } from 'lucide-react'
4
+ import { Network, AlertTriangle, Boxes, X } from 'lucide-react'
5
5
  import {
6
6
  clampLensToSelection,
7
7
  deriveLiveSelection,
@@ -21,10 +21,14 @@ import {
21
21
  useDebouncedValue,
22
22
  } from '@skyhook-io/k8s-ui'
23
23
  import { TimelineList } from './TimelineList'
24
+ import { TimelineLoadError, TimelineStaleNote } from './TimelineLoadError'
25
+ import { ViewModeToggle } from './ViewModeToggle'
26
+ import { useStickyError } from './useStickyError'
24
27
  import type { ActivityFilterKey } from './TimelineList'
25
28
  import { TimelineSwimlanes } from './TimelineSwimlanes'
26
29
  import { RetainedTimelineScrubber, extendSelection, type ScrubberDomainInfo } from './RetainedTimelineScrubber'
27
30
  import { LocalTimelineScrubber } from './LocalTimelineScrubber'
31
+ import { TimelineRetentionBanner } from './TimelineRetentionNote'
28
32
  import { useTopology, useApplications } from '../../api/client'
29
33
  import { useTimelineSource } from '../../context/TimelineSource'
30
34
  import type { Topology } from '../../types'
@@ -274,6 +278,7 @@ interface TimelineViewProps {
274
278
  requiresNamespaceFilter?: boolean
275
279
  availableNamespaces?: { name: string }[]
276
280
  onNamespaceSelect?: (ns: string) => void
281
+ onOpenDiagnostics?: () => void
277
282
  }
278
283
 
279
284
  export function resolveApplicationTimelineScope(searchParams: URLSearchParams, namespaces: string[]) {
@@ -292,7 +297,7 @@ export function resolveApplicationTimelineScope(searchParams: URLSearchParams, n
292
297
  }
293
298
  }
294
299
 
295
- export function TimelineView({ namespaces, onResourceClick, initialViewMode, initialFilter, initialTimeRange, requiresNamespaceFilter, availableNamespaces, onNamespaceSelect }: TimelineViewProps) {
300
+ export function TimelineView({ namespaces, onResourceClick, initialViewMode, initialFilter, initialTimeRange, requiresNamespaceFilter, availableNamespaces, onNamespaceSelect, onOpenDiagnostics }: TimelineViewProps) {
296
301
  // URL is the source of truth for every control below (deep-linkable +
297
302
  // back/forward-restorable). Read on mount, written on user change.
298
303
  const [searchParams, setSearchParams] = useSearchParams()
@@ -682,7 +687,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
682
687
  // Fetch all activity - zoom controls what's visible in the UI. This ring feeds
683
688
  // the swimlanes and the local strip's histogram, so it also runs in list mode
684
689
  // when that strip is shown; the list itself fetches its own 2000.
685
- const { data: activity, isLoading, isError, error, refetch, truncated: ringTruncated } = timelineSource.useEvents({
690
+ const { data: activity, isLoading, isFetching, isError, error, refetch, truncated: ringTruncated } = timelineSource.useEvents({
686
691
  namespaces: timelineNamespaces,
687
692
  timeRange: 'all',
688
693
  includeK8sEvents: true,
@@ -700,6 +705,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
700
705
  fromMs: isRetained ? selection.fromMs : undefined,
701
706
  toMs: isRetained ? selection.toMs : undefined,
702
707
  })
708
+ const loadError = useStickyError(error, activity != null)
703
709
 
704
710
  // Topology powers both swimlane hierarchy and application-scoped attribution.
705
711
  const { data: rawTopology } = useTopology(timelineNamespaces, 'resources', {
@@ -829,11 +835,16 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
829
835
  // The scrubber sits above whichever view is active, sharing one selection
830
836
  // across list + swimlane. Retained draws its server-overview strip; local
831
837
  // derives the strip client-side from the loaded ring and omits the gap band.
838
+ // The host's note about how far back history goes: without it the stretch
839
+ // before the limit reads as "nothing happened". Without a scrubber the list
840
+ // view shows it itself, above its own range selector.
841
+ const retentionBanner = showScrubber || viewMode === 'swimlane' ? <TimelineRetentionBanner /> : null
832
842
  const wrap = (node: ReactNode): ReactNode => {
833
843
  if (!showScrubber) {
834
844
  return (
835
845
  <div className="flex-1 flex flex-col min-h-0">
836
846
  {appScopeBar}
847
+ {retentionBanner}
837
848
  <div className="flex-1 flex flex-col min-h-0">{node}</div>
838
849
  </div>
839
850
  )
@@ -850,6 +861,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
850
861
  return (
851
862
  <div className="flex-1 flex flex-col min-h-0">
852
863
  {appScopeBar}
864
+ {retentionBanner}
853
865
  {/* A row-capped ring load means the OLDEST part of the retention
854
866
  window is not loaded — say so, or an old selection reads as
855
867
  "nothing happened back then". */}
@@ -862,13 +874,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
862
874
  {/* Failing background polls with a loaded ring: keep the data, say
863
875
  it's going stale. The full-screen error is reserved for no-data. */}
864
876
  {isError && activity && (
865
- <div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
866
- <AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
867
- Live updates are failing — the timeline may be stale.
868
- <button type="button" onClick={() => refetch()} className="underline hover:text-theme-text-primary">
869
- Retry now
870
- </button>
871
- </div>
877
+ <TimelineStaleNote error={error} subject="timeline" onRetry={() => refetch()} />
872
878
  )}
873
879
  {isRetained ? (
874
880
  <RetainedTimelineScrubber
@@ -960,29 +966,23 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
960
966
  // a load failure. With a loaded ring on screen, a failing background poll
961
967
  // must NOT blank it (gate on data before error); the stale-data banner
962
968
  // below carries the warning instead.
963
- if (isError && !activity) {
969
+ if (loadError && !activity) {
964
970
  // Surface the server's own message — a generic "failed to load" would
965
971
  // swallow whatever the hub said. "Try again" is a full resync: the
966
972
  // retained source drops its delta cursor and reloads the whole ring.
967
- const detail = error?.message?.trim()
968
973
  return wrap(
969
974
  <div className="flex-1 flex flex-col">
970
975
  <div className="flex items-center justify-between px-4 py-2 border-b border-theme-border">
971
976
  <div />
972
977
  <ViewModeToggle viewMode={viewMode} onViewModeChange={setViewMode} />
973
978
  </div>
974
- <div className="flex-1 flex flex-col items-center justify-center text-theme-text-tertiary gap-3 px-6">
975
- <AlertTriangle className="w-10 h-10 text-amber-400/70" />
976
- <p className="text-base">Failed to load timeline data</p>
977
- {detail && <p className="max-w-md text-center text-sm text-theme-text-tertiary">{detail}</p>}
978
- <button
979
- onClick={() => refetch()}
980
- className="flex items-center gap-2 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border-light rounded-lg hover:bg-theme-hover transition-colors"
981
- >
982
- <RefreshCw className="w-3.5 h-3.5" />
983
- Try again
984
- </button>
985
- </div>
979
+ <TimelineLoadError
980
+ className="flex-1"
981
+ error={loadError}
982
+ retrying={isFetching}
983
+ onRetry={() => refetch()}
984
+ onOpenDiagnostics={onOpenDiagnostics}
985
+ />
986
986
  </div>
987
987
  )
988
988
  }
@@ -1057,27 +1057,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
1057
1057
  appScoped={Boolean(focusedAppKey)}
1058
1058
  topology={stableTopology}
1059
1059
  appScopeLoading={focusedAppLoading}
1060
+ onOpenDiagnostics={onOpenDiagnostics}
1060
1061
  />
1061
1062
  )
1062
1063
  }
1063
-
1064
- function ViewModeToggle({ viewMode, onViewModeChange }: { viewMode: TimelineViewMode, onViewModeChange: (mode: TimelineViewMode) => void }) {
1065
- return (
1066
- <div className="flex items-center gap-1 bg-theme-base rounded-lg p-0.5 border border-theme-border">
1067
- <button
1068
- type="button"
1069
- onClick={() => onViewModeChange('list')}
1070
- className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'list' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
1071
- >
1072
- List
1073
- </button>
1074
- <button
1075
- type="button"
1076
- onClick={() => onViewModeChange('swimlane')}
1077
- className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'swimlane' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
1078
- >
1079
- Swimlane
1080
- </button>
1081
- </div>
1082
- )
1083
- }
@@ -0,0 +1,20 @@
1
+ export function ViewModeToggle({ viewMode, onViewModeChange }: { viewMode: 'list' | 'swimlane', onViewModeChange: (mode: 'list' | 'swimlane') => void }) {
2
+ return (
3
+ <div className="flex items-center gap-1 bg-theme-base rounded-lg p-0.5 border border-theme-border">
4
+ <button
5
+ type="button"
6
+ onClick={() => onViewModeChange('list')}
7
+ className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'list' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
8
+ >
9
+ List
10
+ </button>
11
+ <button
12
+ type="button"
13
+ onClick={() => onViewModeChange('swimlane')}
14
+ className={`px-2 py-1 text-xs rounded-md transition-colors ${viewMode === 'swimlane' ? 'bg-theme-surface text-theme-text-primary shadow-sm' : 'text-theme-text-secondary hover:text-theme-text-primary'}`}
15
+ >
16
+ Swimlane
17
+ </button>
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1,47 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot, type Root } from 'react-dom/client'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+ import { useStickyError } from './useStickyError'
6
+
7
+ const seen: (Error | null)[] = []
8
+ function Harness({ error, hasData }: { error: Error | null; hasData: boolean }) {
9
+ seen.push(useStickyError(error, hasData))
10
+ return null
11
+ }
12
+
13
+ let root: Root
14
+ let container: HTMLDivElement
15
+ beforeEach(() => {
16
+ seen.length = 0
17
+ container = document.createElement('div')
18
+ root = createRoot(container)
19
+ })
20
+ afterEach(() => act(() => root.unmount()))
21
+ const render = (error: Error | null, hasData: boolean) => act(() => root.render(<Harness error={error} hasData={hasData} />))
22
+ const last = () => seen[seen.length - 1]
23
+
24
+ describe('useStickyError', () => {
25
+ it('keeps the error while a refetch with no data clears it', () => {
26
+ const down = new Error('database down')
27
+ render(down, false)
28
+ expect(last()).toBe(down)
29
+ render(null, false)
30
+ expect(last()).toBe(down)
31
+ })
32
+
33
+ it('drops the error once data arrives', () => {
34
+ render(new Error('database down'), false)
35
+ render(null, true)
36
+ expect(last()).toBeNull()
37
+ })
38
+
39
+ // A page with data on screen whose poll starts failing: the hook must settle,
40
+ // not bounce between the error and null until React gives up.
41
+ it('settles when an error arrives while data is on screen', () => {
42
+ render(null, true)
43
+ render(new Error('database down'), true)
44
+ expect(last()).toBeNull()
45
+ expect(seen.length).toBeLessThan(10)
46
+ })
47
+ })
@@ -0,0 +1,15 @@
1
+ import { useState } from 'react'
2
+
3
+ // React Query clears the error of a query that has never loaded each time it
4
+ // refetches, so a failure that persists across polls would flicker to a loader
5
+ // on every interval. This keeps the last error on screen until data arrives.
6
+ // Once data is loaded it returns null: a failing poll then belongs to the
7
+ // stale-data banner, not the full-screen error.
8
+ export function useStickyError(error: Error | null | undefined, hasData: boolean): Error | null {
9
+ const [shown, setShown] = useState<Error | null>(null)
10
+ const next = hasData ? null : (error ?? shown)
11
+ // Adjusting state during render: converges in one extra pass because `next`
12
+ // is a fixed point once stored.
13
+ if (next !== shown) setShown(next)
14
+ return next
15
+ }
@@ -0,0 +1,144 @@
1
+ import { useMemo } from 'react'
2
+ import { Virtuoso } from 'react-virtuoso'
3
+ import { clsx } from 'clsx'
4
+ import { ArrowRight, Crosshair, Minus } from 'lucide-react'
5
+ import type { AggregatedFlow } from '../../types'
6
+ import { SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
7
+ import { Tooltip } from '../ui/Tooltip'
8
+ import type { TrafficGraphSelection } from './TrafficGraph'
9
+ import {
10
+ connectionRows, displayVolume, edgesOnPort, flowDrops, flowErrors, formatRate, graphEndpointId, hasStatusData, isExternalKind,
11
+ type GraphFlow, type TrafficFocus,
12
+ } from './trafficFilters'
13
+
14
+ interface TrafficConnectionsTableProps {
15
+ flows: AggregatedFlow[]
16
+ isRateBased: boolean
17
+ selection: TrafficGraphSelection | null
18
+ onSelect: (selection: TrafficGraphSelection | null) => void
19
+ onFocus: (focus: TrafficFocus) => void
20
+ /** The focused endpoint, whose name is not offered as a focus again. */
21
+ focusedId?: string
22
+ }
23
+
24
+ const GRID = 'grid grid-cols-[minmax(0,1fr)_1rem_minmax(0,1fr)_6.5rem_5rem_4.5rem_4.5rem] items-center gap-x-3'
25
+ // Without HTTP status anywhere in view (plain TCP) a 5xx column would read as
26
+ // "no errors" rather than "not measured".
27
+ const GRID_NO_STATUS = 'grid grid-cols-[minmax(0,1fr)_1rem_minmax(0,1fr)_6.5rem_5rem_4.5rem] items-center gap-x-3'
28
+
29
+ function rowSelection(flow: AggregatedFlow): TrafficGraphSelection {
30
+ return {
31
+ type: 'edge',
32
+ sourceId: graphEndpointId(flow.source),
33
+ destId: graphEndpointId(flow.destination),
34
+ port: flow.port,
35
+ directionUnknown: !!flow.directionUnknown,
36
+ }
37
+ }
38
+
39
+ function selectedRows(rows: GraphFlow[], selection: TrafficGraphSelection | null): Set<GraphFlow> {
40
+ if (selection?.type !== 'edge' || selection.port === undefined) return new Set()
41
+ const between = rows.filter(flow =>
42
+ graphEndpointId(flow.source) === selection.sourceId && graphEndpointId(flow.destination) === selection.destId &&
43
+ !!flow.directionUnknown === !!selection.directionUnknown)
44
+ return new Set(edgesOnPort(between, f => f, selection.port))
45
+ }
46
+
47
+ /**
48
+ * The view's connections as rows, for when there are too many to draw. Where
49
+ * the problems are comes first. Selecting a row loads its records into the
50
+ * flow list, as selecting an edge does; a name narrows the view to it.
51
+ */
52
+ export function TrafficConnectionsTable({ flows, isRateBased, selection, onSelect, onFocus, focusedId }: TrafficConnectionsTableProps) {
53
+ const rows = useMemo(() => connectionRows(flows), [flows])
54
+ const selected = useMemo(() => selectedRows(rows, selection), [rows, selection])
55
+ const showStatus = useMemo(() => flows.some(hasStatusData), [flows])
56
+ const grid = showStatus ? GRID : GRID_NO_STATUS
57
+
58
+ const endpoint = (e: AggregatedFlow['source']) => {
59
+ const id = graphEndpointId(e)
60
+ const label = (
61
+ <span className="min-w-0">
62
+ <span className="block truncate text-theme-text-primary">{e.name}</span>
63
+ {e.namespace && <span className="block truncate text-[10px] text-theme-text-tertiary">{e.namespace}</span>}
64
+ </span>
65
+ )
66
+ if (id === focusedId || e.kind === 'Internet') return <span className="flex min-w-0 items-center">{label}</span>
67
+ return (
68
+ <Tooltip content={`Focus on ${e.name}`} wrapperClassName="min-w-0">
69
+ <button
70
+ type="button"
71
+ onClick={ev => {
72
+ ev.stopPropagation()
73
+ onFocus({ namespace: e.namespace || undefined, name: e.name })
74
+ }}
75
+ className="group flex min-w-0 max-w-full items-center gap-1 text-left hover:underline"
76
+ >
77
+ {label}
78
+ <Crosshair className="h-3 w-3 shrink-0 opacity-0 group-hover:opacity-70" />
79
+ </button>
80
+ </Tooltip>
81
+ )
82
+ }
83
+
84
+ return (
85
+ <div className="flex h-full min-h-0 flex-col">
86
+ <div className={clsx(grid, 'px-3 py-1.5 text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary border-b border-theme-border')}>
87
+ <span>Source</span>
88
+ <span />
89
+ <span>Destination</span>
90
+ <span>Port</span>
91
+ <span className="text-right">{isRateBased ? 'Req/s' : 'Conns'}</span>
92
+ {showStatus && <span className="text-right">5xx</span>}
93
+ <span className="text-right">Dropped</span>
94
+ </div>
95
+ <Virtuoso
96
+ className="flex-1"
97
+ data={rows}
98
+ computeItemKey={(_, flow) => `${graphEndpointId(flow.source)}->${graphEndpointId(flow.destination)}:${flow.port}${flow.directionUnknown ? ':u' : ''}`}
99
+ itemContent={(_, flow) => {
100
+ const sel = rowSelection(flow)
101
+ const isSelected = selected.has(flow)
102
+ const errors = flowErrors(flow)
103
+ const drops = flowDrops(flow)
104
+ return (
105
+ <div
106
+ role="row"
107
+ aria-selected={isSelected}
108
+ onClick={() => onSelect(isSelected ? null : sel)}
109
+ className={clsx(
110
+ grid,
111
+ 'cursor-pointer px-3 py-1.5 text-xs border-b border-theme-border/50 transition-colors',
112
+ isSelected ? 'bg-theme-elevated' : 'hover:bg-theme-hover',
113
+ )}
114
+ >
115
+ {endpoint(flow.source)}
116
+ {flow.directionUnknown
117
+ ? <Tooltip content="Direction unknown"><Minus className="h-3 w-3 text-theme-text-tertiary" /></Tooltip>
118
+ : <ArrowRight className="h-3 w-3 text-theme-text-tertiary" />}
119
+ {endpoint(flow.destination)}
120
+ <span className="truncate text-theme-text-secondary tabular-nums">
121
+ {flow.port > 0 ? flow.port : '—'}
122
+ {(flow.l7Protocol || flow.protocol) && (
123
+ <span className="ml-1 text-theme-text-tertiary">{flow.l7Protocol || flow.protocol.toUpperCase()}</span>
124
+ )}
125
+ {isExternalKind(flow.destination.kind) && <span className="ml-1 text-theme-text-tertiary">ext</span>}
126
+ </span>
127
+ <span className="text-right tabular-nums text-theme-text-secondary">
128
+ {isRateBased ? formatRate(displayVolume(flow, true)) : flow.connections.toLocaleString()}
129
+ </span>
130
+ {showStatus && (
131
+ <span className={clsx('text-right tabular-nums', errors > 0 ? clsx(SEVERITY_TEXT.error, 'font-medium') : 'text-theme-text-tertiary')}>
132
+ {errors > 0 ? (isRateBased ? formatRate(errors) : errors.toLocaleString()) : hasStatusData(flow) ? '0' : '—'}
133
+ </span>
134
+ )}
135
+ <span className={clsx('text-right tabular-nums', drops > 0 ? clsx(SEVERITY_TEXT.error, 'font-medium') : 'text-theme-text-tertiary')}>
136
+ {drops > 0 ? drops.toLocaleString() : '—'}
137
+ </span>
138
+ </div>
139
+ )
140
+ }}
141
+ />
142
+ </div>
143
+ )
144
+ }
@@ -10,6 +10,7 @@ import {
10
10
  Filter,
11
11
  Info,
12
12
  Puzzle,
13
+ Boxes,
13
14
  } from 'lucide-react'
14
15
  import { clsx } from 'clsx'
15
16
  import { SEVERITY_BADGE } from '@skyhook-io/k8s-ui/utils/badge-colors'
@@ -41,6 +42,8 @@ interface TrafficFilterSidebarProps {
41
42
  setShowNamespaceGroups: (v: boolean) => void
42
43
  collapseInternet: boolean
43
44
  setCollapseInternet: (v: boolean) => void
45
+ groupByWorkload: boolean
46
+ setGroupByWorkload: (v: boolean) => void
44
47
  addonMode: AddonMode
45
48
  setAddonMode: (v: AddonMode) => void
46
49
 
@@ -83,6 +86,9 @@ interface TrafficFilterSidebarProps {
83
86
  namespaces: Array<{ name: string; nodeCount: number }>
84
87
  hiddenNamespaces: Set<string>
85
88
  onToggleNamespace: (ns: string) => void
89
+ /** Set while the view is focused on an endpoint: the namespace choices are
90
+ * kept but not applied, so a neighbor in a hidden namespace still shows. */
91
+ namespacesPausedFor?: string
86
92
 
87
93
  }
88
94
 
@@ -150,6 +156,8 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
150
156
  setShowNamespaceGroups,
151
157
  collapseInternet,
152
158
  setCollapseInternet,
159
+ groupByWorkload,
160
+ setGroupByWorkload,
153
161
  addonMode,
154
162
  setAddonMode,
155
163
  aggregateExternal,
@@ -177,6 +185,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
177
185
  namespaces,
178
186
  hiddenNamespaces,
179
187
  onToggleNamespace,
188
+ namespacesPausedFor,
180
189
  }: TrafficFilterSidebarProps) {
181
190
  const [namespacesExpanded, setNamespacesExpanded] = useState(false)
182
191
 
@@ -298,6 +307,13 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
298
307
  onToggle={() => setCollapseInternet(!collapseInternet)}
299
308
  icon={Globe}
300
309
  />
310
+ <ToggleOption
311
+ label="Group by Workload"
312
+ description="Draw a workload's pods as one node. Off, each pod gets its own node"
313
+ enabled={groupByWorkload}
314
+ onToggle={() => setGroupByWorkload(!groupByWorkload)}
315
+ icon={Boxes}
316
+ />
301
317
  </div>
302
318
  </div>
303
319
 
@@ -494,7 +510,12 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
494
510
  </button>
495
511
  </div>
496
512
  </div>
497
- <div className="space-y-0.5">
513
+ {namespacesPausedFor && (
514
+ <p className="mb-1.5 text-[10px] text-theme-text-tertiary">
515
+ Paused while focused on {namespacesPausedFor}: its neighbors show whatever their namespace.
516
+ </p>
517
+ )}
518
+ <div className={clsx('space-y-0.5', namespacesPausedFor && 'opacity-50 pointer-events-none')} aria-disabled={!!namespacesPausedFor}>
498
519
  {visibleNamespaces.map(({ name, nodeCount }) => {
499
520
  const isHidden = hiddenNamespaces.has(name)
500
521
  return (
@@ -7,10 +7,16 @@ interface TrafficFlowListContextValue {
7
7
  responsesCallerOriented: boolean
8
8
  graphSelection: TrafficGraphSelection | null
9
9
  clearSelection: () => void
10
+ /** The records for the current selection are still being fetched. */
11
+ loading?: boolean
12
+ /** Says which records the list holds when it is not all of them. */
13
+ note?: string
10
14
  }
11
15
 
16
+ const emptyValue: TrafficFlowListContextValue = { flows: [], responsesCallerOriented: false, graphSelection: null, clearSelection: () => {} }
17
+
12
18
  // Module-level store — TrafficView writes, dock tab reads.
13
- let currentValue: TrafficFlowListContextValue = { flows: [], responsesCallerOriented: false, graphSelection: null, clearSelection: () => {} }
19
+ let currentValue: TrafficFlowListContextValue = emptyValue
14
20
  const listeners = new Set<() => void>()
15
21
 
16
22
  function setValue(val: TrafficFlowListContextValue) {
@@ -53,15 +59,17 @@ export function TrafficFlowListProvider({
53
59
  responsesCallerOriented,
54
60
  graphSelection,
55
61
  clearSelection,
62
+ loading,
63
+ note,
56
64
  children,
57
65
  }: TrafficFlowListContextValue & { children: ReactNode }) {
58
66
  useEffect(() => {
59
- setValue({ flows, responsesCallerOriented, graphSelection, clearSelection })
60
- }, [flows, responsesCallerOriented, graphSelection, clearSelection])
67
+ setValue({ flows, responsesCallerOriented, graphSelection, clearSelection, loading, note })
68
+ }, [flows, responsesCallerOriented, graphSelection, clearSelection, loading, note])
61
69
 
62
70
  useEffect(() => {
63
71
  return () => {
64
- setValue({ flows: [], responsesCallerOriented: false, graphSelection: null, clearSelection: () => {} })
72
+ setValue(emptyValue)
65
73
  setFlowSearch('')
66
74
  }
67
75
  }, [])