@skyhook-io/radar-app 1.16.3 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/App.tsx +138 -93
- package/src/RadarApp.tsx +27 -17
- package/src/api/actions.errorCode.test.ts +40 -0
- package/src/api/actions.test.ts +48 -0
- package/src/api/actions.ts +79 -0
- package/src/api/apiResources.navigation.test.tsx +232 -0
- package/src/api/apiResources.ts +46 -3
- package/src/api/client.goneResource.test.ts +49 -0
- package/src/api/client.helmSource.test.ts +39 -0
- package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
- package/src/api/client.namespaceSwitch.test.ts +107 -0
- package/src/api/client.ts +381 -47
- package/src/api/cnpg-ha.live.test.ts +140 -0
- package/src/api/cnpg-ha.test.ts +78 -0
- package/src/api/cnpg-ha.ts +19 -0
- package/src/api/cnpg-history.ts +123 -0
- package/src/api/cnpg-inspect.ts +102 -0
- package/src/api/cnpg-protection.ts +112 -0
- package/src/api/cnpg-recovery.test.tsx +60 -0
- package/src/api/cnpg-recovery.ts +262 -0
- package/src/api/cnpg-sessions.ts +175 -0
- package/src/api/cnpg-storage.ts +138 -0
- package/src/api/cnpg.legacy-reads.test.ts +43 -0
- package/src/api/cnpg.pooler-runtime.test.ts +15 -0
- package/src/api/cnpg.ts +428 -10
- package/src/api/diagnose.test.ts +40 -0
- package/src/api/diagnose.ts +12 -1
- package/src/api/radarFeatures.render.test.tsx +65 -3
- package/src/api/radarFeatures.ts +6 -0
- package/src/api/reviewedActionMutation.test.tsx +164 -0
- package/src/api/reviewedActionMutation.ts +66 -0
- package/src/api/timelineSource.test.ts +9 -0
- package/src/api/timelineSource.ts +8 -0
- package/src/api/traffic.sources.test.ts +55 -0
- package/src/api/traffic.ts +173 -14
- package/src/components/applications/ApplicationsView.tsx +3 -0
- package/src/components/audit/AuditView.tsx +9 -0
- package/src/components/capacity/CapacityActivity.tsx +3 -31
- package/src/components/capacity/CapacityDemand.tsx +4 -36
- package/src/components/capacity/CapacityOverview.tsx +217 -56
- package/src/components/capacity/CapacityPoolDetail.tsx +24 -62
- package/src/components/capacity/CapacityView.test.tsx +354 -1
- package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
- package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
- package/src/components/capacity/routes.ts +42 -0
- package/src/components/capacity/shared.tsx +23 -150
- package/src/components/capacity/workspaceRoute.ts +18 -0
- package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
- package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
- package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
- package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
- package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
- package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
- package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
- package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
- package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
- package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
- package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
- package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
- package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
- package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
- package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
- package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
- package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
- package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
- package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
- package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
- package/src/components/cnpg/CNPGOperator.tsx +78 -30
- package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
- package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
- package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
- package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
- package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
- package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
- package/src/components/cnpg/CNPGOverview.tsx +238 -63
- package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
- package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
- package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
- package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
- package/src/components/cnpg/CNPGPerformance.tsx +477 -0
- package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
- package/src/components/cnpg/CNPGPooling.tsx +97 -40
- package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
- package/src/components/cnpg/CNPGProtection.tsx +230 -124
- package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
- package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
- package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
- package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
- package/src/components/cnpg/CNPGStorage.tsx +619 -0
- package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
- package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
- package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
- package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
- package/src/components/cnpg/CNPGTrends.test.ts +38 -0
- package/src/components/cnpg/CNPGTrends.tsx +702 -0
- package/src/components/cnpg/CNPGView.tsx +34 -21
- package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
- package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
- package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
- package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
- package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
- package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
- package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
- package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
- package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
- package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
- package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
- package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
- package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
- package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
- package/src/components/cnpg/actions/actionModel.test.ts +111 -0
- package/src/components/cnpg/actions/actionModel.ts +127 -0
- package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
- package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
- package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
- package/src/components/cnpg/archivingRepair.test.ts +104 -0
- package/src/components/cnpg/archivingRepair.ts +145 -0
- package/src/components/cnpg/baseBackup.test.ts +36 -0
- package/src/components/cnpg/baseBackup.ts +36 -0
- package/src/components/cnpg/blocking.test.ts +59 -0
- package/src/components/cnpg/blocking.ts +131 -0
- package/src/components/cnpg/coverageText.test.ts +27 -0
- package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
- package/src/components/cnpg/creation/targetModel.test.ts +137 -0
- package/src/components/cnpg/creation/targetModel.ts +149 -0
- package/src/components/cnpg/fleetStatus.test.ts +46 -0
- package/src/components/cnpg/fleetStatus.ts +43 -0
- package/src/components/cnpg/formFields.ts +2 -0
- package/src/components/cnpg/grantText.test.tsx +11 -0
- package/src/components/cnpg/host.test.tsx +24 -0
- package/src/components/cnpg/host.tsx +101 -0
- package/src/components/cnpg/inspectModel.test.ts +120 -0
- package/src/components/cnpg/inspectModel.ts +140 -0
- package/src/components/cnpg/logicalSlots.test.ts +30 -0
- package/src/components/cnpg/logicalSlots.ts +29 -0
- package/src/components/cnpg/lsn.test.ts +40 -0
- package/src/components/cnpg/lsn.ts +43 -0
- package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
- package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
- package/src/components/cnpg/operations/model.test.ts +510 -0
- package/src/components/cnpg/operations/model.ts +499 -0
- package/src/components/cnpg/operations/presentation.test.ts +80 -0
- package/src/components/cnpg/operations/presentation.ts +84 -0
- package/src/components/cnpg/operations/store.ts +98 -0
- package/src/components/cnpg/operatorStatus.test.ts +134 -0
- package/src/components/cnpg/operatorStatus.ts +194 -0
- package/src/components/cnpg/paths.test.ts +54 -0
- package/src/components/cnpg/paths.ts +47 -4
- package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
- package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
- package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
- package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
- package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
- package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
- package/src/components/cnpg/protection/scheduleModel.ts +16 -0
- package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
- package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
- package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
- package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
- package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
- package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
- package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
- package/src/components/cnpg/recovery/restoreModel.ts +685 -0
- package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
- package/src/components/cnpg/refresh.test.ts +38 -0
- package/src/components/cnpg/refresh.ts +17 -0
- package/src/components/cnpg/refreshNotice.test.tsx +29 -0
- package/src/components/cnpg/routes.test.ts +34 -3
- package/src/components/cnpg/routes.ts +60 -48
- package/src/components/cnpg/runtimeAssessment.ts +198 -0
- package/src/components/cnpg/runtimeModel.test.ts +175 -0
- package/src/components/cnpg/runtimeModel.ts +196 -0
- package/src/components/cnpg/runtimeParts.test.tsx +17 -0
- package/src/components/cnpg/runtimeParts.tsx +79 -0
- package/src/components/cnpg/shared.test.tsx +59 -0
- package/src/components/cnpg/shared.tsx +100 -182
- package/src/components/cnpg/storageModel.test.ts +107 -0
- package/src/components/cnpg/storageModel.ts +101 -0
- package/src/components/cnpg/trendSamples.test.ts +148 -0
- package/src/components/cnpg/trendSamples.ts +222 -0
- package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
- package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
- package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
- package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
- package/src/components/cnpg/useCNPGNavigate.ts +21 -0
- package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
- package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
- package/src/components/cnpg/workspaceRoute.ts +23 -0
- package/src/components/cost/ApplicationCostTab.test.ts +7 -0
- package/src/components/cost/ApplicationCostTab.tsx +7 -7
- package/src/components/cost/CostView.test.ts +2 -1
- package/src/components/cost/CostView.tsx +0 -2
- package/src/components/cost/WorkloadCostTab.tsx +2 -4
- package/src/components/cost/errors.ts +3 -1
- package/src/components/cost/source.test.ts +10 -0
- package/src/components/cost/source.ts +5 -1
- package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
- package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
- package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
- package/src/components/diagnose/ApplyDialog.tsx +26 -28
- package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
- package/src/components/diagnose/DiagnoseContext.tsx +136 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
- package/src/components/diagnose/InvestigationView.tsx +39 -2
- package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
- package/src/components/diagnose/agentCatalog.ts +2 -2
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
- package/src/components/dock/BottomDock.tsx +2 -0
- package/src/components/dock/TerminalTab.test.tsx +17 -0
- package/src/components/dock/TerminalTab.tsx +7 -3
- package/src/components/dock/TrafficFlowListTab.tsx +27 -6
- package/src/components/execution/BatchExecutionView.tsx +11 -11
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
- package/src/components/gitops/GitOpsView.tsx +16 -23
- package/src/components/helm/ChartBrowser.tsx +3 -3
- package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
- package/src/components/helm/HelmCompareRoute.tsx +57 -26
- package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
- package/src/components/helm/RevisionHistory.tsx +3 -2
- package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
- package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
- package/src/components/helm/ValuesViewer.test.tsx +301 -0
- package/src/components/helm/ValuesViewer.tsx +169 -78
- package/src/components/home/ActivitySummary.tsx +2 -1
- package/src/components/home/ClusterHealthCard.tsx +7 -0
- package/src/components/home/HomeView.tsx +20 -9
- package/src/components/home/ProblemsPanel.test.tsx +81 -0
- package/src/components/home/TopologyPreview.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/issues/IssuesPane.render.test.tsx +122 -0
- package/src/components/issues/IssuesPane.test.ts +65 -1
- package/src/components/issues/IssuesPane.tsx +188 -25
- package/src/components/issues/uncheckedSources.ts +21 -0
- package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
- package/src/components/resource/PVCUsageBar.test.tsx +24 -0
- package/src/components/resource/PVCUsageBar.tsx +15 -6
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resources/CompositeRenderer.tsx +4 -4
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
- package/src/components/resources/ResourcesView.tsx +26 -12
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
- package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
- package/src/components/settings/SettingsDialog.tsx +150 -34
- package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
- package/src/components/timeline/TimelineList.tsx +25 -24
- package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
- package/src/components/timeline/TimelineLoadError.tsx +96 -0
- package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
- package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
- package/src/components/timeline/TimelineView.tsx +25 -45
- package/src/components/timeline/ViewModeToggle.tsx +20 -0
- package/src/components/timeline/useStickyError.test.tsx +47 -0
- package/src/components/timeline/useStickyError.ts +15 -0
- package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
- package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
- package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
- package/src/components/traffic/TrafficGraph.tsx +109 -26
- package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
- package/src/components/traffic/TrafficView.tsx +582 -149
- package/src/components/traffic/trafficFilters.test.ts +398 -1
- package/src/components/traffic/trafficFilters.ts +408 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
- package/src/components/ui/Markdown.test.tsx +65 -0
- package/src/components/ui/Markdown.tsx +144 -127
- package/src/components/whats-new/releaseNotes.ts +63 -1
- package/src/components/workload/WorkloadView.test.ts +11 -1
- package/src/components/workload/WorkloadView.tsx +162 -529
- package/src/components/workspace/layout.test.tsx +29 -0
- package/src/components/workspace/layout.tsx +259 -0
- package/src/components/workspace/table.ts +7 -0
- package/src/context/TimelineCustomization.tsx +15 -0
- package/src/contexts/CapabilitiesContext.tsx +1 -1
- package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
- package/src/hooks/useGitOpsWriteGuard.ts +193 -0
- package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
- package/src/integrations/resourceHost.test.tsx +55 -0
- package/src/integrations/resourceHost.ts +99 -0
- package/src/integrations/resourceHosts.test.tsx +141 -0
- package/src/integrations/resourceHosts.tsx +119 -0
- package/src/integrations/workspaceRoute.ts +20 -0
- package/src/integrations/workspaceRoutes.test.ts +40 -0
- package/src/integrations/workspaceRoutes.ts +22 -0
- package/src/integrations/workspaceScreens.tsx +14 -0
- package/src/utils/drawer-trail.ts +35 -0
- package/src/utils/kindFilterStore.test.ts +147 -0
- package/src/utils/kindFilterStore.ts +91 -0
- package/src/utils/navigation.test.ts +13 -1
- package/src/utils/navigation.ts +39 -2
- package/src/utils/page-links.ts +21 -0
- package/src/utils/tabStorage.ts +40 -0
- package/src/utils/viewMemory.test.ts +78 -0
- package/src/utils/viewMemory.ts +54 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
|
-
import { useNavigate } from 'react-router-dom'
|
|
3
2
|
import { Database, FileCheck2, Settings2, ShieldCheck, Waypoints } from 'lucide-react'
|
|
4
|
-
import { buildCNPGFleet, type CNPGFleet, type SidebarCategoryWorkspace } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { applyCNPGDisk, applyCNPGFleetMetrics, buildCNPGFleet, type CNPGDiskReading, type CNPGFleet, type SidebarCategoryWorkspace } from '@skyhook-io/k8s-ui'
|
|
5
4
|
import type { APIResource } from '../../types'
|
|
6
5
|
import { useCNPGWorkspace } from '../../api/cnpg'
|
|
6
|
+
import { useRadarFeature } from '../../api/client'
|
|
7
|
+
import { useCNPGFleetDisk } from '../../api/cnpg-storage'
|
|
8
|
+
import { useCNPGFleetMetrics } from '../../api/cnpg-history'
|
|
7
9
|
import { CNPG_SCREENS, type CNPGScreen } from './routes'
|
|
10
|
+
import { useCNPGNavigate } from './useCNPGNavigate'
|
|
8
11
|
|
|
9
12
|
export const CNPG_SIDEBAR_CATEGORY = 'CloudNativePG'
|
|
10
13
|
|
|
@@ -20,24 +23,67 @@ export function cnpgDiscovered(apiResources: APIResource[] | undefined): boolean
|
|
|
20
23
|
return !!apiResources?.some((r) => r.group === 'postgresql.cnpg.io')
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
// Disk use joins the fleet here, so low-disk clusters count toward Needs
|
|
27
|
+
// attention on every screen and sidebar badge that reads the enriched fleet;
|
|
28
|
+
// measured replication lag and disk growth join the same way. Both are
|
|
29
|
+
// Prometheus fan-outs per namespace, so callers outside the CloudNativePG views
|
|
30
|
+
// pass withMetrics=false.
|
|
31
|
+
export function useCNPGFleet(namespaces: string[], enabled = true, withMetrics = true) {
|
|
24
32
|
const query = useCNPGWorkspace(namespaces, { enabled })
|
|
25
|
-
const
|
|
33
|
+
const { support } = useRadarFeature('cnpgWorkspace')
|
|
34
|
+
const enrich = enabled && withMetrics && support === 'supported' && !!query.data?.installed
|
|
35
|
+
const disk = useCNPGFleetDisk(namespaces, enrich)
|
|
36
|
+
const metrics = useCNPGFleetMetrics(namespaces, enrich)
|
|
37
|
+
const fleet = useMemo<CNPGFleet | null>(
|
|
38
|
+
() =>
|
|
39
|
+
query.data?.installed
|
|
40
|
+
? applyCNPGFleetMetrics(
|
|
41
|
+
applyCNPGDisk(buildCNPGFleet(query.data), enrich ? disk.data?.clusters ?? (disk.error ? diskFailed(query.data.objects.clusters ?? [], disk.error) : undefined) : undefined),
|
|
42
|
+
enrich ? metrics.data?.clusters : undefined,
|
|
43
|
+
enrich ? metrics.data : undefined,
|
|
44
|
+
)
|
|
45
|
+
: null,
|
|
46
|
+
[query.data, disk.data, disk.error, metrics.data, enrich],
|
|
47
|
+
)
|
|
26
48
|
return { query, fleet }
|
|
27
49
|
}
|
|
28
50
|
|
|
29
|
-
|
|
51
|
+
// A failed disk read is stated per cluster; left undefined it would render as
|
|
52
|
+
// still loading.
|
|
53
|
+
function diskFailed(clusters: any[], err: unknown): CNPGDiskReading[] {
|
|
54
|
+
const reason = `Disk usage could not be read: ${err instanceof Error ? err.message : 'request failed'}`
|
|
55
|
+
return clusters.map((c) => ({ namespace: c?.metadata?.namespace ?? '', name: c?.metadata?.name ?? '', state: 'error', reason, claims: 0, measured: 0 }))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const UNMEASURED_NOTE = 'Disk use and replication lag are counted on the CloudNativePG views.'
|
|
59
|
+
|
|
60
|
+
function destinationCount(screen: CNPGScreen, fleet: CNPGFleet | null, withMetrics: boolean): { count?: number | null; lowerBound?: boolean; title?: string } {
|
|
30
61
|
if (!fleet) return {}
|
|
31
|
-
const
|
|
62
|
+
const partial = fleet.incompleteKinds.length > 0
|
|
63
|
+
const note = partial ? ' Some CloudNativePG data is not readable, so this is a lower bound.' : ''
|
|
64
|
+
// Disk use and measured lag only ever add problems, so a count without them
|
|
65
|
+
// is a lower bound, and its zero is not "none".
|
|
66
|
+
const at = (count: number, what: string, unmeasured?: string) => {
|
|
67
|
+
const lowerBound = partial || !!unmeasured
|
|
68
|
+
const extra = unmeasured ? ` ${unmeasured}` : ''
|
|
69
|
+
if (lowerBound && count === 0) {
|
|
70
|
+
return {
|
|
71
|
+
count: null,
|
|
72
|
+
lowerBound: true,
|
|
73
|
+
title: partial ? `Unknown: some CloudNativePG data could not be read.${extra}` : `None found in the workspace read.${extra}`,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { count, lowerBound, title: `${lowerBound ? 'At least ' : ''}${count} ${what}.${note}${extra}` }
|
|
77
|
+
}
|
|
32
78
|
switch (screen) {
|
|
33
79
|
case 'overview':
|
|
34
|
-
return
|
|
80
|
+
return at(fleet.attentionCount, 'clusters need attention', withMetrics ? undefined : UNMEASURED_NOTE)
|
|
35
81
|
case 'protection':
|
|
36
|
-
return
|
|
82
|
+
return at(fleet.categoryCounts.protection, 'clusters with failing backups or WAL archiving')
|
|
37
83
|
case 'declarations':
|
|
38
|
-
return
|
|
84
|
+
return at(fleet.categoryCounts.declarations, 'clusters with declarations that are not reconciled')
|
|
39
85
|
case 'pooling':
|
|
40
|
-
return
|
|
86
|
+
return at(fleet.categoryCounts.pooling, 'clusters with Pooler problems')
|
|
41
87
|
default:
|
|
42
88
|
return {}
|
|
43
89
|
}
|
|
@@ -52,25 +98,29 @@ export function useCNPGSidebarWorkspace({
|
|
|
52
98
|
apiResources,
|
|
53
99
|
namespaces,
|
|
54
100
|
active,
|
|
101
|
+
withMetrics = !!active,
|
|
55
102
|
}: {
|
|
56
103
|
apiResources: APIResource[] | undefined
|
|
57
104
|
namespaces: string[]
|
|
58
105
|
active?: { screen: CNPGScreen; child?: { label: string; title?: string } }
|
|
106
|
+
/** Count disk use and replication lag too. Off by default outside the CloudNativePG views: every Resources page renders this sidebar. */
|
|
107
|
+
withMetrics?: boolean
|
|
59
108
|
}): Record<string, SidebarCategoryWorkspace> | undefined {
|
|
60
|
-
const navigate =
|
|
61
|
-
const
|
|
62
|
-
const { fleet } = useCNPGFleet(namespaces,
|
|
109
|
+
const navigate = useCNPGNavigate()
|
|
110
|
+
const available = cnpgDiscovered(apiResources)
|
|
111
|
+
const { fleet } = useCNPGFleet(namespaces, available, withMetrics)
|
|
63
112
|
const nsKey = namespaces.join(',')
|
|
64
113
|
|
|
65
114
|
return useMemo(() => {
|
|
66
|
-
if (!
|
|
115
|
+
if (!available) return undefined
|
|
67
116
|
const destinations = CNPG_SCREENS.map((s) => {
|
|
68
|
-
const { count, title } = destinationCount(s.id, fleet)
|
|
117
|
+
const { count, lowerBound, title } = destinationCount(s.id, fleet, withMetrics)
|
|
69
118
|
return {
|
|
70
119
|
id: s.id,
|
|
71
120
|
label: s.label,
|
|
72
121
|
icon: ICONS[s.id],
|
|
73
122
|
count,
|
|
123
|
+
countLowerBound: lowerBound,
|
|
74
124
|
countTitle: title,
|
|
75
125
|
active: active?.screen === s.id,
|
|
76
126
|
child: active?.screen === s.id ? active.child : undefined,
|
|
@@ -84,5 +134,5 @@ export function useCNPGSidebarWorkspace({
|
|
|
84
134
|
scopeNote: nsKey ? `Counts for namespace ${nsKey.split(',').join(', ')}` : undefined,
|
|
85
135
|
},
|
|
86
136
|
}
|
|
87
|
-
}, [
|
|
137
|
+
}, [available, fleet, withMetrics, active?.screen, active?.child?.label, active?.child?.title, navigate, nsKey]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
88
138
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WorkspaceRoute } from '../../integrations/workspaceRoute'
|
|
2
|
+
import { CNPG_SCREENS, parseCNPGRoute } from './routes'
|
|
3
|
+
|
|
4
|
+
export const cnpgWorkspaceRoute: WorkspaceRoute = {
|
|
5
|
+
id: 'cnpg',
|
|
6
|
+
path: '/cnpg',
|
|
7
|
+
label: 'CloudNativePG',
|
|
8
|
+
navView: 'resources',
|
|
9
|
+
namespaceScope: 'view',
|
|
10
|
+
pageTitle(pathname) {
|
|
11
|
+
const route = parseCNPGRoute(pathname)
|
|
12
|
+
if (route.detail) return route.detail.name
|
|
13
|
+
const screen = CNPG_SCREENS.find(screen => screen.id === route.screen)
|
|
14
|
+
return `CloudNativePG ${screen?.label ?? 'Clusters'}`
|
|
15
|
+
},
|
|
16
|
+
contextSwitchSearch(pathname, search) {
|
|
17
|
+
// A detail's context pin prevents loading a same-named object after a switch.
|
|
18
|
+
const next = new URLSearchParams()
|
|
19
|
+
const context = search.get('ctx')
|
|
20
|
+
if (context && pathname.startsWith('/cnpg/')) next.set('ctx', context)
|
|
21
|
+
return next
|
|
22
|
+
},
|
|
23
|
+
}
|
|
@@ -195,6 +195,13 @@ describe('getApplicationCostState', () => {
|
|
|
195
195
|
).toBe('not_found')
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
+
it('does not report a cross-origin refusal as missing access', () => {
|
|
199
|
+
const refused = new ApiError('Radar refused this request because it came from http://radar.internal', 403, {
|
|
200
|
+
error_code: 'cross_origin_refused',
|
|
201
|
+
})
|
|
202
|
+
expect(getApplicationCostState(undefined, undefined, { currentError: refused })).toBe('load_error')
|
|
203
|
+
})
|
|
204
|
+
|
|
198
205
|
it('queries only steady-state workload kinds', () => {
|
|
199
206
|
const workloads = [
|
|
200
207
|
{ kind: 'Deployment', namespace: 'prod', name: 'api' },
|
|
@@ -3,6 +3,7 @@ import { AlertCircle, Coins, HelpCircle, Loader2, TrendingUp } from 'lucide-reac
|
|
|
3
3
|
import type { AppRow, AppWorkload } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import {
|
|
5
5
|
COST_DISCOVERY_GRACE_MS,
|
|
6
|
+
crossOriginRefusalMessage,
|
|
6
7
|
useOpenCostApplicationCost,
|
|
7
8
|
useOpenCostApplicationCostTrend,
|
|
8
9
|
type CostTimeRange,
|
|
@@ -149,7 +150,8 @@ export function ApplicationCostTab({
|
|
|
149
150
|
/>
|
|
150
151
|
)
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
const refusal = crossOriginRefusalMessage(currentQuery.error) ?? crossOriginRefusalMessage(trendQuery.error)
|
|
154
|
+
return <ApplicationCostUnavailable state={state} message={refusal} settingsAvailable={settingsAvailable} />
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
const current = currentQuery.data
|
|
@@ -512,11 +514,11 @@ function applicationCostKey(ref: { kind: string; namespace: string; name: string
|
|
|
512
514
|
function reasonLabel(reason?: CostUnavailableReason) {
|
|
513
515
|
if (reason === 'no_prometheus') return 'Metrics backend not found'
|
|
514
516
|
if (reason === 'no_cost_source') return 'Cost source not found'
|
|
515
|
-
if (reason === 'query_error') return 'Cost
|
|
517
|
+
if (reason === 'query_error') return 'Cost data unavailable'
|
|
516
518
|
if (reason === 'access_denied') return 'No access to this workload'
|
|
517
519
|
if (reason === 'not_found') return 'Workload not found'
|
|
518
520
|
if (reason === 'configuration_mismatch') return 'Kubecost settings are not valid for this cluster'
|
|
519
|
-
if (reason === 'deployment_configuration_error') return 'Radar
|
|
521
|
+
if (reason === 'deployment_configuration_error') return 'Radar configuration is invalid'
|
|
520
522
|
if (reason === 'metrics_settings_error') return 'Saved metrics settings need review'
|
|
521
523
|
if (reason === 'cost_settings_error') return 'Saved cost settings need review'
|
|
522
524
|
return 'No workload cost metrics'
|
|
@@ -568,10 +570,8 @@ function ApplicationCostUnavailable({
|
|
|
568
570
|
costIntegrationUnavailableMessage(state, settingsAvailable) ??
|
|
569
571
|
(state === 'no_prometheus'
|
|
570
572
|
? 'No compatible metrics backend was found. OpenCost application cost requires OpenCost metrics in a PromQL-compatible backend.'
|
|
571
|
-
: state === '
|
|
572
|
-
? '
|
|
573
|
-
: state === 'history_unsupported'
|
|
574
|
-
? 'Historical application cost is not available for Kubecost yet.'
|
|
573
|
+
: state === 'history_unsupported'
|
|
574
|
+
? 'Historical application cost is not available for Kubecost yet.'
|
|
575
575
|
: state === 'access_denied'
|
|
576
576
|
? 'Cost data is unavailable because these workloads are not accessible with your current permissions.'
|
|
577
577
|
: state === 'not_found'
|
|
@@ -17,7 +17,8 @@ describe('costUnavailableMessage', () => {
|
|
|
17
17
|
expect(costUnavailableMessage('source_unavailable')).toContain('Settings → Cost')
|
|
18
18
|
expect(costUnavailableMessage('authentication_error')).toContain('API key')
|
|
19
19
|
expect(costUnavailableMessage('configuration_mismatch')).toContain('not valid for this cluster')
|
|
20
|
-
expect(costUnavailableMessage('deployment_configuration_error')).toContain('
|
|
20
|
+
expect(costUnavailableMessage('deployment_configuration_error')).toContain('launch flags')
|
|
21
|
+
expect(costUnavailableMessage('deployment_configuration_error')).toContain('Helm values')
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
it('distinguishes a scoped empty result from missing source data', () => {
|
|
@@ -402,8 +402,6 @@ export function costUnavailableMessage(
|
|
|
402
402
|
return 'No compatible cost source found — connect OpenCost metrics through a PromQL-compatible backend or configure Kubecost'
|
|
403
403
|
case 'no_metrics':
|
|
404
404
|
return 'Cost data is not ready — the selected source returned no allocation data'
|
|
405
|
-
case 'query_error':
|
|
406
|
-
return 'Cost data is temporarily unavailable — the selected source query failed'
|
|
407
405
|
case 'access_denied':
|
|
408
406
|
return 'You do not have access to view cluster cost data'
|
|
409
407
|
default:
|
|
@@ -363,10 +363,8 @@ function WorkloadCostUnavailable({
|
|
|
363
363
|
costIntegrationUnavailableMessage(state, settingsAvailable) ??
|
|
364
364
|
(state === 'no_prometheus'
|
|
365
365
|
? 'No compatible metrics backend was found. OpenCost workload cost requires OpenCost metrics in a PromQL-compatible backend.'
|
|
366
|
-
: state === '
|
|
367
|
-
? '
|
|
368
|
-
: state === 'history_unsupported'
|
|
369
|
-
? 'Historical workload cost is not available for Kubecost yet.'
|
|
366
|
+
: state === 'history_unsupported'
|
|
367
|
+
? 'Historical workload cost is not available for Kubecost yet.'
|
|
370
368
|
: state === 'access_denied'
|
|
371
369
|
? 'You do not have access to view cost for this workload.'
|
|
372
370
|
: state === 'not_found'
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ApiError, type CostUnavailableReason } from '../../api/client'
|
|
1
|
+
import { ApiError, crossOriginRefusalMessage, type CostUnavailableReason } from '../../api/client'
|
|
2
2
|
|
|
3
3
|
export function costUnavailableReasonFromError(error: unknown): CostUnavailableReason | undefined {
|
|
4
4
|
if (!(error instanceof ApiError)) return undefined
|
|
5
|
+
// Not a permission problem: the server's message says what to fix.
|
|
6
|
+
if (crossOriginRefusalMessage(error)) return undefined
|
|
5
7
|
if (error.status === 403) return 'access_denied'
|
|
6
8
|
if (error.status === 404) return 'not_found'
|
|
7
9
|
return undefined
|
|
@@ -58,3 +58,13 @@ describe('cost source presentation', () => {
|
|
|
58
58
|
expect(costIntegrationUnavailableMessage('authentication_error', false)).not.toContain('Settings')
|
|
59
59
|
})
|
|
60
60
|
})
|
|
61
|
+
|
|
62
|
+
it('gives connection guidance without claiming an unreachable source ran a query', () => {
|
|
63
|
+
const standalone = costIntegrationUnavailableMessage('query_error', true)
|
|
64
|
+
expect(standalone).toContain('Settings → Metrics')
|
|
65
|
+
expect(standalone).toContain('Settings → Cost')
|
|
66
|
+
expect(standalone).not.toContain('query failed')
|
|
67
|
+
const embedded = costIntegrationUnavailableMessage('query_error', false)
|
|
68
|
+
expect(embedded).toContain('Ask your operator')
|
|
69
|
+
expect(embedded).not.toContain('Settings')
|
|
70
|
+
})
|
|
@@ -107,6 +107,10 @@ export function costIntegrationUnavailableMessage(
|
|
|
107
107
|
settingsAvailable = true,
|
|
108
108
|
): string | undefined {
|
|
109
109
|
switch (reason) {
|
|
110
|
+
case 'query_error':
|
|
111
|
+
return settingsAvailable
|
|
112
|
+
? 'The selected cost source could not provide data. Check its connection in Settings → Metrics (OpenCost) or Settings → Cost (Kubecost).'
|
|
113
|
+
: 'The selected cost source could not provide data. Ask your operator to check its connection and query health.'
|
|
110
114
|
case 'no_cost_source':
|
|
111
115
|
return settingsAvailable
|
|
112
116
|
? 'No compatible cost source was found. Connect OpenCost metrics in Settings → Metrics or configure Kubecost in Settings → Cost.'
|
|
@@ -120,7 +124,7 @@ export function costIntegrationUnavailableMessage(
|
|
|
120
124
|
case 'cost_settings_error':
|
|
121
125
|
return 'This cluster’s saved cost settings need review before cost data can load. Review them in Settings → Cost.'
|
|
122
126
|
case 'deployment_configuration_error':
|
|
123
|
-
return '
|
|
127
|
+
return "Radar's operator-supplied cost or metrics configuration is invalid. Check local launch flags or the deployment environment variables and Helm values."
|
|
124
128
|
case 'authentication_error':
|
|
125
129
|
return settingsAvailable
|
|
126
130
|
? 'Kubecost rejected the configured API key. Update it in Settings → Cost.'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { AgentSetupNotice } from "./AgentSetupNotice";
|
|
4
|
+
import type { DiagnoseSetup } from "./DiagnoseContext";
|
|
5
|
+
|
|
6
|
+
const render = (
|
|
7
|
+
setupState: DiagnoseSetup,
|
|
8
|
+
{ checkingAgents = false, cliOverride = false } = {},
|
|
9
|
+
) =>
|
|
10
|
+
renderToStaticMarkup(
|
|
11
|
+
<AgentSetupNotice
|
|
12
|
+
setupState={setupState}
|
|
13
|
+
cliOverride={cliOverride}
|
|
14
|
+
checkingAgents={checkingAgents}
|
|
15
|
+
agentsCheckFailed={false}
|
|
16
|
+
recheckAgents={async () => {}}
|
|
17
|
+
/>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
describe("AgentSetupNotice", () => {
|
|
21
|
+
it("offers installs and a re-check, never a restart, when no CLI is found", () => {
|
|
22
|
+
const html = render("needs-install");
|
|
23
|
+
expect(html).toContain("Claude Code");
|
|
24
|
+
expect(html).toContain("OpenCode");
|
|
25
|
+
expect(html).toContain("Check again");
|
|
26
|
+
expect(html).not.toMatch(/restart/i);
|
|
27
|
+
// A desktop user can't set an environment variable for a Dock launch.
|
|
28
|
+
expect(html).not.toContain("RADAR_AI_CLI_BIN");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("shows the check in progress", () => {
|
|
32
|
+
const html = render("needs-install", { checkingAgents: true });
|
|
33
|
+
expect(html).toContain("Checking…");
|
|
34
|
+
expect(html).toContain("disabled");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("names the override only when the server says it is set", () => {
|
|
38
|
+
for (const state of ["needs-restart", "needs-install"] as const) {
|
|
39
|
+
const html = render(state, { cliOverride: true });
|
|
40
|
+
expect(html).toContain("RADAR_AI_CLI_BIN");
|
|
41
|
+
expect(html).toContain("restart Radar");
|
|
42
|
+
// Detection is off while the variable is set, so an install can't help.
|
|
43
|
+
expect(html).not.toContain("Install one of these");
|
|
44
|
+
// A working file put back at the pinned path is picked up without one.
|
|
45
|
+
expect(html).toContain("Check again");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("says nothing it can't know when an agent is reported but off", () => {
|
|
50
|
+
// Radar Hub reports a hosted agent that is supported but not enabled when
|
|
51
|
+
// its own runner is down: no CLI, no startup output, no override.
|
|
52
|
+
const html = render("needs-restart");
|
|
53
|
+
expect(html).toContain("aren't available right now");
|
|
54
|
+
expect(html).not.toMatch(/RADAR_AI_CLI_BIN|agent CLI|startup output/);
|
|
55
|
+
expect(html).not.toContain("Install one of these");
|
|
56
|
+
expect(html).toContain("Check again");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
2
3
|
import { Sparkles, Copy, Check, ExternalLink, RotateCw } from "lucide-react";
|
|
4
|
+
import { copyText } from "@skyhook-io/k8s-ui/utils/clipboard";
|
|
3
5
|
import { SUPPORTED_AGENTS, type AgentInstall } from "./agentCatalog";
|
|
4
6
|
import { type DiagnoseSetup } from "./DiagnoseContext";
|
|
7
|
+
import { openExternal } from "../../utils/navigation";
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
const REPORT_URL =
|
|
10
|
+
"https://github.com/skyhook-io/radar/issues/new?labels=bug&template=bug_report.md";
|
|
11
|
+
|
|
12
|
+
function CopyButton({ text, label }: { text: string; label: string }) {
|
|
7
13
|
const [copied, setCopied] = useState(false);
|
|
8
14
|
return (
|
|
9
15
|
<button
|
|
10
16
|
type="button"
|
|
11
17
|
onClick={() => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
// The shared helper, not navigator.clipboard directly: a Radar served
|
|
19
|
+
// over plain HTTP is an insecure origin where the async API rejects,
|
|
20
|
+
// and the tick must not claim a copy that didn't happen.
|
|
21
|
+
void copyText(text).then((ok) => {
|
|
22
|
+
if (!ok) return;
|
|
23
|
+
setCopied(true);
|
|
24
|
+
setTimeout(() => setCopied(false), 1100);
|
|
25
|
+
});
|
|
15
26
|
}}
|
|
16
27
|
className="shrink-0 rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
17
|
-
aria-label={copied ? "Copied" :
|
|
28
|
+
aria-label={copied ? "Copied" : label}
|
|
18
29
|
>
|
|
19
30
|
{copied ? (
|
|
20
31
|
<Check className="h-3.5 w-3.5 text-emerald-500" />
|
|
@@ -46,68 +57,148 @@ function AgentRow({ agent }: { agent: AgentInstall }) {
|
|
|
46
57
|
<code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap font-mono text-xs text-theme-text-secondary">
|
|
47
58
|
{agent.install}
|
|
48
59
|
</code>
|
|
49
|
-
<CopyButton text={agent.install} />
|
|
60
|
+
<CopyButton text={agent.install} label="Copy install command" />
|
|
50
61
|
</div>
|
|
51
62
|
</div>
|
|
52
63
|
);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
function CheckAgainButton({
|
|
67
|
+
checking,
|
|
68
|
+
onCheck,
|
|
69
|
+
}: {
|
|
70
|
+
checking: boolean;
|
|
71
|
+
onCheck: () => void;
|
|
72
|
+
}) {
|
|
73
|
+
return (
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
disabled={checking}
|
|
77
|
+
onClick={onCheck}
|
|
78
|
+
className="btn-brand mt-4 inline-flex items-center gap-1.5 px-3 py-1.5 text-sm"
|
|
79
|
+
>
|
|
80
|
+
<RotateCw className={clsx("h-3.5 w-3.5", checking && "animate-spin")} />
|
|
81
|
+
{checking ? "Checking…" : "Check again"}
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Shown in the AI surface's Home when investigations are eligible here but not
|
|
87
|
+
// runnable yet:
|
|
88
|
+
// - cliOverride: RADAR_AI_CLI_BIN pins the engine to one CLI, so detection is
|
|
89
|
+
// off and installing another changes nothing. Investigations being off means
|
|
90
|
+
// the variable names a file Radar can't run. A working file put at that path
|
|
91
|
+
// is picked up on the next check; changing the variable needs a restart.
|
|
92
|
+
// - "needs-install": no agent CLI found. The server picks one up as soon as
|
|
93
|
+
// it's installed, so the notice offers a re-check rather than a restart.
|
|
94
|
+
// - "needs-restart": an agent is reported but investigations are off. Locally
|
|
95
|
+
// that's a race with an install in progress; an embedding host reports it
|
|
96
|
+
// when its own runner is down. Neither has a cause the user can act on, so
|
|
97
|
+
// say only that and offer a re-check.
|
|
59
98
|
export function AgentSetupNotice({
|
|
60
99
|
setupState,
|
|
100
|
+
cliOverride,
|
|
101
|
+
checkingAgents,
|
|
102
|
+
agentsCheckFailed,
|
|
103
|
+
recheckAgents,
|
|
61
104
|
}: {
|
|
62
105
|
setupState: DiagnoseSetup;
|
|
106
|
+
cliOverride: boolean;
|
|
107
|
+
checkingAgents: boolean;
|
|
108
|
+
agentsCheckFailed: boolean;
|
|
109
|
+
recheckAgents: () => Promise<void>;
|
|
110
|
+
}) {
|
|
111
|
+
// The state the panel was in when a check started. "Still…" is only true when
|
|
112
|
+
// the finished check left it there.
|
|
113
|
+
const [checkedState, setCheckedState] = useState<DiagnoseSetup | null>(null);
|
|
114
|
+
const check = () => {
|
|
115
|
+
void recheckAgents().then(() => setCheckedState(setupState));
|
|
116
|
+
};
|
|
117
|
+
const showResult = checkedState === setupState && !checkingAgents;
|
|
118
|
+
const unreachable = showResult && agentsCheckFailed && (
|
|
119
|
+
<p className="mt-2 text-xs text-theme-text-secondary">
|
|
120
|
+
Couldn't check. Try again.
|
|
121
|
+
</p>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (cliOverride) {
|
|
125
|
+
return (
|
|
126
|
+
<SetupFrame title="Radar can't run your RADAR_AI_CLI_BIN">
|
|
127
|
+
<p className="mt-1 text-sm text-theme-text-secondary">
|
|
128
|
+
Fix the path, or remove it and restart Radar.
|
|
129
|
+
</p>
|
|
130
|
+
<CheckAgainButton checking={checkingAgents} onCheck={check} />
|
|
131
|
+
{unreachable ||
|
|
132
|
+
(showResult && (
|
|
133
|
+
<p className="mt-2 text-xs text-theme-text-secondary">
|
|
134
|
+
Still can't run it.
|
|
135
|
+
</p>
|
|
136
|
+
))}
|
|
137
|
+
</SetupFrame>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (setupState === "needs-restart") {
|
|
141
|
+
return (
|
|
142
|
+
<SetupFrame title="AI investigations aren't available right now">
|
|
143
|
+
<p className="mt-1 text-sm text-theme-text-secondary">
|
|
144
|
+
Try again in a moment.
|
|
145
|
+
</p>
|
|
146
|
+
<CheckAgainButton checking={checkingAgents} onCheck={check} />
|
|
147
|
+
{unreachable ||
|
|
148
|
+
(showResult && (
|
|
149
|
+
<p className="mt-2 text-xs text-theme-text-secondary">
|
|
150
|
+
Still not available.
|
|
151
|
+
</p>
|
|
152
|
+
))}
|
|
153
|
+
</SetupFrame>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return (
|
|
157
|
+
<SetupFrame title="Set up AI investigations">
|
|
158
|
+
<p className="mt-1 text-sm text-theme-text-secondary">
|
|
159
|
+
Radar runs investigations through a coding agent CLI on your own
|
|
160
|
+
machine, with no Radar cloud and no API key. Install one of these:
|
|
161
|
+
</p>
|
|
162
|
+
<div className="mt-4 flex flex-col gap-2.5">
|
|
163
|
+
{SUPPORTED_AGENTS.map((a) => (
|
|
164
|
+
<AgentRow key={a.name} agent={a} />
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
<CheckAgainButton checking={checkingAgents} onCheck={check} />
|
|
168
|
+
{unreachable ||
|
|
169
|
+
(showResult && (
|
|
170
|
+
<p className="mt-2 text-xs text-theme-text-secondary">
|
|
171
|
+
Still not found. Already installed one?{" "}
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
onClick={() => openExternal(REPORT_URL)}
|
|
175
|
+
className="underline hover:text-theme-text-primary"
|
|
176
|
+
>
|
|
177
|
+
Report it
|
|
178
|
+
</button>
|
|
179
|
+
.
|
|
180
|
+
</p>
|
|
181
|
+
))}
|
|
182
|
+
</SetupFrame>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function SetupFrame({
|
|
187
|
+
title,
|
|
188
|
+
children,
|
|
189
|
+
}: {
|
|
190
|
+
title: string;
|
|
191
|
+
children: ReactNode;
|
|
63
192
|
}) {
|
|
64
|
-
const needsRestart = setupState === "needs-restart";
|
|
65
193
|
return (
|
|
66
194
|
<div className="mx-auto max-w-md px-1 py-6">
|
|
67
195
|
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-xl border border-accent/30 bg-accent/5">
|
|
68
196
|
<Sparkles className="h-5 w-5 text-accent" />
|
|
69
197
|
</div>
|
|
70
198
|
<h3 className="text-base font-semibold text-theme-text-primary">
|
|
71
|
-
{
|
|
72
|
-
? "Restart Radar to enable AI investigations"
|
|
73
|
-
: "Set up AI investigations"}
|
|
199
|
+
{title}
|
|
74
200
|
</h3>
|
|
75
|
-
|
|
76
|
-
{needsRestart ? (
|
|
77
|
-
<>
|
|
78
|
-
A supported agent CLI is now installed, but Radar started before it
|
|
79
|
-
was — restart Radar to pick it up. Investigations then run locally
|
|
80
|
-
on your machine, keyless, using your own agent.
|
|
81
|
-
</>
|
|
82
|
-
) : (
|
|
83
|
-
<>
|
|
84
|
-
Radar runs investigations through a coding agent CLI on your own
|
|
85
|
-
machine — no Radar cloud, no API key. Install one of these, then
|
|
86
|
-
restart Radar:
|
|
87
|
-
</>
|
|
88
|
-
)}
|
|
89
|
-
</p>
|
|
90
|
-
|
|
91
|
-
{!needsRestart && (
|
|
92
|
-
<div className="mt-4 flex flex-col gap-2.5">
|
|
93
|
-
{SUPPORTED_AGENTS.map((a) => (
|
|
94
|
-
<AgentRow key={a.name} agent={a} />
|
|
95
|
-
))}
|
|
96
|
-
</div>
|
|
97
|
-
)}
|
|
98
|
-
|
|
99
|
-
{/* The panel only re-reads /api/agents on load, so after installing or
|
|
100
|
-
restarting the user needs a way to refresh in place rather than
|
|
101
|
-
hunting for a manual browser reload. */}
|
|
102
|
-
<button
|
|
103
|
-
type="button"
|
|
104
|
-
onClick={() => window.location.reload()}
|
|
105
|
-
className="btn-brand mt-4 inline-flex items-center gap-1.5 px-3 py-1.5 text-sm"
|
|
106
|
-
>
|
|
107
|
-
<RotateCw className="h-3.5 w-3.5" />
|
|
108
|
-
{needsRestart ? "Reload after restarting" : "Reload after installing"}
|
|
109
|
-
</button>
|
|
110
|
-
|
|
201
|
+
{children}
|
|
111
202
|
<p className="mt-4 text-xs text-theme-text-tertiary">
|
|
112
203
|
Your agent runs locally. Resource details and logs are sent to its model
|
|
113
204
|
provider under your account, not to Radar.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
6
|
+
import { evaluateGitOpsWriteGuard } from "@skyhook-io/k8s-ui/utils/gitops-write-guard";
|
|
7
|
+
import { ApplyDialog } from "./parts";
|
|
8
|
+
|
|
9
|
+
vi.mock("@skyhook-io/k8s-ui/components/ui/DialogPortal", () => ({
|
|
10
|
+
DialogPortal: ({ open, children }: { open: boolean; children: ReactNode }) => (open ? children : null),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const target = { kind: "Deployment", group: "apps", namespace: "dev", name: "api" };
|
|
14
|
+
const owner = { tool: "argocd" as const, kind: "applications" as const, namespace: "argocd", name: "api" };
|
|
15
|
+
const writes = [{ scope: "spec" as const }];
|
|
16
|
+
// Ownership unknown at first; then the owner resolves to an Argo CD Application.
|
|
17
|
+
const guardWith = (resolved: boolean) =>
|
|
18
|
+
resolved
|
|
19
|
+
? evaluateGitOpsWriteGuard({ target, owner, writes, evidence: { uid: "u", resourceVersion: "1", owner: null, policy: { tool: "argocd", auto: true, selfHeal: true, prune: false, suspended: null }, paths: [] } })
|
|
20
|
+
: evaluateGitOpsWriteGuard({ target, owner: null, writes, ownershipError: "Radar hasn't mapped this resource's owners yet, so it can't tell whether a GitOps tool or Helm manages it." });
|
|
21
|
+
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
afterEach(() => container?.remove());
|
|
24
|
+
|
|
25
|
+
it("asks again when the verdict changes after the user acknowledged", async () => {
|
|
26
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
27
|
+
container = document.createElement("div");
|
|
28
|
+
document.body.appendChild(container);
|
|
29
|
+
const root = createRoot(container);
|
|
30
|
+
const render = (resolved: boolean) =>
|
|
31
|
+
root.render(
|
|
32
|
+
<ApplyDialog open onClose={() => {}} onConfirm={() => {}} agentLabel="Local agent" resourceLabel="Deployment dev/api" context="kind-dev" fix="Raise the memory limit." gitOpsGuard={guardWith(resolved)} />,
|
|
33
|
+
);
|
|
34
|
+
const applyButton = () => [...container.querySelectorAll("button")].find((b) => b.textContent?.includes("Apply fix"))!;
|
|
35
|
+
try {
|
|
36
|
+
await act(async () => render(false));
|
|
37
|
+
expect(applyButton().disabled).toBe(true);
|
|
38
|
+
const checkbox = container.querySelector<HTMLInputElement>('input[type="checkbox"]')!;
|
|
39
|
+
await act(async () => checkbox.click());
|
|
40
|
+
expect(applyButton().disabled).toBe(false);
|
|
41
|
+
await act(async () => render(true));
|
|
42
|
+
expect(applyButton().disabled).toBe(true);
|
|
43
|
+
expect(container.querySelector<HTMLInputElement>('input[type="checkbox"]')!.checked).toBe(false);
|
|
44
|
+
} finally {
|
|
45
|
+
await act(async () => root.unmount());
|
|
46
|
+
}
|
|
47
|
+
});
|