@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
|
@@ -3,6 +3,29 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { ApplyDialog } from "./parts";
|
|
5
5
|
import { AgentSetupNotice } from "./AgentSetupNotice";
|
|
6
|
+
import { evaluateGitOpsWriteGuard } from "@skyhook-io/k8s-ui/utils/gitops-write-guard";
|
|
7
|
+
|
|
8
|
+
const target = { kind: "Deployment", group: "apps", namespace: "dev", name: "api" };
|
|
9
|
+
const APPLY_WRITES = [{ scope: "spec" as const }];
|
|
10
|
+
const unmanaged = evaluateGitOpsWriteGuard({ target, owner: null, writes: APPLY_WRITES });
|
|
11
|
+
const argoManaged = evaluateGitOpsWriteGuard({
|
|
12
|
+
target,
|
|
13
|
+
owner: { tool: "argocd", kind: "applications", namespace: "argocd", name: "api" },
|
|
14
|
+
writes: APPLY_WRITES,
|
|
15
|
+
evidence: {
|
|
16
|
+
uid: "u",
|
|
17
|
+
resourceVersion: "1",
|
|
18
|
+
owner: null,
|
|
19
|
+
policy: { tool: "argocd", auto: true, selfHeal: true, prune: false, suspended: null },
|
|
20
|
+
paths: [],
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function applyButton(html: string) {
|
|
25
|
+
return html
|
|
26
|
+
.match(/<button\b[^>]*>[\s\S]*?<\/button>/g)
|
|
27
|
+
?.find((button) => button.includes("Apply fix"));
|
|
28
|
+
}
|
|
6
29
|
|
|
7
30
|
vi.mock("@skyhook-io/k8s-ui/components/ui/DialogPortal", () => ({
|
|
8
31
|
DialogPortal: ({ open, children }: { open: boolean; children: ReactNode }) =>
|
|
@@ -25,6 +48,7 @@ describe("Apply confirmation context", () => {
|
|
|
25
48
|
resourceLabel="Deployment dev/api"
|
|
26
49
|
context={context}
|
|
27
50
|
fix="Only after verifying credentials, update the Secret reference."
|
|
51
|
+
gitOpsGuard={unmanaged}
|
|
28
52
|
/>,
|
|
29
53
|
);
|
|
30
54
|
expect(html).toContain("Cluster:");
|
|
@@ -48,23 +72,52 @@ describe("Apply confirmation context", () => {
|
|
|
48
72
|
resourceLabel="Deployment dev/api"
|
|
49
73
|
context="kind-dev"
|
|
50
74
|
fix="Update its configuration"
|
|
51
|
-
|
|
75
|
+
gitOpsGuard={argoManaged}
|
|
52
76
|
confidence={0.3}
|
|
53
77
|
/>,
|
|
54
78
|
);
|
|
79
|
+
expect(html).toContain("Argo CD Application argocd/api may revert this change.");
|
|
55
80
|
expect(html).toContain("I understand Argo CD may revert this");
|
|
56
|
-
|
|
57
|
-
.match(/<button\b[^>]*>[\s\S]*?<\/button>/g)
|
|
58
|
-
?.find((button) => button.includes("Apply fix"));
|
|
59
|
-
expect(applyButton).toContain('disabled=""');
|
|
81
|
+
expect(applyButton(html)).toContain('disabled=""');
|
|
60
82
|
expect(html).toContain("low confidence");
|
|
61
83
|
expect(html.match(/kubeconfig/g)).toHaveLength(1);
|
|
62
84
|
});
|
|
63
85
|
});
|
|
64
86
|
|
|
87
|
+
describe("Apply GitOps gate", () => {
|
|
88
|
+
const props = {
|
|
89
|
+
open: true,
|
|
90
|
+
onClose: () => {},
|
|
91
|
+
onConfirm: () => {},
|
|
92
|
+
agentLabel: "Local agent",
|
|
93
|
+
resourceLabel: "Deployment dev/api",
|
|
94
|
+
context: "kind-dev",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
it("enables Apply for an unmanaged target without acknowledgement", () => {
|
|
98
|
+
const html = renderToStaticMarkup(<ApplyDialog {...props} gitOpsGuard={unmanaged} />);
|
|
99
|
+
expect(applyButton(html)).not.toContain('disabled=""');
|
|
100
|
+
expect(html).not.toContain('type="checkbox"');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("blocks Apply while ownership is being checked or unknown", () => {
|
|
104
|
+
const pending = evaluateGitOpsWriteGuard({ target, owner: null, ownerPending: true, writes: APPLY_WRITES });
|
|
105
|
+
const checking = renderToStaticMarkup(<ApplyDialog {...props} gitOpsGuard={pending} />);
|
|
106
|
+
expect(checking).toContain("Checking GitOps ownership");
|
|
107
|
+
expect(applyButton(checking)).toContain('disabled=""');
|
|
108
|
+
expect(applyButton(renderToStaticMarkup(<ApplyDialog {...props} />))).toContain('disabled=""');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
65
112
|
it("distinguishes local execution from sending resource data to the model provider", () => {
|
|
66
113
|
const html = renderToStaticMarkup(
|
|
67
|
-
<AgentSetupNotice
|
|
114
|
+
<AgentSetupNotice
|
|
115
|
+
setupState="needs-install"
|
|
116
|
+
cliOverride={false}
|
|
117
|
+
checkingAgents={false}
|
|
118
|
+
agentsCheckFailed={false}
|
|
119
|
+
recheckAgents={async () => {}}
|
|
120
|
+
/>,
|
|
68
121
|
);
|
|
69
122
|
expect(html).toContain("Your agent runs locally");
|
|
70
123
|
expect(html).toContain("model provider under your account, not to Radar");
|
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
RefreshCw,
|
|
9
9
|
} from "lucide-react";
|
|
10
10
|
import { DialogPortal } from "@skyhook-io/k8s-ui/components/ui/DialogPortal";
|
|
11
|
+
import { GitOpsWriteWarning } from "@skyhook-io/k8s-ui";
|
|
12
|
+
import {
|
|
13
|
+
canConfirmGitOpsWrite,
|
|
14
|
+
gitOpsWriteGuardKey,
|
|
15
|
+
type GitOpsWriteGuard,
|
|
16
|
+
} from "@skyhook-io/k8s-ui/utils/gitops-write-guard";
|
|
11
17
|
import { parseContextName } from "../../utils/context-name";
|
|
12
18
|
import type { Diagnosis, ApplyMutationOutcome } from "../../api/diagnose";
|
|
13
19
|
import { AIMarkdown, CopyButton } from "./AIMarkdown";
|
|
@@ -24,7 +30,8 @@ export function ApplyDialog({
|
|
|
24
30
|
fix,
|
|
25
31
|
reason,
|
|
26
32
|
precondition,
|
|
27
|
-
|
|
33
|
+
gitOpsGuard,
|
|
34
|
+
onOpenGitOpsOwner,
|
|
28
35
|
confidence,
|
|
29
36
|
}: {
|
|
30
37
|
open: boolean;
|
|
@@ -38,7 +45,9 @@ export function ApplyDialog({
|
|
|
38
45
|
reason?: string;
|
|
39
46
|
/** The condition the agent attached to this step; shown before the operator confirms. */
|
|
40
47
|
precondition?: string;
|
|
41
|
-
|
|
48
|
+
/** GitOps/Helm write guard for the target; undefined blocks Apply. */
|
|
49
|
+
gitOpsGuard?: GitOpsWriteGuard;
|
|
50
|
+
onOpenGitOpsOwner?: () => void;
|
|
42
51
|
confidence?: number;
|
|
43
52
|
}) {
|
|
44
53
|
const titleId = useId();
|
|
@@ -47,14 +56,19 @@ export function ApplyDialog({
|
|
|
47
56
|
// A GitOps/Helm-managed resource needs an explicit acknowledgment before applying
|
|
48
57
|
// a direct change — it's the canonical footgun (the controller reverts it). Gating
|
|
49
58
|
// (not just warning) makes the user opt into "yes, I know this may be undone."
|
|
59
|
+
// Apply also waits until ownership has been checked.
|
|
50
60
|
// TODO(SKY-1075): once Radar can connect the user's SCM (GitHub/GitLab/…), replace
|
|
51
61
|
// direct apply on managed resources with "open a PR against the Git source"
|
|
52
62
|
// instead — the durable fix. See Linear SKY-1075.
|
|
53
|
-
|
|
63
|
+
// The guard key the acknowledgment was given for: a different verdict (the
|
|
64
|
+
// owner resolved, the policy changed) asks again.
|
|
65
|
+
const guardKey = gitOpsWriteGuardKey(gitOpsGuard);
|
|
66
|
+
const [ackedKey, setAckedKey] = useState("");
|
|
54
67
|
useEffect(() => {
|
|
55
|
-
if (open)
|
|
68
|
+
if (open) setAckedKey("");
|
|
56
69
|
}, [open]);
|
|
57
|
-
const
|
|
70
|
+
const acked = Boolean(guardKey) && ackedKey === guardKey;
|
|
71
|
+
const applyBlocked = !canConfirmGitOpsWrite(gitOpsGuard, acked);
|
|
58
72
|
return (
|
|
59
73
|
<DialogPortal
|
|
60
74
|
ariaLabelledBy={titleId}
|
|
@@ -130,29 +144,13 @@ export function ApplyDialog({
|
|
|
130
144
|
)}
|
|
131
145
|
</div>
|
|
132
146
|
<div className="shrink-0 space-y-2 p-4">
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<span className="font-medium">Managed by {managedBy}.</span>{" "}
|
|
141
|
-
Unless you turn off auto-sync, a direct change here will be
|
|
142
|
-
undone within minutes when {managedBy} re-syncs from Git — the
|
|
143
|
-
durable fix is to change it in Git (the {managedBy} source).
|
|
144
|
-
</span>
|
|
145
|
-
</div>
|
|
146
|
-
<label className="flex cursor-pointer items-center gap-2 pl-6 text-xs text-theme-text-secondary">
|
|
147
|
-
<input
|
|
148
|
-
type="checkbox"
|
|
149
|
-
checked={acked}
|
|
150
|
-
onChange={(e) => setAcked(e.target.checked)}
|
|
151
|
-
className="h-3.5 w-3.5 accent-amber-500"
|
|
152
|
-
/>
|
|
153
|
-
I understand {managedBy} may revert this — apply anyway.
|
|
154
|
-
</label>
|
|
155
|
-
</div>
|
|
147
|
+
{gitOpsGuard && (
|
|
148
|
+
<GitOpsWriteWarning
|
|
149
|
+
guard={gitOpsGuard}
|
|
150
|
+
acknowledged={acked}
|
|
151
|
+
onAcknowledgedChange={(value) => setAckedKey(value ? guardKey : "")}
|
|
152
|
+
onOpenOwner={onOpenGitOpsOwner}
|
|
153
|
+
/>
|
|
156
154
|
)}
|
|
157
155
|
{lowConfidence && (
|
|
158
156
|
<div className="flex items-start gap-2 rounded border border-theme-border bg-theme-elevated p-3 text-sm text-theme-text-secondary">
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isInvestigationWorkspacePath,
|
|
9
9
|
shouldExitUnavailableWorkspace,
|
|
10
10
|
workspaceRunIDFromPath,
|
|
11
|
+
resolveSetupState,
|
|
11
12
|
} from "./DiagnoseContext";
|
|
12
13
|
|
|
13
14
|
describe("investigation workspace routes", () => {
|
|
@@ -103,3 +104,45 @@ describe("investigationPanelFillsViewport", () => {
|
|
|
103
104
|
expect(investigationPanelFillsViewport(1200, 1000)).toBe(true);
|
|
104
105
|
});
|
|
105
106
|
});
|
|
107
|
+
|
|
108
|
+
describe("resolveSetupState", () => {
|
|
109
|
+
const base = {
|
|
110
|
+
available: false,
|
|
111
|
+
eligibilityResolved: true,
|
|
112
|
+
eligible: true,
|
|
113
|
+
supportedAgentCount: 0,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
it("does not call an unanswered probe a deployment restriction", () => {
|
|
117
|
+
// eligible starts false and the probe's catch leaves it false, so both the
|
|
118
|
+
// in-flight and the failed request land here.
|
|
119
|
+
expect(
|
|
120
|
+
resolveSetupState({
|
|
121
|
+
...base,
|
|
122
|
+
eligibilityResolved: false,
|
|
123
|
+
eligible: false,
|
|
124
|
+
}),
|
|
125
|
+
).toBe("unknown");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("reports off only once the probe said eligible: false", () => {
|
|
129
|
+
expect(resolveSetupState({ ...base, eligible: false })).toBe("off");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("separates a CLI that appeared after boot from none at all", () => {
|
|
133
|
+
expect(resolveSetupState({ ...base, supportedAgentCount: 1 })).toBe(
|
|
134
|
+
"needs-restart",
|
|
135
|
+
);
|
|
136
|
+
expect(resolveSetupState(base)).toBe("needs-install");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("lets a working engine win over an unresolved probe", () => {
|
|
140
|
+
expect(
|
|
141
|
+
resolveSetupState({
|
|
142
|
+
...base,
|
|
143
|
+
available: true,
|
|
144
|
+
eligibilityResolved: false,
|
|
145
|
+
}),
|
|
146
|
+
).toBe("ready");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -60,15 +60,31 @@ export type DiagnoseView = "home" | "investigation";
|
|
|
60
60
|
// Setup readiness of local AI investigations, derived from the agents API:
|
|
61
61
|
// - "ready": an agent is installed and the engine is running (available)
|
|
62
62
|
// - "needs-install": the feature is supported here but no agent CLI is installed
|
|
63
|
-
// - "needs-restart": a supported agent is
|
|
64
|
-
//
|
|
63
|
+
// - "needs-restart": a supported agent is installed but investigations are
|
|
64
|
+
// still off. The server picks up new installs whenever the
|
|
65
|
+
// agent list is fetched, so locally this means RADAR_AI_CLI_BIN
|
|
66
|
+
// pins the engine to a file Radar can't run (cliOverride); an
|
|
67
|
+
// embedding host may report it when its own runner is down
|
|
65
68
|
// - "off": not available in this deployment (proxy/OIDC auth, --no-mcp,
|
|
66
69
|
// or an embed host) — no install nudge would help
|
|
67
|
-
|
|
70
|
+
// - "unknown": the agents probe hasn't answered (in flight, or it failed
|
|
71
|
+
// and was swallowed). Nothing about the machine or the
|
|
72
|
+
// deployment is established yet, so say nothing about either
|
|
73
|
+
export type DiagnoseSetup =
|
|
74
|
+
"ready" | "needs-install" | "needs-restart" | "off" | "unknown";
|
|
68
75
|
|
|
69
76
|
interface DiagnoseCtx {
|
|
70
77
|
available: boolean; // an agent CLI is present (button/entry gate)
|
|
71
78
|
setupState: DiagnoseSetup; // readiness for the setup nudge (see DiagnoseSetup)
|
|
79
|
+
// Re-reads the agent list; the server turns investigations on if a CLI was
|
|
80
|
+
// installed since the last check. Resolves once the new state is applied.
|
|
81
|
+
recheckAgents: () => Promise<void>;
|
|
82
|
+
checkingAgents: boolean;
|
|
83
|
+
// The latest agent check failed to reach the server, so nothing about the
|
|
84
|
+
// machine is known from it.
|
|
85
|
+
agentsCheckFailed: boolean;
|
|
86
|
+
// RADAR_AI_CLI_BIN is set; with "needs-restart" it is what's broken.
|
|
87
|
+
cliOverride: boolean;
|
|
72
88
|
agentLabel: string; // label of the selected agent, e.g. "Claude Code"
|
|
73
89
|
hosted: boolean; // selected agent runs on the host's backend, not this machine
|
|
74
90
|
agents: AgentInfo[]; // supported agents detected on PATH (for the picker)
|
|
@@ -172,6 +188,31 @@ const MIN_APP_LEFT_OF_PANEL = 900;
|
|
|
172
188
|
// area instead. Radar's own minimum width is about 856px, so this is the
|
|
173
189
|
// drawer's floor: below it the investigation is the screen.
|
|
174
190
|
const MIN_APP_PEEK_BESIDE_OVERLAY = 320;
|
|
191
|
+
// resolveSetupState is the ONE mapping from the agents probe to what every
|
|
192
|
+
// surface renders. Exported so it can be pinned by test, since each state drives
|
|
193
|
+
// a different user-facing claim.
|
|
194
|
+
//
|
|
195
|
+
// "unknown" is not the same as "off". `eligible` starts false and the probe's
|
|
196
|
+
// catch leaves it that way, so without a separate state a pending or failed
|
|
197
|
+
// request reads as a confirmed deployment restriction. A non-empty agent list
|
|
198
|
+
// while the engine is off means a drivable CLI appeared after Radar booted.
|
|
199
|
+
export function resolveSetupState({
|
|
200
|
+
available,
|
|
201
|
+
eligibilityResolved,
|
|
202
|
+
eligible,
|
|
203
|
+
supportedAgentCount,
|
|
204
|
+
}: {
|
|
205
|
+
available: boolean;
|
|
206
|
+
eligibilityResolved: boolean;
|
|
207
|
+
eligible: boolean;
|
|
208
|
+
supportedAgentCount: number;
|
|
209
|
+
}): DiagnoseSetup {
|
|
210
|
+
if (available) return "ready";
|
|
211
|
+
if (!eligibilityResolved) return "unknown";
|
|
212
|
+
if (!eligible) return "off";
|
|
213
|
+
return supportedAgentCount > 0 ? "needs-restart" : "needs-install";
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
export function investigationPanelFillsViewport(
|
|
176
217
|
viewportWidth: number,
|
|
177
218
|
panelWidth: number,
|
|
@@ -452,52 +493,79 @@ function RoutedDiagnoseProvider({
|
|
|
452
493
|
const narrow = viewportW - width < MIN_APP_LEFT_OF_PANEL;
|
|
453
494
|
const fullWidthForced = investigationPanelFillsViewport(viewportW, width);
|
|
454
495
|
|
|
496
|
+
const mountedRef = useRef(true);
|
|
455
497
|
useEffect(() => {
|
|
456
|
-
|
|
457
|
-
fetchAgents()
|
|
458
|
-
.then((r) => {
|
|
459
|
-
if (!live) return;
|
|
460
|
-
setConsented(r.consented ?? {});
|
|
461
|
-
setEligible(!!r.eligible);
|
|
462
|
-
const supported = r.agents.filter(
|
|
463
|
-
(a) =>
|
|
464
|
-
a.supported &&
|
|
465
|
-
(a.hosted ||
|
|
466
|
-
(!!a.profiles?.length &&
|
|
467
|
-
a.profiles.every((p) => !!a.consentSurfaces?.[p]))),
|
|
468
|
-
);
|
|
469
|
-
setAvailable(r.enabled && supported.length > 0);
|
|
470
|
-
setAgents(supported);
|
|
471
|
-
// Keep the stored pick only if it's still installed; else default to the
|
|
472
|
-
// first supported agent (matches the server's default selection).
|
|
473
|
-
const stored = readStored(AGENT_KEY) || "";
|
|
474
|
-
const next =
|
|
475
|
-
stored && supported.some((a) => a.name === stored)
|
|
476
|
-
? stored
|
|
477
|
-
: (supported[0]?.name ?? "");
|
|
478
|
-
setSelectedAgentState(next);
|
|
479
|
-
const profiles = supported.find((a) => a.name === next)?.profiles ?? [];
|
|
480
|
-
const storedProfile =
|
|
481
|
-
(readStored(PROFILE_KEY) as ExecutionProfile) || "safeguarded";
|
|
482
|
-
if (!profiles.includes(storedProfile) && profiles[0]) {
|
|
483
|
-
setProfileState(profiles[0]);
|
|
484
|
-
writeStored(PROFILE_KEY, profiles[0]);
|
|
485
|
-
}
|
|
486
|
-
// Model/effort are agent-specific; if the stored agent is gone, its values
|
|
487
|
-
// don't apply to the fallback agent (e.g. a Codex slug under Claude) — drop them.
|
|
488
|
-
if (next !== stored) {
|
|
489
|
-
setModelState("");
|
|
490
|
-
writeStored(MODEL_KEY, "");
|
|
491
|
-
setEffortState("");
|
|
492
|
-
writeStored(EFFORT_KEY, "");
|
|
493
|
-
}
|
|
494
|
-
setAgentEligibilityResolved(true);
|
|
495
|
-
})
|
|
496
|
-
.catch(() => {});
|
|
498
|
+
mountedRef.current = true;
|
|
497
499
|
return () => {
|
|
498
|
-
|
|
500
|
+
mountedRef.current = false;
|
|
499
501
|
};
|
|
500
502
|
}, []);
|
|
503
|
+
const [checkingAgents, setCheckingAgents] = useState(false);
|
|
504
|
+
const [agentsCheckFailed, setAgentsCheckFailed] = useState(false);
|
|
505
|
+
const [cliOverride, setCliOverride] = useState(false);
|
|
506
|
+
// A click and a window-focus re-check can overlap; only the newest one may
|
|
507
|
+
// apply its answer, or a stale "nothing installed" could land last.
|
|
508
|
+
const agentCheckSeq = useRef(0);
|
|
509
|
+
// The agent in use now, picked by hand or by default. A check compares
|
|
510
|
+
// against it, not just the stored pick, so seeing the same agent again
|
|
511
|
+
// keeps the model and effort set for it.
|
|
512
|
+
const agentInUseRef = useRef(selectedAgent);
|
|
513
|
+
const recheckAgents = useCallback(async () => {
|
|
514
|
+
const seq = ++agentCheckSeq.current;
|
|
515
|
+
const latest = () => mountedRef.current && seq === agentCheckSeq.current;
|
|
516
|
+
setCheckingAgents(true);
|
|
517
|
+
try {
|
|
518
|
+
const r = await fetchAgents();
|
|
519
|
+
if (!latest()) return;
|
|
520
|
+
setAgentsCheckFailed(false);
|
|
521
|
+
setCliOverride(!!r.cliOverride);
|
|
522
|
+
setConsented(r.consented ?? {});
|
|
523
|
+
setEligible(!!r.eligible);
|
|
524
|
+
const supported = r.agents.filter(
|
|
525
|
+
(a) =>
|
|
526
|
+
a.supported &&
|
|
527
|
+
(a.hosted ||
|
|
528
|
+
(!!a.profiles?.length &&
|
|
529
|
+
a.profiles.every((p) => !!a.consentSurfaces?.[p]))),
|
|
530
|
+
);
|
|
531
|
+
setAvailable(r.enabled && supported.length > 0);
|
|
532
|
+
setAgents(supported);
|
|
533
|
+
// Keep the stored pick only if it's still installed; else default to the
|
|
534
|
+
// first supported agent. Runs always name it, so the server's own default
|
|
535
|
+
// doesn't apply.
|
|
536
|
+
const stored = readStored(AGENT_KEY) || "";
|
|
537
|
+
const next =
|
|
538
|
+
stored && supported.some((a) => a.name === stored)
|
|
539
|
+
? stored
|
|
540
|
+
: (supported[0]?.name ?? "");
|
|
541
|
+
setSelectedAgentState(next);
|
|
542
|
+
const profiles = supported.find((a) => a.name === next)?.profiles ?? [];
|
|
543
|
+
const storedProfile =
|
|
544
|
+
(readStored(PROFILE_KEY) as ExecutionProfile) || "safeguarded";
|
|
545
|
+
if (!profiles.includes(storedProfile) && profiles[0]) {
|
|
546
|
+
setProfileState(profiles[0]);
|
|
547
|
+
writeStored(PROFILE_KEY, profiles[0]);
|
|
548
|
+
}
|
|
549
|
+
// Model/effort are agent-specific; if the agent changed, its values
|
|
550
|
+
// don't apply to the new one (e.g. a Codex slug under Claude), so drop them.
|
|
551
|
+
if (next !== agentInUseRef.current) {
|
|
552
|
+
setModelState("");
|
|
553
|
+
writeStored(MODEL_KEY, "");
|
|
554
|
+
setEffortState("");
|
|
555
|
+
writeStored(EFFORT_KEY, "");
|
|
556
|
+
}
|
|
557
|
+
agentInUseRef.current = next;
|
|
558
|
+
setAgentEligibilityResolved(true);
|
|
559
|
+
} catch {
|
|
560
|
+
// Leaves the previous state in place; "unknown" until a probe answers.
|
|
561
|
+
if (latest()) setAgentsCheckFailed(true);
|
|
562
|
+
} finally {
|
|
563
|
+
if (latest()) setCheckingAgents(false);
|
|
564
|
+
}
|
|
565
|
+
}, []);
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
void recheckAgents();
|
|
568
|
+
}, [recheckAgents]);
|
|
501
569
|
|
|
502
570
|
const setModel = useCallback((v: string) => {
|
|
503
571
|
setModelState(v);
|
|
@@ -511,6 +579,7 @@ function RoutedDiagnoseProvider({
|
|
|
511
579
|
(name: string) => {
|
|
512
580
|
setSelectedAgentState(name);
|
|
513
581
|
writeStored(AGENT_KEY, name);
|
|
582
|
+
agentInUseRef.current = name;
|
|
514
583
|
const nextProfile = agents.find((agent) => agent.name === name)
|
|
515
584
|
?.profiles?.[0];
|
|
516
585
|
if (nextProfile) {
|
|
@@ -541,15 +610,21 @@ function RoutedDiagnoseProvider({
|
|
|
541
610
|
? "standard"
|
|
542
611
|
: (selectedAgentInfo?.consentSurfaces?.[effectiveProfile] ?? "");
|
|
543
612
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
:
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
613
|
+
const setupState = resolveSetupState({
|
|
614
|
+
available,
|
|
615
|
+
eligibilityResolved: agentEligibilityResolved,
|
|
616
|
+
eligible,
|
|
617
|
+
supportedAgentCount: agents.length,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Coming back from a terminal after installing a CLI is when to look again,
|
|
621
|
+
// so the setup notice clears without the user hunting for a button.
|
|
622
|
+
useEffect(() => {
|
|
623
|
+
if (setupState === "ready" || setupState === "off") return;
|
|
624
|
+
const onFocus = () => void recheckAgents();
|
|
625
|
+
window.addEventListener("focus", onFocus);
|
|
626
|
+
return () => window.removeEventListener("focus", onFocus);
|
|
627
|
+
}, [setupState, recheckAgents]);
|
|
553
628
|
|
|
554
629
|
useEffect(() => {
|
|
555
630
|
const onResize = () => setViewportW(window.innerWidth);
|
|
@@ -701,6 +776,9 @@ function RoutedDiagnoseProvider({
|
|
|
701
776
|
writeFocusedRunID(run.id, true);
|
|
702
777
|
})
|
|
703
778
|
.catch((e) => {
|
|
779
|
+
// The picked agent may have been uninstalled since the list was
|
|
780
|
+
// fetched; refresh it so the picker shows what's installed now.
|
|
781
|
+
void recheckAgents();
|
|
704
782
|
if (seq !== startSeqRef.current) return;
|
|
705
783
|
setStartFailure({
|
|
706
784
|
message:
|
|
@@ -1021,6 +1099,10 @@ function RoutedDiagnoseProvider({
|
|
|
1021
1099
|
const value: DiagnoseCtx = {
|
|
1022
1100
|
available,
|
|
1023
1101
|
setupState,
|
|
1102
|
+
recheckAgents,
|
|
1103
|
+
checkingAgents,
|
|
1104
|
+
agentsCheckFailed,
|
|
1105
|
+
cliOverride,
|
|
1024
1106
|
agentLabel,
|
|
1025
1107
|
hosted,
|
|
1026
1108
|
agents,
|
|
@@ -502,7 +502,8 @@ export function DiagnoseSurface({
|
|
|
502
502
|
};
|
|
503
503
|
|
|
504
504
|
// Feature is eligible here but not runnable yet (no agent installed, or one
|
|
505
|
-
//
|
|
505
|
+
// found that investigations couldn't start with): Home leads with the setup
|
|
506
|
+
// notice instead of an empty list.
|
|
506
507
|
const setupPending =
|
|
507
508
|
d.setupState === "needs-install" || d.setupState === "needs-restart";
|
|
508
509
|
|
|
@@ -617,7 +618,13 @@ export function DiagnoseSurface({
|
|
|
617
618
|
</div>
|
|
618
619
|
) : setupPending ? (
|
|
619
620
|
<div className="flex-1 overflow-y-auto">
|
|
620
|
-
<AgentSetupNotice
|
|
621
|
+
<AgentSetupNotice
|
|
622
|
+
setupState={d.setupState}
|
|
623
|
+
cliOverride={d.cliOverride}
|
|
624
|
+
checkingAgents={d.checkingAgents}
|
|
625
|
+
agentsCheckFailed={d.agentsCheckFailed}
|
|
626
|
+
recheckAgents={d.recheckAgents}
|
|
627
|
+
/>
|
|
621
628
|
</div>
|
|
622
629
|
) : (
|
|
623
630
|
<div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-theme-text-tertiary">
|
|
@@ -639,7 +646,13 @@ export function DiagnoseSurface({
|
|
|
639
646
|
</div>
|
|
640
647
|
) : setupPending ? (
|
|
641
648
|
<div className="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6">
|
|
642
|
-
<AgentSetupNotice
|
|
649
|
+
<AgentSetupNotice
|
|
650
|
+
setupState={d.setupState}
|
|
651
|
+
cliOverride={d.cliOverride}
|
|
652
|
+
checkingAgents={d.checkingAgents}
|
|
653
|
+
agentsCheckFailed={d.agentsCheckFailed}
|
|
654
|
+
recheckAgents={d.recheckAgents}
|
|
655
|
+
/>
|
|
643
656
|
</div>
|
|
644
657
|
) : (
|
|
645
658
|
<InvestigationHome
|
|
@@ -58,7 +58,9 @@ import {
|
|
|
58
58
|
type KeyboardEvent,
|
|
59
59
|
} from "react";
|
|
60
60
|
import { useQueryClient } from "@tanstack/react-query";
|
|
61
|
-
import {
|
|
61
|
+
import { useNavigate } from "react-router-dom";
|
|
62
|
+
import { Badge, gitOpsRouteForOwner, type GitOpsWrite } from "@skyhook-io/k8s-ui";
|
|
63
|
+
import { useGitOpsWriteGuard } from "../../hooks/useGitOpsWriteGuard";
|
|
62
64
|
import {
|
|
63
65
|
Send,
|
|
64
66
|
AlertTriangle,
|
|
@@ -264,6 +266,8 @@ function captureEvidenceCardLayout(container: HTMLElement) {
|
|
|
264
266
|
);
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
const APPLY_WRITES: GitOpsWrite[] = [{ scope: "spec", description: "The proposed change" }];
|
|
270
|
+
|
|
267
271
|
export function InvestigationView({
|
|
268
272
|
run,
|
|
269
273
|
agentLabel,
|
|
@@ -949,6 +953,38 @@ export function InvestigationView({
|
|
|
949
953
|
// Apply: a user-confirmed remediation turn. Any step is applyable; the chosen
|
|
950
954
|
// step's text is sent so the server binds the apply to it.
|
|
951
955
|
const [confirmApply, setConfirmApply] = useState(false);
|
|
956
|
+
// The agent's change can touch any field of the target, so the guard
|
|
957
|
+
// classifies an unknown spec write.
|
|
958
|
+
const { guard: applyGitOpsGuard, ownership: applyOwnership } =
|
|
959
|
+
useGitOpsWriteGuard({
|
|
960
|
+
target: { kind, group: run.group, namespace, name },
|
|
961
|
+
writes: APPLY_WRITES,
|
|
962
|
+
declaredManager: run.managedBy,
|
|
963
|
+
enabled: confirmApply,
|
|
964
|
+
});
|
|
965
|
+
const navigateRouter = useNavigate();
|
|
966
|
+
const {
|
|
967
|
+
owner: applyOwner,
|
|
968
|
+
ownerVerified: applyOwnerVerified,
|
|
969
|
+
helmOwner: applyHelmOwner,
|
|
970
|
+
} = applyOwnership;
|
|
971
|
+
const openApplyOwner = useMemo(() => {
|
|
972
|
+
// A host without resource navigation (Radar Hub's fleet Diagnose panel)
|
|
973
|
+
// has no route for the owner either.
|
|
974
|
+
if (!onOpenResource) return undefined;
|
|
975
|
+
const owner = applyOwner;
|
|
976
|
+
const helmOwner = applyHelmOwner;
|
|
977
|
+
if (owner && applyOwnerVerified) {
|
|
978
|
+
return () => navigateRouter(gitOpsRouteForOwner(owner));
|
|
979
|
+
}
|
|
980
|
+
if (helmOwner) {
|
|
981
|
+
return () =>
|
|
982
|
+
navigateRouter(
|
|
983
|
+
`/helm?release=${encodeURIComponent(`${helmOwner.namespace}/${helmOwner.name}`)}`,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return undefined;
|
|
987
|
+
}, [applyOwner, applyOwnerVerified, applyHelmOwner, navigateRouter, onOpenResource]);
|
|
952
988
|
const [pendingFix, setPendingFix] = useState("");
|
|
953
989
|
const requestApply = (fix: string) => {
|
|
954
990
|
if (interactionsBlocked) return;
|
|
@@ -2546,7 +2582,8 @@ export function InvestigationView({
|
|
|
2546
2582
|
]?.precondition
|
|
2547
2583
|
: undefined
|
|
2548
2584
|
}
|
|
2549
|
-
|
|
2585
|
+
gitOpsGuard={applyGitOpsGuard}
|
|
2586
|
+
onOpenGitOpsOwner={openApplyOwner}
|
|
2550
2587
|
confidence={turns[lastRemediationIdx]?.diagnosis?.confidence}
|
|
2551
2588
|
/>
|
|
2552
2589
|
</div>
|