@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
@@ -16,6 +16,7 @@ import {
16
16
  SEVERITY_TEXT_CLASS,
17
17
  } from './severity'
18
18
  import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
19
+ import { CopyableCommand } from '../trace/TracePanel'
19
20
  import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
20
21
  import { CORE_RESOURCES } from '../../utils/api-resources'
21
22
 
@@ -28,6 +29,9 @@ const AUDIT_INPUT_LABELS = new Map<string, string>([
28
29
  ['replicaset-ownership', 'ReplicaSet ownership'],
29
30
  ['configmap-references', 'ConfigMap references'],
30
31
  ['secret-references', 'Secret references'],
32
+ ['pvc-consumers', 'PVC consumers'],
33
+ ['pv-deletion-events', 'PV deletion events'],
34
+ ['pv-release-time', 'PV release time'],
31
35
  ])
32
36
 
33
37
  // Leading severity glyph, one per tier of the 4-tier ladder: critical = octagon,
@@ -101,6 +105,9 @@ export interface ChecksViewProps {
101
105
  * tab governs hiding there). Operate on the whole check, fleet-wide. */
102
106
  onHideCheck?: (checkID: string, title: string) => void
103
107
  onHideCategory?: (category: string) => void
108
+ /** Per-resource opt-out annotation the source Radar honors. Omit when it
109
+ * doesn't, so the card never suggests an annotation that does nothing. */
110
+ ignoreChecksAnnotation?: string
104
111
  }
105
112
 
106
113
  // A check rolled up across every cluster it fires on — one row of the fleet
@@ -122,7 +129,7 @@ const CHECKS_FILTER_SCHEMA = defineFilterSchema({
122
129
  q: { param: 'q', type: 'text' },
123
130
  })
124
131
 
125
- export function ChecksView({ checks, catalog, anyData, evaluated, missingInputs = [], resourceHref, onResourceClick, clusterLabel, clusterLabelById, clusterFilter: clusterFilterProp, onClusterFilterChange, emptyAction, onHideCheck, onHideCategory }: ChecksViewProps) {
132
+ export function ChecksView({ checks, catalog, anyData, evaluated, missingInputs = [], resourceHref, onResourceClick, clusterLabel, clusterLabelById, clusterFilter: clusterFilterProp, onClusterFilterChange, emptyAction, onHideCheck, onHideCategory, ignoreChecksAnnotation }: ChecksViewProps) {
126
133
  // Severity / category / framework / search live in the URL (shareable,
127
134
  // bookmarkable audit links) via the shared filter-state contract. The cluster
128
135
  // facet is deliberately NOT here — it's a host-controlled seam (see
@@ -430,6 +437,7 @@ export function ChecksView({ checks, catalog, anyData, evaluated, missingInputs
430
437
  onResourceClick={onResourceClick}
431
438
  onHideCheck={onHideCheck}
432
439
  onHideCategory={onHideCategory}
440
+ ignoreChecksAnnotation={ignoreChecksAnnotation}
433
441
  />
434
442
  ))}
435
443
  </ol>
@@ -720,6 +728,7 @@ function FleetCheckRow({
720
728
  onResourceClick,
721
729
  onHideCheck,
722
730
  onHideCategory,
731
+ ignoreChecksAnnotation,
723
732
  }: {
724
733
  fc: FleetCheck
725
734
  meta?: CheckMeta
@@ -730,6 +739,7 @@ function FleetCheckRow({
730
739
  onResourceClick?: (ref: CheckResourceRef) => void
731
740
  onHideCheck?: (checkID: string, title: string) => void
732
741
  onHideCategory?: (category: string) => void
742
+ ignoreChecksAnnotation?: string
733
743
  }) {
734
744
  const clusterCount = fc.clusters.length
735
745
  const single = clusterCount <= 1
@@ -762,6 +772,13 @@ function FleetCheckRow({
762
772
  >
763
773
  <CheckRemediationBlock description={meta?.description} remediation={meta?.remediation} references={meta?.references} />
764
774
 
775
+ {ignoreChecksAnnotation && (
776
+ <CardSection icon={EyeOff} label="Skip for one resource">
777
+ <CardBody>Add this annotation to a resource listed below. Its findings for this check are then hidden.</CardBody>
778
+ <CopyableCommand command={`${ignoreChecksAnnotation}: ${fc.checkID}`} />
779
+ </CardSection>
780
+ )}
781
+
765
782
  <CardSection
766
783
  icon={Layers}
767
784
  label="Affected resources"
@@ -61,19 +61,8 @@ export interface CheckResourceRef {
61
61
  /** Built-in (Radar-detected) finding source. The only V1 source. */
62
62
  export const SOURCE_RADAR_BUILTIN = 'radar_builtin';
63
63
 
64
- /**
65
- * resourceKey mirrors Go `audit.ResourceKey(group, kind, namespace, name)`:
66
- * `group|Kind|namespace|name`. Group first because group and namespace can each
67
- * independently be empty; `|` is delimiter-safe (K8s API groups follow
68
- * DNS-subdomain rules and can't contain it).
69
- */
70
- export function resourceKey(group: string, kind: string, namespace: string, name: string): string {
71
- return `${group}|${kind}|${namespace}|${name}`;
72
- }
73
-
74
- export function resourceRefKey(ref: CheckResourceRef): string {
75
- return resourceKey(ref.group, ref.kind, ref.namespace, ref.name);
76
- }
64
+ // Preserve the Checks import path while sharing exact resource identity.
65
+ export { resourceKey, resourceRefKey } from '../../utils/resource-refs';
77
66
 
78
67
  /**
79
68
  * checkFindingKey is the canonical built-in finding key:
@@ -7,10 +7,13 @@ import {
7
7
  getCNPGScheduledBackupStatus,
8
8
  } from '../resources/resource-utils-cnpg'
9
9
  import type { CNPGWorkspaceResponse } from './workspace'
10
- import { FactGrid, FactRow, RefLink, SummaryHeading, toneTextClass, type CNPGNavigate } from './primitives'
10
+ import { cnpgScheduleBasisNote, formatCNPGRunTime, type CNPGSchedulePreview } from './schedule'
11
+ import { Badge } from '../ui/Badge'
12
+ import { Tooltip } from '../ui/Tooltip'
11
13
  import { ClusterLink, NotReported, Note, ObjectProblems, PhaseBadge, SummaryShell, TimeAgo } from './CNPGSharedSummary'
12
14
  import {
13
15
  backupDestination,
16
+ cnpgScheduleDestinationBlocker,
14
17
  backupsForScheduledBackup,
15
18
  clustersIn,
16
19
  isBackupFromSchedule,
@@ -19,11 +22,15 @@ import {
19
22
  scheduledBackupOf,
20
23
  workspaceList,
21
24
  } from './relations'
25
+ import { type NavigateToRef, RefLink } from '../ui/RefLink'
26
+ import { toneTextClass } from '../ui/status-tone'
27
+ import { FactGrid, FactRow } from '../facts'
28
+ import { SectionHeading } from '../ui/FoldSection'
22
29
 
23
30
  interface SummaryProps {
24
31
  resource: any
25
32
  workspace: CNPGWorkspaceResponse | null
26
- onNavigate?: CNPGNavigate
33
+ onNavigate?: NavigateToRef
27
34
  }
28
35
 
29
36
  const RECENT_RUNS = 5
@@ -87,7 +94,7 @@ export function CNPGBackupSummary({ resource, workspace, onNavigate }: SummaryPr
87
94
  <SummaryShell>
88
95
  <ObjectProblems issues={workspace?.issues} subject={refOf(resource, 'Backup')} onNavigate={onNavigate} />
89
96
 
90
- <SummaryHeading>Outcome</SummaryHeading>
97
+ <SectionHeading>Outcome</SectionHeading>
91
98
  <FactGrid>
92
99
  <FactRow label="Phase">
93
100
  <PhaseBadge status={status} />
@@ -109,7 +116,7 @@ export function CNPGBackupSummary({ resource, workspace, onNavigate }: SummaryPr
109
116
  )}
110
117
  </FactGrid>
111
118
 
112
- <SummaryHeading>Relationships</SummaryHeading>
119
+ <SectionHeading>Relationships</SectionHeading>
113
120
  <FactGrid>
114
121
  <FactRow label="Cluster">
115
122
  <ClusterLink resource={resource} workspace={workspace} onNavigate={onNavigate} />
@@ -152,22 +159,62 @@ export function CNPGBackupSummary({ resource, workspace, onNavigate }: SummaryPr
152
159
  )
153
160
  }
154
161
 
155
- export function CNPGScheduledBackupSummary({ resource, workspace, onNavigate }: SummaryProps) {
162
+ export function CNPGSchedulePreviewFacts({ preview }: { preview: CNPGSchedulePreview }) {
163
+ if (!preview.valid) {
164
+ return (
165
+ <FactRow label="Reading">
166
+ <span className={toneTextClass('degraded')}>Not a schedule the operator can run: {preview.error}</span>
167
+ </FactRow>
168
+ )
169
+ }
170
+ return (
171
+ <>
172
+ {preview.description && <FactRow label="Reading">{preview.description}{!preview.clock?.declared && <span className="ml-1 text-theme-text-tertiary">· UTC estimate</span>}</FactRow>}
173
+ <FactRow label={preview.clock?.declared ? 'Calculated upcoming times' : 'Estimated upcoming times'}>
174
+ <ul className="space-y-0.5">
175
+ {(preview.nextRuns ?? []).map((r, i) => {
176
+ const t = formatCNPGRunTime(r)
177
+ return (
178
+ <li key={r} className="font-mono text-xs">
179
+ <Tooltip content={`Your time: ${t.local}`} position="top">
180
+ <span>{i === 0 && preview.runsImmediately ? `due (${t.utc})` : t.utc}</span>
181
+ </Tooltip>
182
+ </li>
183
+ )
184
+ })}
185
+ </ul>
186
+ <div className="mt-0.5 text-[11.5px] text-theme-text-tertiary">{cnpgScheduleBasisNote(preview)}</div>
187
+ </FactRow>
188
+ </>
189
+ )
190
+ }
191
+
192
+ export function CNPGScheduledBackupSummary({
193
+ resource,
194
+ workspace,
195
+ onNavigate,
196
+ schedulePreview,
197
+ }: SummaryProps & {
198
+ /** The server's reading of spec.schedule; without it the schedule is shown verbatim only. */
199
+ schedulePreview?: CNPGSchedulePreview
200
+ }) {
156
201
  const ns = resource?.metadata?.namespace ?? ''
157
202
  const cron = resource?.spec?.schedule
158
203
  const next = getCNPGScheduledBackupNextSchedule(resource)
159
204
  const runsUnavailable = relationUnavailable(workspace, 'backups', ns, 'Backups')
160
205
  const runs = runsUnavailable ? [] : backupsForScheduledBackup(resource, workspaceList(workspace, 'backups'))
161
206
  const shown = runs.slice(0, RECENT_RUNS)
207
+ const blocker = resource?.spec?.suspend === true ? null : cnpgScheduleDestinationBlocker(resource, clustersIn(workspace))
162
208
 
163
209
  return (
164
210
  <SummaryShell>
165
211
  <ObjectProblems issues={workspace?.issues} subject={refOf(resource, 'ScheduledBackup')} onNavigate={onNavigate} />
166
212
 
167
- <SummaryHeading>Schedule</SummaryHeading>
213
+ <SectionHeading>Schedule</SectionHeading>
168
214
  <FactGrid>
169
215
  <FactRow label="Status">
170
- <PhaseBadge status={getCNPGScheduledBackupStatus(resource)} />
216
+ <span className="inline-flex flex-wrap items-center gap-2"><PhaseBadge status={getCNPGScheduledBackupStatus(resource)} />{blocker && <Badge severity="warning" size="sm">{blocker}</Badge>}</span>
217
+ {blocker && <Note>The resource list status comes from the ScheduledBackup alone.</Note>}
171
218
  </FactRow>
172
219
  <FactRow label="Cluster">
173
220
  <ClusterLink resource={resource} workspace={workspace} onNavigate={onNavigate} />
@@ -182,14 +229,15 @@ export function CNPGScheduledBackupSummary({ resource, workspace, onNavigate }:
182
229
  <NotReported text="Not set" />
183
230
  )}
184
231
  </FactRow>
232
+ {cron && schedulePreview && schedulePreview.schedule === cron && <CNPGSchedulePreviewFacts preview={schedulePreview} />}
185
233
  <FactRow label="Last scheduled">
186
234
  <TimeAgo at={resource?.status?.lastScheduleTime} missing="Never" />
187
235
  </FactRow>
188
- <FactRow label="Next scheduled">{next === '-' ? <NotReported /> : next}</FactRow>
236
+ <FactRow label="Next run reported by the operator">{next === '-' ? <NotReported /> : next}</FactRow>
189
237
  <FactRow label="Method">{methodText(resource) ?? 'Barman object store (in-tree) · default'}</FactRow>
190
238
  </FactGrid>
191
239
 
192
- <SummaryHeading hint={runs.length > shown.length ? `${shown.length} of ${runs.length}` : undefined}>Recent runs</SummaryHeading>
240
+ <SectionHeading hint={runs.length > shown.length ? `${shown.length} of ${runs.length}` : undefined}>Recent runs</SectionHeading>
193
241
  {runsUnavailable ? (
194
242
  <div className="text-sm">
195
243
  <NotReported text={runsUnavailable} />
@@ -0,0 +1,90 @@
1
+ // @vitest-environment jsdom
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { CNPGClusterHASection } from './CNPGClusterHASection'
5
+ import type { CNPGClusterHA } from './ha'
6
+
7
+ const noInstances: CNPGClusterHA = {
8
+ cluster: { namespace: 'db', name: 'pg', uid: 'u' },
9
+ sampledAt: '2026-09-30T12:00:00Z',
10
+ desiredImage: 'pg:17',
11
+ instances: [],
12
+ pods: { state: 'ok' },
13
+ nodes: { state: 'ok' },
14
+ quorum: { enabled: false, object: { state: 'notFound' } },
15
+ pdbs: { state: 'ok', enabled: true, items: [] },
16
+ primaryLease: { state: 'notFound' },
17
+ operatorLease: { state: 'unavailable' },
18
+ jobs: { state: 'ok', items: [] },
19
+ rwEndpoints: { state: 'ok', service: 'pg-rw', pods: [] },
20
+ certificates: [],
21
+ maintenance: { declared: false, inProgress: false, reusePVC: true },
22
+ }
23
+
24
+ describe('CNPGClusterHASection', () => {
25
+ it('says there is nothing to place rather than showing only the zone source', () => {
26
+ const html = renderToStaticMarkup(<CNPGClusterHASection ha={noInstances} showCertificates={false} />)
27
+ expect(html).toContain('No instance Pods to place')
28
+ expect(html).not.toContain('topology.kubernetes.io/zone of each instance')
29
+ })
30
+ })
31
+
32
+ it('shows the read-write Service, its Pods and primary mismatch beside Reachability', () => {
33
+ const ha: CNPGClusterHA = { ...noInstances, rwEndpoints: { state: 'ok', service: 'pg-rw', pods: ['pg-2'] } }
34
+ const html = renderToStaticMarkup(<CNPGClusterHASection ha={ha} currentPrimary="pg-1" onOpenReachability={() => {}} />)
35
+ expect(html).toContain('Read-write Service')
36
+ expect(html).toContain('pg-rw')
37
+ expect(html).toContain('pg-2')
38
+ expect(html).toContain('not on the reported primary pg-1')
39
+ expect(html).toContain('Reachability')
40
+ expect(html).toContain('aria-expanded="true"')
41
+ })
42
+ it('distinguishes absent endpoints from denied endpoint reads', () => {
43
+ expect(renderToStaticMarkup(<CNPGClusterHASection ha={noInstances} />)).toContain('No ready endpoints')
44
+ const denied: CNPGClusterHA = { ...noInstances, rwEndpoints: { state: 'denied', service: 'pg-rw', pods: [], grant: { verb: 'list', resource: 'endpointslices', group: 'discovery.k8s.io', namespace: 'db' } } }
45
+ const html = renderToStaticMarkup(<CNPGClusterHASection ha={denied} />)
46
+ expect(html).toContain('endpointslices')
47
+ expect(html).not.toContain('No ready endpoints')
48
+ })
49
+ it('does not flag deliberately absent read-write endpoints while hibernated', () => {
50
+ const html = renderToStaticMarkup(<CNPGClusterHASection ha={noInstances} currentPrimary="pg-1" hibernated />)
51
+ expect(html).toContain('None expected while hibernated')
52
+ expect(html).not.toContain('No ready endpoints')
53
+ })
54
+
55
+ it('shows the desired image without claiming any instance runs it', () => {
56
+ const html = renderToStaticMarkup(<CNPGClusterHASection ha={noInstances} />)
57
+ expect(html).toContain('Desired image pg:17; no instance running')
58
+ expect(html).not.toContain('every instance runs it')
59
+ })
60
+
61
+ it('describes matching Pod images without claiming the Pods are running', () => {
62
+ const ha: CNPGClusterHA = { ...noInstances, instances: [{ pod: 'pg-1', podUID: 'p1', role: 'primary', ready: false, restartCount: 0, image: 'pg:17', imageMatches: true }] }
63
+ const html = renderToStaticMarkup(<CNPGClusterHASection ha={ha} />)
64
+ expect(html).toContain('observed Pod images match')
65
+ expect(html).not.toContain('instances run it')
66
+ })
67
+
68
+ it('keeps the readiness statement inside expanded HA and lists missing expected instances without an observed role', () => {
69
+ const ha: CNPGClusterHA = { ...noInstances, declaredInstances: 2, expectedInstances: ['orders-1', 'orders-2'], instances: [{ pod: 'orders-1', podUID: 'a', role: 'primary', ready: true, restartCount: 0 }], jobs: { state: 'ok', items: [{ name: 'orders-2-join', role: 'join', phase: 'pending', reason: 'Pod cannot be scheduled: Unschedulable: 0/2 nodes are available: 2 Too many pods. preemption: no victims.' }] } }
70
+ const html = renderToStaticMarkup(<CNPGClusterHASection ha={ha} showInstances={false} />)
71
+ expect(html).toContain('1 of 2 declared instances ready; no instance Pod observed for orders-2')
72
+ expect(html).toContain('orders-1')
73
+ expect(html).toContain('orders-2</span> · not running')
74
+ expect(html).toContain('Cannot be scheduled: both nodes have reached their Pod limit')
75
+ expect(html).toContain('Scheduler message')
76
+ expect(html).toContain('aria-expanded="false"')
77
+ expect(html).toContain('preemption: no victims')
78
+ })
79
+
80
+ it('keeps each Job reason and scheduler disclosure inside its own block', () => {
81
+ const jobs = ['pg-1-initdb', 'pg-2-join'].map((name) => ({ name, role: 'join', phase: 'pending' as const, reason: `Pod cannot be scheduled: ${name}: 0/2 nodes are available: 2 Too many pods.` }))
82
+ const host = document.createElement('div'); host.innerHTML = renderToStaticMarkup(<CNPGClusterHASection ha={{ ...noInstances, jobs: { state: 'ok', items: jobs } }} />)
83
+ const blocks = [...host.querySelectorAll('div.space-y-1.text-xs')]
84
+ expect(blocks).toHaveLength(2)
85
+ for (let i = 0; i < jobs.length; i++) {
86
+ expect(blocks[i].textContent).toContain(jobs[i].name)
87
+ expect(blocks[i].querySelector('[inert]')?.textContent).toContain(jobs[i].reason)
88
+ expect(blocks[i].textContent).not.toContain(jobs[1 - i].name)
89
+ }
90
+ })
@@ -0,0 +1,331 @@
1
+ import { clsx } from 'clsx'
2
+ import { Badge } from '../ui/Badge'
3
+ import { Tooltip } from '../ui/Tooltip'
4
+ import { formatAge, summarizeSchedulerMessage } from '../resources/resource-utils'
5
+ import { PrimaryConflictNote } from './primitives'
6
+ import {
7
+ CNPG_ROLE_DETAIL_TEXT,
8
+ cnpgCertificateViews,
9
+ cnpgCertificatesSummary,
10
+ cnpgHASourceText,
11
+ cnpgHASummary,
12
+ cnpgImageDrift,
13
+ cnpgLeaseHolderPod,
14
+ cnpgLiveGap,
15
+ cnpgPDBFact,
16
+ cnpgPendingRestart,
17
+ cnpgQuorumFact,
18
+ cnpgZoneSpread,
19
+ type CNPGClusterHA,
20
+ type CNPGHAJob,
21
+ type CNPGHALease,
22
+ type CNPGInstanceLive,
23
+ } from './ha'
24
+ import { type NavigateToRef, RefLink } from '../ui/RefLink'
25
+ import { StatusDot, toneTextClass } from '../ui/status-tone'
26
+ import { FactGrid, FactRow, FactValue } from '../facts'
27
+ import { FoldSection, SectionHeading } from '../ui/FoldSection'
28
+
29
+ function Unknown({ text }: { text: string }) {
30
+ return <span className="text-theme-text-tertiary">{text}</span>
31
+ }
32
+
33
+ function LeaseValue({ lease, what }: { lease: CNPGHALease; what: string }) {
34
+ if (lease.state !== 'ok') return <Unknown text={cnpgHASourceText(lease, what)} />
35
+ return (
36
+ <span>
37
+ held by{' '}
38
+ {lease.holder && cnpgLeaseHolderPod(lease.holder) !== lease.holder ? (
39
+ <Tooltip content={lease.holder}>
40
+ <span className="font-mono text-xs break-all">{cnpgLeaseHolderPod(lease.holder)}</span>
41
+ </Tooltip>
42
+ ) : (
43
+ <span className="font-mono text-xs break-all">{lease.holder || '(nobody)'}</span>
44
+ )}
45
+ {lease.renewTime && <span className="text-theme-text-secondary"> · renewed {formatAge(lease.renewTime)} ago</span>}
46
+ {lease.expired && <span className={toneTextClass('degraded')}> · expired</span>}
47
+ {lease.controlledByCluster === false && <span className={toneTextClass('degraded')}> · not owned by this Cluster</span>}
48
+ </span>
49
+ )
50
+ }
51
+
52
+ const JOB_SEVERITY: Record<CNPGHAJob['phase'], 'success' | 'error' | 'info' | 'neutral'> = {
53
+ succeeded: 'success',
54
+ failed: 'error',
55
+ running: 'info',
56
+ active: 'info',
57
+ pending: 'neutral',
58
+ }
59
+
60
+ /**
61
+ * "HA and instances": whether the cluster survives losing an instance, and what
62
+ * a planned switchover will meet. Every row names its source; a fact the caller
63
+ * cannot read says so instead of reading as none.
64
+ */
65
+ export function CNPGClusterHASection({
66
+ ha,
67
+ live,
68
+ liveUnavailable,
69
+ loading,
70
+ error,
71
+ onNavigate,
72
+ primaryConflict,
73
+ title = 'HA and instances',
74
+ showInstances = true,
75
+ showCertificates = true,
76
+ currentPrimary,
77
+ hibernated = false,
78
+ onOpenReachability,
79
+ }: {
80
+ currentPrimary?: string
81
+ hibernated?: boolean
82
+ onOpenReachability?: (service: { namespace: string; name: string }) => void
83
+ title?: string
84
+ /** False where the host lists the instances itself (with their replication state). */
85
+ showInstances?: boolean
86
+ /** False where the host shows certificates elsewhere (CNPGClusterCertificates). */
87
+ showCertificates?: boolean
88
+ /** status.currentPrimary vs the Pod labelled primary, when they disagree. */
89
+ primaryConflict?: { status: string; labelled: string }
90
+ ha?: CNPGClusterHA
91
+ /** Instance-manager facts, when the caller can read them. */
92
+ live?: CNPGInstanceLive[]
93
+ /** Why `live` is absent (e.g. "needs get pods/proxy in db"); read from `live` itself when it is present. */
94
+ liveUnavailable?: string
95
+ loading?: boolean
96
+ error?: string
97
+ onNavigate?: NavigateToRef
98
+ }) {
99
+ if (!ha) {
100
+ return (
101
+ <>
102
+ <SectionHeading>{title}</SectionHeading>
103
+ <div className="text-sm text-theme-text-tertiary">{loading ? 'Reading HA facts…' : `HA facts could not be read${error ? `: ${error}` : ''}`}</div>
104
+ </>
105
+ )
106
+ }
107
+ const ns = ha.cluster.namespace
108
+ const spread = cnpgZoneSpread(ha)
109
+ const drift = cnpgImageDrift(ha)
110
+ const pending = cnpgPendingRestart(live)
111
+ const liveBy = new Map((live ?? []).map((l) => [l.pod, l]))
112
+ const jobs = [...ha.jobs.items].sort((a, b) => (a.phase === 'succeeded' ? 1 : 0) - (b.phase === 'succeeded' ? 1 : 0))
113
+ const versions = new Set((live ?? []).map((l) => l.instanceManagerVersion).filter(Boolean))
114
+
115
+ const haSummary = cnpgHASummary(ha, live, primaryConflict)
116
+ const endpointProblem = !hibernated && currentPrimary && ha.rwEndpoints.state === 'ok' && !ha.rwEndpoints.pods.includes(currentPrimary)
117
+ ? ha.rwEndpoints.pods.length === 0 ? 'no read-write endpoint' : 'read-write endpoint not on the primary'
118
+ : undefined
119
+
120
+ return (
121
+ <>
122
+ <FoldSection title={title} hint={`checked ${formatAge(ha.sampledAt)} ago`} summary={[endpointProblem, haSummary.text].filter(Boolean).join(' · ')} attention={haSummary.attention || !!endpointProblem}>
123
+ {<div className="mb-3 text-sm text-theme-text-secondary">{haSummary.text}</div>}
124
+ {ha.pods.state === 'ok' && <div className="mb-3 flex flex-wrap gap-x-4 gap-y-1 text-xs">
125
+ {ha.instances.map((i) => <span key={i.pod}><RefLink refTo={{ kind: 'Pod', group: '', namespace: ns, name: i.pod }} onNavigate={onNavigate} mono /> · Pod {i.ready ? 'ready' : 'not ready'}</span>)}
126
+ {[...new Set(ha.expectedInstances ?? [])].filter((name) => !ha.instances.some((i) => i.pod === name)).map((name) => <span key={name} className="text-theme-text-tertiary"><span className="font-mono">{name}</span> · not running</span>)}
127
+ </div>}
128
+ <FactGrid>
129
+ <FactRow label="Read-write Service">
130
+ <div className="space-y-1">
131
+ <RefLink refTo={{ kind: 'Service', group: '', namespace: ns, name: ha.rwEndpoints.service }} onNavigate={onNavigate} mono />
132
+ {ha.rwEndpoints.state !== 'ok' ? <Unknown text={cnpgHASourceText(ha.rwEndpoints, 'read-write endpoints')} /> : (
133
+ <>
134
+ <div className="flex flex-wrap gap-2">
135
+ {ha.rwEndpoints.pods.length === 0 ? <span className={hibernated ? 'text-theme-text-secondary' : toneTextClass('unhealthy')}>{hibernated ? 'None expected while hibernated' : 'No ready endpoints'}</span> : ha.rwEndpoints.pods.map((pod) => <RefLink key={pod} refTo={{ kind: 'Pod', group: '', namespace: ns, name: pod }} onNavigate={onNavigate} mono />)}
136
+ </div>
137
+ {!hibernated && currentPrimary && ha.rwEndpoints.pods.length > 0 && !ha.rwEndpoints.pods.includes(currentPrimary) && <div className={clsx('text-xs', toneTextClass('unhealthy'))}>Read-write endpoint is not on the reported primary {currentPrimary}.</div>}
138
+ </>
139
+ )}
140
+ {onOpenReachability && <button type="button" className="text-xs text-accent-text hover:underline" onClick={() => onOpenReachability({ namespace: ns, name: ha.rwEndpoints.service })}>Reachability →</button>}
141
+ <div className="text-[11.5px] text-theme-text-tertiary">Ready Pods from the Service’s EndpointSlices</div>
142
+ </div>
143
+ </FactRow>
144
+ <FactRow label="Failure domains">
145
+ {!spread.known ? (
146
+ <div>
147
+ <Unknown text={ha.pods.state !== 'ok' ? cnpgHASourceText(ha.pods, 'instance Pods') : cnpgHASourceText(ha.nodes, 'Nodes (zones)')} />
148
+ {spread.nodes.length > 0 && (
149
+ <div className="text-xs text-theme-text-secondary">
150
+ Nodes: {spread.nodes.map((n) => `${n.node} (${n.pods.join(', ')})`).join(' · ')}
151
+ </div>
152
+ )}
153
+ </div>
154
+ ) : spread.zones.length === 0 && spread.unlabelled.length === 0 ? (
155
+ <Unknown text="No instance Pods to place" />
156
+ ) : (
157
+ <div>
158
+ <div className="flex flex-wrap gap-x-3">
159
+ {spread.zones.map((z) => (
160
+ <span key={z.zone}>
161
+ <span className="font-mono">{z.zone}</span>
162
+ <span className="text-theme-text-secondary">: {z.pods.join(', ')}</span>
163
+ </span>
164
+ ))}
165
+ {spread.unlabelled.length > 0 && <Unknown text={`no zone label: ${spread.unlabelled.join(', ')}`} />}
166
+ </div>
167
+ {(spread.singleZone || spread.sharedNode) && (
168
+ <div className={clsx('text-xs', toneTextClass('degraded'))}>
169
+ {spread.singleZone ? 'Every instance is in one zone: losing it loses the cluster. ' : ''}
170
+ {spread.sharedNode ? 'Two or more instances share a Node.' : ''}
171
+ </div>
172
+ )}
173
+ <div className="text-[11.5px] text-theme-text-tertiary">topology.kubernetes.io/zone of each instance’s Node</div>
174
+ </div>
175
+ )}
176
+ </FactRow>
177
+
178
+ {showInstances && <FactRow label="Instances">
179
+ {ha.pods.state !== 'ok' ? (
180
+ <Unknown text={cnpgHASourceText(ha.pods, 'instance Pods')} />
181
+ ) : (
182
+ <div className="space-y-1">
183
+ {ha.instances.map((i) => {
184
+ const l = liveBy.get(i.pod)
185
+ return (
186
+ <div key={i.pod} className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
187
+ <Tooltip content={`Pod ${i.ready ? 'ready' : 'not ready'}${l?.roleDetail === 'replayPaused' || l?.roleDetail === 'fileBased' ? ` · ${CNPG_ROLE_DETAIL_TEXT[l.roleDetail]}` : ''}`}>
188
+ <StatusDot tone={!i.ready ? 'unhealthy' : l?.roleDetail === 'replayPaused' || l?.roleDetail === 'fileBased' ? 'degraded' : 'healthy'} />
189
+ </Tooltip>
190
+ <RefLink refTo={{ kind: 'Pod', group: '', namespace: ns, name: i.pod }} onNavigate={onNavigate} mono />
191
+ <span className="text-theme-text-secondary">
192
+ {l?.roleDetail ? CNPG_ROLE_DETAIL_TEXT[l.roleDetail] : i.role === 'unknown' ? 'role unknown' : i.role}
193
+ </span>
194
+ {l?.timeline !== undefined && <span className="text-theme-text-tertiary">timeline {l.timeline}</span>}
195
+ {i.qosClass && <span className="text-theme-text-tertiary">QoS {i.qosClass}</span>}
196
+ {l?.pendingRestart && <Badge severity="warning" size="sm">pending restart</Badge>}
197
+ {i.imageMatches === false && <Badge severity="warning" size="sm">image differs</Badge>}
198
+ {l?.instanceManagerVersion && versions.size > 1 && <span className="text-theme-text-tertiary">CNPG instance manager {l.instanceManagerVersion}</span>}
199
+ </div>
200
+ )
201
+ })}
202
+ {primaryConflict && <PrimaryConflictNote conflict={primaryConflict} />}
203
+ <div className="text-[11.5px] text-theme-text-tertiary">
204
+ {live ? 'Role detail and pending restart from each instance manager' : `Role from Pod labels; role detail ${cnpgLiveGap(undefined, liveUnavailable)}`}
205
+ {versions.size === 1 ? ` · instance manager ${[...versions][0]}` : ''}
206
+ </div>
207
+ </div>
208
+ )}
209
+ </FactRow>}
210
+
211
+ <FactRow label="Pending restart">
212
+ {!pending.known && pending.pods.length === 0 ? (
213
+ <Unknown text={`Unknown: ${cnpgLiveGap(live, liveUnavailable)}`} />
214
+ ) : pending.pods.length === 0 ? (
215
+ <span className="text-theme-text-secondary">None reported</span>
216
+ ) : (
217
+ <span className={toneTextClass('degraded')}>
218
+ {pending.pods.join(', ')} {pending.pods.length === 1 ? 'needs' : 'need'} a restart to apply changed parameters
219
+ {pending.forDecrease ? ' (a lowered setting: the primary restarts first)' : ''}
220
+ {!pending.known ? ` · ${cnpgLiveGap(live, liveUnavailable)}` : ''}
221
+ </span>
222
+ )}
223
+ </FactRow>
224
+
225
+ <FactRow label="Image">
226
+ {!drift.known && drift.drifted.length === 0 ? (
227
+ <Unknown text={!ha.desiredImage ? 'Desired image not reported' : ha.pods.state === 'ok' && ha.instances.length === 0 ? `Desired image ${ha.desiredImage}; no instance running` : `Desired image ${ha.desiredImage}; running images unknown`} />
228
+ ) : drift.drifted.length === 0 ? (
229
+ <span>
230
+ <span className="font-mono text-xs break-all">{ha.desiredImage}</span>
231
+ <span className="text-theme-text-secondary"> · observed Pod images match</span>
232
+ </span>
233
+ ) : (
234
+ <span className={toneTextClass('degraded')}>
235
+ {drift.drifted.map((d) => `${d.pod} has image ${d.image}`).join(' · ')} · desired <span className="font-mono">{ha.desiredImage}</span>
236
+ </span>
237
+ )}
238
+ </FactRow>
239
+
240
+ <FactRow label="Failover quorum">
241
+ <FactValue fact={cnpgQuorumFact(ha.quorum)} />
242
+ </FactRow>
243
+
244
+ <FactRow label="Disruption budgets">
245
+ <FactValue fact={cnpgPDBFact(ha.pdbs)} />
246
+ </FactRow>
247
+
248
+ <FactRow label="Primary lease">
249
+ <LeaseValue lease={ha.primaryLease} what="the primary Lease" />
250
+ </FactRow>
251
+
252
+ <FactRow label="Operator leader">
253
+ <LeaseValue lease={ha.operatorLease} what="the operator’s Lease" />
254
+ </FactRow>
255
+
256
+ <FactRow label="Cluster Jobs">
257
+ {ha.jobs.state !== 'ok' ? (
258
+ <Unknown text={cnpgHASourceText(ha.jobs, 'Jobs')} />
259
+ ) : jobs.length === 0 ? (
260
+ <span className="text-theme-text-secondary">None present</span>
261
+ ) : (
262
+ <div className="space-y-3">
263
+ {jobs.slice(0, 6).map((j) => (
264
+ <div key={j.name} className="space-y-1 text-xs">
265
+ <div className="flex flex-wrap items-center gap-2">
266
+ <Badge severity={JOB_SEVERITY[j.phase]} size="sm">{j.phase}</Badge>
267
+ <span className="text-theme-text-secondary">{j.role ?? 'job'}</span>
268
+ <RefLink refTo={{ kind: 'Job', group: 'batch', namespace: ns, name: j.name }} onNavigate={onNavigate} mono />
269
+ </div>
270
+ {j.reason && (/schedul|Too many pods|Insufficient/i.test(j.reason) ? <div className="w-full">
271
+ <span className={toneTextClass('degraded')}>Cannot be scheduled: {summarizeSchedulerMessage(j.reason, { plain: true })}</span>
272
+ <div className="mt-1"><FoldSection title="Scheduler message" summary="" attention={false}><div className="break-words text-xs text-theme-text-secondary">{j.reason}</div></FoldSection></div>
273
+ </div> : <span className={toneTextClass('degraded')}>{j.reason}</span>)}
274
+ </div>
275
+ ))}
276
+ {jobs.length > 6 && <div className="text-xs text-theme-text-tertiary">+{jobs.length - 6} more</div>}
277
+ </div>
278
+ )}
279
+ </FactRow>
280
+ </FactGrid>
281
+ </FoldSection>
282
+
283
+ {showCertificates && <CNPGClusterCertificates ha={ha} onNavigate={onNavigate} />}
284
+ </>
285
+ )
286
+ }
287
+
288
+ /** Certificate expiry and who renews each certificate, folded to one line unless one needs attention. */
289
+ export function CNPGClusterCertificates({ ha, onNavigate }: { ha: CNPGClusterHA; onNavigate?: NavigateToRef }) {
290
+ const ns = ha.cluster.namespace
291
+ const certs = cnpgCertificateViews(ha.certificates)
292
+ const certSummary = cnpgCertificatesSummary(ha.certificates)
293
+ return (
294
+ <FoldSection title="Certificates" summary={certSummary.text} attention={certSummary.attention}>
295
+ <FactGrid>
296
+ <FactRow label="Expiry">
297
+ {certs.length === 0 ? (
298
+ <Unknown text="No expiry reported by the operator" />
299
+ ) : (
300
+ <div className="space-y-0.5">
301
+ {certs.map((c) => (
302
+ <div key={c.secret} className="text-xs">
303
+ <RefLink refTo={{ kind: 'Secret', group: '', namespace: ns, name: c.secret }} onNavigate={onNavigate} mono />{' '}
304
+ <span className={toneTextClass(c.tone)}>
305
+ {c.expiresAt ? (c.daysLeft !== undefined && c.daysLeft < 0 ? `expired ${c.expiresAt}` : `expires in ${c.daysLeft} d`) : `expiry unreadable (“${c.raw}”)`}
306
+ </span>
307
+ <span className="text-theme-text-secondary">
308
+ {' · '}
309
+ {c.renewal === 'operator' ? 'CloudNativePG renews it' : 'you renew it (spec.certificates)'}
310
+ </span>
311
+ {c.renewal === 'user' &&
312
+ (c.certManager ? (
313
+ <span className="text-theme-text-secondary">
314
+ {' · cert-manager '}
315
+ <RefLink refTo={{ kind: 'Certificate', group: 'cert-manager.io', namespace: ns, name: c.certManager.certificate }} onNavigate={onNavigate} mono />
316
+ </span>
317
+ ) : c.metadata?.state === 'ok' ? (
318
+ <span className="text-theme-text-tertiary"> · not issued by cert-manager</span>
319
+ ) : (
320
+ <span className="text-theme-text-tertiary"> · issuer unknown ({cnpgHASourceText(c.metadata, 'Secret metadata')})</span>
321
+ ))}
322
+ </div>
323
+ ))}
324
+ <div className="text-[11.5px] text-theme-text-tertiary">status.certificates.expirations</div>
325
+ </div>
326
+ )}
327
+ </FactRow>
328
+ </FactGrid>
329
+ </FoldSection>
330
+ )
331
+ }