@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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
|
-
import { useNavigate } from 'react-router-dom'
|
|
3
2
|
import { clsx } from 'clsx'
|
|
4
|
-
import { ArrowRight, Database, FileText, Search } from 'lucide-react'
|
|
3
|
+
import { ArrowRight, Database, FileText, Plus, Search } from 'lucide-react'
|
|
5
4
|
import {
|
|
6
5
|
CNPG_PROBLEM_CATEGORIES,
|
|
6
|
+
CNPG_PROBLEM_TONE,
|
|
7
|
+
cnpgDimensions,
|
|
8
|
+
cnpgReadyInstances,
|
|
7
9
|
FactValue,
|
|
8
10
|
StatusDot,
|
|
9
11
|
Tooltip,
|
|
@@ -13,43 +15,175 @@ import {
|
|
|
13
15
|
} from '@skyhook-io/k8s-ui'
|
|
14
16
|
import type { SelectedResource } from '../../types'
|
|
15
17
|
import { useConnection } from '../../context/ConnectionContext'
|
|
16
|
-
import {
|
|
17
|
-
import { CNPGWorkspaceHeader, CoverageNotice,
|
|
18
|
-
import { cnpgClusterFullPath,
|
|
19
|
-
import {
|
|
18
|
+
import { getApiBase } from '../../api/config'
|
|
19
|
+
import { CNPGWorkspaceHeader, CoverageNotice, coverageEmpty, type CNPGScreenProps } from './shared'
|
|
20
|
+
import { cnpgClusterFullPath, cnpgClusterProblemsPath } from './paths'
|
|
21
|
+
import { currentPageLabel } from '../../utils/page-links'
|
|
22
|
+
import { CNPGOperatorBanner } from './CNPGOperatorBanner'
|
|
23
|
+
import { cnpgInstancePillLabel, cnpgPillsToShow, cnpgRowStatus } from './fleetStatus'
|
|
24
|
+
import { BreakText, FilterChips, ScreenEmptyState } from '../workspace/layout'
|
|
25
|
+
import { ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from '../workspace/table'
|
|
26
|
+
import { sameSelectedResource } from '../../utils/drawer-trail'
|
|
27
|
+
import { useCNPGNavigate } from './useCNPGNavigate'
|
|
28
|
+
import { useCNPGOperations } from './operations/store'
|
|
29
|
+
import { latestCNPGOperation } from './operations/presentation'
|
|
30
|
+
import { CNPGFleetOperation } from './operations/CNPGFleetOperation'
|
|
20
31
|
|
|
21
32
|
type Filter = 'attention' | 'all'
|
|
22
33
|
|
|
34
|
+
// Headers may wrap: at a 1280px window the columns are narrower than their labels.
|
|
35
|
+
const TH_WRAP = TH.replace('whitespace-nowrap', '')
|
|
36
|
+
|
|
37
|
+
// Five pills fit the Ready column; the rest are counted, listed on hover.
|
|
38
|
+
const MAX_PILLS = 5
|
|
39
|
+
|
|
23
40
|
function InstancePills({ row }: { row: CNPGFleetRow }) {
|
|
24
41
|
if (row.pods.length === 0) return null
|
|
42
|
+
const { shown, hidden } = cnpgPillsToShow(row.pods, MAX_PILLS)
|
|
25
43
|
return (
|
|
26
|
-
<div className="mt-1 flex flex-
|
|
27
|
-
{
|
|
44
|
+
<div className="mt-1 flex flex-nowrap items-center gap-0.5 font-sans">
|
|
45
|
+
{shown.map((p) => {
|
|
28
46
|
const tone = p.ready === true ? 'healthy' : p.ready === false ? 'unhealthy' : 'unknown'
|
|
29
47
|
return (
|
|
30
|
-
<Tooltip key={p.name} content={
|
|
31
|
-
<span
|
|
48
|
+
<Tooltip key={p.name} content={cnpgInstancePillLabel(p)}>
|
|
49
|
+
<span
|
|
50
|
+
aria-label={cnpgInstancePillLabel(p)}
|
|
51
|
+
className="inline-flex items-center gap-0.5 rounded border border-theme-border bg-theme-base px-0.5 font-mono text-[10.5px] text-theme-text-secondary"
|
|
52
|
+
>
|
|
32
53
|
<StatusDot tone={tone} size="xs" />
|
|
33
54
|
{p.role === 'primary' ? 'P' : p.role === 'replica' ? 'R' : '?'}
|
|
34
55
|
</span>
|
|
35
56
|
</Tooltip>
|
|
36
57
|
)
|
|
37
58
|
})}
|
|
59
|
+
{hidden.length > 0 && (
|
|
60
|
+
<Tooltip
|
|
61
|
+
content={
|
|
62
|
+
<ul className="space-y-0.5">
|
|
63
|
+
{hidden.map((p) => (
|
|
64
|
+
<li key={p.name}>{cnpgInstancePillLabel(p)}</li>
|
|
65
|
+
))}
|
|
66
|
+
</ul>
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<span aria-label={`${hidden.length} more instances`} className="px-0.5 text-[10.5px] text-theme-text-tertiary">
|
|
70
|
+
+{hidden.length}
|
|
71
|
+
</span>
|
|
72
|
+
</Tooltip>
|
|
73
|
+
)}
|
|
38
74
|
</div>
|
|
39
75
|
)
|
|
40
76
|
}
|
|
41
77
|
|
|
42
|
-
function
|
|
78
|
+
function ReadyCell({ row }: { row: CNPGFleetRow }) {
|
|
79
|
+
const r = cnpgReadyInstances(row)
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{r.note ? (
|
|
83
|
+
<Tooltip content={r.note}>
|
|
84
|
+
<span className={clsx('underline decoration-dotted underline-offset-2', toneTextClass(r.tone ?? 'unknown'))}>{r.text} Pods</span>
|
|
85
|
+
</Tooltip>
|
|
86
|
+
) : (
|
|
87
|
+
r.text
|
|
88
|
+
)}
|
|
89
|
+
{r.podText && <span className="block text-xs text-theme-text-secondary">{r.podText}</span>}
|
|
90
|
+
<InstancePills row={row} />
|
|
91
|
+
</>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The Storage cell is the same assessment as the Cluster page's Storage chip:
|
|
96
|
+
// measured usage, or the WAL an inactive slot holds, or why neither is known.
|
|
97
|
+
function storageFact(row: CNPGFleetRow) {
|
|
98
|
+
const d = cnpgDimensions({ row }).find((x) => x.id === 'storage')!
|
|
99
|
+
return { text: d.text, tone: d.tone, source: d.source }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The one reason no cluster's volume usage is measured, when that is so; the column then says it once. */
|
|
103
|
+
function cnpgStorageUnmeasured(rows: CNPGFleetRow[]): string | null {
|
|
104
|
+
if (rows.length === 0 || rows.some((r) => !r.disk || r.disk.tone !== 'unknown')) return null
|
|
105
|
+
const reasons = new Set(rows.map((r) => (r.disk?.source ?? r.disk?.text ?? '').replace(/this cluster's/g, 'their').replace(/this cluster/g, 'them')))
|
|
106
|
+
return reasons.size === 1 ? [...reasons][0] || 'no measurement' : 'no measurement'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function StorageCell({ row, quiet }: { row: CNPGFleetRow; quiet: boolean }) {
|
|
110
|
+
const fact = storageFact(row)
|
|
111
|
+
if (quiet && fact.tone === 'unknown') {
|
|
112
|
+
return (
|
|
113
|
+
<Tooltip content={fact.source ?? 'Volume usage not measured'}>
|
|
114
|
+
<span className="text-theme-text-tertiary">—</span>
|
|
115
|
+
</Tooltip>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
<FactValue fact={fact} />
|
|
121
|
+
{row.diskGrowth && <div className="text-xs"><FactValue fact={row.diskGrowth} className="text-theme-text-tertiary" /></div>}
|
|
122
|
+
</>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function RowStatusDot({ row }: { row: CNPGFleetRow }) {
|
|
127
|
+
const status = cnpgRowStatus(row)
|
|
128
|
+
return (
|
|
129
|
+
<Tooltip content={status.label}>
|
|
130
|
+
<span role="img" aria-label={status.label} className="inline-flex">
|
|
131
|
+
<StatusDot tone={status.tone} />
|
|
132
|
+
</span>
|
|
133
|
+
</Tooltip>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A token this long cannot wrap at a word boundary within the cell.
|
|
138
|
+
const UNBREAKABLE_TOKEN = 24
|
|
139
|
+
|
|
140
|
+
// A measured problem names what measured it, so a qualified match stays qualified in the fleet.
|
|
141
|
+
function problemTip(p: CNPGFleetRow['problems'][number]): string {
|
|
142
|
+
return p.source === 'measurement' && p.measuredBy ? `${p.title} (measured by ${p.measuredBy})` : p.title
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function AttentionCell({ row, onOpenAll }: { row: CNPGFleetRow; onOpenAll: () => void }) {
|
|
43
146
|
const top = row.problems.find((p) => p.severity !== 'posture') ?? row.problems[0]
|
|
44
147
|
if (!top) return <span className="text-theme-text-tertiary">—</span>
|
|
45
|
-
const
|
|
46
|
-
const
|
|
148
|
+
const others = row.problems.filter((p) => p !== top)
|
|
149
|
+
const headline = top.shortTitle ?? top.title
|
|
150
|
+
const unbreakable = headline.split(/\s+/).some((w) => w.length > UNBREAKABLE_TOKEN)
|
|
47
151
|
return (
|
|
48
152
|
<div className="min-w-0">
|
|
49
|
-
<Tooltip content={top
|
|
50
|
-
<div className={clsx('line-clamp-
|
|
153
|
+
<Tooltip content={problemTip(top)} wrapperClassName="w-full">
|
|
154
|
+
<div className={clsx('[overflow-wrap:normal]', unbreakable ? 'truncate' : 'line-clamp-3', toneTextClass(CNPG_PROBLEM_TONE[top.severity]))}>{headline}</div>
|
|
51
155
|
</Tooltip>
|
|
52
|
-
{
|
|
156
|
+
{top.unverifiedMatch && (
|
|
157
|
+
<div className="text-[11px] text-theme-text-tertiary">measured by {top.measuredBy}</div>
|
|
158
|
+
)}
|
|
159
|
+
{others.length > 0 && (
|
|
160
|
+
<Tooltip
|
|
161
|
+
content={
|
|
162
|
+
<ul className="space-y-1">
|
|
163
|
+
{others.map((p) => (
|
|
164
|
+
<li key={p.id} className="flex items-start gap-1.5">
|
|
165
|
+
<span className="mt-1 shrink-0">
|
|
166
|
+
<StatusDot tone={CNPG_PROBLEM_TONE[p.severity]} size="xs" />
|
|
167
|
+
</span>
|
|
168
|
+
<span>{problemTip(p)}</span>
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
</ul>
|
|
172
|
+
}
|
|
173
|
+
>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
onClick={(e) => {
|
|
177
|
+
e.stopPropagation()
|
|
178
|
+
onOpenAll()
|
|
179
|
+
}}
|
|
180
|
+
aria-label={`${others.length} more problems: open ${row.name} with every problem listed`}
|
|
181
|
+
className="rounded text-xs text-accent-text hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
|
182
|
+
>
|
|
183
|
+
+{others.length} more
|
|
184
|
+
</button>
|
|
185
|
+
</Tooltip>
|
|
186
|
+
)}
|
|
53
187
|
</div>
|
|
54
188
|
)
|
|
55
189
|
}
|
|
@@ -63,13 +197,17 @@ export function CNPGOverview({
|
|
|
63
197
|
onInspect,
|
|
64
198
|
inspected,
|
|
65
199
|
onClearNamespaces,
|
|
200
|
+
onCreate,
|
|
66
201
|
}: CNPGScreenProps) {
|
|
67
|
-
const navigate =
|
|
202
|
+
const navigate = useCNPGNavigate()
|
|
68
203
|
const { connection } = useConnection()
|
|
204
|
+
const operations = useCNPGOperations()
|
|
205
|
+
const apiBase = getApiBase()
|
|
69
206
|
const q = searchParams.get('q') ?? ''
|
|
70
207
|
const cat = (searchParams.get('cat') as CNPGProblemCategory | null) ?? null
|
|
71
208
|
const rawFilter = searchParams.get('filter') as Filter | null
|
|
72
|
-
|
|
209
|
+
// The view is the list of clusters: all of them unless asked for less.
|
|
210
|
+
const filter: Filter = rawFilter === 'attention' ? 'attention' : 'all'
|
|
73
211
|
|
|
74
212
|
const rows = useMemo(() => {
|
|
75
213
|
let list = fleet.rows
|
|
@@ -85,6 +223,14 @@ export function CNPGOverview({
|
|
|
85
223
|
const clustersCov = data.coverage.clusters
|
|
86
224
|
const total = fleet.rows.length
|
|
87
225
|
const context = connection.context || data.context
|
|
226
|
+
// Some namespaces were not read: the count is a floor, not the cluster's total.
|
|
227
|
+
const totalText = `${clustersCov?.state === 'partial' ? '≥' : ''}${total}`
|
|
228
|
+
const createAction = onCreate ? (
|
|
229
|
+
<button type="button" onClick={onCreate} className="btn-secondary inline-flex items-center gap-1.5 px-3 py-1.5 text-sm">
|
|
230
|
+
<Plus className="h-3.5 w-3.5" />
|
|
231
|
+
Create
|
|
232
|
+
</button>
|
|
233
|
+
) : undefined
|
|
88
234
|
|
|
89
235
|
if (total === 0) {
|
|
90
236
|
const state = clustersCov?.state ?? 'notInstalled'
|
|
@@ -95,15 +241,15 @@ export function CNPGOverview({
|
|
|
95
241
|
? { title: 'Loading PostgreSQL clusters', detail: 'Radar is still syncing CloudNativePG Clusters from the API server.' }
|
|
96
242
|
: state === 'error'
|
|
97
243
|
? { title: 'PostgreSQL clusters could not be read', detail: 'Reading CloudNativePG Clusters failed; see the Radar server log.' }
|
|
98
|
-
: state === 'partial'
|
|
99
|
-
? { title: 'No visible PostgreSQL clusters', detail: '
|
|
244
|
+
: state === 'partial' || state === 'uncached'
|
|
245
|
+
? { title: 'No visible PostgreSQL clusters', detail: coverageEmpty(clustersCov, 'PostgreSQL clusters') }
|
|
100
246
|
: namespaces.length > 0
|
|
101
|
-
? { title:
|
|
247
|
+
? { title: 'No PostgreSQL clusters in the selected namespaces', detail: `None in ${namespaces.join(', ')} in ${context}. Clear the namespace filter to see the whole cluster.` }
|
|
102
248
|
: { title: `No PostgreSQL clusters in ${context}`, detail: 'The CloudNativePG CRDs are installed. Clusters, backups and declarations appear here once they exist.' }
|
|
103
249
|
return (
|
|
104
250
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
105
|
-
<CNPGWorkspaceHeader title="
|
|
106
|
-
<
|
|
251
|
+
<CNPGWorkspaceHeader title="Clusters" actions={createAction} />
|
|
252
|
+
<ScreenEmptyState
|
|
107
253
|
icon={Database}
|
|
108
254
|
title={empty.title}
|
|
109
255
|
detail={empty.detail}
|
|
@@ -124,7 +270,7 @@ export function CNPGOverview({
|
|
|
124
270
|
if (q) chips.push({ label: `Search: ${q}`, onClear: () => onSetParams({ q: null }) })
|
|
125
271
|
if (namespaces.length > 0) chips.push({ label: `Namespace: ${namespaces.join(', ')}`, onClear: onClearNamespaces })
|
|
126
272
|
|
|
127
|
-
const segment = (id: Filter, label: string, n: number) => {
|
|
273
|
+
const segment = (id: Filter, label: string, n: number | string) => {
|
|
128
274
|
const on = filter === id
|
|
129
275
|
return (
|
|
130
276
|
<button
|
|
@@ -132,7 +278,7 @@ export function CNPGOverview({
|
|
|
132
278
|
type="button"
|
|
133
279
|
role="tab"
|
|
134
280
|
aria-selected={on}
|
|
135
|
-
onClick={() => onSetParams({ filter: id })}
|
|
281
|
+
onClick={() => onSetParams({ filter: id === 'all' ? null : id })}
|
|
136
282
|
className={clsx(
|
|
137
283
|
'inline-flex items-center gap-1.5 rounded-md px-3 py-1 text-sm font-medium transition-colors',
|
|
138
284
|
on ? 'bg-theme-surface text-theme-text-primary shadow-theme-sm' : 'text-theme-text-secondary hover:text-theme-text-primary',
|
|
@@ -145,27 +291,30 @@ export function CNPGOverview({
|
|
|
145
291
|
}
|
|
146
292
|
|
|
147
293
|
const lowerBound = fleet.incompleteKinds.length > 0
|
|
294
|
+
const attentionText = lowerBound ? fleet.attentionCount > 0 ? `≥${fleet.attentionCount}` : 'Unknown' : String(fleet.attentionCount)
|
|
295
|
+
const storageUnmeasured = cnpgStorageUnmeasured(fleet.rows)
|
|
148
296
|
|
|
149
297
|
return (
|
|
150
298
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
151
299
|
<CNPGWorkspaceHeader
|
|
152
|
-
title="
|
|
300
|
+
title="Clusters"
|
|
153
301
|
subtitle={
|
|
154
302
|
<>
|
|
155
|
-
{
|
|
156
|
-
{lowerBound ? '+' : ''} need attention
|
|
303
|
+
{totalText} PostgreSQL {total === 1 ? 'cluster' : 'clusters'} · {lowerBound && fleet.attentionCount === 0 ? 'Needs attention: unknown' : `${attentionText} need attention`}
|
|
157
304
|
<span className="text-theme-text-tertiary"> · {context}</span>
|
|
158
305
|
</>
|
|
159
306
|
}
|
|
307
|
+
actions={createAction}
|
|
160
308
|
/>
|
|
161
309
|
<div className="min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]">
|
|
162
310
|
<div className="space-y-3 px-5 pb-6 pt-3 xl:px-7">
|
|
163
|
-
<CoverageNotice fleet={fleet} data={data} />
|
|
311
|
+
<CoverageNotice fleet={fleet} data={data} kinds={['clusters', 'pods', 'backups', 'scheduledBackups', 'objectStores', 'poolers', 'databases', 'databaseRoles', 'publications', 'subscriptions']} includeJobs />
|
|
312
|
+
<CNPGOperatorBanner namespaces={fleet.rows.map((r) => r.namespace)} className="" />
|
|
164
313
|
|
|
165
314
|
<div className="flex flex-wrap items-center gap-2">
|
|
166
315
|
<div role="tablist" aria-label="Clusters" className="inline-flex rounded-lg bg-theme-elevated p-0.5">
|
|
167
|
-
{segment('
|
|
168
|
-
{segment('
|
|
316
|
+
{segment('all', 'All clusters', totalText)}
|
|
317
|
+
{segment('attention', 'Needs attention', attentionText)}
|
|
169
318
|
</div>
|
|
170
319
|
{CNPG_PROBLEM_CATEGORIES.filter((c) => fleet.categoryCounts[c.id] > 0).map((c) => {
|
|
171
320
|
const on = cat === c.id
|
|
@@ -181,7 +330,7 @@ export function CNPGOverview({
|
|
|
181
330
|
)}
|
|
182
331
|
>
|
|
183
332
|
{c.label}
|
|
184
|
-
<span className="font-mono">{fleet.categoryCounts[c.id]}</span>
|
|
333
|
+
<span className="font-mono">{lowerBound ? '≥' : ''}{fleet.categoryCounts[c.id]}</span>
|
|
185
334
|
</button>
|
|
186
335
|
)
|
|
187
336
|
})}
|
|
@@ -198,36 +347,40 @@ export function CNPGOverview({
|
|
|
198
347
|
</div>
|
|
199
348
|
|
|
200
349
|
<FilterChips chips={chips} />
|
|
350
|
+
{storageUnmeasured && (
|
|
351
|
+
<div className="text-xs text-theme-text-tertiary">
|
|
352
|
+
Storage: volume usage is not measured for any cluster here ({storageUnmeasured}). Retained WAL and resize state are on each cluster’s Storage tab.
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
201
355
|
|
|
202
356
|
<div className="overflow-hidden rounded-xl border border-theme-border bg-theme-surface shadow-theme-sm">
|
|
203
357
|
<div className={TABLE_WRAP}>
|
|
204
|
-
|
|
358
|
+
{/* Fits an ~850px content area (a 1280px window) without scrolling: the
|
|
359
|
+
two fixed columns take 16rem and the percentages stay under the rest. */}
|
|
360
|
+
<table className="w-full min-w-[780px] table-fixed">
|
|
205
361
|
<colgroup>
|
|
206
362
|
<col className="w-[22%]" />
|
|
207
|
-
<col className="w-[
|
|
363
|
+
<col className="w-[18%]" />
|
|
364
|
+
<col className="w-[13%]" />
|
|
208
365
|
<col className="w-[15%]" />
|
|
209
|
-
<col
|
|
210
|
-
<col className="w-[
|
|
211
|
-
<col className="w-[5%]" />
|
|
212
|
-
<col className="w-[14%]" />
|
|
213
|
-
<col className="w-[10%]" />
|
|
366
|
+
<col />
|
|
367
|
+
<col className="w-[7.5rem]" />
|
|
214
368
|
</colgroup>
|
|
215
369
|
<thead className={TABLE_HEAD}>
|
|
216
370
|
<tr>
|
|
217
|
-
<th className={
|
|
218
|
-
<th className={
|
|
219
|
-
<th className={
|
|
220
|
-
<th className={
|
|
221
|
-
<th className={
|
|
222
|
-
<th className={
|
|
223
|
-
<th className={TH}>Needs attention</th>
|
|
224
|
-
<th className={TH}><span className="sr-only">Actions</span></th>
|
|
371
|
+
<th className={TH_WRAP}>Cluster</th>
|
|
372
|
+
<th className={TH_WRAP}>Replication</th>
|
|
373
|
+
<th className={TH_WRAP}>Storage</th>
|
|
374
|
+
<th className={TH_WRAP}>Backups</th>
|
|
375
|
+
<th className={TH_WRAP}>Needs attention</th>
|
|
376
|
+
<th className={TH_WRAP}><span className="sr-only">Actions</span></th>
|
|
225
377
|
</tr>
|
|
226
378
|
</thead>
|
|
227
379
|
<tbody className={TBODY}>
|
|
228
380
|
{rows.map((row) => {
|
|
229
381
|
const ref: SelectedResource = { kind: 'clusters', group: 'postgresql.cnpg.io', namespace: row.namespace, name: row.name }
|
|
230
|
-
const active =
|
|
382
|
+
const active = sameSelectedResource(inspected, ref)
|
|
383
|
+
const operation = latestCNPGOperation(operations, { apiBase, context, namespace: row.namespace, name: row.name, uid: row.cluster.metadata.uid })
|
|
231
384
|
return (
|
|
232
385
|
<tr
|
|
233
386
|
key={row.key}
|
|
@@ -236,23 +389,44 @@ export function CNPGOverview({
|
|
|
236
389
|
aria-selected={active}
|
|
237
390
|
>
|
|
238
391
|
<td className={TD}>
|
|
239
|
-
<div className="flex items-
|
|
240
|
-
<
|
|
241
|
-
<
|
|
392
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
393
|
+
<span className="mt-1.5 shrink-0"><RowStatusDot row={row} /></span>
|
|
394
|
+
<div className="min-w-0">
|
|
395
|
+
<BreakText value={row.name} after="-" className="font-medium" />
|
|
396
|
+
{operation && <CNPGFleetOperation op={operation} onFollow={() => navigate(cnpgClusterFullPath(row.namespace, row.name, context))} />}
|
|
397
|
+
<div className="flex min-w-0 items-baseline text-xs text-theme-text-tertiary">
|
|
398
|
+
<Tooltip content={`Namespace ${row.namespace}`} wrapperClassName="min-w-0">
|
|
399
|
+
<span className="block truncate">{row.namespace}</span>
|
|
400
|
+
</Tooltip>
|
|
401
|
+
{row.pgVersion && (
|
|
402
|
+
<span className="shrink-0 whitespace-pre">
|
|
403
|
+
{' · '}
|
|
404
|
+
<Tooltip content="PostgreSQL version"><span>PG {row.pgVersion}</span></Tooltip>
|
|
405
|
+
</span>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
<div className="mt-0.5 font-mono text-xs">
|
|
409
|
+
<ReadyCell row={row} />
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
242
412
|
</div>
|
|
243
|
-
<div className="truncate pl-4 text-xs text-theme-text-tertiary">{row.namespace}</div>
|
|
244
|
-
<div className="pl-4"><InstancePills row={row} /></div>
|
|
245
|
-
</td>
|
|
246
|
-
<td className={clsx(TD, 'font-mono')}>
|
|
247
|
-
{row.instances.ready ?? '–'}/{row.instances.desired ?? '–'}
|
|
248
413
|
</td>
|
|
249
414
|
<td className={TD}><FactValue fact={row.replication} /></td>
|
|
415
|
+
<td className={TD}><StorageCell row={row} quiet={!!storageUnmeasured} /></td>
|
|
250
416
|
<td className={TD}><FactValue fact={row.protection.summary} /></td>
|
|
251
|
-
<td className={TD
|
|
252
|
-
|
|
253
|
-
|
|
417
|
+
<td className={clsx(TD, 'overflow-hidden')}>
|
|
418
|
+
<AttentionCell
|
|
419
|
+
row={row}
|
|
420
|
+
onOpenAll={() =>
|
|
421
|
+
navigate(cnpgClusterProblemsPath(row.namespace, row.name, connection.context || undefined), {
|
|
422
|
+
state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
/>
|
|
426
|
+
</td>
|
|
254
427
|
<td className={clsx(TD, 'text-right')}>
|
|
255
|
-
<div className="flex items-center justify-end gap-
|
|
428
|
+
<div className="flex items-center justify-end gap-0.5">
|
|
429
|
+
<Tooltip content="Logs from every instance">
|
|
256
430
|
<button
|
|
257
431
|
type="button"
|
|
258
432
|
aria-label={`Logs from every instance of ${row.name}`}
|
|
@@ -264,10 +438,11 @@ export function CNPGOverview({
|
|
|
264
438
|
state: { returnLabel: currentPageLabel(), returnCtx: connection.context },
|
|
265
439
|
})
|
|
266
440
|
}}
|
|
267
|
-
className="inline-flex items-center
|
|
441
|
+
className="inline-flex items-center rounded-md p-1.5 text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
268
442
|
>
|
|
269
|
-
<FileText className="h-3 w-3" />
|
|
443
|
+
<FileText className="h-3.5 w-3.5" />
|
|
270
444
|
</button>
|
|
445
|
+
</Tooltip>
|
|
271
446
|
<button
|
|
272
447
|
type="button"
|
|
273
448
|
aria-label={`Open ${row.name}`}
|
|
@@ -292,7 +467,7 @@ export function CNPGOverview({
|
|
|
292
467
|
{rows.length === 0 && (
|
|
293
468
|
<div className="px-4 py-6 text-sm text-theme-text-tertiary">
|
|
294
469
|
{filter === 'attention' && !cat && !q
|
|
295
|
-
? 'No clusters need attention.'
|
|
470
|
+
? lowerBound ? 'No attention findings in the readable data; other data was not read.' : 'No clusters need attention.'
|
|
296
471
|
: 'No clusters match these filters.'}{' '}
|
|
297
472
|
<button type="button" onClick={() => onSetParams({ filter: 'all', cat: null, q: null })} className="font-medium text-accent-text hover:underline">
|
|
298
473
|
Show all clusters
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { expect, it } from 'vitest'
|
|
3
|
+
import { toneTextClass } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { ParameterRow, cnpgParameterBoolean } from './CNPGParametersInEffect'
|
|
5
|
+
import { cnpgParametersView } from './inspectModel'
|
|
6
|
+
import type { CNPGParametersResponse } from '../../api/cnpg-inspect'
|
|
7
|
+
|
|
8
|
+
function render(values: string[]) {
|
|
9
|
+
const reports = values.map((value, i) => ({ pod: `orders-${i + 1}`, value }))
|
|
10
|
+
const response: CNPGParametersResponse = { cluster: { name: 'orders', namespace: 'db', uid: 'u' }, sampledAt: '', permission: { exec: 'allowed' }, state: 'ok', declared: [{ name: 'max_connections', value: '100' }], instances: reports.map(({ pod, value }) => ({ pod, role: 'replica', state: 'ok', settings: [{ name: 'max_connections', value, source: 'configuration file', context: 'postmaster', pendingRestart: false, setByClient: false }] })) }
|
|
11
|
+
const row = cnpgParametersView(response).rows[0]
|
|
12
|
+
return renderToStaticMarkup(<table><tbody><ParameterRow row={row} reports={reports} readPods={reports.map((p) => p.pod)} unreadText="Not reported" /></tbody></table>)
|
|
13
|
+
}
|
|
14
|
+
it('renders an agreed value once without repeating the summary read set', () => {
|
|
15
|
+
const html = render(['100', '100', '100'])
|
|
16
|
+
expect(html).toContain('font-mono text-theme-text-primary">100</span>')
|
|
17
|
+
expect(html).not.toContain('on orders-1, orders-2, orders-3')
|
|
18
|
+
expect(html).not.toContain('orders-1: 100')
|
|
19
|
+
})
|
|
20
|
+
it('retains the per-instance list when reported values differ', () => {
|
|
21
|
+
const html = render(['100', '200'])
|
|
22
|
+
expect(html).toContain('orders-1: 100 · orders-2: 200')
|
|
23
|
+
expect(html).toContain(toneTextClass('degraded'))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it.each(['on', 'ON', 'true', 't', 'yes', 'y', '1'])('accepts PostgreSQL true spelling %s', (value) => expect(cnpgParameterBoolean(value)).toBe(true))
|
|
27
|
+
it.each(['off', 'false', 'f', 'no', 'n', '0'])('accepts PostgreSQL false spelling %s', (value) => expect(cnpgParameterBoolean(value)).toBe(false))
|
|
28
|
+
it.each(['o', '', 'truth', '10', 'ofx'])('does not treat %s as a boolean', (value) => expect(cnpgParameterBoolean(value)).toBeUndefined())
|
|
29
|
+
it('hints equivalent booleans without inferring drift from different text', () => {
|
|
30
|
+
const row = { name: 'log_truncate_on_rotation', declared: 'false', value: 'off', takesEffect: 'reload', setByClient: false, pendingRestart: [], unreported: [] }
|
|
31
|
+
const renderRow = () => renderToStaticMarkup(<table><tbody><ParameterRow row={row} reports={[{ pod: 'orders-1', value: row.value }]} readPods={['orders-1']} unreadText="Not reported" /></tbody></table>)
|
|
32
|
+
expect(renderRow()).toContain('same as declared')
|
|
33
|
+
row.declared = 'off'
|
|
34
|
+
expect(renderRow()).not.toContain('same as declared')
|
|
35
|
+
row.declared = 'false'
|
|
36
|
+
row.value = 'on'
|
|
37
|
+
expect(renderRow()).not.toContain('same as declared')
|
|
38
|
+
expect(renderRow()).not.toContain('mismatch')
|
|
39
|
+
row.declared = '0'; row.value = '0'
|
|
40
|
+
expect(renderRow()).not.toContain('same as declared')
|
|
41
|
+
row.declared = '1024MB'; row.value = '1GB'
|
|
42
|
+
expect(renderRow()).not.toContain('mismatch')
|
|
43
|
+
})
|
|
44
|
+
it('names reporting subsets and restart exceptions in the row', () => {
|
|
45
|
+
const row = { name: 'work_mem', declared: '8MB', value: '8MB', takesEffect: 'reload', setByClient: false, pendingRestart: [], unreported: ['orders-2'] }
|
|
46
|
+
const html = renderToStaticMarkup(<table><tbody><ParameterRow row={row} reports={[{ pod: 'orders-1', value: '8MB' }]} readPods={['orders-1', 'orders-2']} unreadText="Not reported" /></tbody></table>)
|
|
47
|
+
expect(html).toContain('on orders-1')
|
|
48
|
+
expect(html).toContain('Not reported by orders-2')
|
|
49
|
+
row.unreported = []; row.pendingRestart = ['orders-1'] as never[]
|
|
50
|
+
expect(renderToStaticMarkup(<table><tbody><ParameterRow row={row} reports={[{ pod: 'orders-1', value: '8MB' }]} readPods={['orders-1']} unreadText="Not reported" /></tbody></table>)).toContain('Restart pending on orders-1')
|
|
51
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SectionHeading, formatAge, formatGrant, toneTextClass } from '@skyhook-io/k8s-ui'
|
|
2
|
+
import { useCNPGParameters } from '../../api/cnpg-inspect'
|
|
3
|
+
import { cnpgParametersView, type CNPGParameterRow } from './inspectModel'
|
|
4
|
+
import { RefreshFailedNotice } from '../workspace/layout'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The parameters the Cluster declares beside what each instance's PostgreSQL
|
|
8
|
+
* reports: whether a change took effect, and whether it waits for a restart.
|
|
9
|
+
* Observation only — changes go through the Cluster spec.
|
|
10
|
+
*/
|
|
11
|
+
export function CNPGParametersInEffect({ namespace, name, declared, scope }: { namespace: string; name: string; declared: Record<string, string>; scope?: { declaredInstances?: number; expectedInstances?: string[] } }) {
|
|
12
|
+
const q = useCNPGParameters(namespace, name)
|
|
13
|
+
const d = q.data
|
|
14
|
+
const declarations = Object.entries(declared).sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => ({ name, value }))
|
|
15
|
+
const view = cnpgParametersView(d ?? {
|
|
16
|
+
cluster: { namespace, name, uid: '' }, sampledAt: '', permission: { exec: 'unknown' }, state: 'error', instances: [],
|
|
17
|
+
declared: declarations,
|
|
18
|
+
}, declarations, scope)
|
|
19
|
+
const unavailable = !d && q.error instanceof Error
|
|
20
|
+
? `Not read: ${q.error.message}`
|
|
21
|
+
: !d ? (q.isLoading ? 'Not read: reading each instance…' : 'Not read: instance values unavailable')
|
|
22
|
+
: d.state === 'denied' ? `Reading instance values needs ${formatGrant(d.permission.grant) ?? `create pods/exec in namespace ${namespace}`}.`
|
|
23
|
+
: d.error ? `Not read: ${d.error}` : null
|
|
24
|
+
const content = (
|
|
25
|
+
<>
|
|
26
|
+
<RefreshFailedNotice queries={[q]} className="mb-2" />
|
|
27
|
+
{unavailable && <p className="mb-2 text-sm text-theme-text-tertiary">{unavailable}</p>}
|
|
28
|
+
{d && !unavailable && declarations.length > 0 && <p className={`mb-2 text-sm ${toneTextClass(view.summary.tone)}`}>{view.summary.text}</p>}
|
|
29
|
+
{declarations.length === 0 && <p className="text-sm text-theme-text-tertiary">No parameters declared in spec.postgresql.parameters.</p>}
|
|
30
|
+
{declarations.length > 0 && (
|
|
31
|
+
<div className="overflow-x-auto">
|
|
32
|
+
<table className="w-full text-sm">
|
|
33
|
+
<thead>
|
|
34
|
+
<tr className="border-b border-theme-border text-left text-[11px] uppercase tracking-wide text-theme-text-tertiary">
|
|
35
|
+
<th className="py-1.5 pr-4 font-medium">Parameter</th>
|
|
36
|
+
<th className="py-1.5 pr-4 font-medium">Declared</th>
|
|
37
|
+
<th className="py-1.5 pr-4 font-medium">Reported by instance</th>
|
|
38
|
+
<th className="py-1.5 font-medium">A change applies on</th>
|
|
39
|
+
</tr>
|
|
40
|
+
</thead>
|
|
41
|
+
<tbody>
|
|
42
|
+
{view.rows.map((r) => view.read.length === 0 ? (
|
|
43
|
+
<tr key={r.name} className="border-b border-theme-border/60 align-top last:border-0">
|
|
44
|
+
<td className="py-1.5 pr-4 font-mono text-xs text-theme-text-primary">{r.name}</td>
|
|
45
|
+
<td className="py-1.5 pr-4 font-mono text-xs text-theme-text-secondary">{r.declared === '' ? <span className="text-theme-text-tertiary">(empty)</span> : r.declared}</td>
|
|
46
|
+
<td /><td />
|
|
47
|
+
</tr>
|
|
48
|
+
) : (
|
|
49
|
+
<ParameterRow key={r.name} row={r} readPods={view.read.map((i) => i.pod)}
|
|
50
|
+
unreadText={d?.skipped?.includes(r.name) ? 'Not read: not a parameter name'
|
|
51
|
+
: !d ? (q.isLoading ? 'Not read: loading' : 'Not read: request failed')
|
|
52
|
+
: d.state === 'denied' ? 'Not read: pods/exec denied'
|
|
53
|
+
: !d.declared.some((p) => p.name.toLowerCase() === r.name.toLowerCase()) ? 'Not sampled'
|
|
54
|
+
: view.read.length === 0 ? (d.instances.length === 0 ? 'Not read: no instance Pod' : 'Not read: no instance answered')
|
|
55
|
+
: 'Not reported'}
|
|
56
|
+
reports={view.read.flatMap((i) => {
|
|
57
|
+
const setting = i.settings!.find((s) => s.name.toLowerCase() === r.name.toLowerCase())
|
|
58
|
+
return setting?.value != null ? [{ pod: i.pod, value: setting.value }] : []
|
|
59
|
+
})} />
|
|
60
|
+
))}
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
{d && view.read.length > 0 && <p className="mt-2 text-[11.5px] text-theme-text-tertiary">
|
|
66
|
+
Read {d.sampledAt ? `${formatAge(d.sampledAt)} ago` : 'just now'} from a new Radar session on each of {view.read.map((i) => i.pod).join(', ')}. A role, database or
|
|
67
|
+
client can set its own value for a parameter a session can change. PostgreSQL normalizes units and boolean spellings (such as false to off), so different text alone does not mean a different value.
|
|
68
|
+
</p>}
|
|
69
|
+
{d?.skipped && d.skipped.length > 0 && <p className="mt-1 text-xs text-theme-text-tertiary">Not read (not a parameter name): {d.skipped.join(', ')}.</p>}
|
|
70
|
+
{!!d?.omitted && <p className="mt-1 text-xs text-theme-text-tertiary">{d.omitted} declaration{d.omitted === 1 ? ' was' : 's were'} not sampled.</p>}
|
|
71
|
+
{view.unread.length > 0 && (
|
|
72
|
+
<p className={`mt-1 text-xs ${toneTextClass('unknown')}`}>
|
|
73
|
+
Not read on {view.unread.map((i) => `${i.pod} (${i.error ?? i.state})`).join(', ')}.
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
</>
|
|
77
|
+
)
|
|
78
|
+
return <><SectionHeading hint="spec.postgresql.parameters">PostgreSQL parameters</SectionHeading>{content}</>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function ParameterRow({ row, reports, unreadText, readPods }: { row: CNPGParameterRow; reports: { pod: string; value: string }[]; unreadText: string; readPods: string[] }) {
|
|
82
|
+
const namesNeeded = !!row.perInstance || row.pendingRestart.length > 0 || row.unreported.length > 0 || reports.length !== readPods.length || reports.some((p) => !readPods.includes(p.pod))
|
|
83
|
+
const declaredBoolean = cnpgParameterBoolean(row.declared)
|
|
84
|
+
const sameBoolean = declaredBoolean !== undefined && reports.length > 0 && reports.some((p) => p.value !== row.declared) && reports.every((p) => ['on', 'off'].includes(p.value.trim().toLowerCase()) && cnpgParameterBoolean(p.value) === declaredBoolean)
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<tr className="border-b border-theme-border/60 align-top last:border-0">
|
|
88
|
+
<td className="py-1.5 pr-4 font-mono text-xs text-theme-text-primary">{row.name}</td>
|
|
89
|
+
<td className="py-1.5 pr-4 font-mono text-xs text-theme-text-secondary">{row.declared === '' ? <span className="text-theme-text-tertiary">(empty)</span> : row.declared}</td>
|
|
90
|
+
<td className="py-1.5 pr-4 text-xs">
|
|
91
|
+
{reports.length > 0 ? (
|
|
92
|
+
row.perInstance
|
|
93
|
+
? <span className={toneTextClass('degraded')}>{reports.map((p) => `${p.pod}: ${p.value === '' ? '(empty)' : p.value}`).join(' · ')}</span>
|
|
94
|
+
: <><span className="font-mono text-theme-text-primary">{row.value === '' ? '(empty)' : row.value}</span>{namesNeeded && <div className="text-theme-text-tertiary">on {reports.map((p) => p.pod).join(', ')}</div>}</>
|
|
95
|
+
) : row.setByClient ? (
|
|
96
|
+
<span className="text-theme-text-tertiary">Set by each connection, so not readable here</span>
|
|
97
|
+
) : (
|
|
98
|
+
<span className="text-theme-text-tertiary">{unreadText}</span>
|
|
99
|
+
)}
|
|
100
|
+
{sameBoolean && <span className="ml-1.5 text-theme-text-tertiary">same as declared</span>}
|
|
101
|
+
{row.source && row.source !== 'configuration file' && !row.setByClient && <span className="ml-1.5 text-theme-text-tertiary">source: {row.source}</span>}
|
|
102
|
+
{row.pendingRestart.length > 0 && <div className={toneTextClass('degraded')}>Restart pending on {row.pendingRestart.join(', ')}</div>}
|
|
103
|
+
{row.unreported.length > 0 && unreadText === 'Not reported' && (
|
|
104
|
+
<div className="text-theme-text-tertiary">Not reported by {row.unreported.join(', ')}</div>
|
|
105
|
+
)}
|
|
106
|
+
</td>
|
|
107
|
+
<td className="py-1.5 text-xs text-theme-text-secondary">{row.takesEffect}</td>
|
|
108
|
+
</tr>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function cnpgParameterBoolean(value: string): boolean | undefined {
|
|
113
|
+
const normalized = value.trim().toLowerCase()
|
|
114
|
+
if (normalized === '1') return true
|
|
115
|
+
if (normalized === '0') return false
|
|
116
|
+
if (!normalized) return undefined
|
|
117
|
+
const matches = ['on', 'off', 'true', 'false', 'yes', 'no'].filter((spelling) => spelling.startsWith(normalized))
|
|
118
|
+
if (matches.length !== 1) return undefined
|
|
119
|
+
return ['on', 'true', 'yes'].includes(matches[0])
|
|
120
|
+
}
|