@skyhook-io/radar-app 1.16.3 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/App.tsx +138 -93
- package/src/RadarApp.tsx +27 -17
- package/src/api/actions.errorCode.test.ts +40 -0
- package/src/api/actions.test.ts +48 -0
- package/src/api/actions.ts +79 -0
- package/src/api/apiResources.navigation.test.tsx +232 -0
- package/src/api/apiResources.ts +46 -3
- package/src/api/client.goneResource.test.ts +49 -0
- package/src/api/client.helmSource.test.ts +39 -0
- package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
- package/src/api/client.namespaceSwitch.test.ts +107 -0
- package/src/api/client.ts +381 -47
- package/src/api/cnpg-ha.live.test.ts +140 -0
- package/src/api/cnpg-ha.test.ts +78 -0
- package/src/api/cnpg-ha.ts +19 -0
- package/src/api/cnpg-history.ts +123 -0
- package/src/api/cnpg-inspect.ts +102 -0
- package/src/api/cnpg-protection.ts +112 -0
- package/src/api/cnpg-recovery.test.tsx +60 -0
- package/src/api/cnpg-recovery.ts +262 -0
- package/src/api/cnpg-sessions.ts +175 -0
- package/src/api/cnpg-storage.ts +138 -0
- package/src/api/cnpg.legacy-reads.test.ts +43 -0
- package/src/api/cnpg.pooler-runtime.test.ts +15 -0
- package/src/api/cnpg.ts +428 -10
- package/src/api/diagnose.test.ts +40 -0
- package/src/api/diagnose.ts +12 -1
- package/src/api/radarFeatures.render.test.tsx +65 -3
- package/src/api/radarFeatures.ts +6 -0
- package/src/api/reviewedActionMutation.test.tsx +164 -0
- package/src/api/reviewedActionMutation.ts +66 -0
- package/src/api/timelineSource.test.ts +9 -0
- package/src/api/timelineSource.ts +8 -0
- package/src/api/traffic.sources.test.ts +55 -0
- package/src/api/traffic.ts +173 -14
- package/src/components/applications/ApplicationsView.tsx +3 -0
- package/src/components/audit/AuditView.tsx +9 -0
- package/src/components/capacity/CapacityActivity.tsx +3 -31
- package/src/components/capacity/CapacityDemand.tsx +4 -36
- package/src/components/capacity/CapacityOverview.tsx +217 -56
- package/src/components/capacity/CapacityPoolDetail.tsx +24 -62
- package/src/components/capacity/CapacityView.test.tsx +354 -1
- package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
- package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
- package/src/components/capacity/routes.ts +42 -0
- package/src/components/capacity/shared.tsx +23 -150
- package/src/components/capacity/workspaceRoute.ts +18 -0
- package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
- package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
- package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
- package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
- package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
- package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
- package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
- package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
- package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
- package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
- package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
- package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
- package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
- package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
- package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
- package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
- package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
- package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
- package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
- package/src/components/cnpg/CNPGOperator.tsx +78 -30
- package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
- package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
- package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
- package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
- package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
- package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
- package/src/components/cnpg/CNPGOverview.tsx +238 -63
- package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
- package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
- package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
- package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
- package/src/components/cnpg/CNPGPerformance.tsx +477 -0
- package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
- package/src/components/cnpg/CNPGPooling.tsx +97 -40
- package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
- package/src/components/cnpg/CNPGProtection.tsx +230 -124
- package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
- package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
- package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
- package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
- package/src/components/cnpg/CNPGStorage.tsx +619 -0
- package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
- package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
- package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
- package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
- package/src/components/cnpg/CNPGTrends.test.ts +38 -0
- package/src/components/cnpg/CNPGTrends.tsx +702 -0
- package/src/components/cnpg/CNPGView.tsx +34 -21
- package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
- package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
- package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
- package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
- package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
- package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
- package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
- package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
- package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
- package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
- package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
- package/src/components/cnpg/actions/actionModel.test.ts +111 -0
- package/src/components/cnpg/actions/actionModel.ts +127 -0
- package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
- package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
- package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
- package/src/components/cnpg/archivingRepair.test.ts +104 -0
- package/src/components/cnpg/archivingRepair.ts +145 -0
- package/src/components/cnpg/baseBackup.test.ts +36 -0
- package/src/components/cnpg/baseBackup.ts +36 -0
- package/src/components/cnpg/blocking.test.ts +59 -0
- package/src/components/cnpg/blocking.ts +131 -0
- package/src/components/cnpg/coverageText.test.ts +27 -0
- package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
- package/src/components/cnpg/creation/targetModel.test.ts +137 -0
- package/src/components/cnpg/creation/targetModel.ts +149 -0
- package/src/components/cnpg/fleetStatus.test.ts +46 -0
- package/src/components/cnpg/fleetStatus.ts +43 -0
- package/src/components/cnpg/formFields.ts +2 -0
- package/src/components/cnpg/grantText.test.tsx +11 -0
- package/src/components/cnpg/host.test.tsx +24 -0
- package/src/components/cnpg/host.tsx +101 -0
- package/src/components/cnpg/inspectModel.test.ts +120 -0
- package/src/components/cnpg/inspectModel.ts +140 -0
- package/src/components/cnpg/logicalSlots.test.ts +30 -0
- package/src/components/cnpg/logicalSlots.ts +29 -0
- package/src/components/cnpg/lsn.test.ts +40 -0
- package/src/components/cnpg/lsn.ts +43 -0
- package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
- package/src/components/cnpg/operations/model.test.ts +510 -0
- package/src/components/cnpg/operations/model.ts +499 -0
- package/src/components/cnpg/operations/presentation.test.ts +80 -0
- package/src/components/cnpg/operations/presentation.ts +84 -0
- package/src/components/cnpg/operations/store.ts +98 -0
- package/src/components/cnpg/operatorStatus.test.ts +134 -0
- package/src/components/cnpg/operatorStatus.ts +194 -0
- package/src/components/cnpg/paths.test.ts +54 -0
- package/src/components/cnpg/paths.ts +47 -4
- package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
- package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
- package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
- package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
- package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
- package/src/components/cnpg/protection/scheduleModel.ts +16 -0
- package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
- package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
- package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
- package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
- package/src/components/cnpg/recovery/restoreModel.ts +685 -0
- package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
- package/src/components/cnpg/refresh.test.ts +38 -0
- package/src/components/cnpg/refresh.ts +17 -0
- package/src/components/cnpg/refreshNotice.test.tsx +29 -0
- package/src/components/cnpg/routes.test.ts +34 -3
- package/src/components/cnpg/routes.ts +60 -48
- package/src/components/cnpg/runtimeAssessment.ts +198 -0
- package/src/components/cnpg/runtimeModel.test.ts +175 -0
- package/src/components/cnpg/runtimeModel.ts +196 -0
- package/src/components/cnpg/runtimeParts.test.tsx +17 -0
- package/src/components/cnpg/runtimeParts.tsx +79 -0
- package/src/components/cnpg/shared.test.tsx +59 -0
- package/src/components/cnpg/shared.tsx +100 -182
- package/src/components/cnpg/storageModel.test.ts +107 -0
- package/src/components/cnpg/storageModel.ts +101 -0
- package/src/components/cnpg/trendSamples.test.ts +148 -0
- package/src/components/cnpg/trendSamples.ts +222 -0
- package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
- package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
- package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
- package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
- package/src/components/cnpg/useCNPGNavigate.ts +21 -0
- package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
- package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
- package/src/components/cnpg/workspaceRoute.ts +23 -0
- package/src/components/cost/ApplicationCostTab.test.ts +7 -0
- package/src/components/cost/ApplicationCostTab.tsx +7 -7
- package/src/components/cost/CostView.test.ts +2 -1
- package/src/components/cost/CostView.tsx +0 -2
- package/src/components/cost/WorkloadCostTab.tsx +2 -4
- package/src/components/cost/errors.ts +3 -1
- package/src/components/cost/source.test.ts +10 -0
- package/src/components/cost/source.ts +5 -1
- package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
- package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
- package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
- package/src/components/diagnose/ApplyDialog.tsx +26 -28
- package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
- package/src/components/diagnose/DiagnoseContext.tsx +136 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
- package/src/components/diagnose/InvestigationView.tsx +39 -2
- package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
- package/src/components/diagnose/agentCatalog.ts +2 -2
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
- package/src/components/dock/BottomDock.tsx +2 -0
- package/src/components/dock/TerminalTab.test.tsx +17 -0
- package/src/components/dock/TerminalTab.tsx +7 -3
- package/src/components/dock/TrafficFlowListTab.tsx +27 -6
- package/src/components/execution/BatchExecutionView.tsx +11 -11
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
- package/src/components/gitops/GitOpsView.tsx +16 -23
- package/src/components/helm/ChartBrowser.tsx +3 -3
- package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
- package/src/components/helm/HelmCompareRoute.tsx +57 -26
- package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
- package/src/components/helm/RevisionHistory.tsx +3 -2
- package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
- package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
- package/src/components/helm/ValuesViewer.test.tsx +301 -0
- package/src/components/helm/ValuesViewer.tsx +169 -78
- package/src/components/home/ActivitySummary.tsx +2 -1
- package/src/components/home/ClusterHealthCard.tsx +7 -0
- package/src/components/home/HomeView.tsx +20 -9
- package/src/components/home/ProblemsPanel.test.tsx +81 -0
- package/src/components/home/TopologyPreview.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/issues/IssuesPane.render.test.tsx +122 -0
- package/src/components/issues/IssuesPane.test.ts +65 -1
- package/src/components/issues/IssuesPane.tsx +188 -25
- package/src/components/issues/uncheckedSources.ts +21 -0
- package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
- package/src/components/resource/PVCUsageBar.test.tsx +24 -0
- package/src/components/resource/PVCUsageBar.tsx +15 -6
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resources/CompositeRenderer.tsx +4 -4
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
- package/src/components/resources/ResourcesView.tsx +26 -12
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
- package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
- package/src/components/settings/SettingsDialog.tsx +150 -34
- package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
- package/src/components/timeline/TimelineList.tsx +25 -24
- package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
- package/src/components/timeline/TimelineLoadError.tsx +96 -0
- package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
- package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
- package/src/components/timeline/TimelineView.tsx +25 -45
- package/src/components/timeline/ViewModeToggle.tsx +20 -0
- package/src/components/timeline/useStickyError.test.tsx +47 -0
- package/src/components/timeline/useStickyError.ts +15 -0
- package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
- package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
- package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
- package/src/components/traffic/TrafficGraph.tsx +109 -26
- package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
- package/src/components/traffic/TrafficView.tsx +582 -149
- package/src/components/traffic/trafficFilters.test.ts +398 -1
- package/src/components/traffic/trafficFilters.ts +408 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
- package/src/components/ui/Markdown.test.tsx +65 -0
- package/src/components/ui/Markdown.tsx +144 -127
- package/src/components/whats-new/releaseNotes.ts +63 -1
- package/src/components/workload/WorkloadView.test.ts +11 -1
- package/src/components/workload/WorkloadView.tsx +162 -529
- package/src/components/workspace/layout.test.tsx +29 -0
- package/src/components/workspace/layout.tsx +259 -0
- package/src/components/workspace/table.ts +7 -0
- package/src/context/TimelineCustomization.tsx +15 -0
- package/src/contexts/CapabilitiesContext.tsx +1 -1
- package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
- package/src/hooks/useGitOpsWriteGuard.ts +193 -0
- package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
- package/src/integrations/resourceHost.test.tsx +55 -0
- package/src/integrations/resourceHost.ts +99 -0
- package/src/integrations/resourceHosts.test.tsx +141 -0
- package/src/integrations/resourceHosts.tsx +119 -0
- package/src/integrations/workspaceRoute.ts +20 -0
- package/src/integrations/workspaceRoutes.test.ts +40 -0
- package/src/integrations/workspaceRoutes.ts +22 -0
- package/src/integrations/workspaceScreens.tsx +14 -0
- package/src/utils/drawer-trail.ts +35 -0
- package/src/utils/kindFilterStore.test.ts +147 -0
- package/src/utils/kindFilterStore.ts +91 -0
- package/src/utils/navigation.test.ts +13 -1
- package/src/utils/navigation.ts +39 -2
- package/src/utils/page-links.ts +21 -0
- package/src/utils/tabStorage.ts +40 -0
- package/src/utils/viewMemory.test.ts +78 -0
- package/src/utils/viewMemory.ts +54 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { createElement } from 'react'
|
|
3
|
+
import { buildCNPGFleet, CNPG_WORKSPACE_KEYS, type CNPGWorkspaceResponse } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { CNPGDeclarations, cnpgDeclarationBlocks, type DeclItem } from './CNPGDeclarations'
|
|
6
|
+
|
|
7
|
+
const item = (kind: DeclItem['kind'], pgName: string, database: string | undefined, state: DeclItem['state'] = 'applied'): DeclItem => ({
|
|
8
|
+
key: `${kind}/${pgName}`,
|
|
9
|
+
kind,
|
|
10
|
+
pgName,
|
|
11
|
+
database,
|
|
12
|
+
state,
|
|
13
|
+
resource: { kind, namespace: 'pg', name: pgName } as DeclItem['resource'],
|
|
14
|
+
isField: false,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const items = [
|
|
18
|
+
item('Database', 'demo_app', 'demo_app'),
|
|
19
|
+
item('Publication', 'demo_app_pub', 'demo_app'),
|
|
20
|
+
item('Subscription', 'demo_app_sub', 'demo_app', 'failed'),
|
|
21
|
+
item('Database', 'demo_reporting', 'demo_reporting'),
|
|
22
|
+
item('Publication', 'missing_pub', 'no_such_database', 'failed'),
|
|
23
|
+
item('DatabaseRole', 'reader', undefined),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
describe('cnpgDeclarationBlocks', () => {
|
|
27
|
+
it('nests what lives in each database under it, undeclared databases after the declared ones, roles last', () => {
|
|
28
|
+
const blocks = cnpgDeclarationBlocks(items, null)
|
|
29
|
+
expect(blocks.map((b) => [b.database, b.head?.pgName, b.children.map((c) => c.pgName)])).toEqual([
|
|
30
|
+
['demo_app', 'demo_app', ['demo_app_pub', 'demo_app_sub']],
|
|
31
|
+
['demo_reporting', 'demo_reporting', []],
|
|
32
|
+
['no_such_database', undefined, ['missing_pub']],
|
|
33
|
+
[undefined, undefined, ['reader']],
|
|
34
|
+
])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('keeps a database as context for a matching item in it, and drops what does not match', () => {
|
|
38
|
+
const blocks = cnpgDeclarationBlocks(items, 'failed')
|
|
39
|
+
expect(blocks.map((b) => [b.database, b.head?.pgName, b.children.map((c) => c.pgName)])).toEqual([
|
|
40
|
+
['demo_app', 'demo_app', ['demo_app_sub']],
|
|
41
|
+
['no_such_database', undefined, ['missing_pub']],
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
vi.mock('../../context/ConnectionContext', () => ({ useConnection: () => ({ connection: { context: 'test' } }) }))
|
|
47
|
+
|
|
48
|
+
const G = 'postgresql.cnpg.io/v1'
|
|
49
|
+
const declaration = (cluster: string, name: string, applied?: boolean, stale = false) => ({ apiVersion: G, kind: 'Database', metadata: { name, namespace: 'pg', generation: 2 }, spec: { name, cluster: { name: cluster } }, status: { applied, observedGeneration: stale ? 1 : 2 } })
|
|
50
|
+
|
|
51
|
+
function renderDeclarations(query: string, pgName?: string, partial: false | 'pg' | 'other' = false) {
|
|
52
|
+
const data: CNPGWorkspaceResponse = {
|
|
53
|
+
installed: true, context: 'test', namespaces: null,
|
|
54
|
+
coverage: Object.fromEntries(CNPG_WORKSPACE_KEYS.map((k) => [k, { state: 'full' }])),
|
|
55
|
+
objects: {
|
|
56
|
+
clusters: ['a', 'b'].map((name) => ({ apiVersion: G, kind: 'Cluster', metadata: { name, namespace: 'pg' }, spec: {} })),
|
|
57
|
+
databases: [declaration('a', 'failed-a', false), declaration('a', 'stale-a', true, true), declaration('b', 'failed-b1', false), declaration('b', 'failed-b2', false), declaration('b', 'pending-b')],
|
|
58
|
+
}, issues: [], audit: [], backupsOmitted: 0,
|
|
59
|
+
}
|
|
60
|
+
if (partial) data.coverage.publications = { state: 'partial', allowedNamespaces: [partial] }
|
|
61
|
+
if (pgName) data.objects.databases![0].spec.name = pgName
|
|
62
|
+
return renderToStaticMarkup(createElement(CNPGDeclarations, {
|
|
63
|
+
data, fleet: buildCNPGFleet(data), namespaces: [], searchParams: new URLSearchParams(query), onSetParams: () => {}, onInspect: () => {}, inspected: null, onClearNamespaces: () => {},
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
it('counts the selected cluster’s actual items before applying the state filter', () => {
|
|
68
|
+
const html = renderDeclarations('cluster=pg/a&show=failed')
|
|
69
|
+
expect(html).toMatch(/Not applied[^<]*<[^>]*>1</)
|
|
70
|
+
expect(html).toMatch(/Pending[^<]*<[^>]*>1</)
|
|
71
|
+
expect(html).toContain('failed-a')
|
|
72
|
+
expect(html).not.toContain('failed-b1')
|
|
73
|
+
expect(html).not.toContain('stale-a')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('shows stale success as pending in the declaration list', () => {
|
|
77
|
+
const html = renderDeclarations('cluster=pg/a&show=pending')
|
|
78
|
+
expect(html).toContain('stale-a')
|
|
79
|
+
expect(html).toContain('awaiting the operator for the current spec')
|
|
80
|
+
expect(html).not.toContain('failed-a')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('offers inspection with the Kubernetes object identity beside the database name', () => {
|
|
84
|
+
const html = renderDeclarations('cluster=pg/a', 'appdb')
|
|
85
|
+
expect(html).toContain('appdb')
|
|
86
|
+
expect(html).toContain('Inspect →')
|
|
87
|
+
expect(html).toContain('Database failed-a')
|
|
88
|
+
expect(html).toContain('role="button"')
|
|
89
|
+
expect(html).toContain('tabindex="0"')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('qualifies declaration filter counts over partial coverage including zero matches', () => {
|
|
93
|
+
expect(renderDeclarations('cluster=pg/a', undefined, 'other')).toMatch(/Not applied[^<]*<[^>]*>≥1</)
|
|
94
|
+
expect(renderDeclarations('cluster=pg/a', undefined, 'other')).toMatch(/Pending[^<]*<[^>]*>≥1</)
|
|
95
|
+
const empty = renderDeclarations('cluster=pg/missing', undefined, 'other')
|
|
96
|
+
expect(empty).toMatch(/Not applied[^<]*<[^>]*>Unknown</)
|
|
97
|
+
expect(empty).toMatch(/Pending[^<]*<[^>]*>Unknown</)
|
|
98
|
+
expect(renderDeclarations('cluster=pg/missing')).toMatch(/Not applied[^<]*<[^>]*>0</)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('keeps filtered counts exact in a fully read namespace even when another namespace is unread', () => {
|
|
102
|
+
const html = renderDeclarations('cluster=pg/a', undefined, 'pg')
|
|
103
|
+
expect(html).toMatch(/Not applied[^<]*<[^>]*>1</)
|
|
104
|
+
expect(html).toMatch(/Pending[^<]*<[^>]*>1</)
|
|
105
|
+
expect(renderDeclarations('cluster=pg/missing', undefined, 'pg')).toMatch(/Not applied[^<]*<[^>]*>0</)
|
|
106
|
+
expect(renderDeclarations('', undefined, 'pg')).toMatch(/Not applied[^<]*<[^>]*>≥3</)
|
|
107
|
+
})
|
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from 'react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { AlertTriangle } from 'lucide-react'
|
|
4
|
-
import { Badge, cnpgGitOpsSource, isApiGroup, toneTextClass, type CNPGFleetRow } from '@skyhook-io/k8s-ui'
|
|
5
|
-
import type { SelectedResource } from '../../types'
|
|
6
4
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
5
|
+
Badge,
|
|
6
|
+
CNPGLogicalPathView,
|
|
7
|
+
cnpgDatabaseRoleFacts,
|
|
8
|
+
coverageReadable,
|
|
9
|
+
cnpgRoleState,
|
|
10
|
+
cnpgDatabaseRoleMeta,
|
|
11
|
+
cnpgManagedBy,
|
|
12
|
+
managedByLabel,
|
|
13
|
+
type CNPGWorkspaceResponse,
|
|
14
|
+
cnpgLogicalPaths,
|
|
15
|
+
cnpgLogicalSlotFact,
|
|
16
|
+
isApiGroup,
|
|
17
|
+
refToSelectedResource,
|
|
18
|
+
relationUnavailable,
|
|
19
|
+
toneTextClass,
|
|
20
|
+
type CNPGFleetRow,
|
|
21
|
+
type CNPGLogicalPath,
|
|
22
|
+
} from '@skyhook-io/k8s-ui'
|
|
23
|
+
import { useCNPGPublisherSlots } from './logicalSlots'
|
|
24
|
+
import type { SelectedResource } from '../../types'
|
|
25
|
+
import { CNPGWorkspaceHeader, CoverageNotice, clusterResource, cnpgResource, coverageEmpty, worstCoverage, type CNPGScreenProps } from './shared'
|
|
26
|
+
import { FilterChips, namespaceChip, RefreshFailedNotice, ScreenBody, Segments, Sub } from '../workspace/layout'
|
|
27
|
+
import { sameSelectedResource } from '../../utils/drawer-trail'
|
|
21
28
|
|
|
22
29
|
type State = 'applied' | 'failed' | 'pending'
|
|
23
30
|
|
|
24
|
-
interface DeclItem {
|
|
31
|
+
export interface DeclItem {
|
|
25
32
|
key: string
|
|
26
|
-
kind: 'Database' | 'Publication' | 'Subscription' | 'Managed role'
|
|
33
|
+
kind: 'Database' | 'Publication' | 'Subscription' | 'Managed role' | 'DatabaseRole'
|
|
27
34
|
pgName: string
|
|
28
|
-
|
|
35
|
+
/** The PostgreSQL database the item lives in (a Database's own name); unset for roles, which are cluster-wide. */
|
|
36
|
+
database?: string
|
|
29
37
|
state: State
|
|
30
38
|
meta?: string
|
|
31
39
|
error?: string
|
|
@@ -41,17 +49,48 @@ interface DeclGroup {
|
|
|
41
49
|
items: DeclItem[]
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
/** A database and the Publications and Subscriptions in it; without `database`, the cluster's roles. */
|
|
53
|
+
export interface DeclBlock {
|
|
54
|
+
database?: string
|
|
55
|
+
/** The Database declaration; absent when the database is not declared (the bootstrap database, or one that does not exist). */
|
|
56
|
+
head?: DeclItem
|
|
57
|
+
children: DeclItem[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Groups a cluster's declarations for display: each database with what lives
|
|
62
|
+
* in it, declared databases first, then roles. A filter keeps a database's
|
|
63
|
+
* row as context for a matching Publication or Subscription in it.
|
|
64
|
+
*/
|
|
65
|
+
export function cnpgDeclarationBlocks(items: DeclItem[], show: State | null): DeclBlock[] {
|
|
66
|
+
const match = (i: DeclItem) => !show || i.state === show
|
|
67
|
+
const byDatabase = new Map<string, DeclBlock>()
|
|
68
|
+
const roles: DeclItem[] = []
|
|
69
|
+
for (const i of items) {
|
|
70
|
+
if (i.database === undefined) {
|
|
71
|
+
if (match(i)) roles.push(i)
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
let b = byDatabase.get(i.database)
|
|
75
|
+
if (!b) {
|
|
76
|
+
b = { database: i.database, children: [] }
|
|
77
|
+
byDatabase.set(i.database, b)
|
|
78
|
+
}
|
|
79
|
+
if (i.kind === 'Database' && !b.head) b.head = i
|
|
80
|
+
else b.children.push(i)
|
|
81
|
+
}
|
|
82
|
+
const out: DeclBlock[] = []
|
|
83
|
+
const blocks = [...byDatabase.values()].sort((a, b) => (a.head ? 0 : 1) - (b.head ? 0 : 1))
|
|
84
|
+
for (const b of blocks) {
|
|
85
|
+
const children = b.children.filter(match)
|
|
86
|
+
if ((b.head && match(b.head)) || children.length > 0) out.push({ ...b, children })
|
|
87
|
+
}
|
|
88
|
+
if (roles.length > 0) out.push({ children: roles })
|
|
89
|
+
return out
|
|
49
90
|
}
|
|
50
91
|
|
|
51
|
-
function gitopsSource(obj: any): string | undefined {
|
|
52
|
-
|
|
53
|
-
if (!src) return undefined
|
|
54
|
-
return `${src.tool === 'argocd' ? 'Argo CD' : 'Flux'} ${src.name}`
|
|
92
|
+
function gitopsSource(managedBy: CNPGWorkspaceResponse['managedBy'], obj: any): string | undefined {
|
|
93
|
+
return managedByLabel(cnpgManagedBy({ managedBy }, obj))
|
|
55
94
|
}
|
|
56
95
|
|
|
57
96
|
const STATE_BADGE: Record<State, { severity: 'success' | 'warning' | 'neutral'; text: string }> = {
|
|
@@ -73,7 +112,7 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
73
112
|
const clusterFilter = searchParams.get('cluster')
|
|
74
113
|
const show = (searchParams.get('show') as 'failed' | 'pending' | null) ?? null
|
|
75
114
|
|
|
76
|
-
const
|
|
115
|
+
const scopedGroups = useMemo(() => {
|
|
77
116
|
const byCluster = new Map<string, DeclGroup>()
|
|
78
117
|
const group = (ns: string, cluster: string) => {
|
|
79
118
|
const k = `${ns}/${cluster}`
|
|
@@ -93,16 +132,16 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
93
132
|
const ns = d.metadata?.namespace ?? ''
|
|
94
133
|
const cluster = d.spec?.cluster?.name ?? '(no cluster)'
|
|
95
134
|
const g = group(ns, cluster)
|
|
96
|
-
const st =
|
|
135
|
+
const st = cnpgRoleState(d)
|
|
97
136
|
g.items.push({
|
|
98
137
|
key: `db/${ns}/${d.metadata?.name}`,
|
|
99
138
|
kind: 'Database',
|
|
100
139
|
pgName: d.spec?.name ?? d.metadata?.name,
|
|
101
|
-
|
|
140
|
+
database: d.spec?.name ?? d.metadata?.name,
|
|
102
141
|
state: st,
|
|
103
142
|
meta: d.spec?.owner ? `owner ${d.spec.owner}` : undefined,
|
|
104
143
|
error: st === 'failed' ? d.status?.message : undefined,
|
|
105
|
-
source: gitopsSource(d),
|
|
144
|
+
source: gitopsSource(data.managedBy, d),
|
|
106
145
|
resource: cnpgResource('databases', ns, d.metadata?.name),
|
|
107
146
|
isField: false,
|
|
108
147
|
})
|
|
@@ -110,19 +149,16 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
110
149
|
for (const p of list) {
|
|
111
150
|
if (p.metadata?.namespace !== ns || p.spec?.cluster?.name !== d.spec?.cluster?.name || p.spec?.dbname !== d.spec?.name) continue
|
|
112
151
|
placed.add(p)
|
|
113
|
-
const pst =
|
|
152
|
+
const pst = cnpgRoleState(p)
|
|
114
153
|
g.items.push({
|
|
115
154
|
key: `${kind}/${ns}/${p.metadata?.name}`,
|
|
116
155
|
kind,
|
|
117
156
|
pgName: p.spec?.name ?? p.metadata?.name,
|
|
118
|
-
|
|
157
|
+
database: d.spec?.name ?? d.metadata?.name,
|
|
119
158
|
state: pst,
|
|
120
|
-
meta:
|
|
121
|
-
kind === 'Publication'
|
|
122
|
-
? p.spec?.target?.allTables ? 'all tables' : 'selected objects'
|
|
123
|
-
: `from ${p.spec?.publicationName ?? '?'} on ${p.spec?.externalClusterName ?? '?'}`,
|
|
159
|
+
meta: logicalMeta(kind, p),
|
|
124
160
|
error: pst === 'failed' ? p.status?.message : undefined,
|
|
125
|
-
source: gitopsSource(p),
|
|
161
|
+
source: gitopsSource(data.managedBy, p),
|
|
126
162
|
resource: cnpgResource(kind === 'Publication' ? 'publications' : 'subscriptions', ns, p.metadata?.name),
|
|
127
163
|
isField: false,
|
|
128
164
|
})
|
|
@@ -134,21 +170,38 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
134
170
|
if (placed.has(p)) continue
|
|
135
171
|
const ns = p.metadata?.namespace ?? ''
|
|
136
172
|
const g = group(ns, p.spec?.cluster?.name ?? '(no cluster)')
|
|
137
|
-
const pst =
|
|
173
|
+
const pst = cnpgRoleState(p)
|
|
138
174
|
g.items.push({
|
|
139
175
|
key: `${kind}/${ns}/${p.metadata?.name}`,
|
|
140
176
|
kind,
|
|
141
177
|
pgName: p.spec?.name ?? p.metadata?.name,
|
|
142
|
-
|
|
178
|
+
database: p.spec?.dbname ?? '(no database named)',
|
|
143
179
|
state: pst,
|
|
144
|
-
meta:
|
|
180
|
+
meta: logicalMeta(kind, p),
|
|
145
181
|
error: pst === 'failed' ? p.status?.message : undefined,
|
|
146
|
-
source: gitopsSource(p),
|
|
182
|
+
source: gitopsSource(data.managedBy, p),
|
|
147
183
|
resource: cnpgResource(kind === 'Publication' ? 'publications' : 'subscriptions', ns, p.metadata?.name),
|
|
148
184
|
isField: false,
|
|
149
185
|
})
|
|
150
186
|
}
|
|
151
187
|
}
|
|
188
|
+
for (const r of (data.objects.databaseRoles ?? []).filter(valid)) {
|
|
189
|
+
const ns = r.metadata?.namespace ?? ''
|
|
190
|
+
const clusterName = r.spec?.cluster?.name ?? '(no cluster)'
|
|
191
|
+
const g = group(ns, clusterName)
|
|
192
|
+
const f = cnpgDatabaseRoleFacts(r, g.row?.cluster ?? null)
|
|
193
|
+
g.items.push({
|
|
194
|
+
key: `databaserole/${ns}/${r.metadata?.name}`,
|
|
195
|
+
kind: 'DatabaseRole',
|
|
196
|
+
pgName: f.pgName,
|
|
197
|
+
state: f.state,
|
|
198
|
+
meta: cnpgDatabaseRoleMeta(f),
|
|
199
|
+
error: f.state === 'failed' ? f.message : undefined,
|
|
200
|
+
source: gitopsSource(data.managedBy, r),
|
|
201
|
+
resource: cnpgResource('databaseroles', ns, r.metadata?.name),
|
|
202
|
+
isField: false,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
152
205
|
for (const row of fleet.rows) {
|
|
153
206
|
const roles: any[] = Array.isArray(row.cluster?.spec?.managed?.roles) ? row.cluster.spec.managed.roles : []
|
|
154
207
|
if (roles.length === 0) continue
|
|
@@ -160,7 +213,6 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
160
213
|
key: `role/${row.namespace}/${row.name}/${r.name}`,
|
|
161
214
|
kind: 'Managed role',
|
|
162
215
|
pgName: r.name,
|
|
163
|
-
indent: false,
|
|
164
216
|
state: rs.state,
|
|
165
217
|
meta: r.ensure === 'absent' ? 'ensure absent' : 'spec.managed.roles',
|
|
166
218
|
error: rs.error,
|
|
@@ -172,25 +224,49 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
172
224
|
}
|
|
173
225
|
return [...byCluster.values()]
|
|
174
226
|
.filter((g) => !clusterFilter || `${g.namespace}/${g.cluster}` === clusterFilter)
|
|
175
|
-
|
|
176
|
-
|
|
227
|
+
}, [data.objects.databases, data.objects.publications, data.objects.subscriptions, data.objects.databaseRoles, data.managedBy, fleet.rows, clusterFilter])
|
|
228
|
+
|
|
229
|
+
const groups = useMemo(() =>
|
|
230
|
+
scopedGroups
|
|
231
|
+
.map((g) => ({ ...g, matched: show ? g.items.filter((i) => i.state === show) : g.items, blocks: cnpgDeclarationBlocks(g.items, show) }))
|
|
232
|
+
.filter((g) => g.matched.length > 0)
|
|
177
233
|
.sort((a, b) => {
|
|
178
|
-
const fa = a.
|
|
179
|
-
const fb = b.
|
|
234
|
+
const fa = a.matched.some((i) => i.state === 'failed') ? 0 : 1
|
|
235
|
+
const fb = b.matched.some((i) => i.state === 'failed') ? 0 : 1
|
|
180
236
|
return fa - fb || a.namespace.localeCompare(b.namespace) || a.cluster.localeCompare(b.cluster)
|
|
181
|
-
})
|
|
182
|
-
|
|
237
|
+
}),
|
|
238
|
+
[scopedGroups, show],
|
|
239
|
+
)
|
|
183
240
|
|
|
184
241
|
const totals = useMemo(() => {
|
|
185
242
|
let failed = 0
|
|
186
243
|
let pending = 0
|
|
187
|
-
for (const
|
|
188
|
-
failed +=
|
|
189
|
-
pending +=
|
|
244
|
+
for (const g of scopedGroups) {
|
|
245
|
+
failed += g.items.filter((i) => i.state === 'failed').length
|
|
246
|
+
pending += g.items.filter((i) => i.state === 'pending').length
|
|
190
247
|
}
|
|
191
248
|
return { failed, pending }
|
|
192
|
-
}, [
|
|
193
|
-
const
|
|
249
|
+
}, [scopedGroups])
|
|
250
|
+
const logicalPaths = useMemo(() => {
|
|
251
|
+
const valid = (o: any) => isApiGroup(o?.apiVersion, 'postgresql.cnpg.io')
|
|
252
|
+
const paths = cnpgLogicalPaths(
|
|
253
|
+
(data.objects.subscriptions ?? []).filter(valid),
|
|
254
|
+
data.objects.clusters ?? [],
|
|
255
|
+
(data.objects.publications ?? []).filter(valid),
|
|
256
|
+
data.objects.poolers ?? [],
|
|
257
|
+
(ns) => relationUnavailable(data, 'publications', ns, 'Publications'),
|
|
258
|
+
)
|
|
259
|
+
if (!clusterFilter) return paths
|
|
260
|
+
return paths.filter(
|
|
261
|
+
(p) =>
|
|
262
|
+
`${p.subscription.namespace}/${p.subscription.cluster}` === clusterFilter ||
|
|
263
|
+
(p.publisher.kind === 'cluster' && `${p.publisher.namespace}/${p.publisher.name}` === clusterFilter),
|
|
264
|
+
)
|
|
265
|
+
}, [data, clusterFilter])
|
|
266
|
+
|
|
267
|
+
const declarationCoverage = [data.coverage.clusters, data.coverage.databases, data.coverage.publications, data.coverage.subscriptions, data.coverage.databaseRoles]
|
|
268
|
+
const declCoverage = worstCoverage(...declarationCoverage)
|
|
269
|
+
const countsComplete = declarationCoverage.every((cov) => cov && (cov.state === 'notInstalled' || coverageReadable(cov, clusterFilter?.split('/')[0])))
|
|
194
270
|
|
|
195
271
|
const chips = [
|
|
196
272
|
...(clusterFilter ? [{ label: `Cluster: ${clusterFilter}`, onClear: () => onSetParams({ cluster: null }) }] : []),
|
|
@@ -201,10 +277,10 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
201
277
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
202
278
|
<CNPGWorkspaceHeader
|
|
203
279
|
title="Declarations"
|
|
204
|
-
subtitle="Databases,
|
|
280
|
+
subtitle="Databases, roles, publications and subscriptions, by PostgreSQL cluster. Declared is not the same as reconciled."
|
|
205
281
|
/>
|
|
206
282
|
<ScreenBody>
|
|
207
|
-
<CoverageNotice fleet={fleet} data={data} />
|
|
283
|
+
<CoverageNotice fleet={fleet} data={data} kinds={['clusters', 'databases', 'databaseRoles', 'publications', 'subscriptions', 'poolers']} />
|
|
208
284
|
<div className="flex flex-wrap items-center gap-2">
|
|
209
285
|
<Segments
|
|
210
286
|
label="Declarations"
|
|
@@ -212,8 +288,8 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
212
288
|
onChange={(id) => onSetParams({ show: id === 'all' ? null : id })}
|
|
213
289
|
options={[
|
|
214
290
|
{ id: 'all', label: 'All declarations' },
|
|
215
|
-
{ id: 'failed', label: 'Not applied', count: totals.failed },
|
|
216
|
-
{ id: 'pending', label: 'Pending', count: totals.pending },
|
|
291
|
+
{ id: 'failed', label: 'Not applied', count: countsComplete ? totals.failed : totals.failed > 0 ? `≥${totals.failed}` : 'Unknown' },
|
|
292
|
+
{ id: 'pending', label: 'Pending', count: countsComplete ? totals.pending : totals.pending > 0 ? `≥${totals.pending}` : 'Unknown' },
|
|
217
293
|
]}
|
|
218
294
|
/>
|
|
219
295
|
</div>
|
|
@@ -226,11 +302,13 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
226
302
|
: show === 'pending'
|
|
227
303
|
? 'No declaration in this scope is waiting for the operator.'
|
|
228
304
|
: coverageEmpty(declCoverage, 'declarations')}
|
|
229
|
-
{show &&
|
|
305
|
+
{show && !countsComplete ? ' Some declarations are not readable with your access.' : ''}
|
|
230
306
|
</div>
|
|
231
307
|
) : (
|
|
232
308
|
groups.map((g) => {
|
|
233
|
-
const failed = g.
|
|
309
|
+
const failed = g.matched.filter((i) => i.state === 'failed').length
|
|
310
|
+
const noSources = g.items.every((i) => !i.source)
|
|
311
|
+
const hasDatabases = g.blocks.some((b) => b.database !== undefined)
|
|
234
312
|
return (
|
|
235
313
|
<section key={`${g.namespace}/${g.cluster}`} className="overflow-hidden rounded-xl border border-theme-border bg-theme-surface shadow-theme-sm">
|
|
236
314
|
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1 border-b border-theme-border px-4 py-2.5">
|
|
@@ -245,31 +323,105 @@ export function CNPGDeclarations({ data, fleet, namespaces, searchParams, onSetP
|
|
|
245
323
|
)}
|
|
246
324
|
<span className="text-xs text-theme-text-tertiary">{g.namespace}</span>
|
|
247
325
|
<span className={clsx('text-xs', failed > 0 ? toneTextClass('degraded') : 'text-theme-text-tertiary')}>
|
|
248
|
-
{g.
|
|
326
|
+
{g.matched.length} {g.matched.length === 1 ? 'declaration' : 'declarations'}
|
|
249
327
|
{failed > 0 ? ` · ${failed} not reconciled` : ''}
|
|
250
328
|
{!g.row ? ' · target cluster not visible' : ''}
|
|
251
329
|
</span>
|
|
330
|
+
{noSources && <span className="text-xs text-theme-text-tertiary">no GitOps source recorded on any of them</span>}
|
|
252
331
|
</div>
|
|
253
332
|
<div className="table-divide-subtle">
|
|
254
|
-
{g.
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
333
|
+
{g.blocks.map((b) => (
|
|
334
|
+
<div key={b.database === undefined ? 'roles' : `db/${b.database}`}>
|
|
335
|
+
{b.database === undefined ? (
|
|
336
|
+
hasDatabases && <div className="px-4 pb-1 pt-2.5 text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">Roles</div>
|
|
337
|
+
) : b.head ? (
|
|
338
|
+
<DeclarationRow
|
|
339
|
+
item={b.head}
|
|
340
|
+
sourceStated={noSources}
|
|
341
|
+
active={sameSelectedResource(inspected, b.head.resource)}
|
|
342
|
+
onInspect={() => onInspect(b.head!.resource)}
|
|
343
|
+
/>
|
|
344
|
+
) : (
|
|
345
|
+
<div className="flex flex-wrap items-center gap-2 px-4 pb-1 pt-2.5 text-sm">
|
|
346
|
+
<span className="text-xs text-theme-text-tertiary">Database</span>
|
|
347
|
+
<span className="font-mono text-theme-text-secondary">{b.database}</span>
|
|
348
|
+
<span className="text-xs text-theme-text-tertiary">no Database declaration</span>
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
{b.children.map((i) => (
|
|
352
|
+
<DeclarationRow
|
|
353
|
+
key={i.key}
|
|
354
|
+
item={i}
|
|
355
|
+
nested={b.database !== undefined}
|
|
356
|
+
sourceStated={noSources}
|
|
357
|
+
active={!i.isField && sameSelectedResource(inspected, i.resource)}
|
|
358
|
+
onInspect={() => onInspect(i.resource)}
|
|
359
|
+
/>
|
|
360
|
+
))}
|
|
361
|
+
</div>
|
|
261
362
|
))}
|
|
262
363
|
</div>
|
|
263
364
|
</section>
|
|
264
365
|
)
|
|
265
366
|
})
|
|
266
367
|
)}
|
|
368
|
+
|
|
369
|
+
{!show && logicalPaths.length > 0 && (
|
|
370
|
+
<section className="overflow-hidden rounded-xl border border-theme-border bg-theme-surface shadow-theme-sm">
|
|
371
|
+
<div className="flex flex-wrap items-baseline gap-x-3 border-b border-theme-border px-4 py-2.5">
|
|
372
|
+
<span className="text-sm font-semibold text-theme-text-primary">Logical replication</span>
|
|
373
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
374
|
+
Each Subscription to its publication and the slot the publisher keeps for it · Subscriptions created in SQL are not listed
|
|
375
|
+
</span>
|
|
376
|
+
</div>
|
|
377
|
+
<div className="table-divide-subtle">
|
|
378
|
+
{logicalPaths.map((p) => (
|
|
379
|
+
<LogicalPathRow key={`${p.subscription.namespace}/${p.subscription.name}`} path={p} onInspect={onInspect} />
|
|
380
|
+
))}
|
|
381
|
+
</div>
|
|
382
|
+
</section>
|
|
383
|
+
)}
|
|
267
384
|
</ScreenBody>
|
|
268
385
|
</div>
|
|
269
386
|
)
|
|
270
387
|
}
|
|
271
388
|
|
|
272
|
-
function
|
|
389
|
+
function LogicalPathRow({ path, onInspect }: { path: CNPGLogicalPath; onInspect: CNPGScreenProps['onInspect'] }) {
|
|
390
|
+
const { observed, query } = useCNPGPublisherSlots(path.publisher)
|
|
391
|
+
return (
|
|
392
|
+
<div className="px-4 py-3">
|
|
393
|
+
<CNPGLogicalPathView
|
|
394
|
+
path={path}
|
|
395
|
+
slot={cnpgLogicalSlotFact(path, observed)}
|
|
396
|
+
notice={<RefreshFailedNotice queries={[query]} />}
|
|
397
|
+
onNavigate={(ref) => onInspect(refToSelectedResource(ref))}
|
|
398
|
+
compact
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function logicalMeta(kind: 'Publication' | 'Subscription', p: { spec?: { target?: { allTables?: boolean }; publicationName?: string; externalClusterName?: string } }): string {
|
|
405
|
+
if (kind === 'Publication') return p.spec?.target?.allTables ? 'all tables' : 'selected objects'
|
|
406
|
+
return `from ${p.spec?.publicationName ?? 'an unnamed publication'} on ${p.spec?.externalClusterName ?? 'an unnamed external cluster'}`
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// `sourceStated`: the cluster header already says none of its rows records a
|
|
410
|
+
// GitOps source, so the row has no source column. `nested`: it lives in the
|
|
411
|
+
// database above it; a guide line joins it to that row.
|
|
412
|
+
function DeclarationRow({
|
|
413
|
+
item,
|
|
414
|
+
active,
|
|
415
|
+
onInspect,
|
|
416
|
+
sourceStated,
|
|
417
|
+
nested,
|
|
418
|
+
}: {
|
|
419
|
+
item: DeclItem
|
|
420
|
+
active: boolean
|
|
421
|
+
onInspect: () => void
|
|
422
|
+
sourceStated?: boolean
|
|
423
|
+
nested?: boolean
|
|
424
|
+
}) {
|
|
273
425
|
const badge = STATE_BADGE[item.state]
|
|
274
426
|
let detail: ReactNode = null
|
|
275
427
|
if (item.error) {
|
|
@@ -285,27 +437,33 @@ function DeclarationRow({ item, active, onInspect }: { item: DeclItem; active: b
|
|
|
285
437
|
role="button"
|
|
286
438
|
tabIndex={0}
|
|
287
439
|
onClick={onInspect}
|
|
288
|
-
onKeyDown={(e) => { if (e.key === 'Enter') onInspect() }}
|
|
440
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onInspect() } }}
|
|
289
441
|
className={clsx(
|
|
290
|
-
'grid cursor-pointer
|
|
442
|
+
'relative grid cursor-pointer gap-x-4 px-4 py-2.5 text-sm transition-colors hover:bg-theme-hover/50',
|
|
443
|
+
sourceStated ? 'grid-cols-[minmax(0,1fr)_auto]' : 'grid-cols-[minmax(0,1.6fr)_minmax(0,0.8fr)_minmax(0,1fr)]',
|
|
291
444
|
active && 'selection',
|
|
292
445
|
)}
|
|
293
446
|
>
|
|
294
|
-
<
|
|
447
|
+
{nested && <span aria-hidden className="absolute bottom-0 left-[1.6rem] top-0 w-px bg-theme-border" />}
|
|
448
|
+
<div className={clsx('min-w-0', nested && 'pl-6')}>
|
|
295
449
|
<div className="flex flex-wrap items-center gap-2">
|
|
296
|
-
<span className="text-xs text-theme-text-tertiary">{item.kind}</span>
|
|
297
450
|
<span className="font-mono font-medium text-theme-text-primary">{item.pgName}</span>
|
|
451
|
+
<span className="text-xs text-theme-text-tertiary">· {item.kind}{!item.isField && ` ${item.resource.name}`}</span>
|
|
298
452
|
{item.meta && <span className="text-xs text-theme-text-tertiary">{item.meta}</span>}
|
|
299
453
|
</div>
|
|
300
454
|
{detail}
|
|
301
455
|
</div>
|
|
302
|
-
<div>
|
|
456
|
+
<div className={clsx(sourceStated && 'text-right')}>
|
|
303
457
|
<Badge severity={badge.severity} size="sm">{badge.text}</Badge>
|
|
458
|
+
<div className="mt-1 text-xs text-accent-text">Inspect →</div>
|
|
459
|
+
{item.state === 'pending' && <Sub>awaiting the operator for the current spec</Sub>}
|
|
304
460
|
{item.isField && <Sub>field of the Cluster</Sub>}
|
|
305
461
|
</div>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
462
|
+
{!sourceStated && (
|
|
463
|
+
<div className="min-w-0 text-xs text-theme-text-secondary break-words">
|
|
464
|
+
{item.source ?? <span className="text-theme-text-tertiary">GitOps source not recorded</span>}
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
309
467
|
</div>
|
|
310
468
|
)
|
|
311
469
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { expect, it, vi } from 'vitest'
|
|
5
|
+
import { CNPGDetailPage } from './CNPGDetailPage'
|
|
6
|
+
const state = vi.hoisted(() => ({ extraTabs: undefined as any, support: 'supported' as 'supported' | 'unsupported' | 'unknown', refresh: undefined as undefined | (() => Promise<unknown>), refetch: vi.fn(async () => {}), specTab: undefined as any, renderStatusBadge: undefined as undefined | ((resource: any) => any) }))
|
|
7
|
+
vi.mock('./refresh', () => ({ refetchCNPGDetail: state.refetch }))
|
|
8
|
+
vi.mock('../workload/WorkloadView', () => ({ WorkloadView: ({ onRefresh, specTab, renderStatusBadge, extraTabs }: { onRefresh?: () => Promise<unknown>; extraTabs?: any; specTab?: any; renderStatusBadge?: (resource: any) => any }) => { state.extraTabs = extraTabs; state.refresh = onRefresh; state.specTab = specTab; state.renderStatusBadge = renderStatusBadge; return <div>detail</div> } }))
|
|
9
|
+
vi.mock('../../context/ConnectionContext', () => ({ useConnection: () => ({ connection: { context: 'test' } }) }))
|
|
10
|
+
vi.mock('../../api/client', () => ({ useRadarFeature: () => ({ support: state.support }) }))
|
|
11
|
+
vi.mock('./CNPGOperatorBanner', () => ({ CNPGOperatorBanner: () => null }))
|
|
12
|
+
it('passes the detail target to the awaited CNPG refresh hook', async () => {
|
|
13
|
+
const client = new QueryClient()
|
|
14
|
+
const target = { plural: 'clusters' as const, namespace: 'db', name: 'pg', group: 'postgresql.cnpg.io' }
|
|
15
|
+
renderToStaticMarkup(<QueryClientProvider client={client}><MemoryRouter initialEntries={['/cnpg/clusters/db/pg?ctx=test']}><CNPGDetailPage target={target} namespaces={['db']} onOpenResource={() => {}} /></MemoryRouter></QueryClientProvider>)
|
|
16
|
+
expect(state.refresh).toBeTypeOf('function')
|
|
17
|
+
await state.refresh!()
|
|
18
|
+
expect(state.refetch).toHaveBeenCalledWith(client, target)
|
|
19
|
+
client.clear()
|
|
20
|
+
})
|
|
21
|
+
it('replaces only the CNPG Cluster Configuration body, retaining the spec tab identity', () => {
|
|
22
|
+
const client = new QueryClient()
|
|
23
|
+
for (const target of [
|
|
24
|
+
{ plural: 'clusters' as const, group: 'postgresql.cnpg.io' },
|
|
25
|
+
{ plural: 'backups' as const, group: 'postgresql.cnpg.io' },
|
|
26
|
+
{ plural: 'clusters' as const, group: 'cluster.x-k8s.io' },
|
|
27
|
+
]) {
|
|
28
|
+
renderToStaticMarkup(<QueryClientProvider client={client}><MemoryRouter initialEntries={['/cnpg/clusters/db/pg?ctx=test&tab=spec']}><CNPGDetailPage target={{ ...target, namespace: 'db', name: 'pg' }} namespaces={['db']} onOpenResource={() => {}} /></MemoryRouter></QueryClientProvider>)
|
|
29
|
+
if (target.plural === 'clusters' && target.group === 'postgresql.cnpg.io') {
|
|
30
|
+
expect(state.specTab.label).toBe('Configuration')
|
|
31
|
+
expect(state.renderStatusBadge).toBeTypeOf('function')
|
|
32
|
+
const badge = state.renderStatusBadge!({ spec: { instances: 2 }, status: { readyInstances: 1, phase: 'Cluster in healthy state' } })
|
|
33
|
+
const html = renderToStaticMarkup(badge)
|
|
34
|
+
expect(html).toContain('>Phase</span>Cluster in healthy state')
|
|
35
|
+
expect(html).not.toContain('Degraded')
|
|
36
|
+
expect(badge.props.content).toContain('status.phase, reported by CloudNativePG: Cluster in healthy state')
|
|
37
|
+
const creating = renderToStaticMarkup(state.renderStatusBadge!({ status: { phase: 'Creating a new replica' } }))
|
|
38
|
+
expect(creating).toContain('Creating a new replica')
|
|
39
|
+
expect(creating).not.toMatch(/amber|emerald/)
|
|
40
|
+
expect(renderToStaticMarkup(state.renderStatusBadge!({ status: {} }))).toContain('Not reported')
|
|
41
|
+
expect(state.specTab.render).toBeTypeOf('function')
|
|
42
|
+
expect(state.specTab.lead).toBeUndefined()
|
|
43
|
+
} else { expect(state.specTab).toBeUndefined(); expect(state.renderStatusBadge).toBeUndefined() }
|
|
44
|
+
}
|
|
45
|
+
client.clear()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('keeps the ordinary detail and omits advanced tabs/configuration until advertised', () => {
|
|
49
|
+
const client = new QueryClient()
|
|
50
|
+
for (const support of ['unsupported', 'unknown'] as const) {
|
|
51
|
+
state.support = support
|
|
52
|
+
const html = renderToStaticMarkup(<QueryClientProvider client={client}><MemoryRouter initialEntries={['/cnpg/clusters/db/pg?ctx=test&tab=activity']}><CNPGDetailPage target={{ plural: 'clusters', group: 'postgresql.cnpg.io', namespace: 'db', name: 'pg' }} namespaces={['db']} onOpenResource={() => {}} /></MemoryRouter></QueryClientProvider>)
|
|
53
|
+
expect(html).toContain('detail')
|
|
54
|
+
expect(state.specTab).toBeUndefined()
|
|
55
|
+
expect(state.renderStatusBadge).toBeUndefined()
|
|
56
|
+
expect(state.refresh).toBeUndefined()
|
|
57
|
+
expect(state.extraTabs.map((tab: any) => tab.id)).toEqual(['activity'])
|
|
58
|
+
expect(state.extraTabs[0].render().props).toMatchObject({ namespace: 'db', name: 'pg' })
|
|
59
|
+
}
|
|
60
|
+
state.support = 'supported'
|
|
61
|
+
client.clear()
|
|
62
|
+
})
|