@skyhook-io/k8s-ui 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 (309) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/applications/ApplicationDetail.test.tsx +15 -0
  4. package/src/components/applications/ApplicationDetail.tsx +17 -15
  5. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  6. package/src/components/charts/AreaChart.test.tsx +25 -0
  7. package/src/components/charts/AreaChart.tsx +141 -12
  8. package/src/components/charts/index.ts +1 -0
  9. package/src/components/checks/ChecksView.tsx +18 -1
  10. package/src/components/checks/types.ts +2 -13
  11. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  12. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  13. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  14. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  15. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  16. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  17. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  18. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  19. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  20. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  21. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  22. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  23. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  24. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  25. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  26. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  27. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  28. package/src/components/cnpg/backupRuns.test.ts +17 -0
  29. package/src/components/cnpg/backupRuns.ts +19 -0
  30. package/src/components/cnpg/connect.test.ts +124 -0
  31. package/src/components/cnpg/connect.ts +155 -0
  32. package/src/components/cnpg/databaseRole.test.ts +38 -0
  33. package/src/components/cnpg/databaseRole.ts +80 -0
  34. package/src/components/cnpg/ha.test.ts +446 -0
  35. package/src/components/cnpg/ha.ts +621 -0
  36. package/src/components/cnpg/index.ts +18 -0
  37. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  38. package/src/components/cnpg/logicalReplication.ts +252 -0
  39. package/src/components/cnpg/pooler.test.ts +84 -0
  40. package/src/components/cnpg/pooler.ts +225 -0
  41. package/src/components/cnpg/primitives.tsx +21 -117
  42. package/src/components/cnpg/problems.ts +12 -0
  43. package/src/components/cnpg/relations.test.ts +63 -13
  44. package/src/components/cnpg/relations.ts +61 -52
  45. package/src/components/cnpg/schedule.ts +35 -0
  46. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  47. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  48. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  49. package/src/components/cnpg/workspace.test.ts +380 -9
  50. package/src/components/cnpg/workspace.ts +1137 -136
  51. package/src/components/dock/DockContext.test.tsx +32 -0
  52. package/src/components/dock/DockContext.tsx +18 -4
  53. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  54. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  55. package/src/components/dock/TerminalTab.tsx +20 -8
  56. package/src/components/facts/certainty.tsx +36 -0
  57. package/src/components/facts/facts.test.tsx +24 -0
  58. package/src/components/facts/facts.tsx +62 -0
  59. package/src/components/facts/index.ts +6 -0
  60. package/src/components/facts/managed-by.test.tsx +17 -0
  61. package/src/components/facts/managed-by.tsx +36 -0
  62. package/src/components/facts/public-exports.test.tsx +35 -0
  63. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  64. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  65. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  66. package/src/components/gitops/GitOpsTableView.tsx +9 -24
  67. package/src/components/gitops/detail-helpers.ts +1 -1
  68. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  69. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  70. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  71. package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
  72. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  73. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
  74. package/src/components/gitops/tree/layout.test.ts +111 -0
  75. package/src/components/gitops/tree/layout.ts +128 -0
  76. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  77. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  78. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  79. package/src/components/issues/IssuesView.tsx +40 -5
  80. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  81. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  82. package/src/components/issues/index.ts +3 -1
  83. package/src/components/issues/issue-timing.ts +14 -3
  84. package/src/components/issues/issues.test.ts +50 -2
  85. package/src/components/issues/severity.ts +21 -0
  86. package/src/components/issues/types.ts +23 -0
  87. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  88. package/src/components/logs/LogCore.tsx +25 -17
  89. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  90. package/src/components/logs/LogsViewer.test.tsx +291 -0
  91. package/src/components/logs/LogsViewer.tsx +72 -11
  92. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  93. package/src/components/logs/StructuredLogLine.tsx +22 -30
  94. package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
  95. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  96. package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
  97. package/src/components/logs/useLogBuffer.test.ts +8 -0
  98. package/src/components/logs/useLogStream.ts +28 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  105. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  106. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  107. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  108. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  109. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  110. package/src/components/resources/ResourcesView.tsx +309 -117
  111. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  112. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
  113. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
  114. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  115. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
  116. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  117. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
  118. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
  119. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
  120. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
  121. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
  122. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  124. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
  125. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  127. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
  128. package/src/components/resources/renderers/CNPGShared.tsx +2 -2
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  130. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  131. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  132. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  133. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  134. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  135. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  136. package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
  137. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
  138. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
  139. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
  140. package/src/components/resources/renderers/EventRenderer.tsx +3 -3
  141. package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
  142. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  143. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  144. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
  145. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
  146. package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
  147. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
  148. package/src/components/resources/renderers/HPARenderer.tsx +57 -17
  149. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
  150. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
  151. package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
  152. package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
  153. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  154. package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
  155. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  156. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  157. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  158. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
  159. package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
  160. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
  161. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  163. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  164. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  165. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  166. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  167. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
  170. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
  171. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
  172. package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
  173. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  174. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  175. package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
  176. package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
  177. package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
  178. package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
  179. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
  180. package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
  181. package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
  182. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  183. package/src/components/resources/renderers/PodRenderer.tsx +62 -36
  184. package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
  185. package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
  186. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  187. package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
  188. package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
  189. package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
  190. package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
  191. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
  192. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  193. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
  194. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
  195. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
  196. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
  197. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
  198. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  199. package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
  200. package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
  201. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
  202. package/src/components/resources/resource-utils-capi.ts +1 -1
  203. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  204. package/src/components/resources/resource-utils-cnpg.ts +51 -24
  205. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  206. package/src/components/resources/resource-utils-contour.ts +16 -0
  207. package/src/components/resources/resource-utils-istio.ts +12 -8
  208. package/src/components/resources/resource-utils-keda.ts +1 -7
  209. package/src/components/resources/resource-utils-knative.ts +18 -0
  210. package/src/components/resources/resource-utils-kyverno.ts +1 -7
  211. package/src/components/resources/resource-utils-velero.ts +0 -12
  212. package/src/components/resources/resource-utils.ts +99 -39
  213. package/src/components/resources/rules-column-width.test.ts +1 -1
  214. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  215. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  216. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  217. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  218. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  219. package/src/components/shared/DetailShell.test.tsx +80 -0
  220. package/src/components/shared/DetailShell.tsx +85 -10
  221. package/src/components/shared/DrainPlanDialog.tsx +1 -0
  222. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  223. package/src/components/shared/EditableYamlView.tsx +1 -1
  224. package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
  225. package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
  226. package/src/components/shared/ResourceActionsBar.tsx +20 -24
  227. package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
  228. package/src/components/shared/SetImageDialog.test.ts +75 -6
  229. package/src/components/shared/SetImageDialog.tsx +92 -84
  230. package/src/components/shared/index.ts +4 -1
  231. package/src/components/timeline/TimelineList.tsx +2 -15
  232. package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
  233. package/src/components/timeline/timeline-filters.ts +1 -0
  234. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  235. package/src/components/topology/K8sResourceNode.tsx +44 -2
  236. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  237. package/src/components/topology/TopologyGraph.tsx +8 -74
  238. package/src/components/topology/TopologySearch.tsx +1 -0
  239. package/src/components/topology/internet-groups.test.ts +35 -0
  240. package/src/components/topology/internet-groups.ts +55 -0
  241. package/src/components/topology/layout.ts +1 -1
  242. package/src/components/ui/Badge.tsx +1 -0
  243. package/src/components/ui/FoldSection.tsx +68 -0
  244. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  245. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  246. package/src/components/ui/RefLink.tsx +20 -0
  247. package/src/components/ui/RowActionMenu.tsx +8 -3
  248. package/src/components/ui/ScheduleValue.test.tsx +5 -0
  249. package/src/components/ui/ScheduleValue.tsx +3 -2
  250. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  251. package/src/components/ui/drawer-components.test.tsx +175 -1
  252. package/src/components/ui/drawer-components.tsx +103 -36
  253. package/src/components/ui/index.ts +3 -1
  254. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  255. package/src/components/ui/monacoRuntime.ts +14 -19
  256. package/src/components/ui/status-tone.test.ts +10 -1
  257. package/src/components/ui/status-tone.tsx +29 -0
  258. package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
  259. package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
  260. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  261. package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
  262. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  263. package/src/components/workload/WorkloadView.tsx +297 -104
  264. package/src/index.ts +14 -1
  265. package/src/monaco-deep.d.ts +19 -12
  266. package/src/theme/components.css +13 -0
  267. package/src/types/capacity.ts +21 -2
  268. package/src/types/core.ts +60 -2
  269. package/src/types/gitops-flux-status.test.ts +32 -1
  270. package/src/types/gitops-insights.ts +19 -2
  271. package/src/types/gitops-tree.ts +10 -0
  272. package/src/types/gitops.ts +26 -6
  273. package/src/utils/api-resources.ts +7 -0
  274. package/src/utils/application-history.ts +1 -0
  275. package/src/utils/cnpg-backup.test.ts +26 -0
  276. package/src/utils/cnpg-backup.ts +70 -0
  277. package/src/utils/format.test.ts +49 -2
  278. package/src/utils/format.ts +9 -1
  279. package/src/utils/gateway-references.test.ts +17 -0
  280. package/src/utils/gateway-references.ts +29 -0
  281. package/src/utils/gitops-owner.ts +3 -2
  282. package/src/utils/gitops-write-guard.test.ts +401 -0
  283. package/src/utils/gitops-write-guard.ts +509 -0
  284. package/src/utils/grant.test.ts +19 -0
  285. package/src/utils/grant.ts +35 -0
  286. package/src/utils/index.ts +6 -0
  287. package/src/utils/ingress-class-references.ts +22 -0
  288. package/src/utils/k8s-errors.ts +7 -7
  289. package/src/utils/log-format.test.ts +41 -0
  290. package/src/utils/log-format.ts +71 -15
  291. package/src/utils/log-level.ts +12 -8
  292. package/src/utils/navigation.test.ts +70 -0
  293. package/src/utils/navigation.ts +91 -8
  294. package/src/utils/node-lifecycle.test.ts +36 -0
  295. package/src/utils/node-lifecycle.ts +77 -0
  296. package/src/utils/object-reference-navigation.test.ts +43 -0
  297. package/src/utils/resource-hierarchy.test.ts +69 -0
  298. package/src/utils/resource-hierarchy.ts +41 -7
  299. package/src/utils/resource-icons.ts +2 -0
  300. package/src/utils/resource-refs.test.ts +23 -0
  301. package/src/utils/resource-refs.ts +30 -0
  302. package/src/utils/skeleton-yaml.test.ts +21 -0
  303. package/src/utils/skeleton-yaml.ts +16 -0
  304. package/src/utils/url-path.test.ts +35 -0
  305. package/src/utils/url-path.ts +21 -0
  306. package/src/utils/validators.test.ts +4 -13
  307. package/src/utils/validators.ts +5 -19
  308. package/src/utils/view-transition.test.ts +35 -1
  309. package/src/utils/view-transition.ts +16 -1
@@ -1528,3 +1528,72 @@ describe('structural app members survive the window filter', () => {
1528
1528
  expect(isChildVisibleInWindow(stray, 0, 1000, { pinned: false, userExpanded: false })).toBe(false)
1529
1529
  })
1530
1530
  })
1531
+
1532
+ describe('uses edges in the timeline hierarchy', () => {
1533
+ const node = (kind: string, apiVersion: string, name: string) => ({
1534
+ id: `${kind.toLowerCase()}/team/${name}`, kind, name, status: 'healthy', data: { namespace: 'team', apiVersion },
1535
+ })
1536
+ const topology = {
1537
+ nodes: [
1538
+ node('HorizontalPodAutoscaler', 'autoscaling/v2', 'web'),
1539
+ node('Deployment', 'apps/v1', 'web'),
1540
+ node('Certificate', 'cert-manager.io/v1', 'tls'),
1541
+ node('Issuer', 'cert-manager.io/v1', 'ca'),
1542
+ ],
1543
+ edges: [
1544
+ { id: 'hpa', source: 'horizontalpodautoscaler/team/web', target: 'deployment/team/web', type: 'uses' },
1545
+ { id: 'issuer', source: 'certificate/team/tls', target: 'issuer/team/ca', type: 'uses' },
1546
+ ],
1547
+ } as unknown as Topology
1548
+ const events = [
1549
+ changeEvent('HorizontalPodAutoscaler', 'team', 'web', { apiVersion: 'autoscaling/v2' }),
1550
+ changeEvent('Deployment', 'team', 'web', { apiVersion: 'apps/v1' }),
1551
+ changeEvent('Certificate', 'team', 'tls', { apiVersion: 'cert-manager.io/v1' }),
1552
+ changeEvent('Issuer', 'team', 'ca', { apiVersion: 'cert-manager.io/v1' }),
1553
+ ]
1554
+
1555
+ it('files an autoscaler under its workload but not a dependent under its dependency', () => {
1556
+ const lanes = buildResourceHierarchy({ events, topology, grouping: 'owner' })
1557
+ const top = lanes.map(lane => `${lane.kind}/${lane.name}`).sort()
1558
+ expect(top).toEqual(['Certificate/tls', 'Deployment/web', 'Issuer/ca'])
1559
+ const deployment = lanes.find(lane => lane.kind === 'Deployment')!
1560
+ expect(deployment.children?.map(child => child.kind)).toEqual(['HorizontalPodAutoscaler'])
1561
+ })
1562
+ })
1563
+
1564
+ describe('buildResourceHierarchy Flux sources', () => {
1565
+ it('keeps a shared Flux source as its own lane instead of nesting it under one consumer', () => {
1566
+ const event = (kind: string, name: string, apiVersion: string): TimelineEvent => ({
1567
+ id: `${kind}/flux-system/${name}`,
1568
+ timestamp: '2024-01-01T00:00:00.000Z',
1569
+ source: 'informer',
1570
+ kind,
1571
+ apiVersion,
1572
+ namespace: 'flux-system',
1573
+ name,
1574
+ eventType: 'update',
1575
+ } as TimelineEvent)
1576
+ const events = [
1577
+ event('GitRepository', 'podinfo', 'source.toolkit.fluxcd.io/v1'),
1578
+ event('Kustomization', 'base', 'kustomize.toolkit.fluxcd.io/v1'),
1579
+ event('Kustomization', 'overlay', 'kustomize.toolkit.fluxcd.io/v1'),
1580
+ ]
1581
+ const topology = {
1582
+ nodes: [
1583
+ { id: 'gitrepository/flux-system/podinfo', kind: 'GitRepository', name: 'podinfo', status: 'healthy', data: { namespace: 'flux-system', apiVersion: 'source.toolkit.fluxcd.io/v1' } },
1584
+ { id: 'kustomization/flux-system/base', kind: 'Kustomization', name: 'base', status: 'healthy', data: { namespace: 'flux-system', apiVersion: 'kustomize.toolkit.fluxcd.io/v1' } },
1585
+ { id: 'kustomization/flux-system/overlay', kind: 'Kustomization', name: 'overlay', status: 'healthy', data: { namespace: 'flux-system', apiVersion: 'kustomize.toolkit.fluxcd.io/v1' } },
1586
+ ],
1587
+ edges: [
1588
+ { id: 'e1', source: 'gitrepository/flux-system/podinfo', target: 'kustomization/flux-system/base', type: 'configures' },
1589
+ { id: 'e2', source: 'gitrepository/flux-system/podinfo', target: 'kustomization/flux-system/overlay', type: 'configures' },
1590
+ ],
1591
+ } as unknown as Topology
1592
+ const lanes = buildResourceHierarchy({ events, topology, grouping: 'owner' })
1593
+ expect(lanes.map((lane) => `${lane.kind}/${lane.name}`).sort()).toEqual([
1594
+ 'GitRepository/podinfo',
1595
+ 'Kustomization/base',
1596
+ 'Kustomization/overlay',
1597
+ ])
1598
+ })
1599
+ })
@@ -466,7 +466,7 @@ function sortEventsForRendering(events: TimelineEvent[]): TimelineEvent[] {
466
466
  // then workloads, then their generated children (ReplicaSet/Pod), config last.
467
467
  // Applied at EVERY depth so a Job's Pods sort the same way a root's children do.
468
468
  const CHILD_SORT_KIND_PRIORITY: Record<string, number> = {
469
- Service: 1, Gateway: 1, HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2,
469
+ Service: 1, Gateway: 1, HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2, UDPRoute: 2,
470
470
  Deployment: 2, Rollout: 2, StatefulSet: 2, DaemonSet: 2,
471
471
  Job: 3, CronJob: 3,
472
472
  ReplicaSet: 3, Pod: 4, ConfigMap: 5, Secret: 5,
@@ -699,6 +699,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
699
699
 
700
700
  // Source 2: Topology edges (for Service→Deployment, Ingress→Service, ConfigMap→Deployment).
701
701
  if (topology?.edges) {
702
+ const topologyNodeById = new Map(topology.nodes?.map(node => [node.id, node]) ?? [])
702
703
  for (const edge of topology.edges) {
703
704
  const sourceId = topologyLaneIdByNodeId.get(edge.source)
704
705
  const targetId = topologyLaneIdByNodeId.get(edge.target)
@@ -760,11 +761,11 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
760
761
  const targetKind = parseLaneId(targetId)!.kind
761
762
 
762
763
  // Gateway→Route: Gateway is parent of Route
763
- if (sourceKind === 'Gateway' && (targetKind === 'HTTPRoute' || targetKind === 'GRPCRoute' || targetKind === 'TCPRoute' || targetKind === 'TLSRoute')) {
764
+ if (sourceKind === 'Gateway' && (targetKind === 'HTTPRoute' || targetKind === 'GRPCRoute' || targetKind === 'TCPRoute' || targetKind === 'TLSRoute' || targetKind === 'UDPRoute')) {
764
765
  link(targetId, sourceId, targetExists)
765
766
  }
766
767
  // Route→Service: reverse (Service is representative, like Ingress)
767
- else if ((sourceKind === 'HTTPRoute' || sourceKind === 'GRPCRoute' || sourceKind === 'TCPRoute' || sourceKind === 'TLSRoute') && targetKind === 'Service') {
768
+ else if ((sourceKind === 'HTTPRoute' || sourceKind === 'GRPCRoute' || sourceKind === 'TCPRoute' || sourceKind === 'TLSRoute' || sourceKind === 'UDPRoute') && targetKind === 'Service') {
768
769
  link(sourceId, targetId, sourceExists)
769
770
  }
770
771
  // Ingress→Service: reverse relationship (Service is representative)
@@ -777,8 +778,17 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
777
778
  }
778
779
  }
779
780
 
780
- // configures/uses/protects: ConfigMap→Deployment, HPA→Deployment, PDB→Deployment (target is parent)
781
- if (edge.type === 'configures' || edge.type === 'uses' || edge.type === 'protects') {
781
+ // configures/protects: ConfigMap→Deployment, PDB→Deployment (target is parent).
782
+ // A Flux source feeds many reconcilers and its timeline is its own, so it
783
+ // never nests under one of them.
784
+ if ((edge.type === 'configures' && !isFluxSource(topologyNodeById.get(edge.source))) || edge.type === 'protects') {
785
+ link(sourceId, targetId, sourceExists)
786
+ }
787
+ // uses: only an autoscaler or a mounted claim belongs under the workload it
788
+ // serves. Other uses edges point from a dependent to what it depends on
789
+ // (Certificate→Issuer, Gateway→GatewayClass); nesting there would file
790
+ // every dependent under one shared dependency.
791
+ if (edge.type === 'uses' && attachesToTarget(topologyNodeById.get(edge.source))) {
782
792
  link(sourceId, targetId, sourceExists)
783
793
  }
784
794
  }
@@ -805,7 +815,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
805
815
  const appLabelEligibleKinds = new Set([
806
816
  'Service', 'Deployment', 'Rollout', 'StatefulSet', 'DaemonSet',
807
817
  'Job', 'CronJob', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute',
808
- 'TCPRoute', 'TLSRoute', 'ConfigMap', 'Secret',
818
+ 'TCPRoute', 'TLSRoute', 'UDPRoute', 'ConfigMap', 'Secret',
809
819
  'Application', 'Kustomization', 'HelmRelease', 'GitRepository',
810
820
  'Workflow', 'CronWorkflow',
811
821
  'KnativeService', 'KnativeConfiguration', 'KnativeRevision', 'KnativeRoute',
@@ -836,7 +846,7 @@ export function buildResourceHierarchy(options: HierarchyOptions): ResourceLane[
836
846
 
837
847
  const kindPriority: Record<string, number> = {
838
848
  Service: 1, Ingress: 2, Gateway: 2,
839
- HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2,
849
+ HTTPRoute: 2, GRPCRoute: 2, TCPRoute: 2, TLSRoute: 2, UDPRoute: 2,
840
850
  Deployment: 3, Rollout: 3, StatefulSet: 3, DaemonSet: 3,
841
851
  Job: 4, CronJob: 4, Workflow: 4, CronWorkflow: 4,
842
852
  ConfigMap: 5, Secret: 5,
@@ -1468,3 +1478,27 @@ export function countEventsInHierarchy(lanes: ResourceLane[]): number {
1468
1478
  lanes.forEach(walk)
1469
1479
  return count
1470
1480
  }
1481
+
1482
+ // The uses-edge sources that attach to their target workload: autoscalers and
1483
+ // PersistentVolumeClaims, matched on exact API group.
1484
+ const ATTACHED_USES_SOURCES = new Set([
1485
+ 'autoscaling/HorizontalPodAutoscaler',
1486
+ 'autoscaling.k8s.io/VerticalPodAutoscaler',
1487
+ 'keda.sh/ScaledObject',
1488
+ 'keda.sh/ScaledJob',
1489
+ '/PersistentVolumeClaim',
1490
+ ])
1491
+
1492
+ function isFluxSource(node: Topology['nodes'][number] | undefined): boolean {
1493
+ if (!node) return false
1494
+ const apiVersion = typeof node.data?.apiVersion === 'string' ? node.data.apiVersion : ''
1495
+ return apiVersionToGroup(apiVersion) === 'source.toolkit.fluxcd.io'
1496
+ }
1497
+
1498
+ function attachesToTarget(node: Topology['nodes'][number] | undefined): boolean {
1499
+ if (!node) return false
1500
+ const kind = topologyNodeResourceKind(node)
1501
+ const apiVersion = typeof node.data?.apiVersion === 'string' ? node.data.apiVersion : ''
1502
+ const group = canonicalResourceGroup(kind, apiVersionToGroup(apiVersion)) ?? ''
1503
+ return ATTACHED_USES_SOURCES.has(`${group}/${kind}`)
1504
+ }
@@ -111,6 +111,7 @@ const KIND_ICON_MAP: Record<string, LucideIcon> = {
111
111
  httproute: Globe,
112
112
  grpcroute: Globe,
113
113
  tcproute: Globe,
114
+ udproute: Globe,
114
115
  tlsroute: Globe,
115
116
 
116
117
  // Config
@@ -219,6 +220,7 @@ const KIND_ICON_MAP: Record<string, LucideIcon> = {
219
220
  // pseudo-kinds (cnpgcluster/…) belong here only once pkg/topology's
220
221
  // KindForGVK emits them — it has no CNPG case today.
221
222
  pooler: Waypoints,
223
+ databaserole: UserCog,
222
224
 
223
225
  // Cluster API
224
226
  capicluster: Server,
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { ResourceRef } from '../types/core'
3
+ import { dedupeResourceRefs, omitResourceRefs, resourceRefKey } from './resource-refs'
4
+
5
+ describe('resource reference identity', () => {
6
+ const core: ResourceRef = { kind: 'Service', namespace: 'team', name: 'app' }
7
+ const knative: ResourceRef = { ...core, group: 'serving.knative.dev' }
8
+
9
+ it('deduplicates core aliases while preserving an identically named custom resource', () => {
10
+ const refs = [core, { ...core, group: '' }, knative]
11
+ expect(dedupeResourceRefs(refs)).toEqual([core, knative])
12
+ expect(refs).toHaveLength(3)
13
+ expect(resourceRefKey(core)).not.toBe(resourceRefKey(knative))
14
+ })
15
+
16
+ it('omits only the exact mirrored identity without mutating either list', () => {
17
+ const refs = [core, knative]
18
+ const excluded = [{ ...core, group: '' }]
19
+ expect(omitResourceRefs(refs, excluded)).toEqual([knative])
20
+ expect(refs).toEqual([core, knative])
21
+ expect(excluded).toHaveLength(1)
22
+ })
23
+ })
@@ -0,0 +1,30 @@
1
+ import type { ResourceRef } from '../types/core'
2
+
3
+ /**
4
+ * resourceKey mirrors Go `audit.ResourceKey(group, kind, namespace, name)`:
5
+ * `group|Kind|namespace|name`. Group first because group and namespace can each
6
+ * independently be empty; `|` is delimiter-safe (K8s API groups follow
7
+ * DNS-subdomain rules and can't contain it).
8
+ */
9
+ export function resourceKey(group: string, kind: string, namespace: string, name: string): string {
10
+ return `${group}|${kind}|${namespace}|${name}`;
11
+ }
12
+
13
+ export function resourceRefKey(ref: ResourceRef): string {
14
+ return resourceKey(ref.group ?? '', ref.kind, ref.namespace, ref.name);
15
+ }
16
+
17
+ export function dedupeResourceRefs(refs: ResourceRef[]): ResourceRef[] {
18
+ const seen = new Set<string>()
19
+ return refs.filter(ref => {
20
+ const key = resourceRefKey(ref)
21
+ if (seen.has(key)) return false
22
+ seen.add(key)
23
+ return true
24
+ })
25
+ }
26
+
27
+ export function omitResourceRefs(refs: ResourceRef[], excluded: ResourceRef[]): ResourceRef[] {
28
+ const excludedKeys = new Set(excluded.map(resourceRefKey))
29
+ return refs.filter(ref => !excludedKeys.has(resourceRefKey(ref)))
30
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getSkeletonYaml } from './skeleton-yaml'
3
+
4
+ describe('getSkeletonYaml', () => {
5
+ it('starts a CloudNativePG Cluster from a spec the operator accepts', () => {
6
+ const yaml = getSkeletonYaml('Cluster', 'postgresql.cnpg.io')
7
+ expect(yaml).toContain('apiVersion: postgresql.cnpg.io/v1')
8
+ expect(yaml).toContain('instances: 3')
9
+ expect(yaml).toContain('size: 1Gi')
10
+ })
11
+
12
+ it('leaves a Cluster API Cluster on the generic template', () => {
13
+ const yaml = getSkeletonYaml('Cluster', 'cluster.x-k8s.io')
14
+ expect(yaml).toContain('apiVersion: cluster.x-k8s.io/v1')
15
+ expect(yaml).not.toContain('instances')
16
+ })
17
+
18
+ it('keeps core kinds on their own skeleton', () => {
19
+ expect(getSkeletonYaml('Pod')).toContain('containers:')
20
+ })
21
+ })
@@ -198,8 +198,24 @@ spec:
198
198
  image: nginx:latest`,
199
199
  }
200
200
 
201
+ // Keyed by group as well as kind: CloudNativePG and Cluster API both name a
202
+ // kind Cluster.
203
+ const groupSkeletons: Record<string, string> = {
204
+ 'postgresql.cnpg.io/Cluster': `apiVersion: postgresql.cnpg.io/v1
205
+ kind: Cluster
206
+ metadata:
207
+ name: my-cluster
208
+ namespace: default
209
+ spec:
210
+ instances: 3
211
+ storage:
212
+ size: 1Gi`,
213
+ }
214
+
201
215
  // Returns skeleton YAML for a known kind, or a generic template for unknown kinds.
202
216
  export function getSkeletonYaml(kind: string, group?: string): string {
217
+ const grouped = group ? groupSkeletons[`${group}/${kind}`] : undefined
218
+ if (grouped) return grouped
203
219
  const skeleton = skeletons[kind]
204
220
  if (skeleton) return skeleton
205
221
 
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { normalizeURLForComparison, stripTrailingSlashes } from './url-path'
3
+
4
+ describe('stripTrailingSlashes', () => {
5
+ it.each([
6
+ ['', ''],
7
+ ['/', ''],
8
+ ['////', ''],
9
+ ['/api///', '/api'],
10
+ ['https://store.example//bucket///', 'https://store.example//bucket'],
11
+ ['https://store.example//bucket?prefix=/', 'https://store.example//bucket?prefix='],
12
+ ['/api//nested', '/api//nested'],
13
+ ])('preserves the existing path normalization for %s', (value, expected) => {
14
+ expect(stripTrailingSlashes(value)).toBe(expected)
15
+ })
16
+
17
+ it('handles long slash runs without scanning each possible start', () => {
18
+ const slashes = '/'.repeat(100_000)
19
+ expect(stripTrailingSlashes(`prefix${slashes}`)).toBe('prefix')
20
+ expect(stripTrailingSlashes(`prefix${slashes}suffix`)).toBe(`prefix${slashes}suffix`)
21
+ })
22
+ })
23
+
24
+ it('compares equivalent origins while retaining declared path semantics', () => {
25
+ expect(normalizeURLForComparison('https://STORE.example:443/prefix/')).toBe(normalizeURLForComparison('https://store.example/prefix'))
26
+ expect(normalizeURLForComparison('http://STORE.example:080/')).toBe(normalizeURLForComparison('http://store.example'))
27
+ expect(normalizeURLForComparison('s3://BUCKET/path/')).toBe(normalizeURLForComparison('s3://bucket/path'))
28
+ expect(normalizeURLForComparison('https://store.example/Prefix')).not.toBe(normalizeURLForComparison('https://store.example/prefix'))
29
+ expect(normalizeURLForComparison('https://store.example/a/../b')).not.toBe(normalizeURLForComparison('https://store.example/b'))
30
+ expect(normalizeURLForComparison('https://store.example:8443/path')).not.toBe(normalizeURLForComparison('https://store.example/path'))
31
+ expect(normalizeURLForComparison('')).toBe('')
32
+ expect(normalizeURLForComparison('invalid')).toBeNull()
33
+ expect(normalizeURLForComparison('https://host\\prefix')).toBeNull()
34
+ expect(normalizeURLForComparison('https://user:password@store.example')).toBeNull()
35
+ })
@@ -0,0 +1,21 @@
1
+ export function stripTrailingSlashes(value: string): string {
2
+ let end = value.length
3
+ while (end > 0 && value[end - 1] === '/') end--
4
+ return value.slice(0, end)
5
+ }
6
+
7
+ export function normalizeURLForComparison(value: string): string | null {
8
+ if (!value) return ''
9
+ if (value.includes('\\')) return null
10
+ try {
11
+ const url = new URL(value)
12
+ const authorityStart = value.indexOf('://')
13
+ if (authorityStart < 0 || !url.hostname || url.username || url.password || value.includes('?') || value.includes('#')) return null
14
+ const pathStart = value.indexOf('/', authorityStart + 3)
15
+ const path = pathStart < 0 ? '' : value.slice(pathStart)
16
+ const port = url.port || (url.protocol === 'https:' ? '443' : url.protocol === 'http:' ? '80' : '')
17
+ return `${url.protocol}//${url.hostname.toLowerCase()}:${port}${stripTrailingSlashes(path)}`
18
+ } catch {
19
+ return null
20
+ }
21
+ }
@@ -82,25 +82,16 @@ describe('validateRFC1123Subdomain', () => {
82
82
  if (!r.valid) expect(r.error).toMatch(/253/)
83
83
  })
84
84
 
85
- it('rejects single labels longer than 63 chars even when total is under 253', () => {
86
- // K8s' IsDNS1123Subdomain enforces ≤ 63 chars per dot-
87
- // separated label on top of the 253-char total. Without the
88
- // per-label check, a single 200-char label slipped through
89
- // client-side and was rejected only server-side.
90
- const huge = 'a'.repeat(200)
91
- const r = validateRFC1123Subdomain(huge)
92
- expect(r.valid).toBe(false)
93
- if (!r.valid) expect(r.error).toMatch(/63/)
85
+ it('accepts long undotted names under the Kubernetes total limit', () => {
86
+ expect(validateRFC1123Subdomain('a'.repeat(200)).valid).toBe(true)
94
87
  })
95
88
 
96
89
  it('accepts a 63-char label at the boundary', () => {
97
90
  expect(validateRFC1123Subdomain('a'.repeat(63)).valid).toBe(true)
98
91
  })
99
92
 
100
- it('rejects only the offending label when other labels are fine', () => {
101
- const r = validateRFC1123Subdomain(`ok.${'a'.repeat(64)}.also-ok`)
102
- expect(r.valid).toBe(false)
103
- if (!r.valid) expect(r.error).toMatch(/63/)
93
+ it('accepts long dot-separated parts under the Kubernetes total limit', () => {
94
+ expect(validateRFC1123Subdomain(`ok.${'a'.repeat(64)}.also-ok`).valid).toBe(true)
104
95
  })
105
96
  })
106
97
 
@@ -17,7 +17,7 @@ export type ValidationResult =
17
17
  | { valid: false; error: string }
18
18
 
19
19
  const RFC1123_LABEL_MAX = 63
20
- // Kubernetes namespace / pod / service names are RFC 1123 labels:
20
+ // Kubernetes namespace names are RFC 1123 labels:
21
21
  // lowercase alphanumeric or '-', start and end alphanumeric, ≤63 chars.
22
22
  // Source: k8s.io/apimachinery/pkg/util/validation.IsDNS1123Label.
23
23
  const RFC1123_LABEL_RE = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/
@@ -34,9 +34,7 @@ const HELM_RELEASE_NAME_MAX = 53
34
34
  // https://helm.sh/docs/chart_best_practices/conventions/#chart-names
35
35
 
36
36
  /**
37
- * Validates a Kubernetes RFC 1123 label. Used for namespace names,
38
- * pod / service / configmap / secret names — anything Kubernetes
39
- * accepts as a single-segment identifier.
37
+ * Matches Kubernetes IsDNS1123Label, including namespace names.
40
38
  */
41
39
  export function validateRFC1123Label(input: string): ValidationResult {
42
40
  if (input.length === 0) {
@@ -64,9 +62,9 @@ export function validateRFC1123Label(input: string): ValidationResult {
64
62
  }
65
63
 
66
64
  /**
67
- * Validates a Kubernetes RFC 1123 subdomain (labels joined by dots).
68
- * Used for Helm chart names, image registries — anything that allows
69
- * dotted segments. NOT for pod/namespace names; use the label form.
65
+ * Matches Kubernetes IsDNS1123Subdomain: the whole name is capped at 253
66
+ * characters, without a separate 63-character cap on dot-separated parts.
67
+ * Namespace names use validateRFC1123Label instead.
70
68
  */
71
69
  export function validateRFC1123Subdomain(input: string): ValidationResult {
72
70
  if (input.length === 0) {
@@ -90,18 +88,6 @@ export function validateRFC1123Subdomain(input: string): ValidationResult {
90
88
  error: 'must contain only lowercase letters, numbers, hyphens, and dots, and each segment must start/end with a letter or number',
91
89
  }
92
90
  }
93
- // Per-label cap: K8s' IsDNS1123Subdomain enforces ≤ 63 chars
94
- // PER dot-separated label on top of the 253-char total. The
95
- // regex above doesn't catch that; without this check a single
96
- // 200-char label passes here and only fails server-side.
97
- for (const label of input.split('.')) {
98
- if (label.length > RFC1123_LABEL_MAX) {
99
- return {
100
- valid: false,
101
- error: `each dot-separated label must be at most ${RFC1123_LABEL_MAX} characters (got ${label.length} for "${label}")`,
102
- }
103
- }
104
- }
105
91
  return { valid: true }
106
92
  }
107
93
 
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest'
2
- import { isInvalidStateError, startViewTransitionSafe } from './view-transition'
2
+ import { isInvalidStateError, isWebKitGTK, startViewTransitionSafe } from './view-transition'
3
3
 
4
4
  // SKY-833 bug 49: rapid tab / route switches surfaced
5
5
  // "InvalidStateError: Transition was aborted because of invalid state"
@@ -79,3 +79,37 @@ describe('startViewTransitionSafe', () => {
79
79
  expect(isInvalidStateError(new TypeError('y'))).toBe(false)
80
80
  })
81
81
  })
82
+
83
+ const WEBKITGTK_UA =
84
+ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15'
85
+
86
+ describe('isWebKitGTK', () => {
87
+ it('matches WebKitGTK user agents', () => {
88
+ expect(isWebKitGTK(WEBKITGTK_UA)).toBe(true)
89
+ expect(isWebKitGTK('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko)')).toBe(true)
90
+ })
91
+
92
+ it('does not match other engines on Linux, or Safari', () => {
93
+ expect(isWebKitGTK('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36')).toBe(false)
94
+ expect(isWebKitGTK('Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0')).toBe(false)
95
+ expect(isWebKitGTK('Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36')).toBe(false)
96
+ expect(isWebKitGTK('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15')).toBe(false)
97
+ })
98
+ })
99
+
100
+ describe('startViewTransitionSafe under WebKitGTK', () => {
101
+ it('applies the update directly instead of starting a transition', () => {
102
+ const originalDoc = (globalThis as { document?: unknown }).document
103
+ const startSpy = vi.fn()
104
+ ;(globalThis as { document: object }).document = { startViewTransition: startSpy }
105
+ const uaSpy = vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(WEBKITGTK_UA)
106
+
107
+ const update = vi.fn()
108
+ startViewTransitionSafe(update)
109
+ expect(update).toHaveBeenCalledTimes(1)
110
+ expect(startSpy).not.toHaveBeenCalled()
111
+
112
+ uaSpy.mockRestore()
113
+ ;(globalThis as { document?: unknown }).document = originalDoc
114
+ })
115
+ })
@@ -14,6 +14,13 @@
14
14
  *
15
15
  * Other rejection types (programming errors, unsupported configs)
16
16
  * are re-thrown so we don't accidentally hide real bugs.
17
+ *
18
+ * Under WebKitGTK (the Linux desktop app's webview) the update is applied
19
+ * without a transition: when WebKitGTK has no renderer buffer transport —
20
+ * WEBKIT_DISABLE_DMABUF_RENDERER or WEBKIT_DISABLE_COMPOSITING_MODE set, a
21
+ * distro patch, or an EGL stack without GBM/surfaceless platforms — starting
22
+ * a view transition segfaults the UI process. The page cannot tell which
23
+ * case it is in, so it skips the cross-fade on every WebKitGTK host.
17
24
  */
18
25
  export function startViewTransitionSafe(update: () => void): void {
19
26
  // Some lib.dom.d.ts versions don't yet expose `startViewTransition`
@@ -27,7 +34,7 @@ export function startViewTransitionSafe(update: () => void): void {
27
34
  }
28
35
  }
29
36
 
30
- if (typeof doc.startViewTransition !== 'function') {
37
+ if (typeof doc.startViewTransition !== 'function' || isWebKitGTK(navigator.userAgent)) {
31
38
  update()
32
39
  return
33
40
  }
@@ -60,3 +67,11 @@ export function isInvalidStateError(err: unknown): boolean {
60
67
  (err as { name: unknown }).name === 'InvalidStateError'
61
68
  )
62
69
  }
70
+
71
+ /**
72
+ * WebKitGTK identifies as AppleWebKit on Linux. Chromium-based browsers on
73
+ * Linux and Android carry AppleWebKit too, so they are excluded by name.
74
+ */
75
+ export function isWebKitGTK(userAgent: string): boolean {
76
+ return /\bLinux\b/.test(userAgent) && /AppleWebKit/.test(userAgent) && !/Chrome|Chromium|Android/.test(userAgent)
77
+ }