@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
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Fragment, useState, type ReactNode } from 'react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Badge } from '../ui/Badge'
|
|
4
4
|
import { Tooltip } from '../ui/Tooltip'
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
5
|
+
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse'
|
|
6
|
+
import { classifyCNPGClusterPhase, cnpgBlockedPhaseExplanation, CNPG_BARMAN_OBJECTSTORE_GROUP, CNPG_GROUP } from '../resources/resource-utils-cnpg'
|
|
7
|
+
import { cnpgClusterPlugins, cnpgPluginPhase, cnpgReadyInstances, type CNPGFleetRow, type CNPGInstance } from './workspace'
|
|
8
|
+
import type { CNPGDimension } from './ha'
|
|
9
|
+
import { PrimaryConflictNote, ProblemCallout, cnpgProblemForPresentation } from './primitives'
|
|
10
|
+
import { Note } from './CNPGSharedSummary'
|
|
11
|
+
import { type NavigateToRef, RefLink } from '../ui/RefLink'
|
|
12
|
+
import { StatusDot, toneTextClass } from '../ui/status-tone'
|
|
13
|
+
import { FactGrid, FactRow, FactSource, FactValue, ManagedByText, managedByLabel } from '../facts'
|
|
14
|
+
import { ProblemList } from '../problems'
|
|
15
|
+
import { formatAge } from '../resources/resource-utils'
|
|
16
|
+
import { FoldSection, SectionHeading } from '../ui/FoldSection'
|
|
17
|
+
|
|
18
|
+
function ReadyCount({ row }: { row: CNPGFleetRow }) {
|
|
19
|
+
const r = cnpgReadyInstances(row)
|
|
20
|
+
if (row.instances.ready === null) return <>{r.text}{r.podText && <span className="block text-xs text-theme-text-secondary">{r.podText}</span>}</>
|
|
21
|
+
if (!r.note) return <>{r.text} ready</>
|
|
22
|
+
return (
|
|
23
|
+
<Tooltip content={r.note} position="top">
|
|
24
|
+
<span className={clsx('font-medium', toneTextClass(r.tone ?? 'unknown'))}>
|
|
25
|
+
{r.text} Pods ready <Badge severity="warning" size="sm">status says {row.instances.ready}</Badge>
|
|
26
|
+
</span>
|
|
27
|
+
</Tooltip>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
21
30
|
|
|
22
31
|
export interface CNPGSummaryAction {
|
|
23
32
|
label: string
|
|
@@ -25,10 +34,10 @@ export interface CNPGSummaryAction {
|
|
|
25
34
|
primary?: boolean
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
function InstancePill({ pod, namespace, onNavigate }: { pod: CNPGInstance; namespace: string; onNavigate?:
|
|
37
|
+
function InstancePill({ pod, namespace, onNavigate }: { pod: CNPGInstance; namespace: string; onNavigate?: NavigateToRef }) {
|
|
29
38
|
const tone = pod.ready === true ? 'healthy' : pod.ready === false ? 'unhealthy' : 'unknown'
|
|
30
39
|
const role = pod.role === 'primary' ? 'Primary' : pod.role === 'replica' ? 'Replica' : 'Role unknown'
|
|
31
|
-
const readiness = pod.ready === true ? '
|
|
40
|
+
const readiness = pod.ready === true ? 'Pod ready' : pod.ready === false ? 'Pod not ready' : 'Pod readiness unknown'
|
|
32
41
|
return (
|
|
33
42
|
<Tooltip content={`${pod.name} · ${role} · ${readiness}${pod.node ? ` · ${pod.node}` : ''}`} position="top">
|
|
34
43
|
<button
|
|
@@ -39,7 +48,7 @@ function InstancePill({ pod, namespace, onNavigate }: { pod: CNPGInstance; names
|
|
|
39
48
|
onNavigate ? 'hover:border-accent' : 'cursor-default',
|
|
40
49
|
)}
|
|
41
50
|
>
|
|
42
|
-
<
|
|
51
|
+
<StatusDot tone={tone} />
|
|
43
52
|
<span className="font-mono">{pod.name}</span>
|
|
44
53
|
<span className="text-theme-text-tertiary">{pod.role === 'primary' ? 'P' : pod.role === 'replica' ? 'R' : '?'}</span>
|
|
45
54
|
</button>
|
|
@@ -47,122 +56,311 @@ function InstancePill({ pod, namespace, onNavigate }: { pod: CNPGInstance; names
|
|
|
47
56
|
)
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
/**
|
|
60
|
+
* One dimension's state as a mark beside the tab that explains it: a dot when
|
|
61
|
+
* something needs a look, a hollow ring when it could not be assessed (never a
|
|
62
|
+
* calm colour), nothing when it is fine. The verdict is in the tooltip; the
|
|
63
|
+
* Overview's At a glance has it in words.
|
|
64
|
+
*/
|
|
65
|
+
export function CNPGDimensionMark({ dimension }: { dimension: CNPGDimension }) {
|
|
66
|
+
if (!dimensionMarked(dimension)) return null
|
|
67
|
+
const label = `${dimension.label}: ${dimension.text}`
|
|
68
|
+
return (
|
|
69
|
+
<Tooltip content={<><div>{label}</div><div className="text-theme-text-tertiary">{dimension.source}</div></>} position="bottom">
|
|
70
|
+
<span role="img" aria-label={label} className="inline-flex shrink-0">
|
|
71
|
+
<DimensionGlyph tone={dimension.tone} />
|
|
72
|
+
</span>
|
|
73
|
+
</Tooltip>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function dimensionMarked(dimension: CNPGDimension): boolean {
|
|
78
|
+
return dimension.tone !== 'healthy' && dimension.tone !== 'neutral'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function DimensionGlyph({ tone }: { tone: CNPGDimension['tone'] }) {
|
|
82
|
+
return tone === 'unknown' ? (
|
|
83
|
+
<span aria-hidden className="inline-block h-[7px] w-[7px] shrink-0 rounded-full border border-theme-text-tertiary" />
|
|
84
|
+
) : (
|
|
85
|
+
<StatusDot tone={tone} />
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The verdict behind a tab's mark, as the tab's first line, so a mark always
|
|
91
|
+
* points at words on the tab it marks. Nothing when the dimension is fine.
|
|
92
|
+
*/
|
|
93
|
+
export function CNPGDimensionVerdict({ dimension, className, alwaysShow = false }: { dimension: CNPGDimension; className?: string; alwaysShow?: boolean }) {
|
|
94
|
+
if (!alwaysShow && !dimensionMarked(dimension)) return null
|
|
95
|
+
return (
|
|
96
|
+
<div className={clsx('flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm', className)}>
|
|
97
|
+
<span className="inline-flex self-center">
|
|
98
|
+
<DimensionGlyph tone={dimension.tone} />
|
|
99
|
+
</span>
|
|
100
|
+
<span className="text-theme-text-secondary">{dimension.label}</span>
|
|
101
|
+
<span className={toneTextClass(dimension.tone)}>{dimension.text}</span>
|
|
102
|
+
<span className="text-xs text-theme-text-tertiary">{dimension.source}</span>
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Whether the cluster serves writes, on its title line: the headline the tabs do not carry. */
|
|
108
|
+
export function CNPGServingStatus({ dimension, onSelect }: { dimension: CNPGDimension; onSelect?: () => void }) {
|
|
109
|
+
const body = (
|
|
110
|
+
<>
|
|
111
|
+
<StatusDot tone={dimension.tone} />
|
|
112
|
+
<span className="text-theme-text-secondary">{dimension.label}</span>
|
|
113
|
+
<span className={toneTextClass(dimension.tone)}>{dimension.text}</span>
|
|
114
|
+
</>
|
|
115
|
+
)
|
|
116
|
+
const className = 'inline-flex items-center gap-1.5 whitespace-nowrap text-sm'
|
|
117
|
+
return (
|
|
118
|
+
<Tooltip content={dimension.source} position="bottom">
|
|
119
|
+
{onSelect ? (
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={onSelect}
|
|
123
|
+
aria-label={`${dimension.label}: ${dimension.text}. Open its details`}
|
|
124
|
+
className={clsx(className, 'rounded hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent')}
|
|
125
|
+
>
|
|
126
|
+
{body}
|
|
127
|
+
</button>
|
|
128
|
+
) : (
|
|
129
|
+
<span className={className}>{body}</span>
|
|
130
|
+
)}
|
|
131
|
+
</Tooltip>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** "+N more" that opens the rest of the problems in place, when the host links nowhere else. */
|
|
136
|
+
function MoreProblems({ count, open, onToggle, panelId }: { count: number; open: boolean; onToggle: () => void; panelId: string }) {
|
|
137
|
+
return (
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
aria-expanded={open}
|
|
141
|
+
aria-controls={panelId}
|
|
142
|
+
onClick={onToggle}
|
|
143
|
+
className="inline-flex items-center gap-1 text-accent-text hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent rounded"
|
|
144
|
+
>
|
|
145
|
+
<CollapseChevron open={open} className="h-3 w-3" />
|
|
146
|
+
{open ? 'Hide' : `+${count} more`}
|
|
147
|
+
</button>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
50
151
|
export function CNPGClusterSummary({
|
|
51
152
|
row,
|
|
52
153
|
onNavigate,
|
|
53
154
|
actions,
|
|
54
155
|
problemsLink,
|
|
55
156
|
extra,
|
|
157
|
+
lead,
|
|
158
|
+
dimensions,
|
|
159
|
+
onSelectDimension,
|
|
160
|
+
initialProblemsExpanded = false,
|
|
161
|
+
stateFacts,
|
|
162
|
+
operationalFacts,
|
|
163
|
+
dimensionLinkLabel,
|
|
164
|
+
onOpenOperator,
|
|
165
|
+
framed = false,
|
|
56
166
|
}: {
|
|
57
167
|
row: CNPGFleetRow
|
|
58
|
-
onNavigate?:
|
|
168
|
+
onNavigate?: NavigateToRef
|
|
59
169
|
actions?: CNPGSummaryAction[]
|
|
60
170
|
/** Link to the complete list of this cluster's findings, shown when more than one exists. */
|
|
61
171
|
problemsLink?: (count: number) => ReactNode
|
|
62
172
|
extra?: ReactNode
|
|
173
|
+
/** Rendered first, above the problem callout: standing states such as maintenance mode. */
|
|
174
|
+
lead?: ReactNode
|
|
175
|
+
/** Serving · Replication · Storage · Backups, each from its own source (see cnpgDimensions); the At a glance rows. */
|
|
176
|
+
dimensions?: CNPGDimension[]
|
|
177
|
+
/** Open with every problem listed below the callout (e.g. arriving from the fleet's "+N more"). */
|
|
178
|
+
initialProblemsExpanded?: boolean
|
|
179
|
+
/** Makes each dimension row open where that dimension is explained (its tab). */
|
|
180
|
+
onSelectDimension?: (id: CNPGDimension['id']) => void
|
|
181
|
+
/** The name of the place onSelectDimension opens, for the row's link; the dimension's own label when unset. */
|
|
182
|
+
dimensionLinkLabel?: (id: CNPGDimension['id']) => string
|
|
183
|
+
/** Opens the operator's own diagnosis, offered beside a controller phase that is not healthy. */
|
|
184
|
+
onOpenOperator?: () => void
|
|
185
|
+
/** Extra FactRows appended to About. */
|
|
186
|
+
stateFacts?: ReactNode
|
|
187
|
+
/** Extra FactRows appended to At a glance, e.g. live instance operations. */
|
|
188
|
+
operationalFacts?: ReactNode
|
|
189
|
+
/** Cards for the full page; drawers keep flat sections. */
|
|
190
|
+
framed?: boolean
|
|
63
191
|
}) {
|
|
64
192
|
const top = row.problems[0]
|
|
65
193
|
const rest = row.problems.length - 1
|
|
66
|
-
const p = row.protection
|
|
67
194
|
const ns = row.namespace
|
|
68
195
|
const radarFindings = row.problems.some((x) => x.severity !== 'posture')
|
|
196
|
+
const phase = typeof row.cluster?.status?.phase === 'string' ? row.cluster.status.phase : ''
|
|
197
|
+
const blocked = classifyCNPGClusterPhase(phase) === 'terminal' ? cnpgBlockedPhaseExplanation(phase, row.cluster?.status?.phaseReason) : null
|
|
198
|
+
const [showRest, setShowRest] = useState(initialProblemsExpanded)
|
|
199
|
+
const restDisclosure = useDisclosure(showRest)
|
|
200
|
+
const Frame = framed ? 'section' : Fragment
|
|
201
|
+
const frameProps = framed ? { className: 'mb-4 last:mb-0 rounded-xl border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm' } : {}
|
|
69
202
|
|
|
70
203
|
return (
|
|
71
204
|
<div className="px-4 py-4">
|
|
205
|
+
{lead}
|
|
72
206
|
{top && (
|
|
73
|
-
<ProblemCallout
|
|
207
|
+
<div className="[&_.mt-5]:mt-2"><ProblemCallout
|
|
74
208
|
problem={top}
|
|
75
209
|
onNavigate={onNavigate}
|
|
76
|
-
more={
|
|
77
|
-
|
|
210
|
+
more={
|
|
211
|
+
rest > 0
|
|
212
|
+
? problemsLink?.(row.problems.length) ?? (
|
|
213
|
+
<MoreProblems count={rest} open={showRest} onToggle={() => setShowRest((v) => !v)} panelId={restDisclosure.panelId} />
|
|
214
|
+
)
|
|
215
|
+
: null
|
|
216
|
+
}
|
|
217
|
+
/></div>
|
|
218
|
+
)}
|
|
219
|
+
{top && rest > 0 && !problemsLink && (
|
|
220
|
+
<Collapse open={showRest} id={restDisclosure.panelId}>
|
|
221
|
+
<div className="mb-4 rounded-lg border border-theme-border bg-theme-base p-3">
|
|
222
|
+
<ProblemList rootKind="Cluster" problems={row.problems.slice(1).map(cnpgProblemForPresentation)} onNavigate={onNavigate} />
|
|
223
|
+
</div>
|
|
224
|
+
</Collapse>
|
|
78
225
|
)}
|
|
79
226
|
|
|
80
227
|
{actions && actions.length > 0 && (
|
|
81
228
|
<div className="mb-4 flex flex-wrap gap-2">
|
|
82
229
|
{actions.map((a) => (
|
|
83
|
-
<button key={a.label} type="button" onClick={a.onClick} className={a.primary ?
|
|
230
|
+
<button key={a.label} type="button" onClick={a.onClick} className={clsx(a.primary ? 'btn-brand' : 'btn-secondary', 'inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap px-3 py-1.5 text-sm font-medium')}>
|
|
84
231
|
{a.label}
|
|
85
232
|
</button>
|
|
86
233
|
))}
|
|
87
234
|
</div>
|
|
88
235
|
)}
|
|
89
236
|
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
{
|
|
96
|
-
</
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{row.
|
|
107
|
-
|
|
237
|
+
<Frame {...frameProps}>
|
|
238
|
+
<SectionHeading>At a glance</SectionHeading>
|
|
239
|
+
<FactGrid>
|
|
240
|
+
{dimensions?.map((d) => (
|
|
241
|
+
<FactRow key={d.id} label={d.label}>
|
|
242
|
+
<DimensionValue dimension={d} linkLabel={dimensionLinkLabel?.(d.id) ?? d.label} onOpen={onSelectDimension ? () => onSelectDimension(d.id) : undefined} />
|
|
243
|
+
</FactRow>
|
|
244
|
+
))}
|
|
245
|
+
<FactRow label="Instances">
|
|
246
|
+
<div>
|
|
247
|
+
<span>
|
|
248
|
+
<ReadyCount row={row} />
|
|
249
|
+
{row.cluster?.status?.currentPrimary && !row.primaryConflict && (
|
|
250
|
+
<span className="text-theme-text-secondary"> · primary <span className="font-mono">{row.cluster.status.currentPrimary}</span></span>
|
|
251
|
+
)}
|
|
252
|
+
</span>
|
|
253
|
+
{row.primaryConflict && <PrimaryConflictNote conflict={row.primaryConflict} />}
|
|
254
|
+
{row.pods.length > 0 && (
|
|
255
|
+
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
|
256
|
+
{row.pods.map((pod) => (
|
|
257
|
+
<InstancePill key={pod.name} pod={pod} namespace={ns} onNavigate={onNavigate} />
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
</FactRow>
|
|
263
|
+
<FactRow label="Controller phase">
|
|
264
|
+
<span className="inline-flex flex-wrap items-center gap-2">
|
|
265
|
+
<Badge severity={'neutral'} size="sm">
|
|
266
|
+
{phase || 'Not reported'}
|
|
267
|
+
</Badge>
|
|
268
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
269
|
+
{radarFindings ? 'reported by CNPG · Radar findings above are separate' : 'reported by CNPG'}
|
|
270
|
+
</span>
|
|
271
|
+
{onOpenOperator && (row.controllerStatus.level === 'unhealthy' || row.controllerStatus.level === 'degraded') && (
|
|
272
|
+
<button type="button" onClick={onOpenOperator} className="text-xs text-accent-text hover:underline">
|
|
273
|
+
Operator and plugins →
|
|
274
|
+
</button>
|
|
108
275
|
)}
|
|
109
276
|
</span>
|
|
110
|
-
{
|
|
111
|
-
<div className="mt-
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
277
|
+
{blocked && (
|
|
278
|
+
<div className="mt-0.5 text-[11.5px] text-theme-text-secondary">
|
|
279
|
+
{blocked.body}
|
|
280
|
+
{cnpgPluginPhase(row.cluster) &&
|
|
281
|
+
` Plugins this cluster uses: ${cnpgClusterPlugins(row.cluster).join(', ') || 'none listed'}. The Operator view shows whether each is running and when it last restarted.`}
|
|
115
282
|
</div>
|
|
116
283
|
)}
|
|
117
|
-
</div>
|
|
118
|
-
</FactRow>
|
|
119
|
-
<FactRow label="Replication">
|
|
120
|
-
<FactValue fact={row.replication} />
|
|
121
|
-
<FactSource fact={row.replication} />
|
|
122
|
-
</FactRow>
|
|
123
|
-
{row.replicaCluster && (
|
|
124
|
-
<FactRow label="Replica cluster">
|
|
125
|
-
Follows {row.replicaCluster.source ? <span className="font-mono">{row.replicaCluster.source}</span> : 'an external primary'}
|
|
126
284
|
</FactRow>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
>
|
|
137
|
-
|
|
138
|
-
</RefLink>
|
|
139
|
-
</span>
|
|
140
|
-
)}
|
|
141
|
-
</FactRow>
|
|
142
|
-
<FactRow label="Declarations">
|
|
143
|
-
<FactValue fact={row.declarations.summary} />
|
|
144
|
-
</FactRow>
|
|
145
|
-
<FactRow label="Poolers">
|
|
146
|
-
{row.poolers.length === 0 ? (
|
|
147
|
-
<span className={row.poolersKnown ? 'text-theme-text-secondary' : 'text-theme-text-tertiary'}>
|
|
148
|
-
{row.poolersKnown ? 'None' : 'No access to Poolers'}
|
|
149
|
-
</span>
|
|
150
|
-
) : (
|
|
151
|
-
<span className="flex flex-wrap gap-x-3">
|
|
152
|
-
{row.poolers.map((name) => (
|
|
153
|
-
<RefLink key={name} refTo={{ kind: 'Pooler', group: CNPG_GROUP, namespace: ns, name }} onNavigate={onNavigate} mono />
|
|
154
|
-
))}
|
|
155
|
-
</span>
|
|
285
|
+
{operationalFacts}
|
|
286
|
+
</FactGrid>
|
|
287
|
+
</Frame>
|
|
288
|
+
|
|
289
|
+
<Frame {...frameProps}>
|
|
290
|
+
<SectionHeading>About</SectionHeading>
|
|
291
|
+
<FactGrid>
|
|
292
|
+
{row.replicaCluster && (
|
|
293
|
+
<FactRow label="Replica cluster">
|
|
294
|
+
Follows {row.replicaCluster.source ? <span className="font-mono">{row.replicaCluster.source}</span> : 'an external primary'}
|
|
295
|
+
</FactRow>
|
|
156
296
|
)}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
297
|
+
<FactRow label="PostgreSQL">
|
|
298
|
+
{row.pgVersion ?? 'Unknown'}
|
|
299
|
+
{row.catalog && (
|
|
300
|
+
<span className="text-theme-text-secondary">
|
|
301
|
+
{' · '}
|
|
302
|
+
<RefLink
|
|
303
|
+
refTo={{ kind: row.catalog.kind, group: CNPG_GROUP, namespace: row.catalog.kind === 'ClusterImageCatalog' ? '' : ns, name: row.catalog.name }}
|
|
304
|
+
onNavigate={onNavigate}
|
|
305
|
+
>
|
|
306
|
+
{row.catalog.name}
|
|
307
|
+
</RefLink>
|
|
308
|
+
</span>
|
|
309
|
+
)}
|
|
161
310
|
</FactRow>
|
|
311
|
+
<FactRow label="Declarations">
|
|
312
|
+
<FactValue fact={row.declarations.summary} />
|
|
313
|
+
</FactRow>
|
|
314
|
+
<FactRow label="Poolers">
|
|
315
|
+
{row.poolers.length === 0 ? (
|
|
316
|
+
<span className={row.poolersKnown ? 'text-theme-text-secondary' : 'text-theme-text-tertiary'}>
|
|
317
|
+
{row.poolersKnown ? 'None' : 'No access to Poolers'}
|
|
318
|
+
</span>
|
|
319
|
+
) : (
|
|
320
|
+
<span className="flex flex-wrap gap-x-3">
|
|
321
|
+
{row.poolers.map((name) => (
|
|
322
|
+
<RefLink key={name} refTo={{ kind: 'Pooler', group: CNPG_GROUP, namespace: ns, name }} onNavigate={onNavigate} mono />
|
|
323
|
+
))}
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
326
|
+
</FactRow>
|
|
327
|
+
{row.managedBy && managedByLabel(row.managedBy) && (
|
|
328
|
+
<FactRow label="Declared in">
|
|
329
|
+
<ManagedByText refTo={row.managedBy} onNavigate={onNavigate} />
|
|
330
|
+
</FactRow>
|
|
331
|
+
)}
|
|
332
|
+
{stateFacts}
|
|
333
|
+
</FactGrid>
|
|
334
|
+
</Frame>
|
|
335
|
+
|
|
336
|
+
{extra}
|
|
337
|
+
</div>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function DimensionValue({ dimension: d, linkLabel, onOpen }: { dimension: CNPGDimension; linkLabel: string; onOpen?: () => void }) {
|
|
342
|
+
return (
|
|
343
|
+
<div>
|
|
344
|
+
<span className="inline-flex flex-wrap items-center gap-x-2">
|
|
345
|
+
<StatusDot tone={d.tone} />
|
|
346
|
+
<span className={toneTextClass(d.tone)}>{d.text}</span>
|
|
347
|
+
{onOpen && (
|
|
348
|
+
<button type="button" onClick={onOpen} className="text-xs text-accent-text hover:underline">
|
|
349
|
+
{linkLabel} →
|
|
350
|
+
</button>
|
|
162
351
|
)}
|
|
163
|
-
</
|
|
352
|
+
</span>
|
|
353
|
+
{d.source && <div className="text-[11.5px] text-theme-text-tertiary">{d.source}</div>}
|
|
354
|
+
</div>
|
|
355
|
+
)
|
|
356
|
+
}
|
|
164
357
|
|
|
165
|
-
|
|
358
|
+
/** A cluster's recovery evidence as facts: schedule, destination, newest backup, WAL archiving, recovery window and restore validation. */
|
|
359
|
+
export function CNPGClusterBackupFacts({ row, onNavigate }: { row: CNPGFleetRow; onNavigate?: NavigateToRef }) {
|
|
360
|
+
const p = row.protection
|
|
361
|
+
const ns = row.namespace
|
|
362
|
+
return (
|
|
363
|
+
<>
|
|
166
364
|
<FactGrid>
|
|
167
365
|
<FactRow label="Schedule">
|
|
168
366
|
<FactValue fact={p.schedule} />
|
|
@@ -188,7 +386,7 @@ export function CNPGClusterSummary({
|
|
|
188
386
|
<FactSource fact={p.lastSuccessfulBackup} />
|
|
189
387
|
</FactRow>
|
|
190
388
|
<FactRow label="WAL archiving">
|
|
191
|
-
<
|
|
389
|
+
<CNPGWALArchivingFact fact={p.walArchiving} />
|
|
192
390
|
</FactRow>
|
|
193
391
|
<FactRow label="Recovery window">
|
|
194
392
|
{p.recoveryWindow.from ? (
|
|
@@ -216,7 +414,23 @@ export function CNPGClusterSummary({
|
|
|
216
414
|
<FactSource fact={p.restoreValidation} />
|
|
217
415
|
</FactRow>
|
|
218
416
|
</FactGrid>
|
|
219
|
-
|
|
220
|
-
|
|
417
|
+
|
|
418
|
+
</>
|
|
221
419
|
)
|
|
222
420
|
}
|
|
421
|
+
|
|
422
|
+
export function CNPGWALArchivingFact({ fact, compact = false }: { fact: CNPGFleetRow['protection']['walArchiving']; compact?: boolean }) {
|
|
423
|
+
const c = fact.operatorCondition
|
|
424
|
+
return <div>
|
|
425
|
+
<FactValue fact={fact} className="break-words" />
|
|
426
|
+
<FactSource fact={fact} />
|
|
427
|
+
{fact.detail && <Note>{compact && fact.state === 'no_destination' ? 'No point-in-time recovery' : fact.detail}</Note>}
|
|
428
|
+
{c && <div className="mt-1" onClick={(e) => e.stopPropagation()}>
|
|
429
|
+
<FoldSection title="Operator report" summary="" attention={false}>
|
|
430
|
+
<div className="break-words text-xs text-theme-text-secondary">{c.type}: {c.status}</div>
|
|
431
|
+
<div className="break-words text-xs text-theme-text-secondary">{c.message || 'No message reported'}</div>
|
|
432
|
+
<div className="text-xs text-theme-text-tertiary">Last transition: {c.lastTransitionTime ? <Tooltip content={c.lastTransitionTime}><time dateTime={c.lastTransitionTime}>{formatAge(c.lastTransitionTime)} ago</time></Tooltip> : 'Not reported'}</div>
|
|
433
|
+
</FoldSection>
|
|
434
|
+
</div>}
|
|
435
|
+
</div>
|
|
436
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
5
|
+
import { expect, it, vi } from 'vitest'
|
|
6
|
+
import { CNPGConnectSection } from './CNPGConnectSection'
|
|
7
|
+
|
|
8
|
+
const cluster = { metadata: { name: 'orders', namespace: 'radar-cnpg-prod' }, spec: { bootstrap: { initdb: { database: 'appdb', owner: 'app' } } } }
|
|
9
|
+
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
|
10
|
+
|
|
11
|
+
it('pairs a namespace-qualified port-forward with matching loopback templates and copies each', async () => {
|
|
12
|
+
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
13
|
+
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true })
|
|
14
|
+
const host = document.createElement('div'); const root = createRoot(host)
|
|
15
|
+
act(() => root.render(<CNPGConnectSection cluster={cluster} />))
|
|
16
|
+
expect(host.textContent).toContain('Inside Kubernetes')
|
|
17
|
+
expect(host.textContent).toContain('From this computer')
|
|
18
|
+
for (const [label, expected] of [
|
|
19
|
+
['port-forward command', 'kubectl -n radar-cnpg-prod port-forward service/orders-rw 5432:5432'],
|
|
20
|
+
['local psql command', 'psql -h 127.0.0.1 -p 5432 -U app -d appdb'],
|
|
21
|
+
['local connection string', 'postgresql://app:<password>@127.0.0.1:5432/appdb'],
|
|
22
|
+
]) {
|
|
23
|
+
await act(async () => host.querySelector<HTMLButtonElement>(`[aria-label="Copy ${label}"]`)!.click())
|
|
24
|
+
expect(writeText).toHaveBeenLastCalledWith(expected)
|
|
25
|
+
}
|
|
26
|
+
act(() => root.unmount())
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('uses one three-column grid with every explanation under its own host', () => {
|
|
30
|
+
const html = renderToStaticMarkup(<CNPGConnectSection cluster={cluster} poolers={[{ metadata: { name: 'orders-pooler', namespace: 'radar-cnpg-prod' }, spec: { cluster: { name: 'orders' } } }]} onOpenReachability={() => {}} />)
|
|
31
|
+
const host = document.createElement('div'); host.innerHTML = html
|
|
32
|
+
const list = host.querySelector('ul')!
|
|
33
|
+
expect(list.className).toContain('grid-cols-[5rem_minmax(0,1fr)_auto]')
|
|
34
|
+
expect(list.querySelectorAll('li')).toHaveLength(4)
|
|
35
|
+
for (const row of list.querySelectorAll('li')) {
|
|
36
|
+
expect(row.className).toBe('contents')
|
|
37
|
+
expect(row.children).toHaveLength(3)
|
|
38
|
+
expect(row.children[1].className).toContain('[overflow-wrap:anywhere]')
|
|
39
|
+
expect(row.children[1].querySelector('div')).not.toBeNull()
|
|
40
|
+
expect(row.children[2].textContent).toContain('Reachability')
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('shows command context certainty and per-service availability beside each endpoint', () => {
|
|
45
|
+
const host = document.createElement('div')
|
|
46
|
+
host.innerHTML = renderToStaticMarkup(<CNPGConnectSection cluster={cluster} kubeconfigContext="kind-orders" kubeconfigSource="orders-config" ha={{ rwEndpoints: { state: 'ok', pods: ['orders-1'] }, pods: { state: 'ok' }, instances: [{ pod: 'orders-1', role: 'primary', ready: true }] } as any} />)
|
|
47
|
+
expect(host.textContent).toContain('kubectl --context kind-orders')
|
|
48
|
+
expect(host.textContent).not.toContain('uses your current kubectl context')
|
|
49
|
+
expect(host.textContent).toContain('Context as named in your kubeconfig (orders-config); kubectl must read the same kubeconfig file.')
|
|
50
|
+
expect([...host.querySelectorAll('li')].map((li) => li.textContent)).toEqual([expect.stringContaining('Ready endpoints'), expect.stringContaining('Unavailable: no ready standby'), expect.stringContaining('Ready instance observed')])
|
|
51
|
+
host.innerHTML = renderToStaticMarkup(<CNPGConnectSection cluster={cluster} />)
|
|
52
|
+
expect(host.textContent).toContain('uses your current kubectl context')
|
|
53
|
+
expect(host.textContent).not.toContain('--context')
|
|
54
|
+
expect([...host.querySelectorAll('li')].every((li) => li.textContent?.includes('Not checked'))).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('keeps the unavailable reason beside each unchecked Service', () => {
|
|
58
|
+
const host = document.createElement('div')
|
|
59
|
+
host.innerHTML = renderToStaticMarkup(<CNPGConnectSection cluster={cluster} haUnavailableReason="Availability could not be read: timeout" />)
|
|
60
|
+
for (const li of host.querySelectorAll('li')) expect(li.textContent).toContain('Not checked · Availability could not be read: timeout')
|
|
61
|
+
host.innerHTML = renderToStaticMarkup(<CNPGConnectSection cluster={cluster} ha={{ rwEndpoints: { state: 'denied', grant: { verb: 'list', resource: 'endpointslices', group: 'discovery.k8s.io', namespace: 'radar-cnpg-prod' } }, pods: { state: 'denied', grant: { verb: 'list', resource: 'pods', namespace: 'radar-cnpg-prod' } }, instances: [] } as any} />)
|
|
62
|
+
expect(host.querySelector('li')!.textContent).toContain('needs list endpointslices (discovery.k8s.io) in namespace radar-cnpg-prod')
|
|
63
|
+
expect(host.querySelectorAll('li')[1].textContent).toContain('needs list pods in namespace radar-cnpg-prod')
|
|
64
|
+
})
|