@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,8 +1,9 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { PaneLoader, TimelineList, formatAge, type NavigateToResource } from '@skyhook-io/k8s-ui'
|
|
3
3
|
import { useCNPGClusterActivity } from '../../api/cnpg'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { useCNPGClusterActivityWindow } from '../../api/cnpg-history'
|
|
5
|
+
import { CNPGIntervalBanner, useCNPGIntervalParams } from './CNPGTrends'
|
|
6
|
+
import { Notice, RefreshFailedNotice, Segments } from '../workspace/layout'
|
|
6
7
|
|
|
7
8
|
const RANGES = [
|
|
8
9
|
{ id: '6', label: '6 h' },
|
|
@@ -18,22 +19,30 @@ const RANGES = [
|
|
|
18
19
|
*/
|
|
19
20
|
export function CNPGClusterActivity({ namespace, name, onNavigate }: { namespace: string; name: string; onNavigate?: NavigateToResource }) {
|
|
20
21
|
const [hours, setHours] = useState<string>('24')
|
|
21
|
-
const
|
|
22
|
+
const interval = useCNPGIntervalParams()
|
|
23
|
+
const ranged = useCNPGClusterActivity(namespace, name, Number(hours))
|
|
24
|
+
const windowed = useCNPGClusterActivityWindow(namespace, name, interval?.since ?? '', interval?.until ?? '', !!interval)
|
|
25
|
+
const q = interval ? windowed : ranged
|
|
22
26
|
|
|
23
27
|
return (
|
|
24
28
|
<div className="flex h-full min-h-0 flex-col gap-3 p-4">
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
{interval ? (
|
|
30
|
+
<CNPGIntervalBanner since={interval.since} until={interval.until} note="Selected on a History chart: events and changes inside this interval only." onClear={interval.clear} />
|
|
31
|
+
) : (
|
|
32
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
33
|
+
<Segments label="Range" value={hours} onChange={setHours} options={RANGES.map((r) => ({ id: r.id, label: r.label }))} />
|
|
34
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
35
|
+
Kubernetes events and changes for the Cluster, its instances, Backups, Poolers and declarations.
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
31
39
|
<div className="text-xs text-theme-text-tertiary">
|
|
32
40
|
{q.data?.attributionSince
|
|
33
41
|
? `The earliest recorded event linking a Backup, Pooler or declaration to this cluster is ${formatAge(q.data.attributionSince)} old. Deleted child objects from before Radar recorded that link are not shown.`
|
|
34
42
|
: 'Radar has not recorded any Backup, Pooler or declaration events linked to this cluster yet, so deleted child objects may be missing.'}
|
|
35
43
|
{' '}Events for kinds you cannot list are omitted.
|
|
36
44
|
</div>
|
|
45
|
+
<RefreshFailedNotice queries={[q]} />
|
|
37
46
|
{q.data?.truncated && <Notice>Showing the most recent events only; narrow the range to see all of them.</Notice>}
|
|
38
47
|
{q.error && !q.data ? (
|
|
39
48
|
<Notice>Activity could not be loaded: {q.error instanceof Error ? q.error.message : 'unknown error'}</Notice>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
3
|
+
import { beforeEach, expect, it, vi } from 'vitest'
|
|
4
|
+
import { CNPGClusterLogs } from './CNPGClusterLogs'
|
|
5
|
+
import { guardRadarFeature, radarFeatureSupport } from '../../api/radarFeatures'
|
|
6
|
+
import { fetchJSON } from '../../api/client'
|
|
7
|
+
|
|
8
|
+
const state = vi.hoisted(() => ({ props: undefined as any, capabilities: { features: { resourceIssues: true } } as any, capabilitiesPending: false, fetchStatus: 'idle' as 'idle' | 'fetching' | 'paused', interval: null as null | { since: string; until: string; clear: () => void } }))
|
|
9
|
+
vi.mock('@skyhook-io/k8s-ui', async (original) => ({ ...await original<typeof import('@skyhook-io/k8s-ui')>(), WorkloadLogsViewer: (props: any) => { state.props = props; return <div>logs</div> } }))
|
|
10
|
+
vi.mock('../../api/client', () => ({ fetchJSON: vi.fn(async () => ({ pods: [], logs: [] })), useCapabilities: () => ({ isPending: state.capabilitiesPending, isFetching: state.fetchStatus === 'fetching', fetchStatus: state.fetchStatus, refetch: vi.fn() }), useRadarFeature: () => {
|
|
11
|
+
const support = radarFeatureSupport('cnpgWorkspace', state.capabilities, 'v1.16.0')
|
|
12
|
+
return { support, guard: (fn: () => Promise<unknown>) => guardRadarFeature('cnpgWorkspace', support, {}, fn) }
|
|
13
|
+
} }))
|
|
14
|
+
vi.mock('../../hooks/useDesktopDownload', () => ({ useDesktopDownload: () => undefined }))
|
|
15
|
+
vi.mock('../../context/ThemeContext', () => ({ useTheme: () => ({ theme: 'light' }) }))
|
|
16
|
+
vi.mock('./useCNPGNavigate', () => ({ useCNPGNavigate: () => vi.fn() }))
|
|
17
|
+
vi.mock('./useCNPGSidebarWorkspace', () => ({ useCNPGFleet: () => ({ fleet: null }) }))
|
|
18
|
+
vi.mock('./CNPGTrends', () => ({ useCNPGIntervalParams: () => state.interval, CNPGIntervalBanner: ({ note }: { note: string }) => <div>{note}</div> }))
|
|
19
|
+
beforeEach(() => { state.props = undefined; state.capabilities = { features: { resourceIssues: true } }; state.capabilitiesPending = false; state.fetchStatus = 'idle'; state.interval = null; vi.clearAllMocks(); vi.unstubAllGlobals() })
|
|
20
|
+
function mount(search = '') { return renderToStaticMarkup(<MemoryRouter initialEntries={[{ pathname: '/cnpg/clusters/db/pg', search }]}><CNPGClusterLogs namespace="db" name="pg" /></MemoryRouter>) }
|
|
21
|
+
|
|
22
|
+
it('retains ordinary snapshot selectors and follows on the actual missing-flag capability state', async () => {
|
|
23
|
+
mount()
|
|
24
|
+
await state.props.fetchAll({ container: 'postgres', tailLines: 40, sinceSeconds: 60 })
|
|
25
|
+
expect(fetchJSON).toHaveBeenCalledWith('/cnpg/clusters/db/pg/logs?container=postgres&tailLines=40&sinceSeconds=60', { signal: undefined })
|
|
26
|
+
const eventSource = vi.fn(function (url: string) { return { url } })
|
|
27
|
+
vi.stubGlobal('EventSource', eventSource)
|
|
28
|
+
state.props.createStream({ container: 'postgres' })
|
|
29
|
+
expect(eventSource).toHaveBeenCalledWith('/api/cnpg/clusters/db/pg/logs/stream?container=postgres&tailLines=50', { withCredentials: false })
|
|
30
|
+
})
|
|
31
|
+
it('uses the old postgres default for empty and all selectors in both snapshot and SSE requests', async () => {
|
|
32
|
+
mount()
|
|
33
|
+
expect(state.props.initialContainer).toBe('postgres')
|
|
34
|
+
expect(state.props.allowAllContainers).toBe(false)
|
|
35
|
+
mount('?container=all')
|
|
36
|
+
expect(state.props.initialContainer).toBe('postgres')
|
|
37
|
+
expect(state.props.allowAllContainers).toBe(false)
|
|
38
|
+
const eventSource = vi.fn(function (url: string) { return { url } })
|
|
39
|
+
vi.stubGlobal('EventSource', eventSource)
|
|
40
|
+
for (const container of [undefined, 'all']) {
|
|
41
|
+
await state.props.fetchAll({ container, tailLines: 40, sinceSeconds: 60 })
|
|
42
|
+
const path = vi.mocked(fetchJSON).mock.lastCall![0]
|
|
43
|
+
expect(Object.fromEntries(new URL(path, 'http://radar.test').searchParams)).toEqual({ tailLines: '40', sinceSeconds: '60' })
|
|
44
|
+
state.props.createStream({ container, sinceSeconds: 60 })
|
|
45
|
+
const streamPath = eventSource.mock.lastCall![0]
|
|
46
|
+
expect(Object.fromEntries(new URL(streamPath, 'http://radar.test').searchParams)).toEqual({ tailLines: '50', sinceSeconds: '60' })
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
it('retains exact plugin-container selection supported by the old producer', async () => {
|
|
50
|
+
mount('?container=plugin-barman-cloud')
|
|
51
|
+
expect(state.props.initialContainer).toBe('plugin-barman-cloud')
|
|
52
|
+
await state.props.fetchAll({ container: 'plugin-barman-cloud' })
|
|
53
|
+
expect(fetchJSON).toHaveBeenCalledWith('/cnpg/clusters/db/pg/logs?container=plugin-barman-cloud', { signal: undefined })
|
|
54
|
+
const eventSource = vi.fn(function (url: string) { return { url } })
|
|
55
|
+
vi.stubGlobal('EventSource', eventSource)
|
|
56
|
+
state.props.createStream({ container: 'plugin-barman-cloud' })
|
|
57
|
+
expect(eventSource).toHaveBeenCalledWith('/api/cnpg/clusters/db/pg/logs/stream?container=plugin-barman-cloud&tailLines=50', { withCredentials: false })
|
|
58
|
+
})
|
|
59
|
+
it('only requests all-container snapshots and SSE after confirmed support', async () => {
|
|
60
|
+
const eventSource = vi.fn(function (url: string) { return { url } })
|
|
61
|
+
vi.stubGlobal('EventSource', eventSource)
|
|
62
|
+
state.capabilities = undefined
|
|
63
|
+
mount()
|
|
64
|
+
expect(state.props.initialContainer).toBe('postgres')
|
|
65
|
+
expect(state.props.allowAllContainers).toBe(false)
|
|
66
|
+
await state.props.fetchAll({})
|
|
67
|
+
expect(fetchJSON).toHaveBeenLastCalledWith('/cnpg/clusters/db/pg/logs', { signal: undefined })
|
|
68
|
+
state.props.createStream({})
|
|
69
|
+
expect(eventSource).toHaveBeenLastCalledWith('/api/cnpg/clusters/db/pg/logs/stream?tailLines=50', { withCredentials: false })
|
|
70
|
+
state.capabilities = { features: { cnpgWorkspace: true } }
|
|
71
|
+
mount()
|
|
72
|
+
expect(state.props.initialContainer).toBeUndefined()
|
|
73
|
+
expect(state.props.allowAllContainers).toBe(true)
|
|
74
|
+
mount('?container=all')
|
|
75
|
+
expect(state.props.initialContainer).toBeUndefined()
|
|
76
|
+
await state.props.fetchAll({})
|
|
77
|
+
expect(fetchJSON).toHaveBeenLastCalledWith('/cnpg/clusters/db/pg/logs?container=all', { signal: undefined })
|
|
78
|
+
state.props.createStream({})
|
|
79
|
+
expect(eventSource).toHaveBeenLastCalledWith('/api/cnpg/clusters/db/pg/logs/stream?container=all&tailLines=50', { withCredentials: false })
|
|
80
|
+
})
|
|
81
|
+
it('refuses both-bound interval semantics on that old server without an unbounded request', async () => {
|
|
82
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
83
|
+
expect(mount()).toContain('Clear it to read ordinary logs')
|
|
84
|
+
await expect(state.props.fetchAll({ tailLines: 40 })).rejects.toMatchObject({ feature: 'cnpgWorkspace' })
|
|
85
|
+
expect(fetchJSON).not.toHaveBeenCalled()
|
|
86
|
+
expect(state.props.createStream).toBeUndefined()
|
|
87
|
+
})
|
|
88
|
+
it('uses the advertised interval contract without tailing or streaming', async () => {
|
|
89
|
+
state.capabilities = { features: { cnpgWorkspace: true } }
|
|
90
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
91
|
+
mount()
|
|
92
|
+
await state.props.fetchAll({ tailLines: 40, sinceSeconds: 60 })
|
|
93
|
+
const path = vi.mocked(fetchJSON).mock.calls[0][0]
|
|
94
|
+
const params = new URL(path, 'http://radar.test').searchParams
|
|
95
|
+
expect(Object.fromEntries(params)).toEqual({ sinceTime: state.interval.since, untilTime: state.interval.until, container: 'all' })
|
|
96
|
+
expect(state.props.createStream).toBeUndefined()
|
|
97
|
+
})
|
|
98
|
+
it('waits for capabilities before offering interval semantics, while ordinary logs remain immediate', () => {
|
|
99
|
+
state.capabilities = undefined
|
|
100
|
+
state.capabilitiesPending = true
|
|
101
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
102
|
+
expect(mount()).toContain('Checking interval support')
|
|
103
|
+
expect(state.props).toBeUndefined()
|
|
104
|
+
state.interval = null
|
|
105
|
+
mount()
|
|
106
|
+
expect(state.props.createStream).toBeTypeOf('function')
|
|
107
|
+
})
|
|
108
|
+
it('settles a failed capabilities read without loading forever or issuing an interval request', () => {
|
|
109
|
+
state.capabilities = undefined
|
|
110
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
111
|
+
const html = mount()
|
|
112
|
+
expect(html).toContain('Interval log support could not be checked')
|
|
113
|
+
expect(html).toContain('Clear this interval to read ordinary logs')
|
|
114
|
+
expect(html).toContain('Retry')
|
|
115
|
+
expect(html).not.toContain('Checking interval support')
|
|
116
|
+
expect(state.props).toBeUndefined()
|
|
117
|
+
expect(fetchJSON).not.toHaveBeenCalled()
|
|
118
|
+
})
|
|
119
|
+
it('keeps the clear-interval fallback available while an offline request is paused', () => {
|
|
120
|
+
state.capabilities = undefined
|
|
121
|
+
state.capabilitiesPending = true
|
|
122
|
+
state.fetchStatus = 'paused'
|
|
123
|
+
state.interval = { since: '2026-10-13T00:00:00Z', until: '2026-10-13T01:00:00Z', clear: vi.fn() }
|
|
124
|
+
const html = mount()
|
|
125
|
+
expect(html).toContain('Clear this interval to read ordinary logs')
|
|
126
|
+
expect(html).toContain('Waiting for connection')
|
|
127
|
+
expect(html).not.toContain('Checking interval support')
|
|
128
|
+
expect(state.props).toBeUndefined()
|
|
129
|
+
expect(fetchJSON).not.toHaveBeenCalled()
|
|
130
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
3
|
+
import { expect, it, vi } from 'vitest'
|
|
4
|
+
import { CNPGClusterLogs } from './CNPGClusterLogs'
|
|
5
|
+
const state = vi.hoisted(() => ({ props: {} as any, navigate: vi.fn(), problems: [{ job: 'initdb', severity: 'critical', title: 'First instance cannot be scheduled' }] as any[] }))
|
|
6
|
+
vi.mock('@skyhook-io/k8s-ui', async (original) => ({ ...await original<typeof import('@skyhook-io/k8s-ui')>(), WorkloadLogsViewer: (props: any) => { state.props = props; return <div>{props.emptySourceState}</div> } }))
|
|
7
|
+
vi.mock('../../api/client', () => ({ useCapabilities: () => ({ isPending: false }), useRadarFeature: () => ({ guard: (fn: () => unknown) => fn(), support: 'supported' }), fetchJSON: vi.fn() }))
|
|
8
|
+
vi.mock('../../hooks/useDesktopDownload', () => ({ useDesktopDownload: () => undefined }))
|
|
9
|
+
vi.mock('../../context/ThemeContext', () => ({ useTheme: () => ({ theme: 'light' }) }))
|
|
10
|
+
vi.mock('./useCNPGNavigate', () => ({ useCNPGNavigate: () => state.navigate }))
|
|
11
|
+
vi.mock('./useCNPGSidebarWorkspace', () => ({ useCNPGFleet: () => ({ fleet: { rows: [{ namespace: 'db', name: 'analytics', problems: state.problems }] } }) }))
|
|
12
|
+
vi.mock('./CNPGTrends', () => ({ useCNPGIntervalParams: () => null }))
|
|
13
|
+
it('supplies the first-instance problem and Overview link from the CNPG host', () => {
|
|
14
|
+
const html = renderToStaticMarkup(<MemoryRouter initialEntries={[{ pathname: '/cnpg/clusters/db/analytics', search: '?ctx=test&tab=logs&drawer=x', state: { returnLabel: 'Clusters' } }]}><CNPGClusterLogs namespace="db" name="analytics" /></MemoryRouter>)
|
|
15
|
+
expect(html).toContain('First instance cannot be scheduled')
|
|
16
|
+
expect(html).toContain('See Overview’s problem')
|
|
17
|
+
state.props.emptySourceState.props.children[1].props.onClick()
|
|
18
|
+
expect(state.navigate).toHaveBeenCalledWith('/cnpg/clusters/db/analytics?ctx=test&tab=overview&drawer=x&problems=all', { replace: true, state: { returnLabel: 'Clusters' } })
|
|
19
|
+
expect(state.props.disableSourceControlsWithoutSource).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('does not invent a first-instance cause or use an unrelated problem', () => {
|
|
23
|
+
state.problems = [{ severity: 'critical', title: 'Backup failed' }]
|
|
24
|
+
const html = renderToStaticMarkup(<MemoryRouter><CNPGClusterLogs namespace="db" name="analytics" /></MemoryRouter>)
|
|
25
|
+
expect(html).toContain('No instance log source yet')
|
|
26
|
+
expect(html).toContain('Open Overview')
|
|
27
|
+
expect(html).not.toContain('first instance has not started')
|
|
28
|
+
expect(html).not.toContain('Backup failed')
|
|
29
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
import { query, intervalQuery } from './CNPGClusterLogs'
|
|
3
|
+
it('requests all containers explicitly for snapshots, streams and bounded intervals', () => {
|
|
4
|
+
expect(new URLSearchParams(query({}).slice(1)).get('container')).toBe('all')
|
|
5
|
+
expect(new URLSearchParams(query({}, 50).slice(1)).get('container')).toBe('all')
|
|
6
|
+
expect(new URLSearchParams(intervalQuery({}, 'from', 'to').slice(1)).get('container')).toBe('all')
|
|
7
|
+
expect(new URLSearchParams(query({ container: 'plugin-barman-cloud' }).slice(1)).get('container')).toBe('plugin-barman-cloud')
|
|
8
|
+
expect(new URLSearchParams(intervalQuery({ container: 'postgres' }, 'from', 'to').slice(1)).get('container')).toBe('postgres')
|
|
9
|
+
})
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { useCallback } from 'react'
|
|
2
|
-
import { useSearchParams } from 'react-router-dom'
|
|
3
|
-
import { WorkloadLogsViewer, type WorkloadLogsFetchParams, type WorkloadLogsResult } from '@skyhook-io/k8s-ui'
|
|
4
|
-
import { fetchJSON } from '../../api/client'
|
|
2
|
+
import { useLocation, useSearchParams } from 'react-router-dom'
|
|
3
|
+
import { PaneLoader, WorkloadLogsViewer, type WorkloadLogsFetchParams, type WorkloadLogsResult } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { fetchJSON, useCapabilities, useRadarFeature } from '../../api/client'
|
|
5
5
|
import { getApiBase, getCredentialsMode } from '../../api/config'
|
|
6
6
|
import { useDesktopDownload } from '../../hooks/useDesktopDownload'
|
|
7
7
|
import { useTheme } from '../../context/ThemeContext'
|
|
8
|
+
import { CNPGIntervalBanner, useCNPGIntervalParams } from './CNPGTrends'
|
|
9
|
+
import { useCNPGFleet } from './useCNPGSidebarWorkspace'
|
|
10
|
+
import { useCNPGNavigate } from './useCNPGNavigate'
|
|
11
|
+
import { cnpgClusterProblemsPath, cnpgWithinDetail } from './paths'
|
|
8
12
|
|
|
9
13
|
function logsPath(namespace: string, name: string) {
|
|
10
14
|
return `/cnpg/clusters/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/logs`
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
function query(params: WorkloadLogsFetchParams, tailDefault?: number) {
|
|
17
|
+
export function query(params: WorkloadLogsFetchParams, tailDefault?: number, allContainersSupported = true) {
|
|
14
18
|
const q = new URLSearchParams()
|
|
15
|
-
|
|
19
|
+
const container = params.container || (allContainersSupported ? 'all' : '')
|
|
20
|
+
if (container && (container !== 'all' || allContainersSupported)) q.set('container', container)
|
|
16
21
|
const tail = params.tailLines ?? tailDefault
|
|
17
22
|
if (tail) q.set('tailLines', String(tail))
|
|
18
23
|
if (params.sinceSeconds) q.set('sinceSeconds', String(params.sinceSeconds))
|
|
@@ -20,43 +25,95 @@ function query(params: WorkloadLogsFetchParams, tailDefault?: number) {
|
|
|
20
25
|
return s ? `?${s}` : ''
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
// An interval is read from its start with both bounds applied server-side; the
|
|
29
|
+
// viewer's line-count and since selectors do not apply to it.
|
|
30
|
+
export function intervalQuery(params: WorkloadLogsFetchParams, since: string, until: string) {
|
|
31
|
+
const q = new URLSearchParams({ sinceTime: since, untilTime: until })
|
|
32
|
+
q.set('container', params.container || 'all')
|
|
33
|
+
return `?${q.toString()}`
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
/**
|
|
24
37
|
* Logs merged from every instance Pod of a CloudNativePG Cluster. `?pod=`
|
|
25
|
-
* preselects one instance (the fleet's "Logs" action and "Open instance logs")
|
|
38
|
+
* preselects one instance (the fleet's "Logs" action and "Open instance logs"),
|
|
39
|
+
* `?container=` one container (e.g. the plugin-barman-cloud sidecar);
|
|
40
|
+
* `?since=&until=` bounds them to an interval selected on a History chart.
|
|
26
41
|
*/
|
|
27
42
|
export function CNPGClusterLogs({ namespace, name }: { namespace: string; name: string }) {
|
|
28
43
|
const [searchParams] = useSearchParams()
|
|
44
|
+
const navigate = useCNPGNavigate()
|
|
45
|
+
const location = useLocation()
|
|
46
|
+
const { fleet } = useCNPGFleet([namespace])
|
|
47
|
+
const row = fleet?.rows.find((r) => r.namespace === namespace && r.name === name)
|
|
48
|
+
const problem = row?.problems.find((p) => p.job === 'initdb')
|
|
49
|
+
const overviewPath = `${cnpgClusterProblemsPath(namespace, name, searchParams.get('ctx') ?? undefined)}&tab=overview`
|
|
50
|
+
const inPlace = cnpgWithinDetail(location.pathname, location.search, overviewPath)
|
|
29
51
|
const pod = searchParams.get('pod')
|
|
52
|
+
const container = searchParams.get('container')
|
|
53
|
+
const interval = useCNPGIntervalParams()
|
|
30
54
|
const desktopDownload = useDesktopDownload()
|
|
31
55
|
const { theme } = useTheme()
|
|
56
|
+
const since = interval?.since
|
|
57
|
+
const until = interval?.until
|
|
32
58
|
|
|
59
|
+
const { guard, support } = useRadarFeature('cnpgWorkspace')
|
|
60
|
+
const capabilities = useCapabilities()
|
|
61
|
+
const allContainersSupported = support === 'supported'
|
|
33
62
|
const fetchAll = useCallback(
|
|
34
63
|
(params: WorkloadLogsFetchParams) =>
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
since && until
|
|
65
|
+
? guard(() => fetchJSON<WorkloadLogsResult>(`${logsPath(namespace, name)}${intervalQuery(params, since, until)}`, { signal: params.signal }))
|
|
66
|
+
: fetchJSON<WorkloadLogsResult>(`${logsPath(namespace, name)}${query(params, undefined, allContainersSupported)}`, { signal: params.signal }),
|
|
67
|
+
[guard, namespace, name, since, until, allContainersSupported],
|
|
37
68
|
)
|
|
38
|
-
const
|
|
69
|
+
const stream = useCallback(
|
|
39
70
|
(params: WorkloadLogsFetchParams) =>
|
|
40
|
-
new EventSource(`${getApiBase()}${logsPath(namespace, name)}/stream${query(params, 50)}`, {
|
|
71
|
+
new EventSource(`${getApiBase()}${logsPath(namespace, name)}/stream${query(params, 50, allContainersSupported)}`, {
|
|
41
72
|
withCredentials: getCredentialsMode() === 'include',
|
|
42
73
|
}),
|
|
43
|
-
[namespace, name],
|
|
74
|
+
[namespace, name, allContainersSupported],
|
|
44
75
|
)
|
|
45
76
|
|
|
77
|
+
if (interval && support === 'unknown') return capabilities.isPending && capabilities.fetchStatus !== 'paused'
|
|
78
|
+
? <PaneLoader label="Checking interval support…" className="flex-1" />
|
|
79
|
+
: <div className="space-y-2 p-3">
|
|
80
|
+
<CNPGIntervalBanner since={interval.since} until={interval.until} note="Interval log support could not be checked. Clear this interval to read ordinary logs." onClear={interval.clear} />
|
|
81
|
+
<button type="button" className="btn-secondary" disabled={capabilities.isFetching || capabilities.fetchStatus === 'paused'} onClick={() => void capabilities.refetch()}>{capabilities.fetchStatus === 'paused' ? 'Waiting for connection' : capabilities.isFetching ? 'Checking…' : 'Retry'}</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
46
84
|
return (
|
|
47
|
-
<div className="h-full">
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
<div className="flex h-full flex-col">
|
|
86
|
+
{interval && (
|
|
87
|
+
<div className="px-3 pt-3">
|
|
88
|
+
<CNPGIntervalBanner
|
|
89
|
+
since={interval.since}
|
|
90
|
+
until={interval.until}
|
|
91
|
+
note={support === 'unsupported' ? 'This interval needs a newer Radar. Clear it to read ordinary logs.' : 'Selected on a History chart. Lines from the start of the interval, up to 64 KiB per instance; the line-count selector does not apply and streaming is off.'}
|
|
92
|
+
onClear={interval.clear}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
<div className="min-h-0 flex-1">
|
|
97
|
+
<WorkloadLogsViewer
|
|
98
|
+
key={`${pod ?? ''}|${container ?? ''}|${since ?? ''}|${until ?? ''}|${allContainersSupported}`}
|
|
99
|
+
name={name}
|
|
100
|
+
allowAllContainers={allContainersSupported}
|
|
101
|
+
disableSourceControlsWithoutSource
|
|
102
|
+
emptySourceState={<div className="space-y-2 text-sm text-theme-text-secondary">
|
|
103
|
+
<p>No instance log source yet. {row?.hibernated ? 'The Cluster is hibernated.' : problem?.title}</p>
|
|
104
|
+
<button type="button" className="text-accent-text hover:underline" onClick={() => navigate(inPlace ?? overviewPath, { replace: !!inPlace, state: location.state })}>{problem ? 'See Overview’s problem →' : 'Open Overview →'}</button>
|
|
105
|
+
</div>}
|
|
106
|
+
fetchAll={fetchAll}
|
|
107
|
+
createStream={interval ? undefined : stream}
|
|
108
|
+
overrideDownload={desktopDownload}
|
|
109
|
+
forceDark={theme === 'dark' ? true : undefined}
|
|
110
|
+
// Light logs on a dark app are never wanted: dark theme pins the palette,
|
|
111
|
+
// light theme only sets where it starts and leaves the toggle available.
|
|
112
|
+
defaultDark={false}
|
|
113
|
+
initialPods={pod ? [pod] : undefined}
|
|
114
|
+
initialContainer={container && container !== 'all' ? container : allContainersSupported ? undefined : 'postgres'}
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
60
117
|
</div>
|
|
61
118
|
)
|
|
62
119
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
3
|
+
import { beforeEach, expect, it, vi } from 'vitest'
|
|
4
|
+
import type { CNPGDimension } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import { CNPGBackupsTab, CNPGStorageTab } from './CNPGClusterTabs'
|
|
6
|
+
const state = vi.hoisted(() => ({
|
|
7
|
+
coverage: 'full',
|
|
8
|
+
primary: {} as any,
|
|
9
|
+
storageProps: {} as any,
|
|
10
|
+
schedules: [] as any[],
|
|
11
|
+
dimensions: [] as CNPGDimension[],
|
|
12
|
+
}))
|
|
13
|
+
const cluster = {
|
|
14
|
+
apiVersion: 'postgresql.cnpg.io/v1',
|
|
15
|
+
kind: 'Cluster',
|
|
16
|
+
metadata: { name: 'pg', namespace: 'db' },
|
|
17
|
+
status: { currentPrimary: 'pg-1' } as any,
|
|
18
|
+
spec: {} as any,
|
|
19
|
+
}
|
|
20
|
+
vi.mock('../../api/client', () => ({ useRadarFeature: () => ({ support: 'supported' }) }))
|
|
21
|
+
vi.mock('../../api/cnpg', () => ({
|
|
22
|
+
useCNPGRuntime: () => ({ data: { permission: { proxy: 'denied' }, instances: [state.primary] } }),
|
|
23
|
+
}))
|
|
24
|
+
vi.mock('./useCNPGClusterAssessment', () => ({
|
|
25
|
+
useCNPGClusterAssessment: () => ({
|
|
26
|
+
dimensions: state.dimensions,
|
|
27
|
+
row: {
|
|
28
|
+
cluster,
|
|
29
|
+
problems: [],
|
|
30
|
+
protection: { walArchiving: { text: 'Not archived: no destination configured', tone: 'neutral' } },
|
|
31
|
+
},
|
|
32
|
+
query: {
|
|
33
|
+
data: { coverage: { backups: { state: state.coverage } }, objects: { backups: [], clusters: [cluster] } },
|
|
34
|
+
},
|
|
35
|
+
runtime: {},
|
|
36
|
+
}),
|
|
37
|
+
}))
|
|
38
|
+
vi.mock('./useCNPGSidebarWorkspace', () => ({
|
|
39
|
+
useCNPGFleet: () => ({
|
|
40
|
+
query: {
|
|
41
|
+
data: {
|
|
42
|
+
installed: true,
|
|
43
|
+
coverage: { backups: { state: state.coverage } },
|
|
44
|
+
objects: { backups: [], scheduledBackups: state.schedules },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
fleet: { rows: [{ name: 'pg', namespace: 'db', cluster }] },
|
|
48
|
+
}),
|
|
49
|
+
}))
|
|
50
|
+
vi.mock('../../api/cnpg-recovery', () => ({ useCNPGRestoreCapability: () => ({ data: { allowed: true } }) }))
|
|
51
|
+
vi.mock('./CNPGProtection', () => ({ CNPGProtection: () => null }))
|
|
52
|
+
vi.mock('./CNPGArchivingRepair', () => ({ CNPGArchivingRepair: () => null }))
|
|
53
|
+
vi.mock('./CNPGStorage', () => ({
|
|
54
|
+
CNPGStorage: (props: any) => {
|
|
55
|
+
state.storageProps = props
|
|
56
|
+
return null
|
|
57
|
+
},
|
|
58
|
+
}))
|
|
59
|
+
vi.mock('./recovery/CNPGRestoreButton', () => ({
|
|
60
|
+
CNPGRestoreButton: ({ disabledReason }: { disabledReason?: string }) => (
|
|
61
|
+
<button disabled={!!disabledReason}>Restore</button>
|
|
62
|
+
),
|
|
63
|
+
}))
|
|
64
|
+
vi.mock('../../context/ConnectionContext', () => ({ useConnection: () => ({ connection: { context: 'test' } }) }))
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
state.coverage = 'full'
|
|
67
|
+
state.primary = {}
|
|
68
|
+
state.schedules = []
|
|
69
|
+
state.dimensions = []
|
|
70
|
+
cluster.spec = {}
|
|
71
|
+
delete cluster.status.lastSuccessfulBackup
|
|
72
|
+
})
|
|
73
|
+
it('passes primary identity through to Storage when database reads are denied', () => {
|
|
74
|
+
state.primary = { pod: 'pg-1', role: 'primary', metrics: { state: 'denied' } }
|
|
75
|
+
renderToStaticMarkup(<CNPGStorageTab namespace="db" name="pg" />)
|
|
76
|
+
expect(state.storageProps.primary).toBe(state.primary)
|
|
77
|
+
expect(state.storageProps.runtime.data.permission.proxy).toBe('denied')
|
|
78
|
+
})
|
|
79
|
+
it('uses the shared restore assessment for the Backups button', () => {
|
|
80
|
+
const render = () =>
|
|
81
|
+
renderToStaticMarkup(
|
|
82
|
+
<MemoryRouter>
|
|
83
|
+
<CNPGBackupsTab namespace="db" name="pg" onInspect={() => {}} />
|
|
84
|
+
</MemoryRouter>,
|
|
85
|
+
)
|
|
86
|
+
state.coverage = 'full'
|
|
87
|
+
expect(render()).toContain('disabled=""')
|
|
88
|
+
expect(render()).toContain('Nothing to restore from yet')
|
|
89
|
+
state.coverage = 'denied'
|
|
90
|
+
expect(render()).not.toContain('disabled=""')
|
|
91
|
+
expect(render()).not.toContain('Nothing to restore from yet')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('starts a single guided setup path beside restore, without treating a schedule as protection', () => {
|
|
95
|
+
state.schedules = [{ metadata: { name: 'pg-nightly', namespace: 'db' }, spec: { cluster: { name: 'pg' } } }]
|
|
96
|
+
const html = renderToStaticMarkup(
|
|
97
|
+
<MemoryRouter>
|
|
98
|
+
<CNPGBackupsTab namespace="db" name="pg" onInspect={() => {}} onOpenYaml={() => {}} />
|
|
99
|
+
</MemoryRouter>,
|
|
100
|
+
)
|
|
101
|
+
expect(html).toContain('Set up backups…')
|
|
102
|
+
expect(html).toContain('Nothing to restore from yet')
|
|
103
|
+
expect(html).not.toContain('Configure spec.backup.barmanObjectStore')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('keeps the protection verdict ahead of setup without repeating the blocker', () => {
|
|
107
|
+
state.schedules = [{ metadata: { name: 'pg-nightly', namespace: 'db' }, spec: { cluster: { name: 'pg' } } }]
|
|
108
|
+
state.dimensions = [
|
|
109
|
+
{
|
|
110
|
+
id: 'protection',
|
|
111
|
+
label: 'Backups',
|
|
112
|
+
tone: 'degraded',
|
|
113
|
+
text: 'Backup schedule pg-nightly cannot run: no backup destination',
|
|
114
|
+
source: 'ScheduledBackup method against its target Cluster spec',
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
const html = renderToStaticMarkup(
|
|
118
|
+
<MemoryRouter>
|
|
119
|
+
<CNPGBackupsTab namespace="db" name="pg" onInspect={() => {}} onOpenYaml={() => {}} />
|
|
120
|
+
</MemoryRouter>,
|
|
121
|
+
)
|
|
122
|
+
expect(html.match(/cannot run:/g)).toHaveLength(1)
|
|
123
|
+
expect(html.indexOf(state.dimensions[0].text)).toBeLessThan(html.indexOf('Set up backups…'))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('does not invent a missing destination for an enabled third-party plugin', () => {
|
|
127
|
+
cluster.spec.plugins = [{ name: 'third-party-backup' }]
|
|
128
|
+
const html = renderToStaticMarkup(
|
|
129
|
+
<MemoryRouter>
|
|
130
|
+
<CNPGBackupsTab namespace="db" name="pg" onInspect={() => {}} />
|
|
131
|
+
</MemoryRouter>,
|
|
132
|
+
)
|
|
133
|
+
expect(html).not.toContain('Configure spec.backup')
|
|
134
|
+
delete cluster.spec.plugins
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('passes recovery certainty instead of equating a destination with a usable backup', () => {
|
|
138
|
+
const renderStorage = () => renderToStaticMarkup(<CNPGStorageTab namespace="db" name="pg" />)
|
|
139
|
+
state.coverage = 'full'
|
|
140
|
+
cluster.spec.backup = { barmanObjectStore: { destinationPath: 's3://backups' } }
|
|
141
|
+
renderStorage()
|
|
142
|
+
expect(state.storageProps.restoreState).toBe('none')
|
|
143
|
+
cluster.status.lastSuccessfulBackup = '2026-10-01T00:00:00Z'
|
|
144
|
+
renderStorage()
|
|
145
|
+
expect(state.storageProps.restoreState).toBe('available')
|
|
146
|
+
delete cluster.status.lastSuccessfulBackup
|
|
147
|
+
state.coverage = 'denied'
|
|
148
|
+
renderStorage()
|
|
149
|
+
expect(state.storageProps.restoreState).toBe('unknown')
|
|
150
|
+
state.coverage = 'full'
|
|
151
|
+
delete cluster.spec.backup
|
|
152
|
+
})
|