@skyhook-io/radar-app 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/App.tsx +125 -87
- 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.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 +266 -39
- 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/radarFeatures.render.test.tsx +47 -3
- package/src/api/radarFeatures.ts +4 -1
- 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/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 +6 -55
- package/src/components/capacity/CapacityView.test.tsx +323 -0
- 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 +11 -129
- 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 +228 -122
- 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/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/gitops/ArgoResourceDiffLoader.tsx +4 -2
- package/src/components/gitops/GitOpsView.tsx +16 -23
- 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/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/HomeView.tsx +20 -9
- package/src/components/home/ProblemsPanel.test.tsx +81 -0
- package/src/components/home/TopologyPreview.tsx +2 -2
- 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/settings/LocalIntegrationStatus.test.tsx +52 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
- package/src/components/settings/SettingsDialog.tsx +28 -15
- package/src/components/timeline/TimelineList.tsx +22 -24
- package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
- package/src/components/timeline/TimelineLoadError.tsx +96 -0
- package/src/components/timeline/TimelineView.tsx +18 -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/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 +10 -1
- package/src/components/workload/WorkloadView.tsx +27 -239
- 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/contexts/CapabilitiesContext.tsx +1 -1
- 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,52 +1,38 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { refetchCNPGDetail } from './refresh'
|
|
4
|
+
import { useLocation, useSearchParams } from 'react-router-dom'
|
|
5
|
+
import { Activity, Database, Gauge, HardDrive, Network, Settings2, ShieldCheck, Unplug } from 'lucide-react'
|
|
6
|
+
import { CNPG_GROUP, refToSelectedResource, Tooltip, Badge, type WorkloadExtraTab } from '@skyhook-io/k8s-ui'
|
|
5
7
|
import type { SelectedResource } from '../../types'
|
|
6
8
|
import { useConnection } from '../../context/ConnectionContext'
|
|
7
|
-
import { useContexts } from '../../api/client'
|
|
9
|
+
import { useContexts, useRadarFeature } from '../../api/client'
|
|
8
10
|
import { useContextSwitchFlow } from '../useContextSwitchFlow'
|
|
9
11
|
import { WorkloadView } from '../workload/WorkloadView'
|
|
10
|
-
import { EmptyState } from '../capacity/shared'
|
|
11
12
|
import { CNPGClusterActivity } from './CNPGClusterActivity'
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { CNPGPerformance } from './CNPGPerformance'
|
|
14
|
+
import { CNPGReplicationTab } from './CNPGReplicationTab'
|
|
15
|
+
import { CNPGBackupsTab, CNPGClusterServing, CNPGStorageTab, CNPGTabMark } from './CNPGClusterTabs'
|
|
16
|
+
import { CNPGConfiguration } from './CNPGConfiguration'
|
|
17
|
+
import type { CNPGChartGroup } from './CNPGTrends'
|
|
18
|
+
import { CNPG_DETAIL_KINDS, CNPG_SCREENS, cnpgDetailKindFor, cnpgDetailPath, cnpgScreenPath, cnpgViewNamedForKind, type CNPGDetailTarget } from './routes'
|
|
19
|
+
import { CNPG_CLUSTER_TAB_ORDER, cnpgDimensionTab } from './paths'
|
|
20
|
+
import { currentPageLabel } from '../../utils/page-links'
|
|
21
|
+
import { CNPGOperatorBanner } from './CNPGOperatorBanner'
|
|
22
|
+
import { ScreenEmptyState } from '../workspace/layout'
|
|
23
|
+
import { useCNPGNavigate } from './useCNPGNavigate'
|
|
17
24
|
|
|
25
|
+
// Set by drill-downs; Esc then goes back to that page instead of the home view.
|
|
18
26
|
interface ReturnState {
|
|
19
27
|
returnLabel?: string
|
|
20
28
|
returnCtx?: string
|
|
21
29
|
}
|
|
22
30
|
|
|
23
|
-
function ClusterProtectionTab({ namespace, name, onInspect }: { namespace: string; name: string; onInspect: (r: SelectedResource) => void }) {
|
|
24
|
-
const { query, fleet } = useCNPGFleet([namespace])
|
|
25
|
-
const [searchParams] = useSearchParams()
|
|
26
|
-
return (
|
|
27
|
-
<CNPGScreenGate query={query} fleet={fleet}>
|
|
28
|
-
{(data, readyFleet) => (
|
|
29
|
-
<CNPGProtection
|
|
30
|
-
data={data}
|
|
31
|
-
fleet={readyFleet}
|
|
32
|
-
namespaces={[namespace]}
|
|
33
|
-
searchParams={searchParams}
|
|
34
|
-
onSetParams={() => {}}
|
|
35
|
-
onInspect={onInspect}
|
|
36
|
-
inspected={null}
|
|
37
|
-
onClearNamespaces={() => {}}
|
|
38
|
-
scopeCluster={{ namespace, name }}
|
|
39
|
-
/>
|
|
40
|
-
)}
|
|
41
|
-
</CNPGScreenGate>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
31
|
/**
|
|
46
32
|
* The full detail of a CloudNativePG object, framed by the workspace: the
|
|
47
|
-
* Resources sidebar keeps the workspace destination highlighted
|
|
48
|
-
*
|
|
49
|
-
*
|
|
33
|
+
* Resources sidebar keeps the workspace destination highlighted and the crumb
|
|
34
|
+
* names the object's place. The object's own sections come from Radar's
|
|
35
|
+
* detail view.
|
|
50
36
|
*
|
|
51
37
|
* `ctx` in the URL pins the Kubernetes context; when the active context is a
|
|
52
38
|
* different one, the page says so instead of loading a same-named object.
|
|
@@ -61,9 +47,12 @@ export function CNPGDetailPage({
|
|
|
61
47
|
onOpenResource: (resource: SelectedResource) => void
|
|
62
48
|
}) {
|
|
63
49
|
const location = useLocation()
|
|
64
|
-
const navigate =
|
|
50
|
+
const navigate = useCNPGNavigate()
|
|
65
51
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
66
52
|
const { connection } = useConnection()
|
|
53
|
+
const queryClient = useQueryClient()
|
|
54
|
+
const { plural, group, namespace, name } = target
|
|
55
|
+
const refresh = useCallback(() => refetchCNPGDetail(queryClient, { plural, group, namespace, name }), [queryClient, plural, group, namespace, name])
|
|
67
56
|
const activeContext = connection.context
|
|
68
57
|
const pinnedContext = searchParams.get('ctx')
|
|
69
58
|
|
|
@@ -95,27 +84,95 @@ export function CNPGDetailPage({
|
|
|
95
84
|
[navigate, activeContext, onOpenResource],
|
|
96
85
|
)
|
|
97
86
|
|
|
87
|
+
// The extra tabs require the advertised runtime contracts.
|
|
88
|
+
const cnpgWorkspace = useRadarFeature('cnpgWorkspace').support === 'supported'
|
|
89
|
+
const isCluster = target.plural === 'clusters' && target.group === CNPG_GROUP && cnpgWorkspace
|
|
90
|
+
// A tab change on this page, applied like a tab click: the previous tab's
|
|
91
|
+
// own params (section, chart group, instance) are dropped unless named.
|
|
92
|
+
const goTab = useCallback(
|
|
93
|
+
(tab: string, extra: Record<string, string> = {}) => {
|
|
94
|
+
const params = new URLSearchParams(searchParams)
|
|
95
|
+
for (const k of ['section', 'charts', 'instance', 'pod', 'container', 'since', 'until']) params.delete(k)
|
|
96
|
+
params.set('tab', tab)
|
|
97
|
+
for (const [k, v] of Object.entries(extra)) params.set(k, v)
|
|
98
|
+
setSearchParams(params, { replace: true, state: location.state })
|
|
99
|
+
},
|
|
100
|
+
[searchParams, setSearchParams, location.state],
|
|
101
|
+
)
|
|
102
|
+
const openHistory = useCallback((charts: CNPGChartGroup) => goTab('performance', { section: 'history', charts }), [goTab])
|
|
98
103
|
const extraTabs = useMemo<WorkloadExtraTab[] | undefined>(() => {
|
|
99
|
-
if (target.plural !== 'clusters') return undefined
|
|
104
|
+
if (target.plural !== 'clusters' || target.group !== CNPG_GROUP) return undefined
|
|
105
|
+
const ns = target.namespace
|
|
106
|
+
const name = target.name
|
|
107
|
+
const activity: WorkloadExtraTab = {
|
|
108
|
+
id: 'activity',
|
|
109
|
+
label: 'Activity',
|
|
110
|
+
icon: <Activity className="h-4 w-4" />,
|
|
111
|
+
replaces: 'timeline',
|
|
112
|
+
render: () => <CNPGClusterActivity namespace={ns} name={name} onNavigate={openRelated} />,
|
|
113
|
+
}
|
|
114
|
+
if (!cnpgWorkspace) return [activity]
|
|
100
115
|
return [
|
|
101
116
|
{
|
|
102
|
-
id: '
|
|
103
|
-
label: '
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
render: () =>
|
|
117
|
+
id: 'replication',
|
|
118
|
+
label: 'Replication',
|
|
119
|
+
badge: <CNPGTabMark namespace={ns} name={name} id="replication" />,
|
|
120
|
+
icon: <Network className="h-4 w-4" />,
|
|
121
|
+
render: () => (
|
|
122
|
+
<CNPGReplicationTab
|
|
123
|
+
namespace={ns}
|
|
124
|
+
name={name}
|
|
125
|
+
onOpenLogs={(pod) => goTab('logs', { pod })}
|
|
126
|
+
onOpenHistory={() => openHistory('replication')}
|
|
127
|
+
onNavigate={(ref) => openRelated(refToSelectedResource(ref))}
|
|
128
|
+
/>
|
|
129
|
+
),
|
|
107
130
|
},
|
|
108
131
|
{
|
|
109
|
-
id: '
|
|
110
|
-
label: '
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
id: 'storage',
|
|
133
|
+
label: 'Storage',
|
|
134
|
+
badge: <CNPGTabMark namespace={ns} name={name} id="storage" />,
|
|
135
|
+
icon: <HardDrive className="h-4 w-4" />,
|
|
136
|
+
render: () => <CNPGStorageTab namespace={ns} name={name} onOpenHistory={() => openHistory('storage')} onOpenReplication={() => goTab('replication')} />,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'performance',
|
|
140
|
+
label: 'Performance',
|
|
141
|
+
icon: <Gauge className="h-4 w-4" />,
|
|
142
|
+
render: () => (
|
|
143
|
+
<CNPGPerformance
|
|
144
|
+
namespace={ns}
|
|
145
|
+
name={name}
|
|
146
|
+
onOpenInterval={(tab, since, until) => {
|
|
147
|
+
const params = new URLSearchParams(searchParams)
|
|
148
|
+
params.delete('pod')
|
|
149
|
+
params.set('tab', tab)
|
|
150
|
+
params.set('since', since)
|
|
151
|
+
params.set('until', until)
|
|
152
|
+
setSearchParams(params, { state: location.state })
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'backups',
|
|
159
|
+
label: 'Backups',
|
|
160
|
+
badge: <CNPGTabMark namespace={ns} name={name} id="protection" />,
|
|
161
|
+
icon: <ShieldCheck className="h-4 w-4" />,
|
|
113
162
|
render: () => (
|
|
114
|
-
<
|
|
163
|
+
<CNPGBackupsTab
|
|
164
|
+
namespace={ns}
|
|
165
|
+
name={name}
|
|
166
|
+
onInspect={onOpenResource}
|
|
167
|
+
onOpenYaml={() => goTab('yaml')}
|
|
168
|
+
onOpenLogs={(pod, container) => goTab('logs', { pod, container })}
|
|
169
|
+
onOpenOperator={() => navigate(cnpgScreenPath('operator'), { state: { returnLabel: currentPageLabel(), returnCtx: activeContext } satisfies ReturnState })}
|
|
170
|
+
/>
|
|
115
171
|
),
|
|
116
172
|
},
|
|
173
|
+
activity,
|
|
117
174
|
]
|
|
118
|
-
}, [target.plural, target.namespace, target.name, onOpenResource, openRelated])
|
|
175
|
+
}, [cnpgWorkspace, target.plural, target.group, target.namespace, target.name, goTab, openHistory, onOpenResource, openRelated, navigate, activeContext, searchParams, setSearchParams, location.state])
|
|
119
176
|
|
|
120
177
|
if (pinnedContext && activeContext && pinnedContext !== activeContext) {
|
|
121
178
|
return <NotInContext target={target} pinnedContext={pinnedContext} activeContext={activeContext} homeLabel={home.label} homePath={home.path} />
|
|
@@ -123,61 +180,65 @@ export function CNPGDetailPage({
|
|
|
123
180
|
|
|
124
181
|
const outsideFilter = namespaces.length > 0 && !!target.namespace && !namespaces.includes(target.namespace)
|
|
125
182
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
type="button"
|
|
132
|
-
onClick={() => navigate(-1)}
|
|
133
|
-
aria-label={`Return to ${returnLabel}`}
|
|
134
|
-
className="inline-flex items-center gap-1 rounded-md py-0.5 pr-2 text-theme-text-secondary hover:text-theme-text-primary"
|
|
135
|
-
>
|
|
136
|
-
<ArrowLeft className="h-3.5 w-3.5" />
|
|
137
|
-
{returnLabel}
|
|
138
|
-
</button>
|
|
139
|
-
<span className="h-4 w-px bg-theme-border" aria-hidden />
|
|
140
|
-
</>
|
|
141
|
-
)}
|
|
142
|
-
<nav aria-label="Location" className="flex min-w-0 items-center gap-1.5 text-theme-text-tertiary">
|
|
183
|
+
// Where the object lives, on its title line: workspace / view /. The way
|
|
184
|
+
// back to the previous page is the browser's Back, or Esc.
|
|
185
|
+
const titlePrefix = (
|
|
186
|
+
<div className="flex min-w-0 items-center gap-x-2 text-sm">
|
|
187
|
+
<nav aria-label="Location" className="flex shrink-0 items-center gap-1.5 text-theme-text-tertiary">
|
|
143
188
|
<Database className="h-3.5 w-3.5" />
|
|
144
|
-
<
|
|
145
|
-
|
|
189
|
+
<span>CloudNativePG</span>
|
|
190
|
+
<span>/</span>
|
|
191
|
+
<button type="button" onClick={() => navigate(home.path)} className="hover:text-theme-text-primary hover:underline">
|
|
192
|
+
{home.label}
|
|
146
193
|
</button>
|
|
147
|
-
{home.id !== 'overview' && (
|
|
148
|
-
<>
|
|
149
|
-
<span>/</span>
|
|
150
|
-
<button type="button" onClick={() => navigate(home.path)} className="hover:text-theme-text-primary hover:underline">
|
|
151
|
-
{home.label}
|
|
152
|
-
</button>
|
|
153
|
-
</>
|
|
154
|
-
)}
|
|
155
194
|
<span>/</span>
|
|
156
|
-
<span className="truncate text-theme-text-secondary">{target.name}</span>
|
|
157
195
|
</nav>
|
|
158
|
-
{outsideFilter && (
|
|
159
|
-
<span className="text-xs text-theme-text-tertiary">
|
|
160
|
-
Namespace {target.namespace} is outside your namespace filter; this object stays open.
|
|
161
|
-
</span>
|
|
162
|
-
)}
|
|
163
196
|
</div>
|
|
164
197
|
)
|
|
198
|
+
const namespaceNote = outsideFilter ? (
|
|
199
|
+
<Tooltip content="Your namespace filter leaves this namespace out; an object you open stays open." position="bottom">
|
|
200
|
+
<span className="whitespace-nowrap text-xs text-theme-text-tertiary">· outside your namespace filter</span>
|
|
201
|
+
</Tooltip>
|
|
202
|
+
) : undefined
|
|
165
203
|
|
|
166
204
|
return (
|
|
167
205
|
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
206
|
+
{/* Above the title line, before any status the page shows. */}
|
|
207
|
+
{target.plural === 'clusters' && <CNPGOperatorBanner namespaces={[target.namespace]} className="mx-6 mt-3 shrink-0" />}
|
|
208
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
209
|
+
<WorkloadView
|
|
210
|
+
key={`${target.plural}/${target.namespace}/${target.name}`}
|
|
211
|
+
kind={target.plural}
|
|
212
|
+
namespace={target.namespace}
|
|
213
|
+
name={target.name}
|
|
214
|
+
group={target.group}
|
|
215
|
+
expanded
|
|
216
|
+
onBack={() => (returnLabel ? navigate(-1) : navigate(home.path))}
|
|
217
|
+
hideBackButton
|
|
218
|
+
titlePrefix={titlePrefix}
|
|
219
|
+
inlineBadges
|
|
220
|
+
namespaceNote={namespaceNote}
|
|
221
|
+
hideKindBadge={cnpgViewNamedForKind(target.plural)}
|
|
222
|
+
onNavigateToResource={openRelated}
|
|
223
|
+
onRefresh={cnpgWorkspace ? refresh : undefined}
|
|
224
|
+
extraTabs={extraTabs}
|
|
225
|
+
tabOrder={isCluster ? CNPG_CLUSTER_TAB_ORDER : undefined}
|
|
226
|
+
renderStatusBadge={isCluster ? (resource) => (
|
|
227
|
+
<Tooltip content={resource?.status?.phase ? `status.phase, reported by CloudNativePG: ${resource.status.phase}` : 'CloudNativePG has not reported status.phase'}>
|
|
228
|
+
<Badge severity="neutral">
|
|
229
|
+
<span className="mr-1 font-normal text-theme-text-tertiary">Phase</span>
|
|
230
|
+
{resource?.status?.phase ? resource.status.phase : 'Not reported'}
|
|
231
|
+
</Badge>
|
|
232
|
+
</Tooltip>
|
|
233
|
+
) : undefined}
|
|
234
|
+
statusNote={isCluster ? <CNPGClusterServing namespace={target.namespace} name={target.name} onSelect={() => goTab(cnpgDimensionTab('serving'))} /> : undefined}
|
|
235
|
+
specTab={
|
|
236
|
+
isCluster
|
|
237
|
+
? { label: 'Configuration', icon: <Settings2 className="h-4 w-4" />, render: () => <CNPGConfiguration namespace={target.namespace} name={target.name} onNavigate={openRelated} onSelectTab={goTab} /> }
|
|
238
|
+
: undefined
|
|
239
|
+
}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
181
242
|
</div>
|
|
182
243
|
)
|
|
183
244
|
}
|
|
@@ -195,13 +256,13 @@ function NotInContext({
|
|
|
195
256
|
homeLabel: string
|
|
196
257
|
homePath: string
|
|
197
258
|
}) {
|
|
198
|
-
const navigate =
|
|
259
|
+
const navigate = useCNPGNavigate()
|
|
199
260
|
const { data: contexts } = useContexts()
|
|
200
261
|
const { requestSwitch, confirmDialog } = useContextSwitchFlow()
|
|
201
262
|
const pinned = contexts?.find((c) => c.name === pinnedContext)
|
|
202
263
|
return (
|
|
203
264
|
<>
|
|
204
|
-
<
|
|
265
|
+
<ScreenEmptyState
|
|
205
266
|
icon={Unplug}
|
|
206
267
|
title={`${target.name} is not in ${activeContext}`}
|
|
207
268
|
detail={`This link points at ${CNPG_DETAIL_KINDS[target.plural].kind} ${target.name} in context ${pinnedContext}. Radar does not open a same-named object from another cluster.`}
|
|
@@ -215,9 +276,9 @@ function NotInContext({
|
|
|
215
276
|
<button
|
|
216
277
|
type="button"
|
|
217
278
|
onClick={() => navigate(homePath)}
|
|
218
|
-
className="
|
|
279
|
+
className="btn-secondary px-3 py-1.5 text-sm"
|
|
219
280
|
>
|
|
220
|
-
Go to
|
|
281
|
+
Go to CloudNativePG {homeLabel}
|
|
221
282
|
</button>
|
|
222
283
|
</div>
|
|
223
284
|
}
|
|
@@ -2,7 +2,7 @@ import { useLocation, useSearchParams } from 'react-router-dom'
|
|
|
2
2
|
import { ArrowLeft } from 'lucide-react'
|
|
3
3
|
import { CNPG_KIND_BY_KEY } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import type { SelectedResource } from '../../types'
|
|
5
|
-
import { decodeDrawerTrail, encodeDrawerTrail,
|
|
5
|
+
import { decodeDrawerTrail, encodeDrawerTrail, sameSelectedResource } from '../../utils/drawer-trail'
|
|
6
6
|
|
|
7
7
|
const KIND_BY_PLURAL: Record<string, string> = Object.fromEntries(
|
|
8
8
|
Object.values(CNPG_KIND_BY_KEY).map((k) => [k.plural, k.kind]),
|
|
@@ -19,7 +19,7 @@ export function CNPGDrawerTrailBack({ resource }: { resource: SelectedResource }
|
|
|
19
19
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
20
20
|
if (!location.pathname.startsWith('/cnpg')) return null
|
|
21
21
|
const trail = decodeDrawerTrail(searchParams.get('drawer'))
|
|
22
|
-
if (trail.length < 2 || !
|
|
22
|
+
if (trail.length < 2 || !sameSelectedResource(trail[trail.length - 1], resource)) return null
|
|
23
23
|
const prev = trail[trail.length - 2]
|
|
24
24
|
const back = () => {
|
|
25
25
|
const params = new URLSearchParams(searchParams)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { expect, it, vi } from 'vitest'
|
|
3
|
+
import { CNPGInvestigationAction } from './CNPGClusterTabs'
|
|
4
|
+
const state = vi.hoisted(() => ({ row: undefined as any, dimensions: undefined as any }))
|
|
5
|
+
vi.mock('./useCNPGClusterAssessment', () => ({ useCNPGClusterAssessment: () => state }))
|
|
6
|
+
it('labels investigation for any CNPG problem or degraded dimension including a healthy controller phase', () => {
|
|
7
|
+
const render = vi.fn((ctx) => <button>{ctx.health === 'problem' ? 'Investigate' : 'Sparkle'}</button>)
|
|
8
|
+
const draw = () => renderToStaticMarkup(<CNPGInvestigationAction namespace="db" name="payments" render={render} context={{ kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'payments', health: 'healthy' }} />)
|
|
9
|
+
state.row = { problems: [{ severity: 'warning' }], controllerStatus: { level: 'healthy' } }
|
|
10
|
+
expect(draw()).toContain('Investigate')
|
|
11
|
+
state.row = { problems: [] }
|
|
12
|
+
for (const tone of ['degraded', 'unhealthy', 'alert']) { state.dimensions = [{ tone }]; expect(draw()).toContain('Investigate') }
|
|
13
|
+
state.dimensions = [{ tone: 'healthy' }]; expect(draw()).toContain('Sparkle')
|
|
14
|
+
state.dimensions = [{ tone: 'unknown' }]; draw()
|
|
15
|
+
expect(render).toHaveBeenLastCalledWith(expect.objectContaining({ health: 'unknown' }))
|
|
16
|
+
state.row = undefined; state.dimensions = undefined; draw()
|
|
17
|
+
expect(render).toHaveBeenLastCalledWith(expect.objectContaining({ health: 'unknown', name: 'payments' }))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('honors the requested any-problem rule for posture and degraded protection without altering fleet attention', () => {
|
|
21
|
+
const render = vi.fn(() => null)
|
|
22
|
+
state.row = { attention: false, problems: [{ id: 'audit:cnpgNoDeclarativeBackup', severity: 'posture' }] }
|
|
23
|
+
state.dimensions = [{ tone: 'healthy' }]
|
|
24
|
+
renderToStaticMarkup(<CNPGInvestigationAction namespace="db" name="payments" render={render} context={{ kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'payments', health: 'healthy' }} />)
|
|
25
|
+
expect(render).toHaveBeenLastCalledWith(expect.objectContaining({ health: 'problem' }))
|
|
26
|
+
expect(state.row.attention).toBe(false)
|
|
27
|
+
state.row = { attention: false, problems: [] }; state.dimensions = [{ tone: 'degraded' }]
|
|
28
|
+
renderToStaticMarkup(<CNPGInvestigationAction namespace="db" name="payments" render={render} context={{ kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'payments', health: 'healthy' }} />)
|
|
29
|
+
expect(render).toHaveBeenLastCalledWith(expect.objectContaining({ health: 'problem' }))
|
|
30
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { expect, it, vi } from 'vitest'
|
|
3
|
+
import { buildCNPGFleet, CNPG_WORKSPACE_KEYS, type CNPGWorkspaceResponse } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { CNPGOperator, ConfigBlock } from './CNPGOperator'
|
|
5
|
+
vi.mock('./useCNPGNavigate', () => ({ useCNPGNavigate: () => vi.fn() }))
|
|
6
|
+
const operator = vi.hoisted(() => ({ partial: false }))
|
|
7
|
+
vi.mock('../../api/cnpg', () => ({ useCNPGOperator: () => ({ data: { coverage: { deployments: { state: operator.partial ? 'partial' : 'full' }, services: { state: operator.partial ? 'partial' : 'full' } }, components: operator.partial ? [] : [{ role: 'operator', namespace: 'operator', deployment: 'cnpg', readyReplicas: 1, replicas: 1 }], config: [], diagnosis: [] }, isRefetchError: true, error: new Error('Operator timeout'), dataUpdatedAt: Date.now() - 240_000 }) }))
|
|
8
|
+
vi.mock('../../context/ConnectionContext', () => ({ useConnection: () => ({ connection: { context: 'test' } }) }))
|
|
9
|
+
it('labels retained operator data after a failed refresh', () => {
|
|
10
|
+
const data: CNPGWorkspaceResponse = { installed: true, context: 'test', namespaces: null, coverage: Object.fromEntries(CNPG_WORKSPACE_KEYS.map((k) => [k, { state: 'full' }])), objects: {}, issues: [], audit: [], backupsOmitted: 0 }
|
|
11
|
+
const html = renderToStaticMarkup(<CNPGOperator data={data} fleet={buildCNPGFleet(data)} namespaces={[]} searchParams={new URLSearchParams()} onSetParams={() => {}} onInspect={() => {}} inspected={null} onClearNamespaces={() => {}} />)
|
|
12
|
+
expect(html).toContain('CloudNativePG operator')
|
|
13
|
+
expect(html).toContain('Last refresh failed: Operator timeout')
|
|
14
|
+
expect(html).toContain('4m ago')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders a known-absent ConfigMap as text and does not claim default settings', () => {
|
|
18
|
+
const html = renderToStaticMarkup(<ConfigBlock config={{ kind: 'ConfigMap', namespace: 'operator', name: 'missing-settings', purpose: 'operator', readable: true, exists: false }} onInspect={vi.fn()} />)
|
|
19
|
+
expect(html).toContain('This referenced ConfigMap does not exist; settings from other sources are not shown here')
|
|
20
|
+
expect(html).not.toContain('<button')
|
|
21
|
+
expect(html).not.toContain('defaults')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('states unread operator health once and keeps a short component empty line', () => {
|
|
25
|
+
operator.partial = true
|
|
26
|
+
const data: CNPGWorkspaceResponse = { installed: true, context: 'test', namespaces: null, coverage: Object.fromEntries(CNPG_WORKSPACE_KEYS.map((k) => [k, { state: 'full' }])), objects: {}, issues: [], audit: [], backupsOmitted: 0 }
|
|
27
|
+
const html = renderToStaticMarkup(<CNPGOperator data={data} fleet={buildCNPGFleet(data)} namespaces={[]} searchParams={new URLSearchParams()} onSetParams={() => {}} onInspect={() => {}} inspected={null} onClearNamespaces={() => {}} />)
|
|
28
|
+
expect(html.match(/Operator health cannot be assessed with your access/g)).toHaveLength(1)
|
|
29
|
+
expect(html).toContain('Deployments not read in some namespaces')
|
|
30
|
+
expect(html).toContain('No operator or plugin Deployments found')
|
|
31
|
+
expect(html).not.toContain('No operator Deployment was found')
|
|
32
|
+
expect(html).not.toContain('Some workloads are not readable')
|
|
33
|
+
operator.partial = false
|
|
34
|
+
})
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
|
-
import { Badge, getCNPGImageCatalogEntries, isApiGroup, PaneLoader, Tooltip } from '@skyhook-io/k8s-ui'
|
|
2
|
+
import { Badge, FoldSection, getCNPGImageCatalogEntries, isApiGroup, PaneLoader, StatusDot, Tooltip, toneTextClass, type CNPGFleetRow } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { parse } from 'yaml'
|
|
3
4
|
import { useCNPGOperator, type CNPGOperatorComponent, type CNPGOperatorConfig } from '../../api/cnpg'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
coverageEmpty,
|
|
9
|
-
worstCoverage,
|
|
10
|
-
Mono,
|
|
11
|
-
ScreenBody,
|
|
12
|
-
SectionTable,
|
|
13
|
-
Sub,
|
|
14
|
-
cnpgResource,
|
|
15
|
-
type CNPGScreenProps,
|
|
16
|
-
} from './shared'
|
|
5
|
+
import { CNPGOperatorDiagnosisSection } from './CNPGOperatorDiagnosis'
|
|
6
|
+
import { cnpgOperatorState, cnpgRestartHistory } from './operatorStatus'
|
|
7
|
+
import { CNPGWorkspaceHeader, CoverageNotice, coverageEmpty, worstCoverage, cnpgResource, type CNPGScreenProps } from './shared'
|
|
8
|
+
import { Mono, Notice, RefreshFailedNotice, ScreenBody, SectionTable, Sub } from '../workspace/layout'
|
|
17
9
|
|
|
18
10
|
interface CatalogRow {
|
|
19
11
|
key: string
|
|
@@ -28,7 +20,53 @@ function readiness(c: CNPGOperatorComponent) {
|
|
|
28
20
|
if (c.readyReplicas === null || c.replicas === null) return <span className="text-theme-text-tertiary">Unknown</span>
|
|
29
21
|
if (c.replicas === 0) return <Badge severity="warning" size="sm">Scaled to 0</Badge>
|
|
30
22
|
const ok = c.readyReplicas >= c.replicas
|
|
31
|
-
|
|
23
|
+
const history = cnpgRestartHistory(c)
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<Badge severity={ok ? 'success' : c.readyReplicas === 0 ? 'error' : 'warning'} size="sm">{c.readyReplicas}/{c.replicas} ready</Badge>
|
|
27
|
+
{history ? (
|
|
28
|
+
<Sub>
|
|
29
|
+
<span className={history.recent ? toneTextClass('degraded') : undefined}>{history.text}</span>
|
|
30
|
+
</Sub>
|
|
31
|
+
) : c.pods ? (
|
|
32
|
+
<Sub>no restarts</Sub>
|
|
33
|
+
) : c.podCoverage && c.podCoverage.state !== 'ok' ? (
|
|
34
|
+
<Sub>restarts not read{c.podCoverage.reason ? `: ${c.podCoverage.reason}` : ''}</Sub>
|
|
35
|
+
) : null}
|
|
36
|
+
</>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function CurrentState({ op, clusters }: { op: NonNullable<ReturnType<typeof useCNPGOperator>['data']>; clusters: CNPGFleetRow[] }) {
|
|
41
|
+
const { concerns, confirmed, unread } = cnpgOperatorState(op, Date.now(), clusters)
|
|
42
|
+
return (
|
|
43
|
+
<section className="rounded-xl border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm">
|
|
44
|
+
<h3 className="text-sm font-semibold text-theme-text-primary">Current state</h3>
|
|
45
|
+
{concerns.length > 0 && (
|
|
46
|
+
<ul className="mt-1.5 space-y-1">
|
|
47
|
+
{concerns.map((c) => (
|
|
48
|
+
<li key={c.text} className="flex items-start gap-2 text-sm">
|
|
49
|
+
<span className="flex h-5 shrink-0 items-center"><StatusDot tone={c.tone} /></span>
|
|
50
|
+
<span className={c.tone === 'neutral' ? 'text-theme-text-secondary' : toneTextClass(c.tone)}>{c.text}</span>
|
|
51
|
+
</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
)}
|
|
55
|
+
{confirmed.length > 0 && (
|
|
56
|
+
<p className="mt-1.5 text-sm text-theme-text-secondary">
|
|
57
|
+
{concerns.length === 0 ? 'No concerns in what Radar read: ' : 'Otherwise: '}
|
|
58
|
+
{confirmed.join(', ')}.
|
|
59
|
+
</p>
|
|
60
|
+
)}
|
|
61
|
+
{unread.length > 0 && (
|
|
62
|
+
<ul className="mt-1.5 space-y-0.5">
|
|
63
|
+
{unread.map((u) => (
|
|
64
|
+
<li key={u} className="text-xs text-theme-text-tertiary">{u}</li>
|
|
65
|
+
))}
|
|
66
|
+
</ul>
|
|
67
|
+
)}
|
|
68
|
+
</section>
|
|
69
|
+
)
|
|
32
70
|
}
|
|
33
71
|
|
|
34
72
|
export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenProps) {
|
|
@@ -59,29 +97,23 @@ export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenPr
|
|
|
59
97
|
|
|
60
98
|
const direct = fleet.rows.filter((r) => !r.cluster?.spec?.imageCatalogRef)
|
|
61
99
|
const op = operator.data
|
|
62
|
-
const coverageGaps = op
|
|
63
|
-
? (['deployments', 'services'] as const).filter((k) => op.coverage[k]?.state !== 'full')
|
|
64
|
-
: []
|
|
65
100
|
|
|
66
101
|
return (
|
|
67
102
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
68
103
|
<CNPGWorkspaceHeader
|
|
69
104
|
title="Operator"
|
|
70
|
-
subtitle="Operator and plugin workloads,
|
|
105
|
+
subtitle="Operator and plugin workloads, whether the operator is leading, watching and reachable, image catalogs and operator configuration."
|
|
71
106
|
/>
|
|
72
107
|
<ScreenBody>
|
|
73
|
-
<CoverageNotice fleet={fleet} data={data} />
|
|
108
|
+
<CoverageNotice fleet={fleet} data={data} kinds={['clusters', 'imageCatalogs', 'clusterImageCatalogs']} />
|
|
109
|
+
<RefreshFailedNotice queries={[operator]} />
|
|
74
110
|
{operator.isLoading && !op ? (
|
|
75
111
|
<PaneLoader label="Loading operator…" className="h-32" />
|
|
76
112
|
) : !op ? (
|
|
77
113
|
<Notice>Operator details could not be loaded{operator.error instanceof Error ? `: ${operator.error.message}` : '.'}</Notice>
|
|
78
114
|
) : (
|
|
79
115
|
<>
|
|
80
|
-
{
|
|
81
|
-
<Notice>
|
|
82
|
-
Some workloads are not readable ({coverageGaps.map((k) => `${k}: ${op.coverage[k].state}`).join(', ')}), so an operator or plugin running in those namespaces may be missing below.
|
|
83
|
-
</Notice>
|
|
84
|
-
)}
|
|
116
|
+
<CurrentState op={op} clusters={fleet.rows} />
|
|
85
117
|
<SectionTable
|
|
86
118
|
title="Operator and plugins"
|
|
87
119
|
columns={[
|
|
@@ -104,10 +136,10 @@ export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenPr
|
|
|
104
136
|
</Tooltip>
|
|
105
137
|
),
|
|
106
138
|
},
|
|
107
|
-
{ header: 'Ready', width: '
|
|
139
|
+
{ header: 'Ready', width: '24%', cell: readiness },
|
|
108
140
|
{
|
|
109
141
|
header: 'Workload',
|
|
110
|
-
width: '
|
|
142
|
+
width: '32%',
|
|
111
143
|
cell: (c) =>
|
|
112
144
|
c.deployment ? (
|
|
113
145
|
<>Deployment <Mono>{c.deployment}</Mono><Sub>{c.namespace}</Sub></>
|
|
@@ -126,6 +158,7 @@ export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenPr
|
|
|
126
158
|
inspected={inspected}
|
|
127
159
|
empty="No operator or plugin Deployments found in the namespaces you can read."
|
|
128
160
|
/>
|
|
161
|
+
{op.diagnosis && <CNPGOperatorDiagnosisSection diagnosis={op.diagnosis} fleet={fleet} />}
|
|
129
162
|
</>
|
|
130
163
|
)}
|
|
131
164
|
|
|
@@ -191,12 +224,12 @@ export function CNPGOperator({ data, fleet, onInspect, inspected }: CNPGScreenPr
|
|
|
191
224
|
)
|
|
192
225
|
}
|
|
193
226
|
|
|
194
|
-
function ConfigBlock({ config, onInspect }: { config: CNPGOperatorConfig; onInspect: CNPGScreenProps['onInspect'] }) {
|
|
227
|
+
export function ConfigBlock({ config, onInspect }: { config: CNPGOperatorConfig; onInspect: CNPGScreenProps['onInspect'] }) {
|
|
195
228
|
const open = () => onInspect({ kind: config.kind === 'ConfigMap' ? 'configmaps' : 'secrets', group: '', namespace: config.namespace, name: config.name })
|
|
196
229
|
const title = (
|
|
197
230
|
<div className="flex flex-wrap items-baseline gap-x-2 border-b border-theme-border px-4 py-2.5">
|
|
198
231
|
<span className="text-xs text-theme-text-tertiary">{config.kind}</span>
|
|
199
|
-
<button type="button" onClick={open} className="font-mono text-sm text-accent-text hover:underline">{config.name}</button>
|
|
232
|
+
{config.exists === false ? <span className="font-mono text-sm text-theme-text-primary">{config.name}</span> : <button type="button" onClick={open} className="font-mono text-sm text-accent-text hover:underline">{config.name}</button>}
|
|
200
233
|
<span className="text-xs text-theme-text-tertiary">
|
|
201
234
|
{config.namespace} · {config.purpose === 'monitoring' ? 'monitoring queries' : 'operator settings'}
|
|
202
235
|
</span>
|
|
@@ -206,9 +239,24 @@ function ConfigBlock({ config, onInspect }: { config: CNPGOperatorConfig; onInsp
|
|
|
206
239
|
if (config.kind === 'Secret') {
|
|
207
240
|
body = <div className="px-4 py-2.5 text-sm text-theme-text-secondary">Referenced by the operator. Secret contents are not shown here.</div>
|
|
208
241
|
} else if (config.exists === false) {
|
|
209
|
-
body = <div className="px-4 py-2.5 text-sm text-theme-text-secondary">
|
|
242
|
+
body = <div className="px-4 py-2.5 text-sm text-theme-text-secondary">This referenced ConfigMap does not exist; settings from other sources are not shown here.</div>
|
|
210
243
|
} else if (!config.readable) {
|
|
211
244
|
body = <div className="px-4 py-2.5 text-sm text-theme-text-tertiary">{config.reason ?? 'Not readable with your access.'}</div>
|
|
245
|
+
} else if (config.purpose === 'monitoring') {
|
|
246
|
+
const queries = config.data?.queries ?? ''
|
|
247
|
+
let count: number | null = null
|
|
248
|
+
try {
|
|
249
|
+
const parsed = parse(queries)
|
|
250
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) count = Object.keys(parsed).length
|
|
251
|
+
} catch { /* The raw configuration remains inspectable when it cannot be parsed. */ }
|
|
252
|
+
body = <div className="px-4 py-2.5 text-sm text-theme-text-secondary">
|
|
253
|
+
<p>{count === null ? 'Query count unavailable' : `${count} ${count === 1 ? 'query' : 'queries'}`} · ConfigMap {config.name}</p>
|
|
254
|
+
<FoldSection title="Query definitions" summary="Show SQL and settings" attention={false}>
|
|
255
|
+
<pre className="max-h-80 overflow-auto whitespace-pre-wrap break-normal font-mono text-xs [overflow-wrap:anywhere]">{queries.slice(0, 8000)}</pre>
|
|
256
|
+
{queries.length > 8000 && <p className="mt-1 text-xs text-theme-text-tertiary">Preview truncated to 8,000 of {queries.length.toLocaleString()} characters.</p>}
|
|
257
|
+
<button type="button" onClick={open} className="mt-2 text-xs text-accent-text hover:underline">Inspect ConfigMap {config.name} →</button>
|
|
258
|
+
</FoldSection>
|
|
259
|
+
</div>
|
|
212
260
|
} else {
|
|
213
261
|
const entries = Object.entries(config.data ?? {})
|
|
214
262
|
body =
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import { AlertBanner } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { useCNPGOperatorStatus } from '../../api/cnpg'
|
|
4
|
+
import { cnpgScreenPath } from './routes'
|
|
5
|
+
import { cnpgOperatorBannerModel } from './operatorStatus'
|
|
6
|
+
import { useCNPGNavigate } from './useCNPGNavigate'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shown above CNPG status while the operator that watches these namespaces is
|
|
10
|
+
* observed not leading, or its admission webhook rejects writes: the phase and
|
|
11
|
+
* ready counts below are then what the operator last wrote.
|
|
12
|
+
*/
|
|
13
|
+
export function CNPGOperatorBanner({ namespaces, className }: { namespaces: string[]; className?: string }) {
|
|
14
|
+
const navigate = useCNPGNavigate()
|
|
15
|
+
const q = useCNPGOperatorStatus(namespaces)
|
|
16
|
+
const model = cnpgOperatorBannerModel(q.data?.namespaces, namespaces)
|
|
17
|
+
if (!model) return null
|
|
18
|
+
const { stale, rejects } = model
|
|
19
|
+
const title = stale
|
|
20
|
+
? 'The CloudNativePG operator is not reconciling: status below may be stale'
|
|
21
|
+
: 'CloudNativePG writes are being rejected'
|
|
22
|
+
const scope = stale && stale.namespaces.length < new Set(namespaces).size ? `Clusters in ${stale.namespaces.join(', ')}.` : ''
|
|
23
|
+
// Tighter than the default banner, never smaller: the title, icon, colours
|
|
24
|
+
// and every reason stay; the link moves onto the title line. A host that
|
|
25
|
+
// passes no className keeps the default bottom margin.
|
|
26
|
+
return (
|
|
27
|
+
<AlertBanner
|
|
28
|
+
variant="warning"
|
|
29
|
+
title={title}
|
|
30
|
+
message={[scope, ...(stale?.reasons ?? []), rejects ?? ''].filter(Boolean).join(' ') || undefined}
|
|
31
|
+
action={
|
|
32
|
+
<button type="button" onClick={() => navigate(cnpgScreenPath('operator'))} className="text-xs font-medium text-accent-text hover:underline">
|
|
33
|
+
Open Operator →
|
|
34
|
+
</button>
|
|
35
|
+
}
|
|
36
|
+
className={clsx('px-3 py-2', className ?? 'mb-4')}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|