@skyhook-io/k8s-ui 1.17.0 → 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 (227) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
  67. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  68. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
  69. package/src/components/gitops/tree/layout.test.ts +111 -0
  70. package/src/components/gitops/tree/layout.ts +128 -0
  71. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  72. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  73. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  74. package/src/components/issues/IssuesView.tsx +40 -5
  75. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  76. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  77. package/src/components/issues/index.ts +3 -1
  78. package/src/components/issues/issue-timing.ts +12 -1
  79. package/src/components/issues/issues.test.ts +49 -1
  80. package/src/components/issues/severity.ts +21 -0
  81. package/src/components/issues/types.ts +23 -0
  82. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  83. package/src/components/logs/LogCore.tsx +25 -17
  84. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  85. package/src/components/logs/LogsViewer.test.tsx +291 -0
  86. package/src/components/logs/LogsViewer.tsx +72 -11
  87. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  88. package/src/components/logs/StructuredLogLine.tsx +22 -30
  89. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  90. package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
  91. package/src/components/logs/useLogBuffer.test.ts +8 -0
  92. package/src/components/logs/useLogStream.ts +17 -1
  93. package/src/components/problems/index.ts +1 -0
  94. package/src/components/problems/problems.test.tsx +63 -0
  95. package/src/components/problems/problems.tsx +165 -0
  96. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  97. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  98. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  99. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  100. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  101. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  102. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  103. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  104. package/src/components/resources/ResourcesView.tsx +302 -109
  105. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  106. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  107. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  108. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  109. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  110. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  111. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  112. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  113. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  114. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  115. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  116. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  117. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  118. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  119. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  120. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  121. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  122. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  123. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  124. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  125. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  126. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  127. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  128. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  129. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  130. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  131. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  132. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  133. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  134. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  135. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  136. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  137. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  138. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  139. package/src/components/resources/renderers/PodRenderer.tsx +41 -29
  140. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  141. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  142. package/src/components/resources/resource-utils-capi.ts +1 -1
  143. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  144. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  145. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  146. package/src/components/resources/resource-utils-contour.ts +16 -0
  147. package/src/components/resources/resource-utils-istio.ts +12 -8
  148. package/src/components/resources/resource-utils-knative.ts +18 -0
  149. package/src/components/resources/resource-utils.ts +82 -6
  150. package/src/components/resources/rules-column-width.test.ts +1 -1
  151. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  152. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  153. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  154. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  155. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  156. package/src/components/shared/DetailShell.test.tsx +80 -0
  157. package/src/components/shared/DetailShell.tsx +85 -10
  158. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  159. package/src/components/shared/EditableYamlView.tsx +1 -1
  160. package/src/components/shared/ResourceActionsBar.tsx +11 -4
  161. package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
  162. package/src/components/shared/index.ts +2 -0
  163. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  164. package/src/components/timeline/timeline-filters.ts +1 -0
  165. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  166. package/src/components/topology/K8sResourceNode.tsx +44 -2
  167. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  168. package/src/components/topology/TopologyGraph.tsx +3 -71
  169. package/src/components/topology/TopologySearch.tsx +1 -0
  170. package/src/components/topology/internet-groups.test.ts +35 -0
  171. package/src/components/topology/internet-groups.ts +55 -0
  172. package/src/components/topology/layout.ts +1 -1
  173. package/src/components/ui/Badge.tsx +1 -0
  174. package/src/components/ui/FoldSection.tsx +68 -0
  175. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  176. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  177. package/src/components/ui/RefLink.tsx +20 -0
  178. package/src/components/ui/RowActionMenu.tsx +8 -3
  179. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  180. package/src/components/ui/drawer-components.test.tsx +110 -1
  181. package/src/components/ui/drawer-components.tsx +62 -9
  182. package/src/components/ui/index.ts +3 -1
  183. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  184. package/src/components/ui/monacoRuntime.ts +14 -19
  185. package/src/components/ui/status-tone.test.ts +10 -1
  186. package/src/components/ui/status-tone.tsx +29 -0
  187. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  188. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  189. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  190. package/src/components/workload/WorkloadView.tsx +180 -64
  191. package/src/index.ts +14 -1
  192. package/src/monaco-deep.d.ts +19 -12
  193. package/src/theme/components.css +13 -0
  194. package/src/types/capacity.ts +11 -2
  195. package/src/types/core.ts +29 -2
  196. package/src/types/gitops-flux-status.test.ts +32 -1
  197. package/src/types/gitops-insights.ts +19 -2
  198. package/src/types/gitops-tree.ts +10 -0
  199. package/src/types/gitops.ts +26 -6
  200. package/src/utils/api-resources.ts +7 -0
  201. package/src/utils/application-history.ts +1 -0
  202. package/src/utils/cnpg-backup.test.ts +26 -0
  203. package/src/utils/cnpg-backup.ts +70 -0
  204. package/src/utils/format.test.ts +9 -0
  205. package/src/utils/format.ts +4 -0
  206. package/src/utils/gitops-owner.ts +3 -2
  207. package/src/utils/grant.test.ts +19 -0
  208. package/src/utils/grant.ts +35 -0
  209. package/src/utils/index.ts +4 -0
  210. package/src/utils/k8s-errors.ts +7 -7
  211. package/src/utils/log-format.test.ts +41 -0
  212. package/src/utils/log-format.ts +71 -15
  213. package/src/utils/log-level.ts +12 -8
  214. package/src/utils/navigation.test.ts +57 -0
  215. package/src/utils/navigation.ts +86 -8
  216. package/src/utils/object-reference-navigation.test.ts +43 -0
  217. package/src/utils/resource-hierarchy.test.ts +37 -0
  218. package/src/utils/resource-hierarchy.ts +15 -7
  219. package/src/utils/resource-icons.ts +2 -0
  220. package/src/utils/skeleton-yaml.test.ts +21 -0
  221. package/src/utils/skeleton-yaml.ts +16 -0
  222. package/src/utils/url-path.test.ts +35 -0
  223. package/src/utils/url-path.ts +21 -0
  224. package/src/utils/validators.test.ts +4 -13
  225. package/src/utils/validators.ts +5 -19
  226. package/src/utils/view-transition.test.ts +35 -1
  227. package/src/utils/view-transition.ts +16 -1
@@ -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
+ }