@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
package/src/api/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalResourceGroup } from '@skyhook-io/k8s-ui/utils/api-resources'
|
|
2
2
|
import { knownKindForPluralWithGroup, pluralToKind } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
3
|
-
import { useEffect, useRef } from 'react'
|
|
3
|
+
import { useEffect, useRef, useCallback, useMemo } from 'react'
|
|
4
4
|
import type { KueueAdmissionResponse } from '@skyhook-io/k8s-ui/types/scheduling'
|
|
5
5
|
import type { GitOpsWriteEvidence } from '@skyhook-io/k8s-ui/utils/gitops-write-guard'
|
|
6
6
|
import type {
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
YamlDocumentIdentity,
|
|
22
22
|
YamlSchemaLoadResult,
|
|
23
23
|
} from '@skyhook-io/k8s-ui'
|
|
24
|
-
import { useQuery, useMutation, useQueryClient, skipToken } from '@tanstack/react-query'
|
|
24
|
+
import { useQuery, useMutation, useQueryClient, skipToken, type QueryClient } from '@tanstack/react-query'
|
|
25
25
|
import { showApiError, showApiSuccess } from '../components/ui/Toast'
|
|
26
26
|
import { useIsAuthEnabled, useNamespacedCapabilities } from '../contexts/CapabilitiesContext'
|
|
27
27
|
import type {
|
|
@@ -42,6 +42,8 @@ import type {
|
|
|
42
42
|
ResourceDiff,
|
|
43
43
|
UpgradeInfo,
|
|
44
44
|
BatchUpgradeInfo,
|
|
45
|
+
ChartSourceCandidate,
|
|
46
|
+
ChartSourceStatus,
|
|
45
47
|
ValuesDiff,
|
|
46
48
|
ValuesPreviewResponse,
|
|
47
49
|
HelmRepository,
|
|
@@ -229,6 +231,16 @@ export function isNotFoundError(error: unknown): boolean {
|
|
|
229
231
|
return error instanceof ApiError && error.status === 404;
|
|
230
232
|
}
|
|
231
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The server's explanation when it refused a request because the browser
|
|
236
|
+
* reported it came from another site, or undefined for any other error.
|
|
237
|
+
*/
|
|
238
|
+
export function crossOriginRefusalMessage(error: unknown): string | undefined {
|
|
239
|
+
return error instanceof ApiError && error.data?.error_code === "cross_origin_refused"
|
|
240
|
+
? error.message
|
|
241
|
+
: undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
232
244
|
export function isCapacityCursorInvalidError(error: unknown): boolean {
|
|
233
245
|
return (
|
|
234
246
|
error instanceof ApiError &&
|
|
@@ -287,6 +299,20 @@ export function isKindSyncFailed(error: unknown): boolean {
|
|
|
287
299
|
);
|
|
288
300
|
}
|
|
289
301
|
|
|
302
|
+
// timelineDatabaseError returns the driver's error when a timeline read failed
|
|
303
|
+
// because Radar can't connect to its PostgreSQL database, and null for any other
|
|
304
|
+
// failure. The server redacts the password before it sends the error.
|
|
305
|
+
export function timelineDatabaseError(error: unknown): string | null {
|
|
306
|
+
if (
|
|
307
|
+
!(error instanceof ApiError) ||
|
|
308
|
+
error.status !== 503 ||
|
|
309
|
+
error.data?.error_code !== "timeline_database_unreachable"
|
|
310
|
+
)
|
|
311
|
+
return null;
|
|
312
|
+
const detail = error.data.detail;
|
|
313
|
+
return typeof detail === "string" ? detail : "";
|
|
314
|
+
}
|
|
315
|
+
|
|
290
316
|
// SyncKindState / SyncStatusSnapshot mirror k8score's SyncSnapshot — attached
|
|
291
317
|
// to the connection-status payload while the initial informer sync is running.
|
|
292
318
|
export interface SyncKindState {
|
|
@@ -515,6 +541,10 @@ export type { AuditFinding, ResourceGroup, CheckMeta, Check };
|
|
|
515
541
|
|
|
516
542
|
export interface AuditResponse {
|
|
517
543
|
missingInputs?: string[];
|
|
544
|
+
// Absent on a Radar that predates the per-resource opt-out.
|
|
545
|
+
ignoreChecksAnnotation?: string;
|
|
546
|
+
// checkID → namespace → resources whose findings the annotation hid.
|
|
547
|
+
annotationIgnoredByNamespace?: Record<string, Record<string, number>>;
|
|
518
548
|
summary: DashboardAudit;
|
|
519
549
|
findings: AuditFinding[];
|
|
520
550
|
groups: ResourceGroup[];
|
|
@@ -729,9 +759,13 @@ export interface IssuesResponse {
|
|
|
729
759
|
// state 'degraded' means core workload reads are denied, so an empty list may
|
|
730
760
|
// mean "can't see" rather than "nothing broken" — the UI must say so.
|
|
731
761
|
visibility?: { state?: string; impact?: string };
|
|
762
|
+
// Issue sources Radar couldn't check, so their issues may be missing: an
|
|
763
|
+
// empty list is not an all-clear. Today only "helm"; a newer Radar may send
|
|
764
|
+
// others. Absent when every source was checked, and from older Radars.
|
|
765
|
+
unchecked_sources?: { source: string; reason: string }[];
|
|
732
766
|
}
|
|
733
767
|
|
|
734
|
-
export function useIssues(namespaces: string[] = []) {
|
|
768
|
+
export function useIssues(namespaces: string[] = [], enabled = true) {
|
|
735
769
|
const params =
|
|
736
770
|
namespaces.length > 0 ? `?namespaces=${namespaces.join(",")}` : "";
|
|
737
771
|
return useQuery<IssuesResponse>({
|
|
@@ -739,6 +773,7 @@ export function useIssues(namespaces: string[] = []) {
|
|
|
739
773
|
queryFn: () => fetchJSON(`/issues${params}`),
|
|
740
774
|
staleTime: 30000,
|
|
741
775
|
refetchInterval: ISSUES_REFRESH_INTERVAL_MS,
|
|
776
|
+
enabled,
|
|
742
777
|
});
|
|
743
778
|
}
|
|
744
779
|
|
|
@@ -790,6 +825,38 @@ export function useResourceIssues(
|
|
|
790
825
|
});
|
|
791
826
|
}
|
|
792
827
|
|
|
828
|
+
// The Issues page narrowed to one subject: the same per-resource lookup with
|
|
829
|
+
// ?coverage=1, which also says whether Radar reads the subject's kind and what
|
|
830
|
+
// the caller's RBAC withheld. Polls like the page list.
|
|
831
|
+
export interface SubjectIssuesResponse {
|
|
832
|
+
issues: Issue[];
|
|
833
|
+
coverage: "ok" | "syncing" | "notWatched" | "unknownKind";
|
|
834
|
+
withheld?: { issues: number };
|
|
835
|
+
visibility?: { state?: string; impact?: string };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export function useSubjectIssues(
|
|
839
|
+
subject: { kind: string; group: string; namespace: string; name: string } | null,
|
|
840
|
+
enabled: boolean,
|
|
841
|
+
) {
|
|
842
|
+
const capabilities = useCapabilities();
|
|
843
|
+
const { support, gatedKey, guard } = useRadarFeature('resourceIssueCoverage');
|
|
844
|
+
const pathNs = subject?.namespace ? encodeURIComponent(subject.namespace) : "_";
|
|
845
|
+
const params = new URLSearchParams({ coverage: "1" });
|
|
846
|
+
if (subject?.group) params.set("group", subject.group);
|
|
847
|
+
return useQuery<SubjectIssuesResponse>({
|
|
848
|
+
queryKey: ["issues", "subject", subject?.kind ?? "", subject?.group ?? "", subject?.namespace ?? "", subject?.name ?? "", support, ...gatedKey],
|
|
849
|
+
queryFn: () => {
|
|
850
|
+
if (!capabilities.data && capabilities.error) throw capabilities.error;
|
|
851
|
+
return guard(() => fetchJSON<SubjectIssuesResponse>(`/issues/resource/${encodeURIComponent(subject!.kind)}/${pathNs}/${encodeURIComponent(subject!.name)}?${params}`));
|
|
852
|
+
},
|
|
853
|
+
staleTime: 30000,
|
|
854
|
+
refetchInterval: ISSUES_REFRESH_INTERVAL_MS,
|
|
855
|
+
// The existing route alone cannot prove support for this opt-in shape.
|
|
856
|
+
enabled: enabled && !!subject && !capabilities.isPending,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
793
860
|
import type { Trace as NetworkTrace, InClusterCapability, DrainPlan, DrainPlanPod } from '@skyhook-io/k8s-ui'
|
|
794
861
|
|
|
795
862
|
// useTrace polls the static path-shaped diagnosis for one network entry
|
|
@@ -1781,13 +1848,14 @@ export function useRadarFeature(feature: RadarFeature) {
|
|
|
1781
1848
|
const currentVersion = versionInfo?.currentVersion || radarVersion
|
|
1782
1849
|
const latestVersion = versionInfo?.latestVersion
|
|
1783
1850
|
const support = radarFeatureSupport(feature, capabilities, currentVersion)
|
|
1851
|
+
// Stable across renders, so a callback built on guard keeps its identity.
|
|
1852
|
+
const guard = useCallback(
|
|
1853
|
+
<T,>(request: () => Promise<T>): Promise<T> => guardRadarFeature(feature, support, { currentVersion, latestVersion }, request),
|
|
1854
|
+
[feature, support, currentVersion, latestVersion],
|
|
1855
|
+
)
|
|
1856
|
+
const gatedKey = useMemo(() => (support === 'unsupported' ? ['radar-feature-unsupported'] : []), [support])
|
|
1784
1857
|
|
|
1785
|
-
return {
|
|
1786
|
-
support,
|
|
1787
|
-
gatedKey: support === 'unsupported' ? ['radar-feature-unsupported'] : [],
|
|
1788
|
-
guard: <T,>(request: () => Promise<T>): Promise<T> =>
|
|
1789
|
-
guardRadarFeature(feature, support, { currentVersion, latestVersion }, request),
|
|
1790
|
-
}
|
|
1858
|
+
return { support, gatedKey, guard }
|
|
1791
1859
|
}
|
|
1792
1860
|
|
|
1793
1861
|
// ============================================================================
|
|
@@ -2448,6 +2516,11 @@ export function useApplicationHistory(
|
|
|
2448
2516
|
});
|
|
2449
2517
|
}
|
|
2450
2518
|
|
|
2519
|
+
// How often a page asks again while Radar is still connecting to argocd-server,
|
|
2520
|
+
// and while it retries a connect that failed.
|
|
2521
|
+
const ARGO_CONNECTING_POLL_MS = 2000;
|
|
2522
|
+
const ARGO_RETRYING_POLL_MS = 10_000;
|
|
2523
|
+
|
|
2451
2524
|
export function useGitOpsTree(
|
|
2452
2525
|
kind: string,
|
|
2453
2526
|
namespace: string,
|
|
@@ -2470,6 +2543,14 @@ export function useGitOpsTree(
|
|
|
2470
2543
|
),
|
|
2471
2544
|
enabled: Boolean(kind && name) && (options?.enabled ?? true),
|
|
2472
2545
|
staleTime: 5000,
|
|
2546
|
+
// Radar answers at once while it connects to argocd-server; ask again
|
|
2547
|
+
// until Argo's per-resource health is in.
|
|
2548
|
+
refetchInterval: (query) =>
|
|
2549
|
+
query.state.data?.healthApiPending
|
|
2550
|
+
? ARGO_CONNECTING_POLL_MS
|
|
2551
|
+
: query.state.data?.healthApiRetrying
|
|
2552
|
+
? ARGO_RETRYING_POLL_MS
|
|
2553
|
+
: false,
|
|
2473
2554
|
});
|
|
2474
2555
|
}
|
|
2475
2556
|
|
|
@@ -2502,8 +2583,15 @@ export function useGitOpsInsights(
|
|
|
2502
2583
|
staleTime: 5000,
|
|
2503
2584
|
refetchInterval: (query) => {
|
|
2504
2585
|
const phase = query.state.data?.summary?.operationPhase;
|
|
2505
|
-
|
|
2506
|
-
|
|
2586
|
+
if (phase === "Running" || phase === "Terminating") {
|
|
2587
|
+
return INSIGHTS_RUNNING_POLL_MS;
|
|
2588
|
+
}
|
|
2589
|
+
const summary = query.state.data?.summary;
|
|
2590
|
+
if (summary?.resourceHealthApiPending || query.state.data?.capabilities?.argoConnecting) {
|
|
2591
|
+
return ARGO_CONNECTING_POLL_MS;
|
|
2592
|
+
}
|
|
2593
|
+
return summary?.resourceHealthApiRetrying || query.state.data?.capabilities?.argoRetrying
|
|
2594
|
+
? ARGO_RETRYING_POLL_MS
|
|
2507
2595
|
: false;
|
|
2508
2596
|
},
|
|
2509
2597
|
});
|
|
@@ -3204,23 +3292,31 @@ function retryMetricsQuery(failureCount: number, error: unknown): boolean {
|
|
|
3204
3292
|
return !isMetricsUnavailableError(error) && failureCount < 1;
|
|
3205
3293
|
}
|
|
3206
3294
|
|
|
3295
|
+
// The query behind usePodMetrics, for callers reading several Pods at once
|
|
3296
|
+
// (useQueries) that must share its cache and its null-when-unavailable rule.
|
|
3297
|
+
// null means no metrics for this Pod: the metrics API is missing, or the Pod
|
|
3298
|
+
// has not been scraped yet; one Pod's null cannot tell those apart.
|
|
3299
|
+
export function podMetricsQuery(namespace: string, podName: string, enabled = true) {
|
|
3300
|
+
return {
|
|
3301
|
+
queryKey: ["pod-metrics", namespace, podName] as const,
|
|
3302
|
+
queryFn: () =>
|
|
3303
|
+
fetchMetricsOrNull<PodMetrics>(`/metrics/pods/${namespace}/${podName}`),
|
|
3304
|
+
enabled: Boolean(namespace && podName) && enabled,
|
|
3305
|
+
staleTime: 15000,
|
|
3306
|
+
refetchInterval: 30000,
|
|
3307
|
+
refetchOnMount: "always" as const,
|
|
3308
|
+
refetchOnReconnect: "always" as const,
|
|
3309
|
+
retry: retryMetricsQuery,
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3207
3313
|
// Fetch metrics for a specific pod
|
|
3208
3314
|
export function usePodMetrics(
|
|
3209
3315
|
namespace: string,
|
|
3210
3316
|
podName: string,
|
|
3211
3317
|
options?: { enabled?: boolean },
|
|
3212
3318
|
) {
|
|
3213
|
-
return useQuery<PodMetrics | null>(
|
|
3214
|
-
queryKey: ["pod-metrics", namespace, podName],
|
|
3215
|
-
queryFn: () =>
|
|
3216
|
-
fetchMetricsOrNull<PodMetrics>(`/metrics/pods/${namespace}/${podName}`),
|
|
3217
|
-
enabled: Boolean(namespace && podName) && (options?.enabled ?? true),
|
|
3218
|
-
staleTime: 15000,
|
|
3219
|
-
refetchInterval: 30000,
|
|
3220
|
-
refetchOnMount: "always",
|
|
3221
|
-
refetchOnReconnect: "always",
|
|
3222
|
-
retry: retryMetricsQuery,
|
|
3223
|
-
});
|
|
3319
|
+
return useQuery<PodMetrics | null>(podMetricsQuery(namespace, podName, options?.enabled ?? true));
|
|
3224
3320
|
}
|
|
3225
3321
|
|
|
3226
3322
|
export function usePodEnvironment(namespace: string, podName: string, enabled = true) {
|
|
@@ -3287,6 +3383,7 @@ export interface ContainerMetricsHistory {
|
|
|
3287
3383
|
}
|
|
3288
3384
|
|
|
3289
3385
|
export interface PodMetricsHistory {
|
|
3386
|
+
metricsAPIReachable?: boolean;
|
|
3290
3387
|
namespace: string;
|
|
3291
3388
|
name: string;
|
|
3292
3389
|
containers: ContainerMetricsHistory[];
|
|
@@ -3522,7 +3619,7 @@ export type PrometheusTimeRange =
|
|
|
3522
3619
|
// PVC usage at a moment in time, derived from kubelet_volume_stats_*.
|
|
3523
3620
|
export interface PrometheusPVCUsage {
|
|
3524
3621
|
// Hub packages the frontend independently from per-cluster agent upgrades.
|
|
3525
|
-
status?: "available" | "no_series" | "invalid_data" | "query_failed";
|
|
3622
|
+
status?: "available" | "no_series" | "invalid_data" | "query_failed" | "ambiguous_scope" | "scope_mismatch";
|
|
3526
3623
|
namespace: string;
|
|
3527
3624
|
name: string;
|
|
3528
3625
|
used: number;
|
|
@@ -3670,6 +3767,8 @@ export interface ArgoStatus {
|
|
|
3670
3767
|
// client is live. The two differ right after a restart (configured, reconnecting).
|
|
3671
3768
|
configured: boolean;
|
|
3672
3769
|
connected: boolean;
|
|
3770
|
+
// Not connected yet, and a connection is being made.
|
|
3771
|
+
connecting?: boolean;
|
|
3673
3772
|
// Connected without a token: the install serves reads to everyone, so
|
|
3674
3773
|
// there is nothing to add in Settings.
|
|
3675
3774
|
anonymous?: boolean;
|
|
@@ -3682,8 +3781,12 @@ export function useArgoStatus(enabled = true) {
|
|
|
3682
3781
|
queryKey: ["argocd-status"],
|
|
3683
3782
|
queryFn: () => fetchJSON("/integrations/argocd/status"),
|
|
3684
3783
|
enabled,
|
|
3685
|
-
|
|
3686
|
-
|
|
3784
|
+
// Cheap to ask (it never waits on Argo CD): reopening Settings asks
|
|
3785
|
+
// afresh, so "Connecting…" shows only while it's true.
|
|
3786
|
+
staleTime: 0,
|
|
3787
|
+
// While Radar connects, ask again soon so "Connecting…" doesn't linger.
|
|
3788
|
+
refetchInterval: (query) =>
|
|
3789
|
+
query.state.data?.connecting ? ARGO_CONNECTING_POLL_MS : 60000,
|
|
3687
3790
|
});
|
|
3688
3791
|
}
|
|
3689
3792
|
|
|
@@ -4247,7 +4350,7 @@ export function useUpdateResource() {
|
|
|
4247
4350
|
});
|
|
4248
4351
|
}
|
|
4249
4352
|
|
|
4250
|
-
// Cascade delete preview —
|
|
4353
|
+
// Cascade delete preview — visible related resources deletion may affect.
|
|
4251
4354
|
export interface CascadeDeletePreview {
|
|
4252
4355
|
root: { kind: string; namespace: string; name: string; group?: string };
|
|
4253
4356
|
rootResolved: boolean;
|
|
@@ -4557,6 +4660,7 @@ export interface ApplyResourceResult {
|
|
|
4557
4660
|
namespace: string;
|
|
4558
4661
|
kind: string;
|
|
4559
4662
|
apiVersion: string;
|
|
4663
|
+
uid?: string;
|
|
4560
4664
|
created: boolean;
|
|
4561
4665
|
}
|
|
4562
4666
|
|
|
@@ -6144,11 +6248,79 @@ export function useHelmOCISources() {
|
|
|
6144
6248
|
});
|
|
6145
6249
|
}
|
|
6146
6250
|
|
|
6251
|
+
export function useHelmSourceStatus(namespace: string, releaseName: string, enabled = true) {
|
|
6252
|
+
return useQuery<ChartSourceStatus>({
|
|
6253
|
+
queryKey: ["helm-source-status", namespace, releaseName],
|
|
6254
|
+
queryFn: () => fetchJSON(`/helm/releases/${encodeURIComponent(namespace)}/${encodeURIComponent(releaseName)}/source`),
|
|
6255
|
+
enabled: enabled && Boolean(namespace && releaseName),
|
|
6256
|
+
});
|
|
6257
|
+
}
|
|
6258
|
+
|
|
6259
|
+
async function mutateHelmSource(namespace: string, releaseName: string, source: ChartSourceCandidate): Promise<void> {
|
|
6260
|
+
const response = await apiFetch(apiUrl(`/helm/releases/${encodeURIComponent(namespace)}/${encodeURIComponent(releaseName)}/source`), {
|
|
6261
|
+
method: "PUT",
|
|
6262
|
+
headers: { "Content-Type": "application/json" },
|
|
6263
|
+
body: JSON.stringify(source),
|
|
6264
|
+
});
|
|
6265
|
+
if (!response.ok) {
|
|
6266
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
6267
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
|
|
6271
|
+
export function useSetHelmSource(namespace: string, releaseName: string) {
|
|
6272
|
+
const queryClient = useQueryClient();
|
|
6273
|
+
return useMutation({
|
|
6274
|
+
mutationFn: (source: ChartSourceCandidate) => mutateHelmSource(namespace, releaseName, source),
|
|
6275
|
+
meta: { errorMessage: "Failed to associate chart source", successMessage: "Chart source associated" },
|
|
6276
|
+
onSuccess: () => {
|
|
6277
|
+
queryClient.invalidateQueries({ queryKey: ["helm-source-status", namespace, releaseName] });
|
|
6278
|
+
queryClient.invalidateQueries({ queryKey: ["helm-upgrade-info"] });
|
|
6279
|
+
queryClient.invalidateQueries({ queryKey: ["helm-release-versions"] });
|
|
6280
|
+
queryClient.invalidateQueries({ queryKey: ["helm-batch-upgrade-info"] });
|
|
6281
|
+
},
|
|
6282
|
+
});
|
|
6283
|
+
}
|
|
6284
|
+
|
|
6285
|
+
interface AddHelmRepositoryRequest {
|
|
6286
|
+
name: string
|
|
6287
|
+
url: string
|
|
6288
|
+
namespace?: string
|
|
6289
|
+
releaseName?: string
|
|
6290
|
+
}
|
|
6291
|
+
|
|
6292
|
+
export function useAddHelmRepository() {
|
|
6293
|
+
const queryClient = useQueryClient();
|
|
6294
|
+
return useMutation({
|
|
6295
|
+
mutationFn: async (request: AddHelmRepositoryRequest): Promise<{ status: string; name: string }> => {
|
|
6296
|
+
const response = await apiFetch(apiUrl('/helm/repositories'), {
|
|
6297
|
+
method: "POST",
|
|
6298
|
+
headers: { "Content-Type": "application/json" },
|
|
6299
|
+
body: JSON.stringify(request),
|
|
6300
|
+
});
|
|
6301
|
+
if (!response.ok) {
|
|
6302
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
6303
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
6304
|
+
}
|
|
6305
|
+
return response.json();
|
|
6306
|
+
},
|
|
6307
|
+
meta: { errorMessage: "Failed to add Helm repository", successMessage: "Helm repository added" },
|
|
6308
|
+
// Also on error: the repository can be saved before associating it fails.
|
|
6309
|
+
onSettled: () => {
|
|
6310
|
+
queryClient.invalidateQueries({ queryKey: ["helm-repositories"] });
|
|
6311
|
+
queryClient.invalidateQueries({ queryKey: ["helm-source-status"] });
|
|
6312
|
+
queryClient.invalidateQueries({ queryKey: ["helm-upgrade-info"] });
|
|
6313
|
+
queryClient.invalidateQueries({ queryKey: ["helm-release-versions"] });
|
|
6314
|
+
queryClient.invalidateQueries({ queryKey: ["helm-batch-upgrade-info"] });
|
|
6315
|
+
},
|
|
6316
|
+
});
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6147
6319
|
async function mutateOCISource(
|
|
6148
6320
|
method: "POST" | "DELETE",
|
|
6149
6321
|
source: string,
|
|
6150
6322
|
): Promise<string[]> {
|
|
6151
|
-
const response = await apiFetch(
|
|
6323
|
+
const response = await apiFetch(apiUrl('/helm/oci-sources'), {
|
|
6152
6324
|
method,
|
|
6153
6325
|
headers: { "Content-Type": "application/json" },
|
|
6154
6326
|
body: JSON.stringify({ source }),
|
|
@@ -6160,14 +6332,18 @@ async function mutateOCISource(
|
|
|
6160
6332
|
return response.json();
|
|
6161
6333
|
}
|
|
6162
6334
|
|
|
6163
|
-
// Invalidate
|
|
6164
|
-
// immediately
|
|
6165
|
-
function invalidateHelmAfterSourceChange(
|
|
6335
|
+
// Invalidate source inventory/status and upgrade info so an open source dialog
|
|
6336
|
+
// and upgrade checks immediately reflect source additions or removals.
|
|
6337
|
+
export function invalidateHelmAfterSourceChange(
|
|
6166
6338
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
6167
6339
|
) {
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6340
|
+
return Promise.all([
|
|
6341
|
+
queryClient.invalidateQueries({ queryKey: ["helm-oci-sources"] }),
|
|
6342
|
+
queryClient.invalidateQueries({ queryKey: ["helm-source-status"] }),
|
|
6343
|
+
queryClient.invalidateQueries({ queryKey: ["helm-upgrade-info"] }),
|
|
6344
|
+
queryClient.invalidateQueries({ queryKey: ["helm-release-versions"] }),
|
|
6345
|
+
queryClient.invalidateQueries({ queryKey: ["helm-batch-upgrade-info"] }),
|
|
6346
|
+
])
|
|
6171
6347
|
}
|
|
6172
6348
|
|
|
6173
6349
|
export function useAddOCISource() {
|
|
@@ -6520,13 +6696,16 @@ export const useFluxResume = createGitOpsMutation<FluxResourceVars>({
|
|
|
6520
6696
|
export const useFluxSyncWithSource = createGitOpsMutation<FluxResourceVars>({
|
|
6521
6697
|
getPath: (v) => `/flux/${v.kind}/${v.namespace}/${v.name}/sync-with-source`,
|
|
6522
6698
|
errorMessage: "Failed to sync with source",
|
|
6523
|
-
|
|
6699
|
+
// The server names what it reconciled, and says when a source was skipped.
|
|
6700
|
+
getSuccessMessage: (data) => data.message,
|
|
6524
6701
|
getInvalidateKeys: (v) => [
|
|
6525
6702
|
...fluxInvalidateKeys(v),
|
|
6526
6703
|
// Also invalidate source resources as they were reconciled too
|
|
6527
6704
|
["resources", "gitrepositories"],
|
|
6528
6705
|
["resources", "ocirepositories"],
|
|
6529
6706
|
["resources", "helmrepositories"],
|
|
6707
|
+
["resources", "helmcharts"],
|
|
6708
|
+
["resources", "buckets"],
|
|
6530
6709
|
],
|
|
6531
6710
|
});
|
|
6532
6711
|
|
|
@@ -6782,8 +6961,35 @@ export function useNamespaceScope() {
|
|
|
6782
6961
|
});
|
|
6783
6962
|
}
|
|
6784
6963
|
|
|
6785
|
-
const NAMESPACE_SWITCH_TIMEOUT =
|
|
6964
|
+
const NAMESPACE_SWITCH_TIMEOUT = 15000;
|
|
6786
6965
|
const NAMESPACE_RESCOPE_TIMEOUT = 120000;
|
|
6966
|
+
const NAMESPACE_RECONCILE_TIMEOUT = 5000;
|
|
6967
|
+
|
|
6968
|
+
// After a switch times out on the client the server may still have applied
|
|
6969
|
+
// it. Returns the server's scope when it holds exactly the requested pick, so
|
|
6970
|
+
// a slow success is not reported as a failure.
|
|
6971
|
+
export async function reconcileNamespaceSwitch(
|
|
6972
|
+
namespaces: string[],
|
|
6973
|
+
): Promise<NamespaceScope | null> {
|
|
6974
|
+
const controller = new AbortController();
|
|
6975
|
+
const timeoutId = setTimeout(
|
|
6976
|
+
() => controller.abort(),
|
|
6977
|
+
NAMESPACE_RECONCILE_TIMEOUT,
|
|
6978
|
+
);
|
|
6979
|
+
try {
|
|
6980
|
+
const scope = await fetchJSON<NamespaceScope>(
|
|
6981
|
+
"/cluster/namespace-scope",
|
|
6982
|
+
controller.signal,
|
|
6983
|
+
);
|
|
6984
|
+
const want = [...new Set(namespaces)].sort().join(",");
|
|
6985
|
+
const got = [...new Set(scope.actives)].sort().join(",");
|
|
6986
|
+
return want === got ? scope : null;
|
|
6987
|
+
} catch {
|
|
6988
|
+
return null;
|
|
6989
|
+
} finally {
|
|
6990
|
+
clearTimeout(timeoutId);
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6787
6993
|
|
|
6788
6994
|
export function debugNamespaceLog(
|
|
6789
6995
|
label: string,
|
|
@@ -6799,6 +7005,20 @@ export function debugNamespaceLog(
|
|
|
6799
7005
|
});
|
|
6800
7006
|
}
|
|
6801
7007
|
|
|
7008
|
+
/**
|
|
7009
|
+
* A cache-scoped switch rebuilds the watched inventory. View-filter switches
|
|
7010
|
+
* use namespace-specific query keys and do not invalidate the whole client.
|
|
7011
|
+
*/
|
|
7012
|
+
export async function refreshAfterNamespaceSwitch(
|
|
7013
|
+
queryClient: QueryClient,
|
|
7014
|
+
scope: Pick<NamespaceScope, "cacheScoped" | "actives">,
|
|
7015
|
+
): Promise<void> {
|
|
7016
|
+
if (!scope.cacheScoped) return;
|
|
7017
|
+
const notScope = (query: { queryKey: readonly unknown[] }) =>
|
|
7018
|
+
query.queryKey[0] !== "namespace-scope";
|
|
7019
|
+
await queryClient.invalidateQueries({ predicate: notScope }, { cancelRefetch: false });
|
|
7020
|
+
}
|
|
7021
|
+
|
|
6802
7022
|
export function useSetActiveNamespace() {
|
|
6803
7023
|
const queryClient = useQueryClient();
|
|
6804
7024
|
return useMutation<NamespaceScope, Error, { namespaces: string[] }>({
|
|
@@ -6820,7 +7040,7 @@ export function useSetActiveNamespace() {
|
|
|
6820
7040
|
// flag). If the scope query is missing/stale we can't yet tell a cheap
|
|
6821
7041
|
// view-filter change from a cache-rebuilding rescope, so bias to the long
|
|
6822
7042
|
// timeout — only a confirmed non-scoped session gets the fast switch timeout.
|
|
6823
|
-
// Aborting a real rebuild at
|
|
7043
|
+
// Aborting a real rebuild at the switch timeout surfaces a spurious failure while the server
|
|
6824
7044
|
// keeps going.
|
|
6825
7045
|
const isRescope = currentScope?.cacheScoped !== false;
|
|
6826
7046
|
const timeoutMs = isRescope
|
|
@@ -6854,6 +7074,13 @@ export function useSetActiveNamespace() {
|
|
|
6854
7074
|
error: error instanceof Error ? error.message : String(error),
|
|
6855
7075
|
});
|
|
6856
7076
|
if (error instanceof Error && error.name === "AbortError") {
|
|
7077
|
+
// A rescope publishes its target before the rebuild finishes and can
|
|
7078
|
+
// still roll back, so only a view-filter pick is proven by the scope.
|
|
7079
|
+
const applied = isRescope ? null : await reconcileNamespaceSwitch(namespaces);
|
|
7080
|
+
if (applied) {
|
|
7081
|
+
debugNamespaceLog("mutation:timeout-but-applied", { namespaces });
|
|
7082
|
+
return applied;
|
|
7083
|
+
}
|
|
6857
7084
|
throw new Error(
|
|
6858
7085
|
isRescope
|
|
6859
7086
|
? "Namespace rescope timed out. The cluster may still be loading."
|
|
@@ -6876,9 +7103,7 @@ export function useSetActiveNamespace() {
|
|
|
6876
7103
|
});
|
|
6877
7104
|
}
|
|
6878
7105
|
queryClient.setQueryData<NamespaceScope>(["namespace-scope"], scope);
|
|
6879
|
-
|
|
6880
|
-
queryClient.invalidateQueries();
|
|
6881
|
-
}
|
|
7106
|
+
void refreshAfterNamespaceSwitch(queryClient, scope);
|
|
6882
7107
|
debugNamespaceLog("mutation:success-after-scope-cache-write");
|
|
6883
7108
|
},
|
|
6884
7109
|
onError: () => {
|
|
@@ -7322,6 +7547,8 @@ export interface DiagnosticsSnapshot {
|
|
|
7322
7547
|
};
|
|
7323
7548
|
timeline?: {
|
|
7324
7549
|
storageType: string;
|
|
7550
|
+
degraded?: boolean;
|
|
7551
|
+
degradedReason?: string;
|
|
7325
7552
|
totalEvents: number;
|
|
7326
7553
|
oldestEvent?: string;
|
|
7327
7554
|
newestEvent?: string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { CNPGFleetRow } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import type { CNPGRuntimeResponse } from './cnpg'
|
|
4
|
+
import { cnpgInstanceLive, cnpgLiveStandbyGaps, cnpgReplicationLive, withLiveReplication } from '../components/cnpg/runtimeAssessment'
|
|
5
|
+
|
|
6
|
+
const rt = (status: any): CNPGRuntimeResponse => ({
|
|
7
|
+
cluster: { namespace: 'db', name: 'pg', uid: 'u' },
|
|
8
|
+
sampledAt: '2026-09-30T10:00:00Z',
|
|
9
|
+
permission: { proxy: 'allowed' },
|
|
10
|
+
instances: [
|
|
11
|
+
{ pod: 'pg-1', role: 'primary', status, metrics: { state: 'ok' } },
|
|
12
|
+
{ pod: 'pg-2', role: 'replica', status: { state: 'ok', replication: [] }, metrics: { state: 'ok' } },
|
|
13
|
+
],
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('live replication from an incomplete primary report', () => {
|
|
17
|
+
it('is unknown, never "0 streaming"', () => {
|
|
18
|
+
expect(cnpgReplicationLive(rt({ state: 'partial', incomplete: true, replication: null }))).toBeUndefined()
|
|
19
|
+
expect(cnpgReplicationLive(rt({ state: 'ok', replication: [] }))?.streaming).toBe(0)
|
|
20
|
+
})
|
|
21
|
+
it('carries the incomplete flag to the instance list', () => {
|
|
22
|
+
expect(cnpgInstanceLive(rt({ state: 'partial', incomplete: true }))?.[0].incomplete).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('standbys and slots from the live read', () => {
|
|
27
|
+
const row = (over: Partial<CNPGFleetRow> = {}): CNPGFleetRow =>
|
|
28
|
+
({
|
|
29
|
+
key: 'db/pg',
|
|
30
|
+
namespace: 'db',
|
|
31
|
+
name: 'pg',
|
|
32
|
+
cluster: { metadata: { annotations: {} }, status: { currentPrimary: 'pg-6' } },
|
|
33
|
+
instances: { ready: 3, desired: 3 },
|
|
34
|
+
pods: [],
|
|
35
|
+
replication: { text: '2/2 Pods ready · lag unknown', tone: 'unknown' },
|
|
36
|
+
problems: [],
|
|
37
|
+
attention: false,
|
|
38
|
+
categories: new Set(),
|
|
39
|
+
hibernated: false,
|
|
40
|
+
...over,
|
|
41
|
+
}) as unknown as CNPGFleetRow
|
|
42
|
+
const live = (standby1: any): CNPGRuntimeResponse => ({
|
|
43
|
+
cluster: { namespace: 'db', name: 'pg', uid: 'u' },
|
|
44
|
+
sampledAt: '2026-10-04T10:00:00Z',
|
|
45
|
+
permission: { proxy: 'allowed' },
|
|
46
|
+
instances: [
|
|
47
|
+
{
|
|
48
|
+
pod: 'pg-6',
|
|
49
|
+
role: 'primary',
|
|
50
|
+
status: {
|
|
51
|
+
state: 'ok',
|
|
52
|
+
timeline: 7,
|
|
53
|
+
currentLsn: '3/BC06F9E8',
|
|
54
|
+
replication: [{ applicationName: 'pg-2', state: 'streaming', replayLsn: '3/BC06F9E8' }],
|
|
55
|
+
slots: [
|
|
56
|
+
{ name: '_cnpg_pg_1', type: 'physical', active: false, retainedBytes: 4.7e9 },
|
|
57
|
+
{ name: '_cnpg_pg_2', type: 'physical', active: true, retainedBytes: 0 },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
metrics: { state: 'ok' },
|
|
61
|
+
},
|
|
62
|
+
{ pod: 'pg-1', role: 'replica', status: standby1, metrics: { state: 'ok' } },
|
|
63
|
+
{ pod: 'pg-2', role: 'replica', status: { state: 'ok', timeline: 7, roleDetail: 'streaming' }, metrics: { state: 'ok' } },
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('a standby with no row on the primary is a gap, with what it reports about itself', () => {
|
|
68
|
+
const gaps = cnpgLiveStandbyGaps(row(), live({ state: 'ok', timeline: 4, replayPaused: true, roleDetail: 'replayPaused' }))
|
|
69
|
+
expect(gaps.map((g) => g.pod)).toEqual(['pg-1'])
|
|
70
|
+
expect(gaps[0].evidence).toEqual([
|
|
71
|
+
'it has no row in the primary’s pg_stat_replication',
|
|
72
|
+
'its WAL replay is paused',
|
|
73
|
+
'it is on timeline 4 while the primary is on 7',
|
|
74
|
+
])
|
|
75
|
+
})
|
|
76
|
+
it('a fenced standby, or one running pg_rewind, is not a gap', () => {
|
|
77
|
+
const fenced = row({ cluster: { metadata: { annotations: { 'cnpg.io/fencedInstances': '["pg-1"]' } }, status: {} } } as any)
|
|
78
|
+
expect(cnpgLiveStandbyGaps(fenced, live({ state: 'ok', timeline: 7 }))).toEqual([])
|
|
79
|
+
expect(cnpgLiveStandbyGaps(row(), live({ state: 'ok', timeline: 7, roleDetail: 'pgRewind' }))).toEqual([])
|
|
80
|
+
})
|
|
81
|
+
it('raises the gap and the inactive slot as problems, and names the standby in the replication fact', () => {
|
|
82
|
+
const r = withLiveReplication(row(), live({ state: 'ok', timeline: 4, replayPaused: true }))
|
|
83
|
+
expect(r.replication.text).toBe('1 of 2 expected standbys streaming · pg-1 not connected')
|
|
84
|
+
expect(r.attention).toBe(true)
|
|
85
|
+
expect(r.problems.map((p) => p.id).sort()).toEqual(['slot:db/pg:_cnpg_pg_1', 'standby:db/pg:pg-1'])
|
|
86
|
+
expect(r.problems.find((p) => p.id === 'slot:db/pg:_cnpg_pg_1')!.title).toBe('Inactive slot _cnpg_pg_1 holds 4.4 GiB of WAL on pg-6 for pg-1')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('a live read replaces what it disproves', () => {
|
|
91
|
+
const base = (problems: any[]): CNPGFleetRow =>
|
|
92
|
+
({
|
|
93
|
+
key: 'db/pg',
|
|
94
|
+
namespace: 'db',
|
|
95
|
+
name: 'pg',
|
|
96
|
+
cluster: { metadata: {}, status: {} },
|
|
97
|
+
instances: { ready: 2, desired: 2 },
|
|
98
|
+
pods: [],
|
|
99
|
+
replication: { text: '2/2 Pods ready · pg-2 not receiving WAL', tone: 'degraded' },
|
|
100
|
+
problems,
|
|
101
|
+
attention: true,
|
|
102
|
+
categories: new Set(['availability']),
|
|
103
|
+
hibernated: false,
|
|
104
|
+
}) as unknown as CNPGFleetRow
|
|
105
|
+
const fleetStandby = { id: 'standby:db/pg:pg-2', reason: 'CNPGStandbyNotReceiving', severity: 'warning', category: 'availability', title: 'pg-2 is not receiving WAL from the primary', subject: { kind: 'Pod', group: '', namespace: 'db', name: 'pg-2' }, source: 'measurement' }
|
|
106
|
+
const fleetSlot = { id: 'slot:db/pg:_cnpg_pg_2', reason: 'CNPGInactiveSlot', slot: '_cnpg_pg_2', severity: 'warning', category: 'availability', title: 'Inactive slot _cnpg_pg_2 holds 2 GiB', subject: { kind: 'Cluster', group: 'postgresql.cnpg.io', namespace: 'db', name: 'pg' }, source: 'measurement' }
|
|
107
|
+
const rt = (primaryStatus: any): CNPGRuntimeResponse => ({
|
|
108
|
+
cluster: { namespace: 'db', name: 'pg', uid: 'u' },
|
|
109
|
+
sampledAt: '2026-10-04T10:00:00Z',
|
|
110
|
+
permission: { proxy: 'allowed' },
|
|
111
|
+
instances: [
|
|
112
|
+
{ pod: 'pg-1', role: 'primary', status: primaryStatus, metrics: { state: 'ok' } },
|
|
113
|
+
{ pod: 'pg-2', role: 'replica', status: { state: 'ok', timeline: 1 }, metrics: { state: 'ok' } },
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
it('clears the fleet’s standby and slot problems when the primary streams to it and the slot is active', () => {
|
|
117
|
+
const r = withLiveReplication(
|
|
118
|
+
base([fleetStandby, fleetSlot]),
|
|
119
|
+
rt({ state: 'ok', timeline: 1, replication: [{ applicationName: 'pg-2', state: 'streaming' }], slots: [{ name: '_cnpg_pg_2', type: 'physical', active: true, retainedBytes: 0 }] }),
|
|
120
|
+
)
|
|
121
|
+
expect(r.problems).toEqual([])
|
|
122
|
+
expect(r.attention).toBe(false)
|
|
123
|
+
expect(r.replication.text).toBe('1/1 streaming')
|
|
124
|
+
})
|
|
125
|
+
it('keeps a fleet slot warning the live read cannot disprove', () => {
|
|
126
|
+
// Unmeasured: inactive, retained bytes not reported.
|
|
127
|
+
const unmeasured = withLiveReplication(base([fleetSlot]), rt({ state: 'ok', timeline: 1, replication: [{ applicationName: 'pg-2', state: 'streaming' }], slots: [{ name: '_cnpg_pg_2', type: 'physical', active: false }] }))
|
|
128
|
+
expect(unmeasured.problems.map((p) => p.id)).toEqual(['slot:db/pg:_cnpg_pg_2'])
|
|
129
|
+
// Absent from a capped, partial list.
|
|
130
|
+
const partial = withLiveReplication(base([fleetSlot]), rt({ state: 'partial', replication: null, slots: [] }))
|
|
131
|
+
expect(partial.problems.map((p) => p.id)).toEqual(['slot:db/pg:_cnpg_pg_2'])
|
|
132
|
+
// Absent from a complete list: the slot is gone.
|
|
133
|
+
const gone = withLiveReplication(base([fleetSlot]), rt({ state: 'ok', timeline: 1, replication: [{ applicationName: 'pg-2', state: 'streaming' }], slots: [] }))
|
|
134
|
+
expect(gone.problems).toEqual([])
|
|
135
|
+
})
|
|
136
|
+
it('judges slots from a partial primary report the replication rows cannot use', () => {
|
|
137
|
+
const r = withLiveReplication(base([]), rt({ state: 'partial', incomplete: true, replication: null, slots: [{ name: '_cnpg_pg_2', type: 'physical', active: false, retainedBytes: 3 * 1024 ** 3 }] }))
|
|
138
|
+
expect(r.problems.map((p) => p.id)).toEqual(['slot:db/pg:_cnpg_pg_2'])
|
|
139
|
+
})
|
|
140
|
+
})
|