@skyhook-io/k8s-ui 1.11.0 → 1.13.0
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/package.json +4 -4
- package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
- package/src/components/gitops/GitOpsTableView.tsx +22 -2
- package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
- package/src/components/gitops/detail-helpers.ts +4 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +412 -5
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
- package/src/components/resources/renderers/PolicySection.tsx +197 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
- package/src/components/resources/resource-utils-cnpg.ts +324 -3
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +12 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +3 -0
- package/src/types/core.ts +29 -0
- package/src/types/gitops-flux-status.test.ts +163 -0
- package/src/types/gitops.ts +80 -24
- package/src/types/index.ts +1 -0
- package/src/types/policy.ts +193 -0
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- package/src/utils/resource-icons.ts +29 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type React from 'react'
|
|
1
2
|
import { Database, HardDrive, Activity, Clock, Shield, KeyRound } from 'lucide-react'
|
|
2
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
4
|
import { pluralize } from '../../../utils/pluralize'
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
getCNPGClusterImage,
|
|
8
9
|
getCNPGClusterStorage,
|
|
9
10
|
getCNPGClusterStorageClass,
|
|
11
|
+
getCNPGVolumeHealth,
|
|
10
12
|
getCNPGClusterWALStorage,
|
|
11
13
|
getCNPGClusterBootstrapMethod,
|
|
12
14
|
getCNPGClusterUpdateStrategy,
|
|
@@ -21,15 +23,20 @@ import {
|
|
|
21
23
|
getCNPGWALArchivingFailure,
|
|
22
24
|
getCNPGLastBackupFailure,
|
|
23
25
|
classifyCNPGClusterPhase,
|
|
26
|
+
CNPG_BARMAN_OBJECTSTORE_GROUP,
|
|
24
27
|
} from '../resource-utils-cnpg'
|
|
25
28
|
import { formatAge } from '../resource-utils'
|
|
29
|
+
import { RecoveryTime } from './CNPGShared'
|
|
26
30
|
|
|
27
31
|
interface CNPGClusterRendererProps {
|
|
32
|
+
/** Filled by the host with the Databases, Publications and Subscriptions
|
|
33
|
+
* declared against this cluster — a reverse lookup the package cannot do. */
|
|
34
|
+
declared?: React.ReactNode
|
|
28
35
|
data: any
|
|
29
36
|
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererProps) {
|
|
39
|
+
export function CNPGClusterRenderer({ data, onNavigate, declared}: CNPGClusterRendererProps) {
|
|
33
40
|
const conditions = data.status?.conditions || []
|
|
34
41
|
const instances = data.spec?.instances ?? 0
|
|
35
42
|
// Presence matters — see getCNPGClusterStatus. A cluster whose status has no
|
|
@@ -43,6 +50,7 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
|
|
|
43
50
|
const monitoring = getCNPGClusterMonitoring(data)
|
|
44
51
|
const isReplica = getCNPGClusterIsReplica(data)
|
|
45
52
|
const walStorage = getCNPGClusterWALStorage(data)
|
|
53
|
+
const volumeHealth = getCNPGVolumeHealth(data)
|
|
46
54
|
const postgresParams = getCNPGClusterPostgresParams(data)
|
|
47
55
|
const instanceNames = getCNPGClusterInstanceNames(data)
|
|
48
56
|
const bootstrapMethod = getCNPGClusterBootstrapMethod(data)
|
|
@@ -210,6 +218,14 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
|
|
|
210
218
|
/>
|
|
211
219
|
)}
|
|
212
220
|
|
|
221
|
+
{volumeHealth && volumeHealth.unusable.length > 0 && (
|
|
222
|
+
<AlertBanner
|
|
223
|
+
variant="error"
|
|
224
|
+
title="Unusable storage volume"
|
|
225
|
+
message={`${volumeHealth.unusable.join(', ')} cannot be used because a paired volume is missing. An instance backed by it will not start, and the cluster cannot return to full size until the volume is restored or removed.`}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
|
|
213
229
|
{/* Cluster Overview */}
|
|
214
230
|
<Section title="Cluster Overview" icon={Database} defaultExpanded>
|
|
215
231
|
<PropertyList>
|
|
@@ -220,6 +236,40 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
|
|
|
220
236
|
<Property label="Target Primary" value={targetPrimary} />
|
|
221
237
|
)}
|
|
222
238
|
<Property label="Image" value={getCNPGClusterImage(data)} />
|
|
239
|
+
{/* When the image comes from a catalog rather than a literal tag, the
|
|
240
|
+
catalog is where an "incomplete or invalid image catalog" phase is
|
|
241
|
+
diagnosed — so it is named and reachable rather than implied. */}
|
|
242
|
+
{data?.spec?.imageCatalogRef?.name && (
|
|
243
|
+
<Property
|
|
244
|
+
label="Image Catalog"
|
|
245
|
+
value={
|
|
246
|
+
<span className="inline-flex items-baseline gap-1.5 min-w-0">
|
|
247
|
+
<ResourceLink
|
|
248
|
+
name={data.spec.imageCatalogRef.name}
|
|
249
|
+
kind={
|
|
250
|
+
data.spec.imageCatalogRef.kind === 'ClusterImageCatalog'
|
|
251
|
+
? 'clusterimagecatalogs'
|
|
252
|
+
: 'imagecatalogs'
|
|
253
|
+
}
|
|
254
|
+
namespace={
|
|
255
|
+
data.spec.imageCatalogRef.kind === 'ClusterImageCatalog'
|
|
256
|
+
? ''
|
|
257
|
+
: (data.metadata?.namespace ?? '')
|
|
258
|
+
}
|
|
259
|
+
group="postgresql.cnpg.io"
|
|
260
|
+
onNavigate={onNavigate}
|
|
261
|
+
/>
|
|
262
|
+
{/* A catalog holds one image per major, so the name alone does
|
|
263
|
+
not say which entry this cluster resolves to. */}
|
|
264
|
+
{data.spec.imageCatalogRef.major != null && (
|
|
265
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
266
|
+
{`PostgreSQL ${data.spec.imageCatalogRef.major}`}
|
|
267
|
+
</span>
|
|
268
|
+
)}
|
|
269
|
+
</span>
|
|
270
|
+
}
|
|
271
|
+
/>
|
|
272
|
+
)}
|
|
223
273
|
<Property label="Update Strategy" value={getCNPGClusterUpdateStrategy(data)} />
|
|
224
274
|
{data.status?.writeService && (
|
|
225
275
|
<Property label="Write Service" value={
|
|
@@ -290,6 +340,20 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
|
|
|
290
340
|
<PropertyList>
|
|
291
341
|
<Property label="Data Size" value={getCNPGClusterStorage(data)} />
|
|
292
342
|
<Property label="Storage Class" value={getCNPGClusterStorageClass(data)} />
|
|
343
|
+
{/* Requested size and class are spec-side intentions. These are what the
|
|
344
|
+
operator reports about the volumes that actually exist. */}
|
|
345
|
+
{volumeHealth?.total !== undefined && (
|
|
346
|
+
<Property label="Volumes" value={`${volumeHealth.healthy.length}/${volumeHealth.total} healthy`} />
|
|
347
|
+
)}
|
|
348
|
+
{volumeHealth && volumeHealth.unusable.length > 0 && (
|
|
349
|
+
<Property label="Unusable" value={volumeHealth.unusable.join(', ')} />
|
|
350
|
+
)}
|
|
351
|
+
{volumeHealth && volumeHealth.resizing.length > 0 && (
|
|
352
|
+
<Property label="Resizing" value={volumeHealth.resizing.join(', ')} />
|
|
353
|
+
)}
|
|
354
|
+
{volumeHealth && volumeHealth.dangling.length > 0 && (
|
|
355
|
+
<Property label="Detached" value={volumeHealth.dangling.join(', ')} />
|
|
356
|
+
)}
|
|
293
357
|
</PropertyList>
|
|
294
358
|
{walStorage && (
|
|
295
359
|
<div className="mt-2 pt-2 border-t border-theme-border">
|
|
@@ -323,6 +387,7 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
|
|
|
323
387
|
<ResourceLink
|
|
324
388
|
name={backupConfig.plugin.barmanObjectName}
|
|
325
389
|
kind="objectstores"
|
|
390
|
+
group={CNPG_BARMAN_OBJECTSTORE_GROUP}
|
|
326
391
|
namespace={data.metadata?.namespace || ''}
|
|
327
392
|
onNavigate={onNavigate}
|
|
328
393
|
/>
|
|
@@ -338,13 +403,13 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
|
|
|
338
403
|
<Property label="Retention" value={backupConfig.retentionPolicy} />
|
|
339
404
|
)}
|
|
340
405
|
{backupConfig.lastSuccessfulBackup && (
|
|
341
|
-
<Property label="Last Successful" value={backupConfig.lastSuccessfulBackup} />
|
|
406
|
+
<Property label="Last Successful" value={<RecoveryTime at={backupConfig.lastSuccessfulBackup} />} />
|
|
342
407
|
)}
|
|
343
408
|
{lastFailedBackup && (
|
|
344
|
-
<Property label="Last Failed" value={lastFailedBackup} />
|
|
409
|
+
<Property label="Last Failed" value={<RecoveryTime at={lastFailedBackup} />} />
|
|
345
410
|
)}
|
|
346
411
|
{backupConfig.firstRecoverabilityPoint && (
|
|
347
|
-
<Property label="First Recoverability" value={backupConfig.firstRecoverabilityPoint} />
|
|
412
|
+
<Property label="First Recoverability" value={<RecoveryTime at={backupConfig.firstRecoverabilityPoint} />} />
|
|
348
413
|
)}
|
|
349
414
|
</PropertyList>
|
|
350
415
|
{/* Without this note the section renders empty on plugin-migrated
|
|
@@ -361,6 +426,8 @@ export function CNPGClusterRenderer({ data, onNavigate }: CNPGClusterRendererPro
|
|
|
361
426
|
</Section>
|
|
362
427
|
)}
|
|
363
428
|
|
|
429
|
+
{declared}
|
|
430
|
+
|
|
364
431
|
{/* Certificates */}
|
|
365
432
|
{certExpirations.length > 0 && (
|
|
366
433
|
<Section title="Certificates" icon={KeyRound} defaultExpanded>
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { Database, AlertTriangle, Table2 } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
5
|
+
import { HEALTH_BADGE_COLORS } from '../../../utils/badge-colors'
|
|
6
|
+
import {
|
|
7
|
+
getCNPGDeclarativeStatus,
|
|
8
|
+
getCNPGDeclarativeMessage,
|
|
9
|
+
getCNPGDeclarativeCluster,
|
|
10
|
+
getCNPGReclaimPolicy,
|
|
11
|
+
getCNPGImageCatalogEntries,
|
|
12
|
+
} from '../resource-utils-cnpg'
|
|
13
|
+
|
|
14
|
+
type Nav = (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Database, Publication and Subscription — CNPG's declarative objects.
|
|
18
|
+
*
|
|
19
|
+
* One renderer for three kinds because they share the state that matters:
|
|
20
|
+
* whether the operator has applied them to PostgreSQL, and what it said if it
|
|
21
|
+
* could not. They differ only in which spec fields identify the thing being
|
|
22
|
+
* declared, which the caller supplies.
|
|
23
|
+
*
|
|
24
|
+
* The status is deliberately three-valued. `applied: false` is a failure the
|
|
25
|
+
* operator can explain; an absent `applied` means it has not reconciled yet,
|
|
26
|
+
* and rendering that as a failure would condemn every object in its first
|
|
27
|
+
* seconds.
|
|
28
|
+
*/
|
|
29
|
+
/** Shared shell for the three declarative kinds; not part of the package API. */
|
|
30
|
+
function CNPGDeclarativeRenderer({
|
|
31
|
+
data,
|
|
32
|
+
onNavigate,
|
|
33
|
+
details,
|
|
34
|
+
extra,
|
|
35
|
+
}: {
|
|
36
|
+
data: any
|
|
37
|
+
onNavigate?: Nav
|
|
38
|
+
/** Kind-specific rows: what this object declares. */
|
|
39
|
+
details: Array<{ label: string; value: React.ReactNode }>
|
|
40
|
+
/** Sections the host adds below, e.g. reverse lookups needing a data fetch. */
|
|
41
|
+
extra?: React.ReactNode
|
|
42
|
+
}) {
|
|
43
|
+
const status = getCNPGDeclarativeStatus(data)
|
|
44
|
+
const message = getCNPGDeclarativeMessage(data)
|
|
45
|
+
const cluster = getCNPGDeclarativeCluster(data)
|
|
46
|
+
const reclaim = getCNPGReclaimPolicy(data)
|
|
47
|
+
const namespace = data?.metadata?.namespace ?? ''
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
{/* The operator's own words, verbatim. A failure here means the object
|
|
52
|
+
exists in Kubernetes and not in PostgreSQL — a split an operator will
|
|
53
|
+
not spot from the CR alone. */}
|
|
54
|
+
{status.level === 'unhealthy' && message && (
|
|
55
|
+
<AlertBanner variant="error" title="Not applied to PostgreSQL" message={message} />
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
<Section title="Status" icon={Database} defaultExpanded>
|
|
59
|
+
<PropertyList>
|
|
60
|
+
<Property
|
|
61
|
+
label="Applied"
|
|
62
|
+
value={
|
|
63
|
+
<span className={clsx('badge', HEALTH_BADGE_COLORS[status.level as keyof typeof HEALTH_BADGE_COLORS])}>
|
|
64
|
+
{status.text}
|
|
65
|
+
</span>
|
|
66
|
+
}
|
|
67
|
+
/>
|
|
68
|
+
{cluster && (
|
|
69
|
+
<Property
|
|
70
|
+
label="Cluster"
|
|
71
|
+
value={
|
|
72
|
+
<ResourceLink
|
|
73
|
+
name={cluster}
|
|
74
|
+
kind="clusters"
|
|
75
|
+
namespace={namespace}
|
|
76
|
+
group="postgresql.cnpg.io"
|
|
77
|
+
onNavigate={onNavigate}
|
|
78
|
+
/>
|
|
79
|
+
}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
{details.map((d) => (
|
|
83
|
+
<Property key={d.label} label={d.label} value={d.value} />
|
|
84
|
+
))}
|
|
85
|
+
<Property
|
|
86
|
+
label="On delete"
|
|
87
|
+
value={
|
|
88
|
+
reclaim.destructive ? (
|
|
89
|
+
<span className="text-warning-text">
|
|
90
|
+
{`${reclaim.value} — removing this resource drops it from PostgreSQL`}
|
|
91
|
+
</span>
|
|
92
|
+
) : (
|
|
93
|
+
<span>{`${reclaim.value} — removing this resource leaves PostgreSQL untouched`}</span>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
</PropertyList>
|
|
98
|
+
{/* Pending is not failure, and saying nothing here would leave the
|
|
99
|
+
reader to guess which of the two they are looking at. */}
|
|
100
|
+
{status.level === 'unknown' && (
|
|
101
|
+
<div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">
|
|
102
|
+
The operator has not reported on this yet. It has not failed — it has not been applied.
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
{status.level !== 'unhealthy' && message && (
|
|
106
|
+
<div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">{message}</div>
|
|
107
|
+
)}
|
|
108
|
+
</Section>
|
|
109
|
+
{extra}
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function CNPGDatabaseRenderer({
|
|
115
|
+
data,
|
|
116
|
+
onNavigate,
|
|
117
|
+
usedBy,
|
|
118
|
+
}: {
|
|
119
|
+
data: any
|
|
120
|
+
onNavigate?: Nav
|
|
121
|
+
/** Filled by the host with what replicates out of this database. */
|
|
122
|
+
usedBy?: React.ReactNode
|
|
123
|
+
}) {
|
|
124
|
+
const spec = data?.spec ?? {}
|
|
125
|
+
const details: Array<{ label: string; value: React.ReactNode }> = [
|
|
126
|
+
{ label: 'Database', value: spec.name ?? '-' },
|
|
127
|
+
{ label: 'Owner', value: spec.owner ?? '-' },
|
|
128
|
+
]
|
|
129
|
+
if (spec.encoding) details.push({ label: 'Encoding', value: spec.encoding })
|
|
130
|
+
if (spec.locale) details.push({ label: 'Locale', value: spec.locale })
|
|
131
|
+
if (spec.connectionLimit != null) {
|
|
132
|
+
details.push({ label: 'Connection Limit', value: String(spec.connectionLimit) })
|
|
133
|
+
}
|
|
134
|
+
if (spec.allowConnections === false) {
|
|
135
|
+
details.push({ label: 'Connections', value: 'Not allowed' })
|
|
136
|
+
}
|
|
137
|
+
if (spec.isTemplate === true) details.push({ label: 'Template', value: 'Yes' })
|
|
138
|
+
return <CNPGDeclarativeRenderer data={data} onNavigate={onNavigate} details={details} extra={usedBy} />
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function CNPGPublicationRenderer({
|
|
142
|
+
data,
|
|
143
|
+
onNavigate,
|
|
144
|
+
links,
|
|
145
|
+
}: {
|
|
146
|
+
data: any
|
|
147
|
+
onNavigate?: Nav
|
|
148
|
+
/** Resolved by the host: the PostgreSQL-side names below correspond to real
|
|
149
|
+
* CRs, and naming them without a route is a dead end. */
|
|
150
|
+
links?: { database?: React.ReactNode }
|
|
151
|
+
}) {
|
|
152
|
+
const spec = data?.spec ?? {}
|
|
153
|
+
const target = spec.target ?? {}
|
|
154
|
+
const scope = target.allTables
|
|
155
|
+
? 'All tables'
|
|
156
|
+
: Array.isArray(target.objects)
|
|
157
|
+
? `${target.objects.length} object${target.objects.length === 1 ? '' : 's'}`
|
|
158
|
+
: '-'
|
|
159
|
+
return (
|
|
160
|
+
<CNPGDeclarativeRenderer
|
|
161
|
+
data={data}
|
|
162
|
+
onNavigate={onNavigate}
|
|
163
|
+
details={[
|
|
164
|
+
{ label: 'Publication', value: spec.name ?? '-' },
|
|
165
|
+
{ label: 'In Database', value: links?.database ?? spec.dbname ?? '-' },
|
|
166
|
+
{ label: 'Publishes', value: scope },
|
|
167
|
+
]}
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function CNPGSubscriptionRenderer({
|
|
173
|
+
data,
|
|
174
|
+
onNavigate,
|
|
175
|
+
links,
|
|
176
|
+
}: {
|
|
177
|
+
data: any
|
|
178
|
+
onNavigate?: Nav
|
|
179
|
+
links?: { database?: React.ReactNode; publication?: React.ReactNode }
|
|
180
|
+
}) {
|
|
181
|
+
const spec = data?.spec ?? {}
|
|
182
|
+
return (
|
|
183
|
+
<CNPGDeclarativeRenderer
|
|
184
|
+
data={data}
|
|
185
|
+
onNavigate={onNavigate}
|
|
186
|
+
details={[
|
|
187
|
+
{ label: 'Subscription', value: spec.name ?? '-' },
|
|
188
|
+
{ label: 'In Database', value: links?.database ?? spec.dbname ?? '-' },
|
|
189
|
+
{ label: 'Subscribes To', value: links?.publication ?? spec.publicationName ?? '-' },
|
|
190
|
+
// The upstream is declared on the destination Cluster, not here, which
|
|
191
|
+
// is why a missing one fails at apply time rather than at admission.
|
|
192
|
+
{ label: 'Upstream Cluster', value: spec.externalClusterName ?? '-' },
|
|
193
|
+
]}
|
|
194
|
+
/>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* ImageCatalog and ClusterImageCatalog — the PostgreSQL images a Cluster may
|
|
200
|
+
* use, pinned per major version.
|
|
201
|
+
*
|
|
202
|
+
* Worth its own surface because a Cluster referencing a catalog that lacks its
|
|
203
|
+
* major version reports "incomplete or invalid image catalog" and stops, and
|
|
204
|
+
* the catalog is where that is diagnosed.
|
|
205
|
+
*/
|
|
206
|
+
export function CNPGImageCatalogRenderer({
|
|
207
|
+
data,
|
|
208
|
+
usedBy,
|
|
209
|
+
}: {
|
|
210
|
+
data: any
|
|
211
|
+
/** Filled by the host with the clusters pinned to this catalog — the question
|
|
212
|
+
* an operator has when a cluster reports an invalid catalog. */
|
|
213
|
+
usedBy?: React.ReactNode
|
|
214
|
+
}) {
|
|
215
|
+
const entries = getCNPGImageCatalogEntries(data)
|
|
216
|
+
return (
|
|
217
|
+
<>
|
|
218
|
+
<Section title={`Images (${entries.length})`} icon={Table2} defaultExpanded>
|
|
219
|
+
{entries.length === 0 ? (
|
|
220
|
+
<div className="flex items-start gap-2 text-sm text-theme-text-secondary">
|
|
221
|
+
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
|
|
222
|
+
<span>
|
|
223
|
+
No images are pinned. A cluster pointing at this catalog cannot resolve an image and will
|
|
224
|
+
not start.
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
) : (
|
|
228
|
+
<div className="space-y-1.5">
|
|
229
|
+
{entries.map((e) => (
|
|
230
|
+
<div key={e.major} className="flex items-baseline gap-3 min-w-0">
|
|
231
|
+
<span className="badge shrink-0">{`PostgreSQL ${e.major}`}</span>
|
|
232
|
+
<span className="text-sm text-theme-text-secondary font-mono truncate" title={e.image}>
|
|
233
|
+
{e.image}
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</Section>
|
|
240
|
+
{usedBy}
|
|
241
|
+
</>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { CNPGObjectStoreRenderer } from './CNPGObjectStoreRenderer'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A recovery window is keyed by the archive's server name. It defaults to the
|
|
7
|
+
* cluster's name, but the plugin's `serverName` parameter overrides it — the
|
|
8
|
+
* ObjectStore refuses the field outright and points at the Cluster — so the key
|
|
9
|
+
* and the cluster name are not interchangeable. A link is an assertion that
|
|
10
|
+
* something is on the other end of it.
|
|
11
|
+
*/
|
|
12
|
+
const store = {
|
|
13
|
+
apiVersion: 'barmancloud.cnpg.io/v1',
|
|
14
|
+
kind: 'ObjectStore',
|
|
15
|
+
metadata: { name: 'pg-store', namespace: 'pg' },
|
|
16
|
+
spec: { configuration: { destinationPath: 's3://backups' } },
|
|
17
|
+
status: {
|
|
18
|
+
serverRecoveryWindow: {
|
|
19
|
+
'pg-main': { firstRecoverabilityPoint: '2026-08-01T00:00:00Z', lastSuccessfulBackupTime: '2026-08-10T00:00:00Z' },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const nav = () => {}
|
|
25
|
+
|
|
26
|
+
describe('ObjectStore recovery-window server key', () => {
|
|
27
|
+
it('links the key to the cluster archiving under it', () => {
|
|
28
|
+
const html = renderToString(
|
|
29
|
+
<CNPGObjectStoreRenderer data={store} clusterForServer={new Map([['pg-main', 'pg-main']])} onNavigate={nav} />,
|
|
30
|
+
)
|
|
31
|
+
expect(html).toContain('>pg-main</button>')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// A key belonging to no cluster we found — an in-tree cluster, or one whose
|
|
35
|
+
// archive nothing here accounts for.
|
|
36
|
+
it('renders the key as text when nothing archives under it', () => {
|
|
37
|
+
const html = renderToString(
|
|
38
|
+
<CNPGObjectStoreRenderer data={store} clusterForServer={new Map([['other-server', 'other-cluster']])} onNavigate={nav} />,
|
|
39
|
+
)
|
|
40
|
+
expect(html).toContain('pg-main')
|
|
41
|
+
expect(html).not.toContain('>pg-main</button>')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Absent means unresolved — still loading, lookup failed, or the host cannot
|
|
45
|
+
// ask. None of those establish that the cluster is there.
|
|
46
|
+
it('does not link when nothing resolved the key', () => {
|
|
47
|
+
const html = renderToString(<CNPGObjectStoreRenderer data={store} onNavigate={nav} />)
|
|
48
|
+
expect(html).toContain('pg-main')
|
|
49
|
+
expect(html).not.toContain('>pg-main</button>')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// The whole reason the mapping exists: the key is the archive's name, and a
|
|
53
|
+
// cluster that renamed its archive must still be reachable from it.
|
|
54
|
+
it('links a renamed archive to the cluster behind it, showing the archive name', () => {
|
|
55
|
+
const html = renderToString(
|
|
56
|
+
<CNPGObjectStoreRenderer
|
|
57
|
+
data={store}
|
|
58
|
+
clusterForServer={new Map([['pg-main', 'pg-primary']])}
|
|
59
|
+
onNavigate={nav}
|
|
60
|
+
/>,
|
|
61
|
+
)
|
|
62
|
+
expect(html).toContain('>pg-main</button>')
|
|
63
|
+
expect(html).not.toContain('>pg-primary</button>')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// The recovery window is read before a restore, and it is derived from the
|
|
68
|
+
// ObjectStore alone — which cannot see that WAL archiving stopped on the cluster
|
|
69
|
+
// feeding it. Left to itself the row keeps a green "Recoverable" and a
|
|
70
|
+
// four-day-old successful backup while the recovery point has frozen, and the
|
|
71
|
+
// warning lives on a different screen entirely.
|
|
72
|
+
describe('a window that has stopped advancing', () => {
|
|
73
|
+
const store = {
|
|
74
|
+
metadata: { name: 'pg-store', namespace: 'pg' },
|
|
75
|
+
status: {
|
|
76
|
+
serverRecoveryWindow: {
|
|
77
|
+
'pg-doomed': {
|
|
78
|
+
firstRecoverabilityPoint: '2026-07-29T02:00:00Z',
|
|
79
|
+
lastSuccessfulBackupTime: '2026-08-12T02:00:00Z',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
it('does not call a frozen window recoverable', () => {
|
|
86
|
+
const html = renderToString(
|
|
87
|
+
<CNPGObjectStoreRenderer data={store} archivingFailing={new Set(['pg-doomed'])} />,
|
|
88
|
+
)
|
|
89
|
+
expect(html).toContain('Not advancing')
|
|
90
|
+
expect(html).not.toContain('>Recoverable<')
|
|
91
|
+
expect(html).toContain('WAL archiving has stopped')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('keeps the plain answer when archiving is healthy', () => {
|
|
95
|
+
const html = renderToString(<CNPGObjectStoreRenderer data={store} />)
|
|
96
|
+
expect(html).toContain('Recoverable')
|
|
97
|
+
expect(html).not.toContain('WAL archiving has stopped')
|
|
98
|
+
})
|
|
99
|
+
})
|