@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
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
useCallback,
|
|
3
3
|
useEffect,
|
|
4
4
|
useState,
|
|
5
|
-
type ComponentType,
|
|
6
5
|
type ReactNode,
|
|
7
6
|
} from "react";
|
|
8
7
|
import {
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
} from "lucide-react";
|
|
15
14
|
import {
|
|
16
15
|
FreshnessControl,
|
|
16
|
+
healthToSeverity,
|
|
17
17
|
formatDuration,
|
|
18
18
|
getRadarUpgradeRequirement,
|
|
19
19
|
RadarUpgradeAction,
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
ResourceBar,
|
|
25
25
|
Tooltip,
|
|
26
26
|
WithTooltip,
|
|
27
|
+
certaintyValueLabel,
|
|
28
|
+
CertaintyGlyph,
|
|
27
29
|
type CapacityActivityEpisode,
|
|
28
30
|
type CapacityCertainty,
|
|
29
31
|
type CapacityClaimStage,
|
|
@@ -38,6 +40,7 @@ import {
|
|
|
38
40
|
type CapacityManagerRollupStatus,
|
|
39
41
|
type CapacityManagerSummary,
|
|
40
42
|
type CapacityMemberType,
|
|
43
|
+
type CapacityNodeMember,
|
|
41
44
|
type CapacityPoolSummary,
|
|
42
45
|
type CapacityQuantityObservation,
|
|
43
46
|
type CapacityResourceIdentity,
|
|
@@ -59,22 +62,19 @@ import {
|
|
|
59
62
|
} from "../../api/client";
|
|
60
63
|
import type { SelectedResource } from "../../types";
|
|
61
64
|
import { refToSelectedResource } from "../../utils/navigation";
|
|
65
|
+
import { Notice, ScreenEmptyState } from "../workspace/layout";
|
|
62
66
|
|
|
63
67
|
// ============================================================================
|
|
64
68
|
// Route model + navigation
|
|
65
69
|
// ============================================================================
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
export
|
|
71
|
+
import type { PoolSection } from './routes'
|
|
72
|
+
export { capacityPoolPath, parseCapacityRoute } from './routes'
|
|
73
|
+
export type { CapacityRoute, CapacityTopTab, PoolSection } from './routes'
|
|
74
|
+
|
|
69
75
|
export type CapacityConnectionState =
|
|
70
76
|
"connected" | "disconnected" | "connecting";
|
|
71
77
|
|
|
72
|
-
export interface CapacityRoute {
|
|
73
|
-
topTab: CapacityTopTab;
|
|
74
|
-
poolName?: string;
|
|
75
|
-
poolSection: PoolSection;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
78
|
export const POOL_SECTIONS: { id: PoolSection; label: string }[] = [
|
|
79
79
|
{ id: "summary", label: "Summary" },
|
|
80
80
|
{ id: "workloads", label: "Workloads" },
|
|
@@ -82,40 +82,6 @@ export const POOL_SECTIONS: { id: PoolSection; label: string }[] = [
|
|
|
82
82
|
{ id: "configuration", label: "Configuration" },
|
|
83
83
|
];
|
|
84
84
|
|
|
85
|
-
export function parseCapacityRoute(pathname: string): CapacityRoute {
|
|
86
|
-
const segments = pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
87
|
-
if (segments[0] !== "capacity")
|
|
88
|
-
return { topTab: "overview", poolSection: "summary" };
|
|
89
|
-
if (segments[1] === "pools" && segments[2]) {
|
|
90
|
-
const section = segments[3];
|
|
91
|
-
return {
|
|
92
|
-
topTab: "overview",
|
|
93
|
-
poolName: decodePathSegment(segments[2]),
|
|
94
|
-
poolSection:
|
|
95
|
-
section === "workloads" ||
|
|
96
|
-
section === "members" ||
|
|
97
|
-
section === "configuration"
|
|
98
|
-
? section
|
|
99
|
-
: "summary",
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
if (segments[1] === "demand" || segments[1] === "activity")
|
|
103
|
-
return { topTab: segments[1], poolSection: "summary" };
|
|
104
|
-
return { topTab: "overview", poolSection: "summary" };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function decodePathSegment(value: string): string {
|
|
108
|
-
try {
|
|
109
|
-
return decodeURIComponent(value);
|
|
110
|
-
} catch {
|
|
111
|
-
return value;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function capacityPoolPath(name: string): string {
|
|
116
|
-
return `/capacity/pools/${encodeURIComponent(name)}`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
85
|
export function identityToSelectedResource(
|
|
120
86
|
identity: CapacityResourceIdentity,
|
|
121
87
|
): SelectedResource {
|
|
@@ -264,20 +230,6 @@ function missingUsageMessage(coverage?: CapacitySourceCoverage): string {
|
|
|
264
230
|
// = exact · ≥ lower bound · ≤ upper bound · ? unknown
|
|
265
231
|
// ============================================================================
|
|
266
232
|
|
|
267
|
-
export function certaintyGlyph(certainty: CapacityCertainty): string {
|
|
268
|
-
if (certainty === "exact") return "=";
|
|
269
|
-
if (certainty === "lower_bound") return "≥";
|
|
270
|
-
if (certainty === "upper_bound") return "≤";
|
|
271
|
-
return "?";
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function certaintyValueLabel(certainty: CapacityCertainty): string {
|
|
275
|
-
if (certainty === "exact") return "Exact";
|
|
276
|
-
if (certainty === "lower_bound") return "Lower bound";
|
|
277
|
-
if (certainty === "upper_bound") return "Upper bound";
|
|
278
|
-
return "Unknown certainty";
|
|
279
|
-
}
|
|
280
|
-
|
|
281
233
|
export function observationTitle(
|
|
282
234
|
observation: CapacityQuantityObservation,
|
|
283
235
|
definition?: string,
|
|
@@ -293,28 +245,6 @@ export function observationTitle(
|
|
|
293
245
|
return parts.join(" ");
|
|
294
246
|
}
|
|
295
247
|
|
|
296
|
-
/** Small bordered mono glyph chip; hover reveals certainty/source/asOf. */
|
|
297
|
-
export function CertaintyGlyph({
|
|
298
|
-
certainty,
|
|
299
|
-
title,
|
|
300
|
-
}: {
|
|
301
|
-
certainty: CapacityCertainty;
|
|
302
|
-
title?: string;
|
|
303
|
-
}) {
|
|
304
|
-
return (
|
|
305
|
-
<WithTooltip tip={title ?? certaintyValueLabel(certainty)}>
|
|
306
|
-
<span
|
|
307
|
-
tabIndex={0}
|
|
308
|
-
role="note"
|
|
309
|
-
aria-label={title ?? certaintyValueLabel(certainty)}
|
|
310
|
-
className="cursor-help rounded border border-theme-border-light px-1 font-mono text-[10px] leading-tight text-theme-text-tertiary focus-visible:outline focus-visible:outline-2 focus-visible:outline-skyhook-500"
|
|
311
|
-
>
|
|
312
|
-
{certaintyGlyph(certainty)}
|
|
313
|
-
</span>
|
|
314
|
-
</WithTooltip>
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
248
|
// ============================================================================
|
|
319
249
|
// Quantity / resource formatting
|
|
320
250
|
// ============================================================================
|
|
@@ -603,32 +533,21 @@ export function PoolReadyBadge({ ready }: { ready?: boolean }) {
|
|
|
603
533
|
export function NodeReadyBadge({
|
|
604
534
|
ready,
|
|
605
535
|
cordoned,
|
|
536
|
+
lifecycle,
|
|
606
537
|
}: {
|
|
607
538
|
ready?: boolean;
|
|
608
539
|
cordoned: boolean;
|
|
540
|
+
lifecycle?: CapacityNodeMember["lifecycle"];
|
|
609
541
|
}) {
|
|
610
|
-
if (
|
|
611
|
-
return (
|
|
612
|
-
|
|
613
|
-
NotReady
|
|
614
|
-
</Badge>
|
|
615
|
-
);
|
|
616
|
-
if (ready === undefined)
|
|
617
|
-
return (
|
|
618
|
-
<Badge severity="neutral" size="sm">
|
|
619
|
-
Unknown
|
|
620
|
-
</Badge>
|
|
621
|
-
);
|
|
622
|
-
if (cordoned)
|
|
623
|
-
return (
|
|
624
|
-
<Badge severity="warning" size="sm">
|
|
625
|
-
Cordoned
|
|
626
|
-
</Badge>
|
|
627
|
-
);
|
|
542
|
+
if (lifecycle) {
|
|
543
|
+
return <Badge severity={healthToSeverity(lifecycle.level)} size="sm">{lifecycle.label}</Badge>;
|
|
544
|
+
}
|
|
628
545
|
return (
|
|
629
|
-
<
|
|
630
|
-
|
|
631
|
-
|
|
546
|
+
<WithTooltip tip="Kubernetes readiness only; removal status is unavailable from this Radar.">
|
|
547
|
+
<Badge severity={ready === false ? "error" : ready === undefined ? "neutral" : cordoned ? "warning" : "success"} size="sm">
|
|
548
|
+
{ready === false ? "NotReady (condition)" : ready === undefined ? "Readiness unknown" : cordoned ? "Cordoned" : "Ready (condition)"}
|
|
549
|
+
</Badge>
|
|
550
|
+
</WithTooltip>
|
|
632
551
|
);
|
|
633
552
|
}
|
|
634
553
|
|
|
@@ -1250,15 +1169,6 @@ export function TokenGroup({
|
|
|
1250
1169
|
);
|
|
1251
1170
|
}
|
|
1252
1171
|
|
|
1253
|
-
export function Notice({ children }: { children: ReactNode }) {
|
|
1254
|
-
return (
|
|
1255
|
-
<div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-sm text-theme-text-secondary">
|
|
1256
|
-
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
1257
|
-
<div>{children}</div>
|
|
1258
|
-
</div>
|
|
1259
|
-
);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
1172
|
export function RefreshError({ message }: { message: string }) {
|
|
1263
1173
|
return (
|
|
1264
1174
|
<Notice>
|
|
@@ -1336,31 +1246,6 @@ export function InlineEmpty({
|
|
|
1336
1246
|
);
|
|
1337
1247
|
}
|
|
1338
1248
|
|
|
1339
|
-
export function EmptyState({
|
|
1340
|
-
icon: Icon,
|
|
1341
|
-
title,
|
|
1342
|
-
detail,
|
|
1343
|
-
action,
|
|
1344
|
-
}: {
|
|
1345
|
-
icon: ComponentType<{ className?: string }>;
|
|
1346
|
-
title: string;
|
|
1347
|
-
detail: ReactNode;
|
|
1348
|
-
action?: ReactNode;
|
|
1349
|
-
}) {
|
|
1350
|
-
return (
|
|
1351
|
-
<div className="flex min-h-0 flex-1 items-center justify-center bg-theme-base p-6">
|
|
1352
|
-
<div className="max-w-md text-center">
|
|
1353
|
-
<Icon className="mx-auto h-9 w-9 text-theme-text-tertiary/50" />
|
|
1354
|
-
<h2 className="mt-3 text-lg font-medium text-theme-text-primary">
|
|
1355
|
-
{title}
|
|
1356
|
-
</h2>
|
|
1357
|
-
<p className="mt-1 text-sm text-theme-text-secondary">{detail}</p>
|
|
1358
|
-
{action}
|
|
1359
|
-
</div>
|
|
1360
|
-
</div>
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
1249
|
export function ScrollableContent({ children }: { children: ReactNode }) {
|
|
1365
1250
|
// Expanding/collapsing cards can toggle the scrollbar; a stable gutter
|
|
1366
1251
|
// keeps the centered column from shifting sideways when that happens.
|
|
@@ -1469,7 +1354,7 @@ rules:
|
|
|
1469
1354
|
export function DeniedCapacityState({ detail }: { detail: string }) {
|
|
1470
1355
|
const [copied, setCopied] = useState(false);
|
|
1471
1356
|
return (
|
|
1472
|
-
<
|
|
1357
|
+
<ScreenEmptyState
|
|
1473
1358
|
icon={Shield}
|
|
1474
1359
|
title="Capacity access denied"
|
|
1475
1360
|
detail={detail}
|
|
@@ -1528,7 +1413,7 @@ export function integrationBlock(
|
|
|
1528
1413
|
const upgrade = getRadarUpgradeRequirement(error);
|
|
1529
1414
|
if (upgrade)
|
|
1530
1415
|
return (
|
|
1531
|
-
<
|
|
1416
|
+
<ScreenEmptyState
|
|
1532
1417
|
icon={Gauge}
|
|
1533
1418
|
title={radarUpgradeHeadline("Capacity")}
|
|
1534
1419
|
detail={radarUpgradeDetail(upgrade)}
|
|
@@ -1540,7 +1425,7 @@ export function integrationBlock(
|
|
|
1540
1425
|
/>
|
|
1541
1426
|
);
|
|
1542
1427
|
return (
|
|
1543
|
-
<
|
|
1428
|
+
<ScreenEmptyState
|
|
1544
1429
|
icon={AlertTriangle}
|
|
1545
1430
|
title="Capacity unavailable"
|
|
1546
1431
|
detail={errorMessage(error)}
|
|
@@ -1558,7 +1443,7 @@ export function integrationBlock(
|
|
|
1558
1443
|
);
|
|
1559
1444
|
if (response.state === "not_detected")
|
|
1560
1445
|
return (
|
|
1561
|
-
<
|
|
1446
|
+
<ScreenEmptyState
|
|
1562
1447
|
icon={Gauge}
|
|
1563
1448
|
title="Karpenter not detected"
|
|
1564
1449
|
detail="Capacity appears automatically when this cluster exposes Karpenter NodePools."
|
|
@@ -1847,15 +1732,3 @@ export function PoolSelector({
|
|
|
1847
1732
|
</div>
|
|
1848
1733
|
);
|
|
1849
1734
|
}
|
|
1850
|
-
|
|
1851
|
-
// ============================================================================
|
|
1852
|
-
// Shared table cell classes (keeps every capacity table visually identical)
|
|
1853
|
-
// ============================================================================
|
|
1854
|
-
|
|
1855
|
-
export const TABLE_WRAP = "overflow-x-auto";
|
|
1856
|
-
export const TABLE_HEAD =
|
|
1857
|
-
"border-b border-theme-border bg-theme-base/60 text-[11px] uppercase tracking-wide text-theme-text-tertiary";
|
|
1858
|
-
export const TH = "px-3 py-2.5 text-left font-medium whitespace-nowrap";
|
|
1859
|
-
export const TD = "px-3 py-2.5 align-top text-sm text-theme-text-primary";
|
|
1860
|
-
export const TBODY = "table-divide-subtle";
|
|
1861
|
-
export const ROW_HOVER = "transition-colors hover:bg-theme-hover/50";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkspaceRoute } from '../../integrations/workspaceRoute'
|
|
2
|
+
import { parseCapacityRoute } from './routes'
|
|
3
|
+
|
|
4
|
+
export const capacityWorkspaceRoute: WorkspaceRoute = {
|
|
5
|
+
id: 'capacity',
|
|
6
|
+
path: '/capacity',
|
|
7
|
+
label: 'Capacity',
|
|
8
|
+
navView: 'capacity',
|
|
9
|
+
namespaceScope: 'cluster',
|
|
10
|
+
namespaceFilterTooltip: 'Capacity is reported across the cluster — the namespace filter doesn’t apply here.',
|
|
11
|
+
pageTitle(pathname) {
|
|
12
|
+
const route = parseCapacityRoute(pathname)
|
|
13
|
+
if (route.poolName) return route.poolName
|
|
14
|
+
if (route.topTab === 'demand') return 'Capacity Demand'
|
|
15
|
+
if (route.topTab === 'activity') return 'Capacity Activity'
|
|
16
|
+
return 'Capacity'
|
|
17
|
+
},
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { beforeEach, expect, it, vi } from 'vitest'
|
|
3
|
+
import { cnpgHost } from './host'
|
|
4
|
+
import { renderCNPGHeaderActions } from './actions/renderCNPGHeaderActions'
|
|
5
|
+
import { radarFeatureSupport } from '../../api/radarFeatures'
|
|
6
|
+
const state = vi.hoisted(() => ({ capabilities: { features: { resourceIssues: true } } as any }))
|
|
7
|
+
vi.mock('../../api/client', async original => ({ ...await original<typeof import('../../api/client')>(), useRadarFeature: () => ({ support: radarFeatureSupport('cnpgWorkspace', state.capabilities, 'v1.16.0') }) }))
|
|
8
|
+
vi.mock('./CNPGClusterTabs', () => ({ CNPGInvestigationAction: () => <span>CNPG investigation</span> }))
|
|
9
|
+
vi.mock('./actions/CNPGClusterActions', () => ({ CNPGClusterActions: () => <span>CNPG write actions</span> }))
|
|
10
|
+
vi.mock('./actions/CNPGConnectButton', () => ({ CNPGConnectButton: () => null }))
|
|
11
|
+
vi.mock('./actions/CNPGScheduleActions', () => ({ CNPGScheduleActions: () => null }))
|
|
12
|
+
vi.mock('./actions/CNPGPoolerActions', () => ({ CNPGPoolerActions: () => null }))
|
|
13
|
+
vi.mock('./operations/CNPGOperationTracker', () => ({ CNPGOperationTracker: () => null }))
|
|
14
|
+
vi.mock('./recovery/CNPGRestoreButton', () => ({ CNPGRestoreButton: () => null }))
|
|
15
|
+
beforeEach(() => { state.capabilities = { features: { resourceIssues: true } } })
|
|
16
|
+
const context = { kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'pg', compact: false }
|
|
17
|
+
const resource = { apiVersion: 'postgresql.cnpg.io/v1', kind: 'Cluster', metadata: { name: 'pg', namespace: 'db', uid: 'uid-pg' } }
|
|
18
|
+
it('preserves ordinary Diagnose and omits new action affordances on the shipped missing-flag state', () => {
|
|
19
|
+
const original = vi.fn(() => <span>ordinary investigation</span>)
|
|
20
|
+
expect(renderToStaticMarkup(cnpgHost.diagnoseAction(original)!(context))).toContain('ordinary investigation')
|
|
21
|
+
expect(original).toHaveBeenCalledWith(context)
|
|
22
|
+
expect(renderToStaticMarkup(renderCNPGHeaderActions({ resource, namespace: 'db', name: 'pg', compact: false }))).toBe('')
|
|
23
|
+
})
|
|
24
|
+
it('mounts the CNPG decorations once their contracts are advertised', () => {
|
|
25
|
+
state.capabilities = { features: { cnpgWorkspace: true } }
|
|
26
|
+
const original = vi.fn(() => <span>ordinary investigation</span>)
|
|
27
|
+
expect(renderToStaticMarkup(cnpgHost.diagnoseAction(original)!(context))).toContain('CNPG investigation')
|
|
28
|
+
expect(original).not.toHaveBeenCalled()
|
|
29
|
+
expect(renderToStaticMarkup(renderCNPGHeaderActions({ resource, namespace: 'db', name: 'pg', compact: false }))).toContain('CNPG write actions')
|
|
30
|
+
})
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { cnpgClusterPlugins, cnpgPluginPhase, formatAge, getCNPGClusterBarmanPlugin, StatusDot, toneTextClass, type CNPGFleetRow } from '@skyhook-io/k8s-ui'
|
|
2
|
+
import type { SelectedResource } from '../../types'
|
|
3
|
+
import type { CNPGRuntimeInstance } from '../../api/cnpg'
|
|
4
|
+
import { backupMethod, cnpgArchiveDestination, cnpgRecoveryBase, resumeBoundary, type CNPGRecoveryBase } from './archivingRepair'
|
|
5
|
+
|
|
6
|
+
const RESUMED_WINDOW_MS = 24 * 60 * 60 * 1000
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* WAL archiving, as a repair: why it fails, where the destination and its
|
|
10
|
+
* credentials are declared, where the archiver logs, then whether archiving
|
|
11
|
+
* resumed and a fresh base backup exists. Shown while archiving fails, and
|
|
12
|
+
* for a day after it resumes so the fix can be confirmed here.
|
|
13
|
+
*/
|
|
14
|
+
export function CNPGArchivingRepair({
|
|
15
|
+
row,
|
|
16
|
+
primary,
|
|
17
|
+
objectStores,
|
|
18
|
+
backups,
|
|
19
|
+
onOpenLogs,
|
|
20
|
+
onOpenOperator,
|
|
21
|
+
onInspect,
|
|
22
|
+
}: {
|
|
23
|
+
row: CNPGFleetRow
|
|
24
|
+
primary?: CNPGRuntimeInstance
|
|
25
|
+
objectStores: any[]
|
|
26
|
+
/** This namespace's CNPG Backups, or null when they could not be read. */
|
|
27
|
+
backups: any[] | null
|
|
28
|
+
onOpenLogs?: (pod: string, container: string) => void
|
|
29
|
+
onOpenOperator?: () => void
|
|
30
|
+
onInspect: (r: SelectedResource) => void
|
|
31
|
+
}) {
|
|
32
|
+
const wal = row.protection.walArchiving
|
|
33
|
+
const arch = primary && (primary.status.state === 'ok' || primary.status.state === 'partial') ? primary.status.archiving : undefined
|
|
34
|
+
const failedAt = arch?.lastFailedAt ? Date.parse(arch.lastFailedAt) : NaN
|
|
35
|
+
const archivedAt = arch?.lastArchivedAt ? Date.parse(arch.lastArchivedAt) : NaN
|
|
36
|
+
const failingNow = wal.tone === 'unhealthy' || (Number.isFinite(failedAt) && (!Number.isFinite(archivedAt) || failedAt > archivedAt))
|
|
37
|
+
// Only the instance manager records that archiving failed: the condition
|
|
38
|
+
// turning True also happens when archiving is first set up, so it marks a
|
|
39
|
+
// resume only after a failure the instance manager saw. Its stats restart
|
|
40
|
+
// with the primary, and the panel then has nothing left to show.
|
|
41
|
+
const runtimeResumed = Number.isFinite(failedAt) && Number.isFinite(archivedAt) && archivedAt > failedAt
|
|
42
|
+
const boundary = resumeBoundary(row.cluster, failedAt)
|
|
43
|
+
const resumed = !failingNow && wal.tone === 'healthy' && runtimeResumed && Number.isFinite(boundary) && Date.now() - boundary < RESUMED_WINDOW_MS
|
|
44
|
+
if (!failingNow && !resumed) return null
|
|
45
|
+
|
|
46
|
+
// A plugin archives WAL only when it is marked isWALArchiver; otherwise the
|
|
47
|
+
// in-tree barmanObjectStore (if any) does, from the postgres container.
|
|
48
|
+
const barman = getCNPGClusterBarmanPlugin(row.cluster)
|
|
49
|
+
const plugin = barman?.isWALArchiver ? barman : null
|
|
50
|
+
const storeName = plugin?.barmanObjectName
|
|
51
|
+
const store = storeName ? objectStores.find((s) => s?.metadata?.namespace === row.namespace && s?.metadata?.name === storeName) : undefined
|
|
52
|
+
const inTree = row.cluster?.spec?.backup?.barmanObjectStore
|
|
53
|
+
const dest = store
|
|
54
|
+
? cnpgArchiveDestination(store.spec?.configuration, `ObjectStore ${storeName}`)
|
|
55
|
+
: inTree
|
|
56
|
+
? cnpgArchiveDestination(inTree, 'the Cluster’s spec.backup.barmanObjectStore')
|
|
57
|
+
: undefined
|
|
58
|
+
const archiverContainer = plugin ? 'plugin-barman-cloud' : 'postgres'
|
|
59
|
+
const pluginPhase = cnpgPluginPhase(row.cluster)
|
|
60
|
+
const primaryPod = primary?.pod ?? row.cluster?.status?.currentPrimary
|
|
61
|
+
const lastBackup = row.protection.lastSuccessfulBackup
|
|
62
|
+
const base = resumed ? cnpgRecoveryBase(row, backups, boundary, plugin ? { method: 'plugin', plugin: plugin.name } : { method: 'barmanObjectStore' }, arch?.lastFailedWal) : undefined
|
|
63
|
+
const baseDone = base?.state === 'verified'
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<section className="rounded-xl border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm">
|
|
67
|
+
<h3 className={`text-sm font-semibold ${failingNow ? toneTextClass('unhealthy') : toneTextClass('healthy')}`}>
|
|
68
|
+
{failingNow ? 'WAL archiving is failing' : 'WAL archiving resumed'}
|
|
69
|
+
</h3>
|
|
70
|
+
<div className="mt-1.5 space-y-1.5 text-sm">
|
|
71
|
+
{failingNow && (
|
|
72
|
+
<p className="text-theme-text-primary">
|
|
73
|
+
{wal.tone === 'unhealthy' ? (
|
|
74
|
+
wal.detail ? (
|
|
75
|
+
<>
|
|
76
|
+
The operator reports: <span className="font-mono text-xs">{wal.detail}</span>
|
|
77
|
+
</>
|
|
78
|
+
) : (
|
|
79
|
+
'The operator reports ContinuousArchiving False without a message.'
|
|
80
|
+
)
|
|
81
|
+
) : (
|
|
82
|
+
`The primary’s instance manager reports a failed archive after its last archived WAL${wal.tone === 'healthy' ? '; the operator’s ContinuousArchiving condition still reads True' : ''}.`
|
|
83
|
+
)}
|
|
84
|
+
</p>
|
|
85
|
+
)}
|
|
86
|
+
{arch && (
|
|
87
|
+
<p className="text-xs text-theme-text-secondary">
|
|
88
|
+
{arch.lastArchivedAt ? `Last archived ${formatAge(arch.lastArchivedAt)} ago${arch.lastArchivedWal ? ` (${arch.lastArchivedWal})` : ''}` : 'Nothing archived since the instance manager started'}
|
|
89
|
+
{arch.lastFailedAt ? ` · last failure ${formatAge(arch.lastFailedAt)} ago${arch.lastFailedWal ? ` (${arch.lastFailedWal})` : ''}` : ''}
|
|
90
|
+
{arch.readyWalFiles !== undefined ? ` · ${arch.readyWalFiles} files waiting` : ''} · from the primary’s instance manager
|
|
91
|
+
</p>
|
|
92
|
+
)}
|
|
93
|
+
{failingNow && dest && (
|
|
94
|
+
<div className="text-xs text-theme-text-secondary">
|
|
95
|
+
<div>
|
|
96
|
+
Destination <span className="font-mono text-theme-text-primary">{dest.path ?? 'path not declared'}</span>
|
|
97
|
+
{dest.endpointURL && <> via <span className="font-mono">{dest.endpointURL}</span></>}, declared in{' '}
|
|
98
|
+
{store ? (
|
|
99
|
+
<button type="button" className="text-accent-text hover:underline" onClick={() => onInspect({ kind: 'objectstores', group: 'barmancloud.cnpg.io', namespace: row.namespace, name: storeName! })}>
|
|
100
|
+
{dest.declaredIn}
|
|
101
|
+
</button>
|
|
102
|
+
) : (
|
|
103
|
+
dest.declaredIn
|
|
104
|
+
)}
|
|
105
|
+
.
|
|
106
|
+
</div>
|
|
107
|
+
<div className="mt-0.5">
|
|
108
|
+
Credentials:{' '}
|
|
109
|
+
{dest.identity && (
|
|
110
|
+
<>
|
|
111
|
+
{dest.identity}
|
|
112
|
+
{dest.secrets.length > 0 && ' · '}
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
{!dest.identity && dest.secrets.length === 0
|
|
116
|
+
? 'none referenced'
|
|
117
|
+
: dest.secrets.map((s, i) => (
|
|
118
|
+
<span key={`${s.secret}/${s.key}/${s.what}`}>
|
|
119
|
+
{i > 0 && ' · '}
|
|
120
|
+
{s.what} in Secret{' '}
|
|
121
|
+
<button type="button" className="font-mono text-accent-text hover:underline" onClick={() => onInspect({ kind: 'secrets', group: '', namespace: row.namespace, name: s.secret })}>
|
|
122
|
+
{s.secret}
|
|
123
|
+
</button>
|
|
124
|
+
{s.key ? <span className="font-mono">/{s.key}</span> : null}
|
|
125
|
+
</span>
|
|
126
|
+
))}
|
|
127
|
+
. Radar names them and does not read them.
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
{failingNow && primaryPod && (
|
|
132
|
+
<p className="text-xs text-theme-text-secondary">
|
|
133
|
+
The primary archives from its <span className="font-mono">{archiverContainer}</span> container; the failing command’s output is in its logs.{' '}
|
|
134
|
+
{onOpenLogs && (
|
|
135
|
+
<button type="button" className="text-accent-text hover:underline" onClick={() => onOpenLogs(primaryPod, archiverContainer)}>
|
|
136
|
+
{archiverContainer} logs of {primaryPod} →
|
|
137
|
+
</button>
|
|
138
|
+
)}
|
|
139
|
+
</p>
|
|
140
|
+
)}
|
|
141
|
+
{failingNow && pluginPhase && (
|
|
142
|
+
<p className="text-xs text-theme-text-secondary">
|
|
143
|
+
Separately, the Cluster’s phase says the operator {pluginPhase === 'unknownPlugin' ? 'does not know a plugin this cluster requires' : 'hit an error talking to one of this cluster’s plugins'} (spec.plugins:{' '}
|
|
144
|
+
{cnpgClusterPlugins(row.cluster).join(', ') || 'none listed'}), so it cannot reconcile this cluster until that clears.{' '}
|
|
145
|
+
{onOpenOperator && (
|
|
146
|
+
<button type="button" className="text-accent-text hover:underline" onClick={onOpenOperator}>
|
|
147
|
+
Operator and plugins →
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
150
|
+
</p>
|
|
151
|
+
)}
|
|
152
|
+
<ol className="mt-1 space-y-1 text-xs">
|
|
153
|
+
<li className="flex items-start gap-2">
|
|
154
|
+
<span className="flex h-4 shrink-0 items-center"><StatusDot tone={failingNow ? 'unknown' : 'healthy'} /></span>
|
|
155
|
+
<span className={failingNow ? 'text-theme-text-secondary' : 'text-theme-text-primary'}>
|
|
156
|
+
Archiving resumes: the operator reports ContinuousArchiving True and a WAL file archives after the last failure.
|
|
157
|
+
{failingNow ? ' Not yet.' : ` Done${arch?.lastArchivedAt ? `, ${formatAge(arch.lastArchivedAt)} ago` : ''}.`}
|
|
158
|
+
</span>
|
|
159
|
+
</li>
|
|
160
|
+
<li className="flex items-start gap-2">
|
|
161
|
+
<span className="flex h-4 shrink-0 items-center"><StatusDot tone={baseDone ? 'healthy' : 'unknown'} /></span>
|
|
162
|
+
<span className={baseDone ? 'text-theme-text-primary' : 'text-theme-text-secondary'}>
|
|
163
|
+
A base backup that begins after the last WAL that failed to archive completes (Back up now, above), so a restore from it needs none of that WAL.{' '}
|
|
164
|
+
{failingNow || !base
|
|
165
|
+
? `Not yet. Newest successful backup: ${lastBackup.at ? `${formatAge(lastBackup.at)} ago` : lastBackup.text.toLowerCase()}.`
|
|
166
|
+
: recoveryBaseText(base)}
|
|
167
|
+
</span>
|
|
168
|
+
</li>
|
|
169
|
+
</ol>
|
|
170
|
+
</div>
|
|
171
|
+
</section>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function recoveryBaseText(base: CNPGRecoveryBase): string {
|
|
176
|
+
const label = (b: any) => `Backup ${b?.metadata?.name} (${backupMethod(b)})`
|
|
177
|
+
switch (base.state) {
|
|
178
|
+
case 'unread':
|
|
179
|
+
return 'Radar cannot read this namespace’s Backups, so it cannot tell.'
|
|
180
|
+
case 'none':
|
|
181
|
+
return 'No completed Backup of this cluster has started since archiving resumed.'
|
|
182
|
+
case 'verified':
|
|
183
|
+
return `Done: ${label(base.backup)} begins at WAL ${base.backup.status.beginWal}, after ${base.failedWal}, and completed ${formatAge(base.backup.status.stoppedAt ?? base.backup.status.startedAt)} ago.`
|
|
184
|
+
case 'beginsBefore':
|
|
185
|
+
return `${label(base.backup)} started after archiving resumed but begins at WAL ${base.backup?.status?.beginWal ?? '(not reported)'}, not after ${base.failedWal} — likely from a standby that had not replayed past it. Take another.`
|
|
186
|
+
case 'unverifiable':
|
|
187
|
+
return base.missing === 'beginWal'
|
|
188
|
+
? `${label(base.backup)} started after archiving resumed and completed, but its status does not say which WAL it begins at, so Radar cannot compare it with the last failed one.`
|
|
189
|
+
: `${label(base.backup)} started after archiving resumed and completed; Radar cannot compare where it begins with the last failed WAL, which only the primary’s instance manager reports and is not reporting now.`
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
const state = vi.hoisted(() => ({ data: { pod: 'pg-1', state: 'denied', permission: { exec: 'denied', grant: { verb: 'create', resource: 'pods', subresource: 'exec', namespace: 'db' } }, instances: [] } as any, navigate: vi.fn() }))
|
|
6
|
+
vi.mock('./useCNPGNavigate', () => ({ useCNPGNavigate: () => state.navigate }))
|
|
7
|
+
vi.mock('react-router-dom', async (original) => ({ ...await original<typeof import('react-router-dom')>(), useLocation: () => ({ pathname: '/cnpg/clusters/db/pg', search: '?ctx=test&tab=performance&drawer=x', state: { returnLabel: 'Clusters' } }) }))
|
|
8
|
+
vi.mock('@tanstack/react-query', async (original) => ({ ...await original<typeof import('@tanstack/react-query')>(), useQueries: () => [{ data: { containers: [] } }] }))
|
|
9
|
+
vi.mock('../../api/client', () => ({ podMetricsQuery: () => ({}), usePodMetrics: () => ({ data: { containers: [{ name: 'postgres', usage: { cpu: '30000000n', memory: '59Mi' } }] } }) }))
|
|
10
|
+
vi.mock('../../api/cnpg-sessions', () => ({
|
|
11
|
+
useCNPGSessions: () => ({
|
|
12
|
+
data: state.data,
|
|
13
|
+
isLoading: false,
|
|
14
|
+
error: null,
|
|
15
|
+
isRefetchError: false,
|
|
16
|
+
dataUpdatedAt: Date.now(),
|
|
17
|
+
}),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const { CNPGBlockingSessions } = await import('./CNPGBlockingSessions')
|
|
21
|
+
|
|
22
|
+
describe('CNPGBlockingSessions with exec denied', () => {
|
|
23
|
+
it('explains only the exec grant for blocking detail', () => {
|
|
24
|
+
const html = renderToStaticMarkup(<CNPGBlockingSessions namespace="db" cluster="pg" />)
|
|
25
|
+
expect(html).toContain('Who blocks whom is read inside PostgreSQL')
|
|
26
|
+
expect(html).toContain('create pods/exec')
|
|
27
|
+
expect(html).not.toContain('aggregate')
|
|
28
|
+
expect(html).not.toContain('get pods/proxy')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('keeps query-text prose out of unavailable states while the host owns the prerequisite', () => {
|
|
33
|
+
expect(renderToStaticMarkup(<CNPGBlockingSessions namespace="db" cluster="pg" />)).not.toContain('Query text')
|
|
34
|
+
state.data = { pod: '', state: 'unavailable', permission: { exec: 'unknown' }, instances: [] }
|
|
35
|
+
const html = renderToStaticMarkup(<CNPGBlockingSessions namespace="db" cluster="pg" />)
|
|
36
|
+
expect(html).toContain('Blocking detail not measured')
|
|
37
|
+
expect(html).not.toContain('Available once the primary is running')
|
|
38
|
+
expect(html).not.toContain('See Overview')
|
|
39
|
+
expect(html).not.toContain('could not be read')
|
|
40
|
+
expect(html).not.toContain('Query text')
|
|
41
|
+
})
|
|
42
|
+
it('keeps CPU and memory measurements and limits on separate rows with unbreakable units', () => {
|
|
43
|
+
state.data = { pod: 'pg-1', state: 'ok', permission: { exec: 'allowed' }, sessions: [], instances: [{ pod: 'pg-1', role: 'primary', memoryLimit: '384Mi' }] }
|
|
44
|
+
const html = renderToStaticMarkup(<CNPGBlockingSessions namespace="db" cluster="pg" />)
|
|
45
|
+
expect(html).toMatch(/CPU <span class="whitespace-nowrap">0.03 cores<\/span>/)
|
|
46
|
+
expect(html).toMatch(/Memory <span class="whitespace-nowrap">59 MiB<\/span>/)
|
|
47
|
+
expect(html).toContain('limit <span class="whitespace-nowrap">384 MiB</span>')
|
|
48
|
+
expect(html).not.toContain('Query text')
|
|
49
|
+
expect(html.indexOf('No session is waiting')).toBeLessThan(html.indexOf('Resource usage'))
|
|
50
|
+
})
|