@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.
- package/README.md +41 -0
- package/package.json +3 -3
- package/src/components/applications/ApplicationDetail.test.tsx +15 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -15
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/checks/types.ts +2 -13
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +9 -24
- package/src/components/gitops/detail-helpers.ts +1 -1
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +29 -83
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +40 -60
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +14 -3
- package/src/components/issues/issues.test.ts +50 -2
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.notice.test.tsx +59 -0
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +44 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +28 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +309 -117
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +6 -5
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +5 -4
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +2 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +7 -8
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +9 -17
- package/src/components/resources/renderers/CNPGShared.tsx +2 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/CronJobRenderer.tsx +6 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +3 -2
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +15 -14
- package/src/components/resources/renderers/EventRenderer.tsx +3 -3
- package/src/components/resources/renderers/ExperimentRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -4
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +11 -24
- package/src/components/resources/renderers/GatewayRouteReferences.test.tsx +63 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/HPARenderer.tsx +57 -17
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +12 -25
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/IngressClassReferences.test.tsx +52 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +10 -3
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +36 -5
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -14
- package/src/components/resources/renderers/KedaCredentials.test.tsx +85 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +15 -13
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +6 -3
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/KustomizationRenderer.tsx +2 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +3 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +4 -3
- package/src/components/resources/renderers/LeaseRenderer.tsx +3 -15
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +69 -2
- package/src/components/resources/renderers/NamespaceRenderer.tsx +6 -1
- package/src/components/resources/renderers/NodeRenderer.test.tsx +81 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +56 -15
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -3
- package/src/components/resources/renderers/PodClassReferences.test.tsx +42 -0
- package/src/components/resources/renderers/PodNomination.test.tsx +57 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +62 -36
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/RolloutRenderer.tsx +3 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +34 -5
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +11 -3
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +9 -22
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +2 -2
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +3 -3
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +2 -2
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.tsx +4 -3
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +1 -1
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +2 -2
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -24
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-keda.ts +1 -7
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils-kyverno.ts +1 -7
- package/src/components/resources/resource-utils-velero.ts +0 -12
- package/src/components/resources/resource-utils.ts +99 -39
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/DrainPlanDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/GitOpsWriteWarning.test.tsx +86 -0
- package/src/components/shared/GitOpsWriteWarning.tsx +135 -0
- package/src/components/shared/ResourceActionsBar.tsx +20 -24
- package/src/components/shared/ResourceRendererDispatch.tsx +13 -10
- package/src/components/shared/SetImageDialog.test.ts +75 -6
- package/src/components/shared/SetImageDialog.tsx +92 -84
- package/src/components/shared/index.ts +4 -1
- package/src/components/timeline/TimelineList.tsx +2 -15
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -11
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +8 -74
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/ScheduleValue.test.tsx +5 -0
- package/src/components/ui/ScheduleValue.tsx +3 -2
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +175 -1
- package/src/components/ui/drawer-components.tsx +103 -36
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +101 -0
- package/src/components/workload/WorkloadView.deletedDialog.test.tsx +45 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.secretSave.test.tsx +62 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +297 -104
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +21 -2
- package/src/types/core.ts +60 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +49 -2
- package/src/utils/format.ts +9 -1
- package/src/utils/gateway-references.test.ts +17 -0
- package/src/utils/gateway-references.ts +29 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/gitops-write-guard.test.ts +401 -0
- package/src/utils/gitops-write-guard.ts +509 -0
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/ingress-class-references.ts +22 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +70 -0
- package/src/utils/navigation.ts +91 -8
- package/src/utils/node-lifecycle.test.ts +36 -0
- package/src/utils/node-lifecycle.ts +77 -0
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +69 -0
- package/src/utils/resource-hierarchy.ts +41 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/resource-refs.test.ts +23 -0
- package/src/utils/resource-refs.ts +30 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- package/src/utils/view-transition.ts +16 -1
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { getCNPGPoolerDeploymentName, getCNPGPoolerMode, isCNPGPoolerPaused } from '../resources/resource-utils-cnpg'
|
|
2
4
|
import type { CNPGWorkspaceResponse } from './workspace'
|
|
3
|
-
import { FactGrid, FactRow, FactValue, RefLink, SummaryHeading, type CNPGNavigate } from './primitives'
|
|
4
5
|
import { ClusterLink, NotReported, Note, ObjectProblems, PhaseBadge, SummaryShell } from './CNPGSharedSummary'
|
|
5
6
|
import { refOf } from './relations'
|
|
7
|
+
import {
|
|
8
|
+
POOLER_LIMIT_PARAMETERS,
|
|
9
|
+
aggregatePoolerPools,
|
|
10
|
+
poolerPodPressure,
|
|
11
|
+
poolerPressureCoverage,
|
|
12
|
+
poolerPressureFact,
|
|
13
|
+
observedPause,
|
|
14
|
+
poolerBackendService,
|
|
15
|
+
poolerReadiness,
|
|
16
|
+
type CNPGPoolerLive,
|
|
17
|
+
type CNPGPoolerPoolRow,
|
|
18
|
+
} from './pooler'
|
|
19
|
+
import { type NavigateToRef, RefLink } from '../ui/RefLink'
|
|
20
|
+
import { toneTextClass } from '../ui/status-tone'
|
|
21
|
+
import { FactGrid, FactRow, FactValue } from '../facts'
|
|
22
|
+
import { Badge } from '../ui/Badge'
|
|
23
|
+
import { summarizeSchedulerMessage } from '../resources/resource-utils'
|
|
24
|
+
import { SectionHeading, FoldSection } from '../ui/FoldSection'
|
|
6
25
|
|
|
7
26
|
const TYPE_LABEL: Record<string, string> = {
|
|
8
27
|
rw: 'rw · routes to the primary',
|
|
@@ -14,36 +33,53 @@ export function CNPGPoolerSummary({
|
|
|
14
33
|
resource,
|
|
15
34
|
workspace,
|
|
16
35
|
onNavigate,
|
|
36
|
+
live,
|
|
37
|
+
actions,
|
|
38
|
+
lead,
|
|
17
39
|
}: {
|
|
18
40
|
resource: any
|
|
19
41
|
workspace: CNPGWorkspaceResponse | null
|
|
20
|
-
onNavigate?:
|
|
42
|
+
onNavigate?: NavigateToRef
|
|
43
|
+
/** Live reads a host adds (Deployment readiness, PgBouncer metrics and state). */
|
|
44
|
+
live?: CNPGPoolerLive
|
|
45
|
+
/** Operations rendered beside the paused state (pause / resume). */
|
|
46
|
+
actions?: ReactNode
|
|
47
|
+
/** Rendered first, e.g. a host's notice that a live read is stale. */
|
|
48
|
+
lead?: ReactNode
|
|
21
49
|
}) {
|
|
22
50
|
const ns = resource?.metadata?.namespace ?? ''
|
|
23
51
|
const type = resource?.spec?.type
|
|
24
52
|
const desired = resource?.spec?.instances
|
|
25
|
-
const
|
|
53
|
+
const reported = resource?.status?.instances
|
|
26
54
|
const deployment = getCNPGPoolerDeploymentName(resource)
|
|
55
|
+
const paused = isCNPGPoolerPaused(resource)
|
|
56
|
+
const readiness = poolerReadiness(live?.deployment)
|
|
57
|
+
const observed = observedPause(live?.observed)
|
|
27
58
|
|
|
28
59
|
return (
|
|
29
60
|
<SummaryShell>
|
|
61
|
+
{lead}
|
|
30
62
|
<ObjectProblems issues={workspace?.issues} subject={refOf(resource, 'Pooler')} onNavigate={onNavigate} />
|
|
31
63
|
|
|
32
|
-
<
|
|
64
|
+
<SectionHeading>State</SectionHeading>
|
|
33
65
|
<FactGrid>
|
|
34
66
|
<FactRow label="Status">
|
|
35
|
-
<
|
|
36
|
-
|
|
67
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
68
|
+
<PhaseBadge status={{ text: readiness.text, color: '', level: readiness.level }} />
|
|
69
|
+
{paused && <Badge severity="warning" size="sm">Pause requested</Badge>}
|
|
70
|
+
</div>
|
|
71
|
+
{readiness.detail && <Note>{readiness.detail}</Note>}
|
|
72
|
+
<CNPGPoolerScheduling namespace={ns} pods={live?.pressure?.pods ?? []} onNavigate={onNavigate} />
|
|
37
73
|
</FactRow>
|
|
38
74
|
<FactRow label="Instances">
|
|
39
75
|
<span>
|
|
40
|
-
{typeof
|
|
76
|
+
{typeof reported === 'number' ? `Pooler status reports ${reported} ${reported === 1 ? 'instance' : 'instances'}` : <NotReported text="Pooler instance count not reported" />}
|
|
41
77
|
<span className="text-theme-text-secondary">
|
|
42
78
|
{' · '}
|
|
43
|
-
{typeof desired === 'number' ? `${desired}
|
|
79
|
+
{typeof desired === 'number' ? `${desired} requested` : 'requested count not set'}
|
|
44
80
|
</span>
|
|
45
81
|
</span>
|
|
46
|
-
<Note>
|
|
82
|
+
<Note>status.instances is the operator’s count; observed readiness is on its Deployment and Pods.</Note>
|
|
47
83
|
</FactRow>
|
|
48
84
|
<FactRow label="Deployment">
|
|
49
85
|
{deployment ? (
|
|
@@ -52,19 +88,210 @@ export function CNPGPoolerSummary({
|
|
|
52
88
|
<NotReported />
|
|
53
89
|
)}
|
|
54
90
|
</FactRow>
|
|
55
|
-
<FactRow label="
|
|
56
|
-
<
|
|
91
|
+
<FactRow label="Pause state">
|
|
92
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
93
|
+
<span>{paused ? 'Pause requested' : 'Serving requested (not paused)'}</span>
|
|
94
|
+
{actions}
|
|
95
|
+
</div>
|
|
96
|
+
<Note>spec.pgbouncer.paused is what was asked for; each PgBouncer applies it with PAUSE / RESUME.</Note>
|
|
97
|
+
{paused && <Note>When PgBouncer applies the pause, it holds client connections instead of serving them.</Note>}
|
|
98
|
+
{observed && (
|
|
99
|
+
<div className="mt-1">
|
|
100
|
+
<FactValue fact={{ text: `Observed: ${observed.text}`, tone: observed.level, source: 'Each PgBouncer’s SHOW STATE' }} />
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</FactRow>
|
|
104
|
+
</FactGrid>
|
|
105
|
+
|
|
106
|
+
<SectionHeading hint={live?.pressure ? 'live from each PgBouncer' : undefined}>Connections</SectionHeading>
|
|
107
|
+
{live?.pressure ? (
|
|
108
|
+
<PoolerPressure pressure={live.pressure} />
|
|
109
|
+
) : (
|
|
110
|
+
<FactGrid>
|
|
111
|
+
<FactRow label="Connection pressure">
|
|
112
|
+
<FactValue fact={{ text: 'Not measured — needs PgBouncer metrics, which Radar does not read yet', tone: 'unknown' }} />
|
|
113
|
+
</FactRow>
|
|
114
|
+
</FactGrid>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<SectionHeading>Limits</SectionHeading>
|
|
118
|
+
<FactGrid>
|
|
119
|
+
<FactRow label="Pool mode">
|
|
120
|
+
{getCNPGPoolerMode(resource) === 'transaction' ? 'A client uses a server connection only for each transaction.' : getCNPGPoolerMode(resource) === 'session' ? 'A client keeps one server connection for its whole session.' : 'A client uses a server connection for each statement.'}
|
|
121
|
+
<Note>{getCNPGPoolerMode(resource)}{!resource?.spec?.pgbouncer?.poolMode ? ' (default)' : ''}</Note>
|
|
57
122
|
</FactRow>
|
|
123
|
+
{POOLER_LIMIT_PARAMETERS.map((p) => {
|
|
124
|
+
const v = resource?.spec?.pgbouncer?.parameters?.[p.key]
|
|
125
|
+
return (
|
|
126
|
+
<FactRow key={p.key} label={p.label}>
|
|
127
|
+
{v !== undefined ? (
|
|
128
|
+
<span className="font-mono">{String(v)}</span>
|
|
129
|
+
) : (
|
|
130
|
+
<span className="text-theme-text-secondary">
|
|
131
|
+
default{p.pgbouncerDefault ? <span className="text-theme-text-tertiary"> · PgBouncer uses {p.pgbouncerDefault}</span> : null}
|
|
132
|
+
</span>
|
|
133
|
+
)}
|
|
134
|
+
<Note>
|
|
135
|
+
<span className="font-mono">{p.key}</span>
|
|
136
|
+
</Note>
|
|
137
|
+
</FactRow>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
58
140
|
</FactGrid>
|
|
59
141
|
|
|
60
|
-
<
|
|
142
|
+
<SectionHeading>Routing</SectionHeading>
|
|
61
143
|
<FactGrid>
|
|
62
144
|
<FactRow label="Cluster">
|
|
63
145
|
<ClusterLink resource={resource} workspace={workspace} onNavigate={onNavigate} />
|
|
64
146
|
</FactRow>
|
|
65
147
|
<FactRow label="Type">{type ? TYPE_LABEL[type] ?? type : <NotReported text="Not set" />}</FactRow>
|
|
66
|
-
|
|
148
|
+
{live?.service && (
|
|
149
|
+
<FactRow label="Path">
|
|
150
|
+
<PoolerPath resource={resource} live={live} onNavigate={onNavigate} />
|
|
151
|
+
</FactRow>
|
|
152
|
+
)}
|
|
67
153
|
</FactGrid>
|
|
68
154
|
</SummaryShell>
|
|
69
155
|
)
|
|
70
156
|
}
|
|
157
|
+
|
|
158
|
+
function PoolerPath({ resource, live, onNavigate }: { resource: any; live: CNPGPoolerLive; onNavigate?: NavigateToRef }) {
|
|
159
|
+
const ns = resource?.metadata?.namespace ?? ''
|
|
160
|
+
const svc = live.service!
|
|
161
|
+
const backend = poolerBackendService(resource?.spec?.cluster?.name, resource?.spec?.type)
|
|
162
|
+
const svcState: Record<string, string> = { missing: 'does not exist', unreadable: 'no access', foreign: 'not controlled by this Pooler' }
|
|
163
|
+
return (
|
|
164
|
+
<div className="space-y-0.5">
|
|
165
|
+
<div>
|
|
166
|
+
Service <RefLink refTo={{ kind: 'Service', namespace: ns, name: svc.name }} onNavigate={onNavigate} mono />
|
|
167
|
+
{svc.state === 'ok' ? (
|
|
168
|
+
<span className="text-theme-text-secondary">{svc.port ? ` :${svc.port}` : ''}{svc.type ? ` · ${svc.type}` : ''}</span>
|
|
169
|
+
) : (
|
|
170
|
+
<span className={toneTextClass('unknown')}> · {svcState[svc.state]}</span>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
<div className="text-theme-text-secondary">→ PgBouncer ({resource?.metadata?.name})</div>
|
|
174
|
+
<div className="text-theme-text-secondary">
|
|
175
|
+
→ {backend ? <RefLink refTo={{ kind: 'Service', namespace: ns, name: backend }} onNavigate={onNavigate} mono /> : 'the cluster'}
|
|
176
|
+
{' '}of Cluster {resource?.spec?.cluster?.name ?? '—'}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Pods take their client connections independently, so one can queue while
|
|
183
|
+
// the sum still looks calm.
|
|
184
|
+
function PoolerPodPressure({ pods }: { pods: NonNullable<CNPGPoolerLive['pressure']>['pods'] }) {
|
|
185
|
+
return (
|
|
186
|
+
<table className="mt-3 w-full text-sm">
|
|
187
|
+
<thead className="text-left text-[11px] uppercase tracking-wide text-theme-text-tertiary">
|
|
188
|
+
<tr>
|
|
189
|
+
<th className="py-1 pr-3">PgBouncer Pod</th>
|
|
190
|
+
<th className="pr-3 text-right">Clients active</th>
|
|
191
|
+
<th className="pr-3 text-right">Waiting</th>
|
|
192
|
+
<th className="pr-3 text-right">Servers active</th>
|
|
193
|
+
<th className="text-right">Max wait</th>
|
|
194
|
+
</tr>
|
|
195
|
+
</thead>
|
|
196
|
+
<tbody className="table-divide-subtle">
|
|
197
|
+
{poolerPodPressure(pods).map((r) =>
|
|
198
|
+
r.state === 'ok' || r.state === 'partial' ? (
|
|
199
|
+
<tr key={r.pod}>
|
|
200
|
+
<td className="py-1 pr-3 font-mono text-xs">
|
|
201
|
+
{r.pod}
|
|
202
|
+
{r.state === 'partial' && <span className="ml-1 font-sans text-theme-text-tertiary">partial</span>}
|
|
203
|
+
</td>
|
|
204
|
+
<td className="pr-3 text-right font-mono"><FactValue fact={poolerPressureFact(pods.filter((p) => p.pod === r.pod), 'clActive')} /></td>
|
|
205
|
+
<td className={`pr-3 text-right font-mono ${r.clWaiting ? toneTextClass('degraded') : ''}`}><FactValue fact={poolerPressureFact(pods.filter((p) => p.pod === r.pod), 'clWaiting')} /></td>
|
|
206
|
+
<td className="pr-3 text-right font-mono"><FactValue fact={poolerPressureFact(pods.filter((p) => p.pod === r.pod), 'svActive')} /></td>
|
|
207
|
+
<td className={`text-right font-mono ${r.maxwaitSeconds ? toneTextClass('degraded') : ''}`}><FactValue fact={poolerPressureFact(pods.filter((p) => p.pod === r.pod), 'maxwaitSeconds')} /></td>
|
|
208
|
+
</tr>
|
|
209
|
+
) : (
|
|
210
|
+
<tr key={r.pod}>
|
|
211
|
+
<td className="py-1 pr-3 font-mono text-xs">{r.pod}</td>
|
|
212
|
+
<td colSpan={4} className="text-right text-xs text-theme-text-tertiary">
|
|
213
|
+
Not read: {r.error ?? r.state}
|
|
214
|
+
</td>
|
|
215
|
+
</tr>
|
|
216
|
+
),
|
|
217
|
+
)}
|
|
218
|
+
</tbody>
|
|
219
|
+
</table>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function PoolerPressure({ pressure }: { pressure: NonNullable<CNPGPoolerLive['pressure']> }) {
|
|
224
|
+
if (pressure.state === 'loading') return <div className="text-sm text-theme-text-tertiary">Reading PgBouncer metrics…</div>
|
|
225
|
+
if (pressure.state === 'denied' || pressure.state === 'error') {
|
|
226
|
+
return <FactValue fact={{ text: `Not measured — ${pressure.reason ?? (pressure.state === 'denied' ? 'needs get pods/proxy' : 'read failed')}`, tone: 'unknown' }} />
|
|
227
|
+
}
|
|
228
|
+
const { reporting, limitation, empty } = poolerPressureCoverage(pressure.pods)
|
|
229
|
+
const rows = aggregatePoolerPools(reporting)
|
|
230
|
+
if (reporting.length === 0) {
|
|
231
|
+
return <CNPGPoolerUnmeasured pods={pressure.pods} />
|
|
232
|
+
}
|
|
233
|
+
return (
|
|
234
|
+
<div>
|
|
235
|
+
{rows.length === 0 ? (
|
|
236
|
+
<div className="text-sm text-theme-text-secondary">{empty}</div>
|
|
237
|
+
) : (
|
|
238
|
+
<table className="w-full text-sm">
|
|
239
|
+
<thead className="text-left text-[11px] uppercase tracking-wide text-theme-text-tertiary">
|
|
240
|
+
<tr>
|
|
241
|
+
<th className="py-1 pr-3">Pool</th>
|
|
242
|
+
<th className="pr-3">Mode</th>
|
|
243
|
+
<th className="pr-3 text-right">Clients active</th>
|
|
244
|
+
<th className="pr-3 text-right">Waiting</th>
|
|
245
|
+
<th className="pr-3 text-right">Servers active / idle / used</th>
|
|
246
|
+
<th className="text-right">Max wait</th>
|
|
247
|
+
</tr>
|
|
248
|
+
</thead>
|
|
249
|
+
<tbody className="table-divide-subtle">
|
|
250
|
+
{rows.map((r: CNPGPoolerPoolRow) => (
|
|
251
|
+
<tr key={`${r.database}/${r.user}`}>
|
|
252
|
+
<td className="py-1 pr-3 font-mono text-xs">{r.database}/{r.user}</td>
|
|
253
|
+
<td className="pr-3 text-xs">{r.poolModes.join(', ') || '—'}</td>
|
|
254
|
+
<td className="pr-3 text-right font-mono"><FactValue fact={poolerPressureFact(pressure.pods, 'clActive', r)} /></td>
|
|
255
|
+
<td className={`pr-3 text-right font-mono ${r.clWaiting ? toneTextClass('degraded') : ''}`}><FactValue fact={poolerPressureFact(pressure.pods, 'clWaiting', r)} /></td>
|
|
256
|
+
<td className="pr-3 text-right font-mono"><FactValue fact={poolerPressureFact(pressure.pods, 'svActive', r)} /> / <FactValue fact={poolerPressureFact(pressure.pods, 'svIdle', r)} /> / <FactValue fact={poolerPressureFact(pressure.pods, 'svUsed', r)} /></td>
|
|
257
|
+
<td className={`text-right font-mono ${r.maxwaitSeconds ? toneTextClass('degraded') : ''}`}><FactValue fact={poolerPressureFact(pressure.pods, 'maxwaitSeconds', r)} /></td>
|
|
258
|
+
</tr>
|
|
259
|
+
))}
|
|
260
|
+
</tbody>
|
|
261
|
+
</table>
|
|
262
|
+
)}
|
|
263
|
+
<Note>
|
|
264
|
+
Summed over {reporting.length} of {pressure.pods.length} PgBouncer Pods{limitation ? ` · ${limitation}` : ''}. PgBouncer’s admin and
|
|
265
|
+
authentication pools are excluded.
|
|
266
|
+
</Note>
|
|
267
|
+
{pressure.pods.length > 1 && <PoolerPodPressure pods={pressure.pods} />}
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function CNPGPoolerScheduling({ namespace, pods, onNavigate }: { namespace: string; pods: NonNullable<CNPGPoolerLive['pressure']>['pods']; onNavigate?: NavigateToRef }) {
|
|
273
|
+
const byCause = new Map<string, typeof pods>()
|
|
274
|
+
for (const pod of pods) {
|
|
275
|
+
if (!pod.schedulingReason) continue
|
|
276
|
+
const cause = summarizeSchedulerMessage(pod.schedulingReason, { plain: true })
|
|
277
|
+
const group = byCause.get(cause) ?? []
|
|
278
|
+
group.push(pod)
|
|
279
|
+
byCause.set(cause, group)
|
|
280
|
+
}
|
|
281
|
+
return <>{[...byCause].map(([cause, blocked]) => <div key={cause} className={`mt-1 text-xs ${toneTextClass('degraded')}`}>
|
|
282
|
+
<div>Cannot be scheduled: {cause}.</div>
|
|
283
|
+
{blocked.map((p) => <div key={p.pod}>
|
|
284
|
+
<div className="mt-0.5 text-theme-text-secondary"><Tooltip content={p.pod} wrapperClassName="block max-w-full [&_button]:block [&_button]:max-w-full [&_button]:truncate [&_span.font-mono]:block [&_span.font-mono]:truncate"><RefLink refTo={{ kind: 'Pod', group: '', namespace, name: p.pod }} onNavigate={onNavigate} mono /></Tooltip></div>
|
|
285
|
+
<FoldSection title="Scheduler message" summary="" attention={false}><div className="break-words text-theme-text-secondary">{p.schedulingReason}</div></FoldSection>
|
|
286
|
+
</div>)}
|
|
287
|
+
</div>)}</>
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function CNPGPoolerUnmeasured({ pods }: { pods: NonNullable<CNPGPoolerLive['pressure']>['pods'] }) {
|
|
291
|
+
if (pods.length === 0) return <div className="text-theme-text-tertiary">Not measured: no PgBouncer answered</div>
|
|
292
|
+
return <>{pods.map((p) => <div key={p.pod} className="text-theme-text-tertiary">
|
|
293
|
+
Not measured: {p.state === 'denied' ? 'needs get pods/proxy' : p.reason ?? 'PgBouncer did not answer'}
|
|
294
|
+
<Note>{p.pod}</Note>
|
|
295
|
+
{p.error && <FoldSection title="Measurement details" summary="" attention={false}><div className="break-words text-xs">{p.error}</div></FoldSection>}
|
|
296
|
+
</div>)}</>
|
|
297
|
+
}
|
|
@@ -3,8 +3,11 @@ import { Badge } from '../ui/Badge'
|
|
|
3
3
|
import type { StatusBadge as StatusBadgeValue } from '../resources/resource-utils'
|
|
4
4
|
import { CNPG_GROUP } from '../resources/resource-utils-cnpg'
|
|
5
5
|
import type { CNPGWorkspaceIssue, CNPGWorkspaceResponse } from './workspace'
|
|
6
|
-
import {
|
|
7
|
-
import { clustersIn,
|
|
6
|
+
import { healthToSeverity } from '../../utils/badge-colors'
|
|
7
|
+
import { clustersIn, problemsForObject, relationUnavailable, targetCluster, type CNPGObjectRef } from './relations'
|
|
8
|
+
import { type NavigateToRef, RefLink } from '../ui/RefLink'
|
|
9
|
+
import { FactValue } from '../facts'
|
|
10
|
+
import { ProblemCallout } from './primitives'
|
|
8
11
|
|
|
9
12
|
const MAX_PROBLEMS = 3
|
|
10
13
|
|
|
@@ -20,7 +23,7 @@ export function ObjectProblems({
|
|
|
20
23
|
}: {
|
|
21
24
|
issues: CNPGWorkspaceIssue[] | undefined
|
|
22
25
|
subject: CNPGObjectRef
|
|
23
|
-
onNavigate?:
|
|
26
|
+
onNavigate?: NavigateToRef
|
|
24
27
|
}) {
|
|
25
28
|
const problems = problemsForObject(issues, subject)
|
|
26
29
|
if (problems.length === 0) return null
|
|
@@ -43,7 +46,7 @@ export function ObjectProblems({
|
|
|
43
46
|
|
|
44
47
|
export function PhaseBadge({ status }: { status: StatusBadgeValue }) {
|
|
45
48
|
return (
|
|
46
|
-
<Badge severity={
|
|
49
|
+
<Badge severity={healthToSeverity(status.level)} size="sm">
|
|
47
50
|
{status.text}
|
|
48
51
|
</Badge>
|
|
49
52
|
)
|
|
@@ -71,16 +74,20 @@ export function ClusterLink({
|
|
|
71
74
|
}: {
|
|
72
75
|
resource: any
|
|
73
76
|
workspace: CNPGWorkspaceResponse | null
|
|
74
|
-
onNavigate?:
|
|
77
|
+
onNavigate?: NavigateToRef
|
|
75
78
|
}) {
|
|
76
79
|
const name = resource?.spec?.cluster?.name
|
|
77
80
|
if (!name) return <NotReported text="Not set" />
|
|
78
81
|
const ns = resource?.metadata?.namespace ?? ''
|
|
79
|
-
const
|
|
82
|
+
const clusters = clustersIn(workspace)
|
|
83
|
+
const visible = !!targetCluster(resource, clusters)
|
|
84
|
+
const replaced = !visible && clusters.some((c) => c.metadata?.namespace === ns && c.metadata?.name === name)
|
|
80
85
|
return (
|
|
81
86
|
<span>
|
|
82
|
-
<RefLink refTo={{ kind: 'Cluster', group: CNPG_GROUP, namespace: ns, name }} onNavigate={onNavigate} mono />
|
|
83
|
-
{
|
|
87
|
+
<RefLink refTo={{ kind: 'Cluster', group: CNPG_GROUP, namespace: ns, name }} onNavigate={replaced ? undefined : onNavigate} mono />
|
|
88
|
+
{replaced ? (
|
|
89
|
+
<span className="text-theme-text-tertiary"> · an earlier Cluster of that name; the current one is a different object</span>
|
|
90
|
+
) : workspace && !visible && !relationUnavailable(workspace, 'clusters', ns, 'Clusters') && (
|
|
84
91
|
<span className="text-theme-text-tertiary"> · not found in this namespace</span>
|
|
85
92
|
)}
|
|
86
93
|
</span>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { expect, it, vi } from 'vitest'
|
|
5
|
+
import { CNPGWALArchivingFact } from './CNPGClusterSummary'
|
|
6
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
7
|
+
it('folds the raw condition and exposes relative age with the exact timestamp on hover', () => {
|
|
8
|
+
vi.useFakeTimers(); vi.setSystemTime(new Date('2026-10-01T13:00:00Z'))
|
|
9
|
+
const stamp = '2026-10-01T12:00:00Z'
|
|
10
|
+
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host)
|
|
11
|
+
act(() => root.render(<CNPGWALArchivingFact fact={{ state: 'no_destination' as const, text: 'Not archived: no destination configured', tone: 'neutral', detail: 'WAL is not archived to recovery storage, so point-in-time recovery is unavailable.', operatorCondition: { type: 'ContinuousArchiving', status: 'True', message: 'Continuous archiving is working', lastTransitionTime: stamp } }} />))
|
|
12
|
+
expect(host.textContent).toContain('WAL is not archived to recovery storage')
|
|
13
|
+
const button = host.querySelector('button')!
|
|
14
|
+
expect(button.getAttribute('aria-expanded')).toBe('false')
|
|
15
|
+
const panel = document.getElementById(button.getAttribute('aria-controls')!)!
|
|
16
|
+
expect(panel.firstElementChild!.hasAttribute('inert')).toBe(true)
|
|
17
|
+
act(() => button.click())
|
|
18
|
+
expect(button.getAttribute('aria-expanded')).toBe('true')
|
|
19
|
+
expect(host.textContent).toContain('ContinuousArchiving: True')
|
|
20
|
+
expect(host.textContent).toContain('Continuous archiving is working')
|
|
21
|
+
expect(host.textContent).toContain('1h ago')
|
|
22
|
+
expect(host.querySelector('time')?.dateTime).toBe(stamp)
|
|
23
|
+
act(() => { host.querySelector('time')!.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); vi.advanceTimersByTime(350) })
|
|
24
|
+
expect(document.body.textContent).toContain(stamp)
|
|
25
|
+
act(() => root.unmount()); host.remove(); vi.useRealTimers()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps the compact verdict, consequence and per-row operator disclosure', () => {
|
|
29
|
+
const host = document.createElement('div'); const root = createRoot(host)
|
|
30
|
+
act(() => root.render(<CNPGWALArchivingFact compact fact={{ state: 'no_destination' as const, text: 'Archive destination absent', tone: 'neutral', detail: 'CloudNativePG still reports archiving as working because it accepts each WAL file without keeping it.', operatorCondition: { type: 'ContinuousArchiving', status: 'True', message: 'working' } }} />))
|
|
31
|
+
expect(host.textContent).toContain('Archive destination absent')
|
|
32
|
+
expect(host.textContent).toContain('No point-in-time recovery')
|
|
33
|
+
expect(host.textContent).not.toContain('because')
|
|
34
|
+
const fold = host.querySelector('button')!
|
|
35
|
+
expect(fold.textContent).toContain('Operator report')
|
|
36
|
+
act(() => fold.click())
|
|
37
|
+
expect(host.textContent).toContain('ContinuousArchiving: True')
|
|
38
|
+
act(() => root.unmount())
|
|
39
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { cnpgBackupRunInFlight, cnpgBackupRunsInWindow } from './backupRuns'
|
|
3
|
+
|
|
4
|
+
const now = Date.parse('2026-10-05T12:00:00Z')
|
|
5
|
+
const backup = (phase: string, ageDays: number) => ({ apiVersion: 'postgresql.cnpg.io/v1', metadata: { name: phase }, status: { phase, startedAt: new Date(now - ageDays * 86400000).toISOString() } })
|
|
6
|
+
|
|
7
|
+
describe('backup run window', () => {
|
|
8
|
+
it('retains every in-flight phase regardless of age, including an unknown phase', () => {
|
|
9
|
+
const runs = ['pending', 'started', 'running', 'finalizing', 'walArchivingFailing', 'new-phase'].map((phase) => backup(phase, 10))
|
|
10
|
+
expect(cnpgBackupRunsInWindow(runs, now)).toHaveLength(runs.length)
|
|
11
|
+
expect(runs.every(cnpgBackupRunInFlight)).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
it('applies the cutoff only to settled CNPG runs', () => {
|
|
14
|
+
const runs = [backup('completed', 10), backup('failed', 10), backup('completed', 2), backup('failed', 1), { ...backup('running', 1), apiVersion: 'velero.io/v1' }]
|
|
15
|
+
expect(cnpgBackupRunsInWindow(runs, now).map((r) => r.status.phase)).toEqual(['failed', 'completed'])
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isApiGroup } from '../resources/resource-utils-cnpg'
|
|
2
|
+
|
|
3
|
+
export const CNPG_BACKUP_RUN_WINDOW_MS = 7 * 24 * 60 * 60 * 1000
|
|
4
|
+
|
|
5
|
+
export function cnpgBackupRunInFlight(backup: any): boolean {
|
|
6
|
+
return backup?.status?.phase !== 'completed' && backup?.status?.phase !== 'failed'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function cnpgBackupRunTime(backup: any): string | undefined {
|
|
10
|
+
return backup?.status?.stoppedAt || backup?.status?.startedAt || backup?.metadata?.creationTimestamp
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function cnpgBackupRunsInWindow(backups: any[], now = Date.now()): any[] {
|
|
14
|
+
return backups.filter((b) => {
|
|
15
|
+
if (!isApiGroup(b.apiVersion, 'postgresql.cnpg.io')) return false
|
|
16
|
+
const time = Date.parse(cnpgBackupRunTime(b) ?? '')
|
|
17
|
+
return cnpgBackupRunInFlight(b) || (Number.isFinite(time) && now - time <= CNPG_BACKUP_RUN_WINDOW_MS)
|
|
18
|
+
}).sort((a, b) => Date.parse(cnpgBackupRunTime(b) ?? '') - Date.parse(cnpgBackupRunTime(a) ?? ''))
|
|
19
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { cnpgEndpointAvailability } from './connect'
|
|
2
|
+
import type { CNPGClusterHA } from './ha'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { cnpgConnectInfo, cnpgConnectionURI, cnpgPortForwardCommand, cnpgPsqlCommand } from './connect'
|
|
5
|
+
|
|
6
|
+
const cluster = (spec: any = {}) => ({ apiVersion: 'postgresql.cnpg.io/v1', kind: 'Cluster', metadata: { name: 'pg', namespace: 'db' }, spec: { instances: 3, ...spec } })
|
|
7
|
+
|
|
8
|
+
describe('cnpgConnectInfo', () => {
|
|
9
|
+
it('forwards the actual Service port to the local template port', () => {
|
|
10
|
+
const endpoint = { ...cnpgConnectInfo(cluster()).endpoints[0], port: 6543 }
|
|
11
|
+
expect(cnpgPortForwardCommand(endpoint, 'db')).toBe('kubectl -n db port-forward service/pg-rw 5432:6543')
|
|
12
|
+
expect(cnpgPortForwardCommand(endpoint, 'db', 15432)).toBe('kubectl -n db port-forward service/pg-rw 15432:6543')
|
|
13
|
+
})
|
|
14
|
+
it('lists the default Services and names the Secret by convention when the spec does not', () => {
|
|
15
|
+
const info = cnpgConnectInfo(cluster())
|
|
16
|
+
expect(info.endpoints.map((e) => `${e.role} ${e.host}:${e.port}`)).toEqual(['rw pg-rw.db.svc:5432', 'ro pg-ro.db.svc:5432', 'r pg-r.db.svc:5432'])
|
|
17
|
+
expect(info.database).toEqual({ value: 'app', source: "CloudNativePG's default" })
|
|
18
|
+
expect(info.owner.value).toBe('app')
|
|
19
|
+
expect(info.secret).toEqual({ name: 'pg-app', source: 'name by convention (<cluster>-app)', byConvention: true })
|
|
20
|
+
expect(info.endpoints.find((e) => e.role === 'r')?.selects).toBe('any instance (may reach the primary; not read-only)')
|
|
21
|
+
expect(info.endpoints.find((e) => e.role === 'ro')?.selects).toBe('standbys only (read-only)')
|
|
22
|
+
const extra = cnpgConnectInfo(cluster({ managed: { services: { additional: [{ selectorType: 'r', serviceTemplate: { metadata: { name: 'pg-any' } } }] } } }))
|
|
23
|
+
expect(extra.endpoints.find((e) => e.name === 'pg-any')?.selects).toContain('not read-only')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('reads database, owner and Secret the way CloudNativePG resolves them: recovery, pg_basebackup, then initdb', () => {
|
|
27
|
+
const info = cnpgConnectInfo(
|
|
28
|
+
cluster({
|
|
29
|
+
bootstrap: {
|
|
30
|
+
initdb: { database: 'orders', owner: 'orders_owner', secret: { name: 'init-secret' } },
|
|
31
|
+
recovery: { database: 'restored', secret: { name: 'recovery-secret' } },
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
expect(info.database).toEqual({ value: 'restored', source: 'spec.bootstrap.recovery.database' })
|
|
36
|
+
expect(info.owner).toEqual({ value: 'orders_owner', source: 'spec.bootstrap.initdb.owner' })
|
|
37
|
+
expect(info.secret).toEqual({ name: 'recovery-secret', source: 'spec.bootstrap.recovery.secret.name', byConvention: false })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('treats a distributed-topology replica (replica.primary names another cluster) as a replica, like the operator', () => {
|
|
41
|
+
expect(cnpgConnectInfo(cluster({ replica: { primary: 'pg-east', source: 'pg-east' } })).replicaCluster).toBe(true)
|
|
42
|
+
expect(cnpgConnectInfo(cluster({ replica: { primary: 'pg', source: 'pg-east' } })).replicaCluster).toBe(false)
|
|
43
|
+
expect(cnpgConnectInfo(cluster({ replica: { enabled: true, source: 'pg-east' } })).replicaCluster).toBe(true)
|
|
44
|
+
expect(cnpgConnectInfo(cluster()).replicaCluster).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('owner defaults to the database name', () => {
|
|
48
|
+
expect(cnpgConnectInfo(cluster({ bootstrap: { initdb: { database: 'orders' } } })).owner).toEqual({
|
|
49
|
+
value: 'orders',
|
|
50
|
+
source: "CloudNativePG's default: the database's name",
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('does not invent a database for a monolithic import', () => {
|
|
55
|
+
const info = cnpgConnectInfo(cluster({ bootstrap: { initdb: { import: { type: 'monolith' } } } }))
|
|
56
|
+
expect(info.database.value).toBeUndefined()
|
|
57
|
+
expect(info.owner.value).toBeUndefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('drops disabled default Services and adds managed and Pooler Services with their ports', () => {
|
|
61
|
+
const info = cnpgConnectInfo(
|
|
62
|
+
cluster({
|
|
63
|
+
managed: {
|
|
64
|
+
services: {
|
|
65
|
+
disabledDefaultServices: ['ro', 'r'],
|
|
66
|
+
additional: [{ selectorType: 'rw', serviceTemplate: { metadata: { name: 'pg-lb' }, spec: { type: 'LoadBalancer', ports: [{ port: 6543 }] } } }],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
[
|
|
71
|
+
{ metadata: { name: 'pg-pooler-ro', namespace: 'db' }, spec: { cluster: { name: 'pg' }, type: 'ro' } },
|
|
72
|
+
{ metadata: { name: 'other', namespace: 'db' }, spec: { cluster: { name: 'pg2' } } },
|
|
73
|
+
{ metadata: { name: 'pg-pooler', namespace: 'elsewhere' }, spec: { cluster: { name: 'pg' } } },
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
expect(info.disabled).toEqual(['ro', 'r'])
|
|
77
|
+
expect(info.endpoints.map((e) => `${e.role} ${e.name}:${e.port}${e.portFromTemplate ? '*' : ''}`)).toEqual(['rw pg-rw:5432', 'additional pg-lb:6543*', 'pooler pg-pooler-ro:5432'])
|
|
78
|
+
expect(info.endpoints[2].poolerType).toBe('ro')
|
|
79
|
+
expect(info.endpoints[1].selects).toBe('the primary (read-write)')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('builds templates with a password placeholder only', () => {
|
|
83
|
+
const info = cnpgConnectInfo(cluster({ bootstrap: { initdb: { database: 'orders', owner: 'o w' } } }))
|
|
84
|
+
expect(cnpgConnectionURI(info.endpoints[0], info)).toBe('postgresql://o%20w:<password>@pg-rw.db.svc:5432/orders')
|
|
85
|
+
expect(cnpgPsqlCommand(info.endpoints[0], info)).toBe("psql -h pg-rw.db.svc -p 5432 -U 'o w' -d orders")
|
|
86
|
+
const quoted = cnpgConnectInfo(cluster({ bootstrap: { initdb: { database: "it's" } } }))
|
|
87
|
+
expect(cnpgPsqlCommand(quoted.endpoints[0], quoted)).toBe(`psql -h pg-rw.db.svc -p 5432 -U 'it'\\''s' -d 'it'\\''s'`)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('pins port-forward to a shell-quoted kubeconfig context when supplied', () => {
|
|
92
|
+
const ep = cnpgConnectInfo({ metadata: { name: 'orders', namespace: 'prod' } }).endpoints[0]
|
|
93
|
+
expect(cnpgPortForwardCommand(ep, 'prod', 5432, 'kind-cnpg')).toBe('kubectl --context kind-cnpg -n prod port-forward service/orders-rw 5432:5432')
|
|
94
|
+
expect(cnpgPortForwardCommand(ep, 'prod', 5432, 'test context')).toContain("--context 'test context'")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('reads rw endpoints separately from standby and any-instance Pod readiness', () => {
|
|
98
|
+
const eps = cnpgConnectInfo({ metadata: { name: 'orders', namespace: 'prod' } }).endpoints
|
|
99
|
+
const ha = { rwEndpoints: { state: 'ok', pods: ['orders-1'] }, pods: { state: 'ok' }, instances: [{ pod: 'orders-1', role: 'primary', ready: true }, { pod: 'orders-2', role: 'replica', ready: false }] } as CNPGClusterHA
|
|
100
|
+
expect(eps.map((ep) => cnpgEndpointAvailability(ep, ha).text)).toEqual(['Ready endpoints', 'Unavailable: no ready standby', 'Ready instance observed'])
|
|
101
|
+
expect(cnpgEndpointAvailability(eps[1], { ...ha, instances: [] }).text).toBe('Unavailable: no ready standby')
|
|
102
|
+
expect(eps.map((ep) => cnpgEndpointAvailability(ep).text)).toEqual(['Not checked', 'Not checked', 'Not checked'])
|
|
103
|
+
expect(cnpgEndpointAvailability(eps[0], { ...ha, rwEndpoints: { ...ha.rwEndpoints, state: 'denied' } }).text).toBe('Not checked')
|
|
104
|
+
expect(cnpgEndpointAvailability(eps[1], { ...ha, pods: { state: 'denied' } }).text).toBe('Not checked')
|
|
105
|
+
expect(cnpgEndpointAvailability(eps[1], { ...ha, instances: [{ ...ha.instances[0], role: 'unknown' }] }).text).toBe('Not checked')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('keeps Pooler routing faithful for any-instance and unrecognized selectors', () => {
|
|
109
|
+
for (const [type, selects] of [['rw', 'the primary'], ['ro', 'the standbys'], ['r', 'any instance (may reach the primary; not read-only)'], ['future', 'selector future']]) {
|
|
110
|
+
const ep = cnpgConnectInfo(cluster(), [{ metadata: { name: 'pooler', namespace: 'db' }, spec: { cluster: { name: 'pg' }, type } }]).endpoints.find((e) => e.role === 'pooler')!
|
|
111
|
+
expect(ep.poolerType).toBe(type)
|
|
112
|
+
expect(ep.selects).toBe(`PgBouncer in front of ${selects}`)
|
|
113
|
+
expect(cnpgEndpointAvailability(ep).source).toBe('This Service’s availability has not been read')
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
it('names the denied grant, failed read, unknown role and unread source alongside Not checked', () => {
|
|
117
|
+
const eps = cnpgConnectInfo(cluster()).endpoints
|
|
118
|
+
const ha = { rwEndpoints: { state: 'denied', grant: { verb: 'list', group: 'discovery.k8s.io', resource: 'endpointslices', namespace: 'db' } }, pods: { state: 'error', reason: 'API timeout' }, instances: [] } as unknown as CNPGClusterHA
|
|
119
|
+
expect(cnpgEndpointAvailability(eps[0], ha)).toEqual({ text: 'Not checked', source: 'No access to Service EndpointSlices (needs list endpointslices (discovery.k8s.io) in namespace db)' })
|
|
120
|
+
expect(cnpgEndpointAvailability(eps[1], ha)).toEqual({ text: 'Not checked', source: 'instance Pods could not be read: API timeout' })
|
|
121
|
+
expect(cnpgEndpointAvailability(eps[0], undefined, 'Reading availability…').source).toBe('Reading availability…')
|
|
122
|
+
expect(cnpgEndpointAvailability(eps[0], undefined, 'Availability could not be read: timeout').source).toBe('Availability could not be read: timeout')
|
|
123
|
+
expect(cnpgEndpointAvailability(eps[1], { ...ha, pods: { state: 'ok' }, instances: [{ role: 'unknown', ready: true }] } as CNPGClusterHA).source).toBe('Ready instance roles were not reported')
|
|
124
|
+
})
|