@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.
- package/README.md +41 -0
- package/package.json +3 -3
- 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/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/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 +24 -71
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
- 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 +12 -1
- package/src/components/issues/issues.test.ts +49 -1
- 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.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +17 -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 +302 -109
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- 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 +5 -7
- 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 +7 -15
- 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/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- 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/KedaScaledObjectRenderer.tsx +13 -11
- 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/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +41 -29
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- 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 -20
- 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-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +82 -6
- 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/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +11 -4
- package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- 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 +3 -71
- 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/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- 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 +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +180 -64
- 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 +11 -2
- package/src/types/core.ts +29 -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 +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -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 +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { ArrowRight } from 'lucide-react'
|
|
3
|
+
import { CNPG_GROUP } from '../resources/resource-utils-cnpg'
|
|
4
|
+
import { type Fact } from '../facts'
|
|
5
|
+
import { cnpgLogicalLocation, type CNPGLogicalPath } from './logicalReplication'
|
|
6
|
+
import { type NavigateToRef, RefLink } from '../ui/RefLink'
|
|
7
|
+
import { toneTextClass } from '../ui/status-tone'
|
|
8
|
+
import { FactGrid, FactRow, FactSource, FactValue } from '../facts'
|
|
9
|
+
|
|
10
|
+
// A hop after the first carries its arrow, so a wrapped line never ends on an
|
|
11
|
+
// arrow pointing at nothing.
|
|
12
|
+
function Hop({ label, children, from }: { label: string; children: ReactNode; from?: boolean }) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
15
|
+
{from && <ArrowRight className="mt-4 h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />}
|
|
16
|
+
<div className="min-w-0">
|
|
17
|
+
<div className="text-[11px] uppercase tracking-wide text-theme-text-tertiary">{label}</div>
|
|
18
|
+
<div className="min-w-0 break-words text-sm">{children}</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One Subscription's path to its publisher: publication, the slot the
|
|
26
|
+
* publisher keeps for it, and whether that slot survives a publisher
|
|
27
|
+
* failover. `slot` is the host's reading of the publisher primary; absent
|
|
28
|
+
* means it was not read.
|
|
29
|
+
*/
|
|
30
|
+
export function CNPGLogicalPathView({
|
|
31
|
+
path,
|
|
32
|
+
slot,
|
|
33
|
+
onNavigate,
|
|
34
|
+
compact,
|
|
35
|
+
notice,
|
|
36
|
+
}: {
|
|
37
|
+
path: CNPGLogicalPath
|
|
38
|
+
slot?: Fact
|
|
39
|
+
onNavigate?: NavigateToRef
|
|
40
|
+
compact?: boolean
|
|
41
|
+
/** The host's word on the slot reading, e.g. that its latest refresh failed. */
|
|
42
|
+
notice?: ReactNode
|
|
43
|
+
}) {
|
|
44
|
+
const s = path.subscription
|
|
45
|
+
const pub = path.publication
|
|
46
|
+
const publisher = path.publisher
|
|
47
|
+
const slotFact: Fact = slot ?? { text: path.slot.name ? `Slot ${path.slot.name}: not read` : path.slot.reason ?? 'No slot', tone: 'unknown' }
|
|
48
|
+
const chain = (
|
|
49
|
+
<div className="flex flex-wrap items-start gap-x-2 gap-y-1">
|
|
50
|
+
<Hop label="Subscription">
|
|
51
|
+
<RefLink refTo={{ kind: 'Subscription', group: CNPG_GROUP, namespace: s.namespace, name: s.name }} onNavigate={onNavigate} mono>
|
|
52
|
+
{s.sqlName ?? s.name}
|
|
53
|
+
</RefLink>
|
|
54
|
+
<span className="text-xs text-theme-text-tertiary"> on {cnpgLogicalLocation(s.cluster ?? 'an unknown cluster', s.dbname)}</span>
|
|
55
|
+
</Hop>
|
|
56
|
+
<Hop label="Publication" from>
|
|
57
|
+
{pub.object ? (
|
|
58
|
+
<RefLink refTo={{ kind: 'Publication', group: CNPG_GROUP, namespace: pub.object.namespace, name: pub.object.name }} onNavigate={onNavigate} mono>
|
|
59
|
+
{pub.name}
|
|
60
|
+
</RefLink>
|
|
61
|
+
) : pub.name ? (
|
|
62
|
+
<span className="font-mono">{pub.name}</span>
|
|
63
|
+
) : (
|
|
64
|
+
<span className="text-theme-text-tertiary">name unknown</span>
|
|
65
|
+
)}
|
|
66
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
67
|
+
{' '}
|
|
68
|
+
on{' '}
|
|
69
|
+
{publisher.kind === 'cluster' ? (
|
|
70
|
+
<RefLink refTo={{ kind: 'Cluster', group: CNPG_GROUP, namespace: publisher.namespace, name: publisher.name }} onNavigate={onNavigate}>
|
|
71
|
+
{publisher.namespace === s.namespace && publisher.name === s.cluster ? 'the same cluster' : `${publisher.namespace}/${publisher.name}`}
|
|
72
|
+
</RefLink>
|
|
73
|
+
) : (
|
|
74
|
+
<span>{path.externalCluster.host ?? (path.externalCluster.name ? `external cluster ${path.externalCluster.name}` : 'an unnamed external cluster')}</span>
|
|
75
|
+
)}
|
|
76
|
+
{pub.dbname ? `/${pub.dbname}` : ' · database unknown'}
|
|
77
|
+
</span>
|
|
78
|
+
</Hop>
|
|
79
|
+
<Hop label="Slot on the publisher" from>
|
|
80
|
+
<FactValue fact={slotFact} />
|
|
81
|
+
</Hop>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
if (compact) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="space-y-1">
|
|
87
|
+
{notice}
|
|
88
|
+
{chain}
|
|
89
|
+
<div className={`text-xs ${toneTextClass(path.failover.tone)}`}>Failover: {path.failover.text}</div>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
return (
|
|
94
|
+
<div className="space-y-3">
|
|
95
|
+
{notice}
|
|
96
|
+
{chain}
|
|
97
|
+
<FactGrid>
|
|
98
|
+
<FactRow label="Publisher">
|
|
99
|
+
{publisher.kind === 'cluster' ? (
|
|
100
|
+
<span>
|
|
101
|
+
{publisher.namespace}/{publisher.name} <span className="text-xs text-theme-text-tertiary">via {publisher.via}</span>
|
|
102
|
+
</span>
|
|
103
|
+
) : (
|
|
104
|
+
<span className="text-theme-text-secondary">Outside Radar's view: {publisher.reason}</span>
|
|
105
|
+
)}
|
|
106
|
+
<div className="mt-0.5 text-[11.5px] text-theme-text-tertiary">
|
|
107
|
+
{path.externalCluster.name
|
|
108
|
+
? `From the subscriber's spec.externalClusters[${path.externalCluster.name}].connectionParameters.host`
|
|
109
|
+
: "The subscriber's spec.externalClusterName is not set, so no external cluster names the host"}
|
|
110
|
+
</div>
|
|
111
|
+
</FactRow>
|
|
112
|
+
<FactRow label="Publication object">
|
|
113
|
+
{pub.object ? (
|
|
114
|
+
<span className={pub.object.applied === false ? toneTextClass('degraded') : undefined}>
|
|
115
|
+
{pub.object.applied === true ? 'Applied' : pub.object.applied === false ? 'Not applied' : 'Pending'}
|
|
116
|
+
</span>
|
|
117
|
+
) : (
|
|
118
|
+
<span className="text-theme-text-tertiary">
|
|
119
|
+
{publisher.kind !== 'cluster'
|
|
120
|
+
? 'Unknown'
|
|
121
|
+
: pub.unavailable
|
|
122
|
+
? `Unknown: ${pub.unavailable} in ${publisher.namespace}`
|
|
123
|
+
: 'No Publication object declares it: it may exist in SQL only'}
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
</FactRow>
|
|
127
|
+
<FactRow label="Slot">
|
|
128
|
+
<FactValue fact={slotFact} />
|
|
129
|
+
<FactSource fact={slotFact} />
|
|
130
|
+
</FactRow>
|
|
131
|
+
<FactRow label="Survives failover">
|
|
132
|
+
<FactValue fact={path.failover} />
|
|
133
|
+
<FactSource fact={path.failover} />
|
|
134
|
+
</FactRow>
|
|
135
|
+
<FactRow label="Subscription status">
|
|
136
|
+
{s.applied === false ? (
|
|
137
|
+
<span className={toneTextClass('degraded')}>Not applied{s.message ? `: ${s.message}` : ''}</span>
|
|
138
|
+
) : s.applied === true ? (
|
|
139
|
+
'Applied'
|
|
140
|
+
) : (
|
|
141
|
+
<span className="text-theme-text-tertiary">Pending</span>
|
|
142
|
+
)}
|
|
143
|
+
<div className="mt-0.5 text-[11.5px] text-theme-text-tertiary">
|
|
144
|
+
Subscription status. Apply errors and lag are not reported: CloudNativePG's default exporter has no pg_stat_subscription query.
|
|
145
|
+
</div>
|
|
146
|
+
</FactRow>
|
|
147
|
+
</FactGrid>
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -8,9 +8,12 @@ import {
|
|
|
8
8
|
getCNPGObjectStoreRetention,
|
|
9
9
|
} from '../resources/resource-utils-cnpg'
|
|
10
10
|
import type { CNPGWorkspaceResponse } from './workspace'
|
|
11
|
-
import { FactGrid, FactRow, FactValue, RefLink, SummaryHeading, toneTextClass, type CNPGNavigate } from './primitives'
|
|
12
11
|
import { NotReported, Note, ObjectProblems, SummaryShell, TimeAgo } from './CNPGSharedSummary'
|
|
13
12
|
import { clustersIn, inferredObjectStoreHealth, refOf, relationUnavailable, usersOfObjectStore } from './relations'
|
|
13
|
+
import { type NavigateToRef, RefLink } from '../ui/RefLink'
|
|
14
|
+
import { toneTextClass } from '../ui/status-tone'
|
|
15
|
+
import { FactGrid, FactRow, FactValue } from '../facts'
|
|
16
|
+
import { SectionHeading } from '../ui/FoldSection'
|
|
14
17
|
|
|
15
18
|
function utc(at: string | undefined): string {
|
|
16
19
|
if (!at || !Number.isFinite(Date.parse(at))) return 'unknown'
|
|
@@ -24,7 +27,7 @@ export function CNPGObjectStoreSummary({
|
|
|
24
27
|
}: {
|
|
25
28
|
resource: any
|
|
26
29
|
workspace: CNPGWorkspaceResponse | null
|
|
27
|
-
onNavigate?:
|
|
30
|
+
onNavigate?: NavigateToRef
|
|
28
31
|
}) {
|
|
29
32
|
const ns = resource?.metadata?.namespace ?? ''
|
|
30
33
|
const clustersUnavailable = relationUnavailable(workspace, 'clusters', ns, 'Clusters')
|
|
@@ -46,7 +49,7 @@ export function CNPGObjectStoreSummary({
|
|
|
46
49
|
onNavigate={onNavigate}
|
|
47
50
|
/>
|
|
48
51
|
|
|
49
|
-
<
|
|
52
|
+
<SectionHeading hint="inferred">Upload health</SectionHeading>
|
|
50
53
|
{clustersUnavailable ? (
|
|
51
54
|
<div className="text-sm">
|
|
52
55
|
<NotReported text={`Unknown · ${clustersUnavailable}, which are the only evidence`} />
|
|
@@ -89,7 +92,7 @@ export function CNPGObjectStoreSummary({
|
|
|
89
92
|
</>
|
|
90
93
|
)}
|
|
91
94
|
|
|
92
|
-
<
|
|
95
|
+
<SectionHeading hint="ObjectStore status">Recovery window</SectionHeading>
|
|
93
96
|
{windows.length === 0 ? (
|
|
94
97
|
<div className="text-sm">
|
|
95
98
|
<NotReported text="No server has reported a recovery window" />
|
|
@@ -136,7 +139,7 @@ export function CNPGObjectStoreSummary({
|
|
|
136
139
|
</FactGrid>
|
|
137
140
|
)}
|
|
138
141
|
|
|
139
|
-
<
|
|
142
|
+
<SectionHeading>Destination</SectionHeading>
|
|
140
143
|
<FactGrid>
|
|
141
144
|
<FactRow label="Path">{destination !== '-' ? <span className="font-mono">{destination}</span> : <NotReported text="Not set" />}</FactRow>
|
|
142
145
|
<FactRow label="Provider">{provider ?? <NotReported />}</FactRow>
|
|
@@ -152,7 +155,7 @@ export function CNPGObjectStoreSummary({
|
|
|
152
155
|
<FactRow label="Retention">{retention ?? <NotReported text="Not set" />}</FactRow>
|
|
153
156
|
</FactGrid>
|
|
154
157
|
|
|
155
|
-
<
|
|
158
|
+
<SectionHeading>Used by</SectionHeading>
|
|
156
159
|
{clustersUnavailable ? (
|
|
157
160
|
<div className="text-sm">
|
|
158
161
|
<NotReported text={clustersUnavailable} />
|
|
@@ -2,9 +2,10 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { renderToString } from 'react-dom/server'
|
|
3
3
|
import { CNPGBackupSummary, CNPGScheduledBackupSummary } from './CNPGBackupSummary'
|
|
4
4
|
import { CNPGObjectStoreSummary } from './CNPGObjectStoreSummary'
|
|
5
|
-
import { CNPGDatabaseSummary } from './CNPGDeclarativeSummary'
|
|
5
|
+
import { CNPGDatabaseSummary, CNPGPublicationSummary, CNPGSubscriptionSummary } from './CNPGDeclarativeSummary'
|
|
6
6
|
import { CNPGPoolerSummary } from './CNPGPoolerSummary'
|
|
7
7
|
import { CNPGImageCatalogSummary } from './CNPGImageCatalogSummary'
|
|
8
|
+
import { ClusterLink } from './CNPGSharedSummary'
|
|
8
9
|
import { CNPG_WORKSPACE_KEYS, type CNPGWorkspaceKey, type CNPGWorkspaceResponse } from './workspace'
|
|
9
10
|
|
|
10
11
|
const PG = 'postgresql.cnpg.io/v1'
|
|
@@ -77,6 +78,22 @@ describe('CNPGBackupSummary', () => {
|
|
|
77
78
|
expect(t).toContain('an earlier schedule of that name')
|
|
78
79
|
})
|
|
79
80
|
|
|
81
|
+
it('distinguishes a replaced Cluster from a missing or unread one without linking to its replacement', () => {
|
|
82
|
+
const previous = { ...b, status: { ...b.status, pluginMetadata: { clusterUID: 'old' } } }
|
|
83
|
+
const replacement = { ...mainCluster, metadata: { ...mainCluster.metadata, uid: 'new' } }
|
|
84
|
+
const html = renderToString(<ClusterLink resource={previous} workspace={ws({ clusters: [replacement] })} onNavigate={nav} />)
|
|
85
|
+
expect(text(html)).toContain('main · an earlier Cluster of that name; the current one is a different object')
|
|
86
|
+
expect(html).not.toContain('<button')
|
|
87
|
+
expect(text(html)).not.toContain('not found')
|
|
88
|
+
const missing = text(renderToString(<ClusterLink resource={previous} workspace={ws({ clusters: [] })} onNavigate={nav} />))
|
|
89
|
+
expect(missing).toContain('not found in this namespace')
|
|
90
|
+
const unread = text(renderToString(<ClusterLink resource={previous} workspace={ws({}, { coverage: { clusters: { state: 'denied' } } })} onNavigate={nav} />))
|
|
91
|
+
expect(unread).not.toContain('not found')
|
|
92
|
+
expect(unread).not.toContain('earlier Cluster')
|
|
93
|
+
const current = renderToString(<ClusterLink resource={b} workspace={ws({ clusters: [replacement] })} onNavigate={nav} />)
|
|
94
|
+
expect(current).toContain('<button')
|
|
95
|
+
})
|
|
96
|
+
|
|
80
97
|
it('shows its own issues on top', () => {
|
|
81
98
|
const issues = [
|
|
82
99
|
{ id: 'i1', severity: 'critical' as const, kind: 'Backup', group: 'postgresql.cnpg.io', namespace: 'pg', name: 'main-20260901', reason: 'CNPGBackupFailed', message: 'Backup failed' },
|
|
@@ -101,6 +118,34 @@ describe('CNPGScheduledBackupSummary', () => {
|
|
|
101
118
|
expect(t).toContain('Backups older than 7 days are not listed')
|
|
102
119
|
})
|
|
103
120
|
|
|
121
|
+
it("shows the server's reading and next runs only for the schedule it read", () => {
|
|
122
|
+
const sched = { apiVersion: PG, kind: 'ScheduledBackup', metadata: { name: 'nightly', namespace: 'pg' }, spec: { cluster: { name: 'main' }, schedule: '0 30 2 * * *' } }
|
|
123
|
+
const preview = {
|
|
124
|
+
schedule: '0 30 2 * * *',
|
|
125
|
+
valid: true,
|
|
126
|
+
description: 'every day at 02:30:00 UTC',
|
|
127
|
+
nextRuns: ['2026-10-01T02:30:00Z', '2026-10-02T02:30:00Z', '2026-10-03T02:30:00Z'],
|
|
128
|
+
basis: 'lastCheckTime' as const,
|
|
129
|
+
clock: { zone: 'UTC', declared: true, source: 'Operator Deployment declares TZ=UTC' },
|
|
130
|
+
}
|
|
131
|
+
const t = text(renderToString(<CNPGScheduledBackupSummary resource={sched} workspace={ws({})} schedulePreview={preview} />))
|
|
132
|
+
expect(t).toContain('every day at 02:30:00 UTC')
|
|
133
|
+
expect(t).toContain('2026-10-01 02:30:00 UTC')
|
|
134
|
+
expect(t).toContain('Calculated upcoming times')
|
|
135
|
+
expect(t).toContain('Next run reported by the operatorNot reported')
|
|
136
|
+
expect(t).toContain("operator's last check")
|
|
137
|
+
const estimate = text(renderToString(<CNPGScheduledBackupSummary resource={sched} workspace={ws({})} schedulePreview={{ ...preview, clock: { zone: 'UTC', declared: false, source: 'Operator clock is unknown' } }} />))
|
|
138
|
+
expect(estimate).toContain('Estimated upcoming times')
|
|
139
|
+
expect(estimate).toContain('UTC estimate')
|
|
140
|
+
expect(estimate).not.toContain('Calculated upcoming times')
|
|
141
|
+
const stale = text(renderToString(<CNPGScheduledBackupSummary resource={sched} workspace={ws({})} schedulePreview={{ ...preview, schedule: '0 0 0 * * *' }} />))
|
|
142
|
+
expect(stale).not.toContain('every day at')
|
|
143
|
+
const due = text(renderToString(<CNPGScheduledBackupSummary resource={sched} workspace={ws({})} schedulePreview={{ ...preview, runsImmediately: true }} />))
|
|
144
|
+
expect(due).toContain('due (2026-10-01 02:30:00 UTC)')
|
|
145
|
+
expect(due).toContain('A run is due on the declared clock')
|
|
146
|
+
expect(due).toContain('at most one catch-up backup')
|
|
147
|
+
})
|
|
148
|
+
|
|
104
149
|
it('says when Backups are not readable instead of listing none', () => {
|
|
105
150
|
const sched = { apiVersion: PG, kind: 'ScheduledBackup', metadata: { name: 'nightly', namespace: 'pg' }, spec: { cluster: { name: 'main' } } }
|
|
106
151
|
const t = text(renderToString(<CNPGScheduledBackupSummary resource={sched} workspace={ws({}, { coverage: { backups: { state: 'denied' } } })} />))
|
|
@@ -196,10 +241,45 @@ describe('CNPGPoolerSummary', () => {
|
|
|
196
241
|
it('reports unknown scheduled count and unmeasured pressure', () => {
|
|
197
242
|
const pooler = { apiVersion: PG, kind: 'Pooler', metadata: { name: 'main-rw', namespace: 'pg' }, spec: { cluster: { name: 'main' }, type: 'rw', instances: 2 } }
|
|
198
243
|
const t = text(renderToString(<CNPGPoolerSummary resource={pooler} workspace={ws({})} onNavigate={nav} />))
|
|
199
|
-
expect(t).toContain('
|
|
244
|
+
expect(t).toContain('Pooler instance count not reported')
|
|
200
245
|
expect(t).toContain('Not measured')
|
|
201
246
|
expect(t).toContain('main-rw')
|
|
202
247
|
})
|
|
248
|
+
|
|
249
|
+
it('shows Deployment readiness, limits with PgBouncer defaults, observed pause and the Service path when live data is provided', () => {
|
|
250
|
+
const pooler = {
|
|
251
|
+
apiVersion: PG,
|
|
252
|
+
kind: 'Pooler',
|
|
253
|
+
metadata: { name: 'main-rw', namespace: 'pg' },
|
|
254
|
+
spec: { cluster: { name: 'main' }, type: 'rw', instances: 2, pgbouncer: { paused: true, parameters: { max_client_conn: '200' } } },
|
|
255
|
+
status: { instances: 2 },
|
|
256
|
+
}
|
|
257
|
+
const t = text(
|
|
258
|
+
renderToString(
|
|
259
|
+
<CNPGPoolerSummary
|
|
260
|
+
resource={pooler}
|
|
261
|
+
workspace={ws({})}
|
|
262
|
+
onNavigate={nav}
|
|
263
|
+
live={{
|
|
264
|
+
deployment: { name: 'main-rw', state: 'ok', replicas: 2, readyReplicas: 1 },
|
|
265
|
+
service: { name: 'main-rw', state: 'ok', type: 'ClusterIP', port: 5432 },
|
|
266
|
+
pressure: { state: 'ok', pods: [{ pod: 'a', state: 'ok', pools: [{ database: 'app', user: 'app', clActive: 4, clWaiting: 2 }] }] },
|
|
267
|
+
observed: { state: 'ok', pods: [{ pod: 'a', state: 'ok', paused: true }, { pod: 'b', state: 'ok', paused: false }] },
|
|
268
|
+
}}
|
|
269
|
+
/>,
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
expect(t).toContain('from Deployment main-rw')
|
|
273
|
+
expect(t).toContain('Pause requested')
|
|
274
|
+
expect(t).toContain('1/2 ready')
|
|
275
|
+
expect(t).toContain('Pause state')
|
|
276
|
+
expect(t).toContain('Observed: Paused on 1 of 2 PgBouncers')
|
|
277
|
+
expect(t).toContain('PgBouncer uses 20')
|
|
278
|
+
expect(t).toContain('200')
|
|
279
|
+
expect(t).toContain('main-rw')
|
|
280
|
+
expect(t).toContain('app/app')
|
|
281
|
+
expect(t).not.toContain('Not measured')
|
|
282
|
+
})
|
|
203
283
|
})
|
|
204
284
|
|
|
205
285
|
describe('CNPGImageCatalogSummary', () => {
|
|
@@ -234,3 +314,114 @@ describe('CNPGImageCatalogSummary', () => {
|
|
|
234
314
|
expect(t).toContain('No access to Clusters')
|
|
235
315
|
})
|
|
236
316
|
})
|
|
317
|
+
|
|
318
|
+
describe('logical replication summaries', () => {
|
|
319
|
+
const src = { apiVersion: PG, kind: 'Cluster', metadata: { name: 'src', namespace: 'pg' }, spec: { instances: 2 } }
|
|
320
|
+
const dst = { apiVersion: PG, kind: 'Cluster', metadata: { name: 'dst', namespace: 'pg' }, spec: { instances: 1, externalClusters: [{ name: 'src', connectionParameters: { host: 'src-rw', dbname: 'app' } }] } }
|
|
321
|
+
const pub = { apiVersion: PG, kind: 'Publication', metadata: { name: 'orders-pub', namespace: 'pg' }, spec: { cluster: { name: 'src' }, name: 'orders_pub', dbname: 'app', target: { allTables: true } }, status: { applied: true } }
|
|
322
|
+
const sub = { apiVersion: PG, kind: 'Subscription', metadata: { name: 'orders-sub', namespace: 'pg' }, spec: { cluster: { name: 'dst' }, name: 'orders_sub', dbname: 'app', publicationName: 'orders_pub', externalClusterName: 'src' }, status: { applied: true } }
|
|
323
|
+
const w = ws({ clusters: [src, dst], publications: [pub], subscriptions: [sub] })
|
|
324
|
+
|
|
325
|
+
it('shows the subscription path with the slot unread and the failover verdict', () => {
|
|
326
|
+
const t = text(renderToString(<CNPGSubscriptionSummary resource={sub} workspace={w} onNavigate={nav} />))
|
|
327
|
+
expect(t).toContain('Replication path')
|
|
328
|
+
expect(t).toContain('orders_pub')
|
|
329
|
+
expect(t).toContain('Slot orders_sub: not read')
|
|
330
|
+
expect(t).toContain('Lost on failover')
|
|
331
|
+
expect(t).toContain('no pg_stat_subscription query')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('never says no Publication declares it when Publications are unreadable', () => {
|
|
335
|
+
const denied = ws({ clusters: [src, dst], subscriptions: [sub] }, { coverage: { publications: { state: 'denied' } } })
|
|
336
|
+
const t = text(renderToString(<CNPGSubscriptionSummary resource={sub} workspace={denied} onNavigate={nav} />))
|
|
337
|
+
expect(t).toContain('Unknown: No access to Publications in pg')
|
|
338
|
+
expect(t).not.toContain('No Publication object declares it')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('lists the subscribers of a publication', () => {
|
|
342
|
+
const t = text(renderToString(<CNPGPublicationSummary resource={pub} workspace={w} onNavigate={nav} />))
|
|
343
|
+
expect(t).toContain('Subscribers')
|
|
344
|
+
expect(t).toContain('orders_sub')
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('shows the stale declaration as pending in its drawer', () => {
|
|
349
|
+
const resource = { apiVersion: PG, kind: 'Database', metadata: { name: 'app', namespace: 'pg', generation: 3 }, spec: { name: 'app' }, status: { applied: true, observedGeneration: 2 } }
|
|
350
|
+
const t = text(renderToString(<CNPGDatabaseSummary resource={resource} workspace={ws({})} />))
|
|
351
|
+
expect(t).toContain('Pending · awaiting the operator for the current spec')
|
|
352
|
+
expect(t).toContain('AppliedPending')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('keeps Deployment readiness beside the pause request even when no Pods are ready', () => {
|
|
356
|
+
const resource = { metadata: { name: 'p' }, spec: { pgbouncer: { paused: true } } }
|
|
357
|
+
const t = text(renderToString(<CNPGPoolerSummary resource={resource} workspace={ws({})} live={{ deployment: { name: 'p', state: 'ok', replicas: 2, readyReplicas: 0 } }} />))
|
|
358
|
+
expect(t).toContain('0/2 ready')
|
|
359
|
+
expect(t).toContain('Pause requested')
|
|
360
|
+
expect(t).not.toContain('Observed: Paused')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('puts the pending Pooler cause under readiness and names the Pod whose metric read failed', () => {
|
|
364
|
+
const html = renderToString(<CNPGPoolerSummary resource={{ metadata: { name: 'pooler', namespace: 'pg' } }} workspace={ws({})} onNavigate={nav} live={{ deployment: { name: 'pooler', state: 'ok', replicas: 1, readyReplicas: 0 }, pressure: { state: 'ok', pods: [{ pod: 'pooler-pod', state: 'unreachable', error: 'address not allowed', schedulingReason: 'Unschedulable: insufficient cpu' }] } }} />)
|
|
365
|
+
const t = text(html)
|
|
366
|
+
expect(t).toContain('0/1 ready')
|
|
367
|
+
expect(t).toContain('Cannot be scheduled: insufficient cpu.')
|
|
368
|
+
expect(t).toContain('Not measured: PgBouncer did not answer')
|
|
369
|
+
expect(t).toContain('pooler-pod')
|
|
370
|
+
expect(t).toContain('Measurement details')
|
|
371
|
+
expect(t).toContain('address not allowed')
|
|
372
|
+
expect(t.indexOf('Cannot be scheduled')).toBeGreaterThan(-1)
|
|
373
|
+
expect(t.indexOf('Cannot be scheduled')).toBeLessThan(t.indexOf('pooler-pod'))
|
|
374
|
+
expect(t.indexOf('Cannot be scheduled')).toBeLessThan(t.indexOf('Connections'))
|
|
375
|
+
expect(t.indexOf('address not allowed')).toBeGreaterThan(t.indexOf('Connections'))
|
|
376
|
+
expect(html).toContain('button')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('never calls an incomplete empty Pooler read idle and names the unread Pod', () => {
|
|
380
|
+
const t = text(renderToString(<CNPGPoolerSummary resource={{}} workspace={ws({})} live={{ pressure: { state: 'ok', pods: [{ pod: 'a', state: 'ok', pools: [] }, { pod: 'b', state: 'unreachable', error: 'timeout' }] } }} />))
|
|
381
|
+
expect(t).toContain('No pools seen in what was read')
|
|
382
|
+
expect(t).toContain('b: not read (timeout)')
|
|
383
|
+
expect(t).not.toContain('Idle:')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('leads Pooler observations with the scheduling cause and labels the operator count', () => {
|
|
387
|
+
const pod = { pod: 'orders-pooler-pod', state: 'unreachable', reason: 'PgBouncer has not started (Pod cannot be scheduled)', schedulingReason: 'Unschedulable: 0/2 nodes are available: 2 Too many pods. preemption: no victims.' }
|
|
388
|
+
const html = renderToString(<CNPGPoolerSummary resource={{ metadata: { name: 'p', namespace: 'db' }, spec: { instances: 1 }, status: { instances: 1 } }} workspace={ws({})} live={{ pressure: { state: 'ok', pods: [pod] }, observed: { state: 'ok', pods: [{ ...pod, error: pod.reason }] } }} />)
|
|
389
|
+
const t = text(html)
|
|
390
|
+
expect(t).toContain('Pooler status reports 1 instance · 1 requested')
|
|
391
|
+
expect(t).toContain('Cannot be scheduled: both nodes have reached their Pod limit')
|
|
392
|
+
expect(t).toContain('Not measured: PgBouncer has not started (Pod cannot be scheduled)')
|
|
393
|
+
expect(t).toContain('1 not read: orders-pooler-pod (PgBouncer has not started (Pod cannot be scheduled))')
|
|
394
|
+
expect(html).toContain('aria-expanded="false"')
|
|
395
|
+
expect(t).toContain('preemption: no victims.')
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('shows the schedule destination blocker only when the target Cluster is readable', () => {
|
|
399
|
+
const schedule = { metadata: { name: 'payments-nightly', namespace: 'pg' }, spec: { cluster: { name: 'payments' } } }
|
|
400
|
+
const cluster = { apiVersion: PG, kind: 'Cluster', metadata: { name: 'payments', namespace: 'pg' }, spec: {} }
|
|
401
|
+
const render = (workspace: CNPGWorkspaceResponse) => text(renderToString(<CNPGScheduledBackupSummary resource={schedule} workspace={workspace} />))
|
|
402
|
+
const t = render(ws({ clusters: [cluster] }))
|
|
403
|
+
expect(t).toContain('Enabled · not run yetNo backup destination')
|
|
404
|
+
expect(t).toContain('The resource list status comes from the ScheduledBackup alone.')
|
|
405
|
+
expect(render(ws({}))).not.toContain('No backup destination')
|
|
406
|
+
expect(render(ws({}))).not.toContain('The resource list status')
|
|
407
|
+
expect(render(ws({ clusters: [{ ...cluster, spec: { backup: { barmanObjectStore: { destinationPath: 's3://backups' } } } }] }))).not.toContain('The resource list status')
|
|
408
|
+
expect(text(renderToString(<CNPGScheduledBackupSummary resource={{ ...schedule, spec: { ...schedule.spec, suspend: true } }} workspace={ws({ clusters: [cluster] })} />))).not.toContain('The resource list status')
|
|
409
|
+
expect(text(renderToString(<CNPGScheduledBackupSummary resource={{ ...schedule, spec: { ...schedule.spec, suspend: true } }} workspace={ws({ clusters: [cluster] })} />))).not.toContain('No backup destination')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('names visible database declarations without claiming a SQL inventory', () => {
|
|
413
|
+
const resource = { metadata: { name: 'orders-appdb', namespace: 'pg' }, spec: { name: 'appdb', cluster: { name: 'orders' } } }
|
|
414
|
+
const t = text(renderToString(<CNPGDatabaseSummary resource={resource} workspace={ws({})} />))
|
|
415
|
+
expect(t).toContain('No visible Publication declarations')
|
|
416
|
+
expect(t).toContain('No visible Subscription declarations')
|
|
417
|
+
expect(t.match(/Objects created in SQL are not shown/g)).toHaveLength(1)
|
|
418
|
+
const denied = text(renderToString(<CNPGDatabaseSummary resource={resource} workspace={ws({}, { coverage: { publications: { state: 'denied' }, subscriptions: { state: 'denied' } } })} />))
|
|
419
|
+
expect(denied).toContain('No access to Publications')
|
|
420
|
+
expect(denied).not.toContain('No visible Publication declarations')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it.each([['session', 'A client keeps one server connection for its whole session.'], ['transaction', 'A client uses a server connection only for each transaction.']])('explains %s pool mode before its raw value', (poolMode, explanation) => {
|
|
424
|
+
const t = text(renderToString(<CNPGPoolerSummary resource={{ spec: { pgbouncer: { poolMode } } }} workspace={ws({})} />))
|
|
425
|
+
expect(t).toContain(explanation)
|
|
426
|
+
expect(t.indexOf(explanation)).toBeLessThan(t.indexOf(poolMode, t.indexOf(explanation) + explanation.length))
|
|
427
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
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 { renderToStaticMarkup } from 'react-dom/server'
|
|
6
|
+
import { CNPGPoolerScheduling } from './CNPGPoolerSummary'
|
|
7
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
8
|
+
it('leads with the cause, truncates a secondary Pod link, exposes its full name on hover and navigates once', () => {
|
|
9
|
+
vi.useFakeTimers()
|
|
10
|
+
const pod = 'orders-pooler-rw-6f9e349dec-abcdefghij'
|
|
11
|
+
const navigate = vi.fn()
|
|
12
|
+
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host)
|
|
13
|
+
act(() => root.render(<CNPGPoolerScheduling namespace="db" pods={[{ pod, state: 'unreachable', schedulingReason: '0/2 nodes are available: 2 Too many pods. preemption: no victims.' }]} onNavigate={navigate} />))
|
|
14
|
+
expect(host.firstElementChild!.firstElementChild!.textContent).toBe('Cannot be scheduled: both nodes have reached their Pod limit.')
|
|
15
|
+
const link = [...host.querySelectorAll('button')].find((b) => b.textContent === pod)!
|
|
16
|
+
expect(link.parentElement!.className).toContain('[&_button]:truncate')
|
|
17
|
+
expect(link.parentElement!.parentElement!.className).toContain('text-theme-text-secondary')
|
|
18
|
+
act(() => { link.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); vi.advanceTimersByTime(350) })
|
|
19
|
+
expect(document.querySelector('[role="tooltip"]')!.textContent).toBe(pod)
|
|
20
|
+
act(() => link.click())
|
|
21
|
+
expect(navigate).toHaveBeenCalledExactlyOnceWith({ kind: 'Pod', group: '', namespace: 'db', name: pod })
|
|
22
|
+
act(() => root.unmount()); host.remove(); vi.useRealTimers()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('groups a shared scheduling cause while retaining every Pod and its full scheduler message', () => {
|
|
26
|
+
const first = '0/2 nodes are available: 2 Too many pods. preemption: no victims.'
|
|
27
|
+
const second = '0/2 nodes are available: 2 Too many pods. preemption: no candidates.'
|
|
28
|
+
const html = renderToStaticMarkup(<CNPGPoolerScheduling namespace="db" pods={[{ pod: 'a', state: 'unreachable', schedulingReason: first }, { pod: 'b', state: 'unreachable', schedulingReason: second }, { pod: 'c', state: 'unreachable', schedulingReason: 'insufficient cpu' }]} onNavigate={() => {}} />)
|
|
29
|
+
expect(html.match(/Cannot be scheduled: both nodes have reached their Pod limit/g)).toHaveLength(1)
|
|
30
|
+
expect(html).toContain('Cannot be scheduled: insufficient cpu')
|
|
31
|
+
const host = document.createElement('div'); host.innerHTML = html
|
|
32
|
+
expect([...host.querySelectorAll('button')].filter((b) => ['a', 'b', 'c'].includes(b.textContent!)).map((b) => b.textContent)).toEqual(['a', 'b', 'c'])
|
|
33
|
+
expect(html).toContain(first)
|
|
34
|
+
expect(html).toContain(second)
|
|
35
|
+
expect(html.match(/Scheduler message/g)).toHaveLength(3)
|
|
36
|
+
})
|