@skyhook-io/radar-app 1.14.2 → 1.14.4
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/package.json +2 -2
- package/src/api/client.ts +32 -0
- package/src/api/diagnose.ts +7 -1
- package/src/components/CloudConnectFlow.tsx +21 -7
- package/src/components/CloudFunnelButton.tsx +85 -8
- package/src/components/cloudConnectHandoff.test.ts +10 -1
- package/src/components/cloudConnectHandoff.ts +15 -1
- package/src/components/diagnose/AIMarkdown.tsx +87 -0
- package/src/components/diagnose/ActivityTurn.tsx +1244 -0
- package/src/components/diagnose/AgentControls.tsx +603 -0
- package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
- package/src/components/diagnose/AnalysisStory.tsx +13 -18
- package/src/components/diagnose/ApplyDialog.tsx +289 -0
- package/src/components/diagnose/AssessmentCard.tsx +1579 -0
- package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
- package/src/components/diagnose/EvidenceCard.tsx +611 -0
- package/src/components/diagnose/Home.test.tsx +1 -3
- package/src/components/diagnose/Home.tsx +2 -1
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
- package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
- package/src/components/diagnose/InvestigationView.tsx +39 -51
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +658 -2
- package/src/components/diagnose/investigationCase.ts +135 -26
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +682 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
- package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +136 -0
- package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
- package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
- package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
- package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
- package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
- package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
- package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
- package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
- package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
- package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
- package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
- package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
- package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
- package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
- package/src/components/diagnose/investigationEvidence/index.ts +1 -1
- package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +13 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
- package/src/components/diagnose/investigationEvidence/types.ts +97 -2
- package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
- package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
- package/src/components/diagnose/investigationState.test.ts +2 -2
- package/src/components/diagnose/investigationState.ts +2 -2
- package/src/components/diagnose/investigationStory.ts +1 -1
- package/src/components/diagnose/parts.test.tsx +15 -1
- package/src/components/diagnose/parts.tsx +26 -3904
- package/src/components/diagnose/target.ts +9 -3
- package/src/components/diagnose/toolCallLabel.test.ts +47 -1
- package/src/components/diagnose/toolCallLabel.ts +41 -2
- package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +27 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
- package/src/components/resources/PodFilePreview.tsx +1 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +20 -1
- package/src/components/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +96 -60
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/workload/WorkloadView.tsx +7 -1
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -54,8 +54,10 @@ describe("resolveStoryPlacements", () => {
|
|
|
54
54
|
expect(repeated.placedAt.get(0)).toBe(0);
|
|
55
55
|
const html = renderToStaticMarkup(
|
|
56
56
|
<AnalysisStory
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
placements={resolveStoryPlacements(
|
|
58
|
+
"Opening paragraph.\n\n[[radar:evidence=2]]\n\nMore.",
|
|
59
|
+
resolver({ 2: "unlinked" }),
|
|
60
|
+
)}
|
|
59
61
|
renderPlacement={() => null}
|
|
60
62
|
onReveal={vi.fn()}
|
|
61
63
|
/>,
|
|
@@ -88,10 +90,10 @@ describe("resolveStoryPlacements", () => {
|
|
|
88
90
|
);
|
|
89
91
|
const html = renderToStaticMarkup(
|
|
90
92
|
<AnalysisStory
|
|
91
|
-
|
|
92
|
-
"Envoy boots [[radar:evidence=0]] then hangs on xDS.\n\nLater, see [[radar:evidence=0]] again."
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
placements={resolveStoryPlacements(
|
|
94
|
+
"Envoy boots [[radar:evidence=0]] then hangs on xDS.\n\nLater, see [[radar:evidence=0]] again.",
|
|
95
|
+
resolver({ 0: target(0, "a") }),
|
|
96
|
+
)}
|
|
95
97
|
renderPlacement={() => <div data-card>card</div>}
|
|
96
98
|
onReveal={vi.fn()}
|
|
97
99
|
openRequest={1}
|
|
@@ -104,10 +106,10 @@ describe("resolveStoryPlacements", () => {
|
|
|
104
106
|
it("keeps an inline mention a reference when the agent places that card on its own line later, pointing down", () => {
|
|
105
107
|
const html = renderToStaticMarkup(
|
|
106
108
|
<AnalysisStory
|
|
107
|
-
|
|
108
|
-
"First [[radar:evidence=0]] here.\n\nThen:\n\n[[radar:evidence=0]]"
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
placements={resolveStoryPlacements(
|
|
110
|
+
"First [[radar:evidence=0]] here.\n\nThen:\n\n[[radar:evidence=0]]",
|
|
111
|
+
resolver({ 0: target(0, "a") }),
|
|
112
|
+
)}
|
|
111
113
|
renderPlacement={() => <div data-card>card</div>}
|
|
112
114
|
onReveal={vi.fn()}
|
|
113
115
|
openRequest={1}
|
|
@@ -187,8 +189,10 @@ describe("resolveStoryPlacements", () => {
|
|
|
187
189
|
).join("\n\n");
|
|
188
190
|
const html = renderToStaticMarkup(
|
|
189
191
|
<AnalysisStory
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
placements={resolveStoryPlacements(
|
|
193
|
+
`${markers}\n\nSee [[radar:evidence=7]] here.`,
|
|
194
|
+
resolver(table),
|
|
195
|
+
)}
|
|
192
196
|
renderPlacement={() => <div data-card>card</div>}
|
|
193
197
|
onReveal={vi.fn()}
|
|
194
198
|
openRequest={1}
|
|
@@ -199,11 +203,12 @@ describe("resolveStoryPlacements", () => {
|
|
|
199
203
|
|
|
200
204
|
const ownLine = renderToStaticMarkup(
|
|
201
205
|
<AnalysisStory
|
|
202
|
-
|
|
203
|
-
{ length: 7 },
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
placements={resolveStoryPlacements(
|
|
207
|
+
Array.from({ length: 7 }, (_, i) => `[[radar:evidence=${i}]]`).join(
|
|
208
|
+
"\n\n",
|
|
209
|
+
),
|
|
210
|
+
resolver(table),
|
|
211
|
+
)}
|
|
207
212
|
renderPlacement={() => <div data-card>card</div>}
|
|
208
213
|
onReveal={vi.fn()}
|
|
209
214
|
openRequest={1}
|
|
@@ -238,12 +243,14 @@ describe("AnalysisStory", () => {
|
|
|
238
243
|
const render = (report: string, open = false) =>
|
|
239
244
|
renderToStaticMarkup(
|
|
240
245
|
<AnalysisStory
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
246
|
+
placements={resolveStoryPlacements(
|
|
247
|
+
report,
|
|
248
|
+
resolver({
|
|
249
|
+
0: target(0, "crash"),
|
|
250
|
+
1: target(1, "chart"),
|
|
251
|
+
2: "unlinked",
|
|
252
|
+
}),
|
|
253
|
+
)}
|
|
247
254
|
renderPlacement={(t, { compact }) => (
|
|
248
255
|
<div data-placed={t.domId} data-compact={compact ? "1" : "0"}>
|
|
249
256
|
{t.item.observation?.title}
|
|
@@ -288,8 +295,7 @@ describe("AnalysisStory", () => {
|
|
|
288
295
|
expect(collapsed).not.toContain("Expand");
|
|
289
296
|
const withTrailing = renderToStaticMarkup(
|
|
290
297
|
<AnalysisStory
|
|
291
|
-
|
|
292
|
-
resolveItem={resolver({})}
|
|
298
|
+
placements={resolveStoryPlacements("One paragraph only.", resolver({}))}
|
|
293
299
|
renderPlacement={() => null}
|
|
294
300
|
onReveal={vi.fn()}
|
|
295
301
|
trailing={<p data-trailing>Also checked: nothing else.</p>}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useId,
|
|
1
|
+
import { useEffect, useId, useState, type ReactNode } from "react";
|
|
2
2
|
import { clsx } from "clsx";
|
|
3
3
|
import { TRANSITION_CHEVRON } from "@skyhook-io/k8s-ui/utils/animation";
|
|
4
4
|
import { AlertTriangle, ChevronDown, FileSearch } from "lucide-react";
|
|
@@ -50,17 +50,20 @@ function isLoss(
|
|
|
50
50
|
* marker reference it, and anything unresolvable is a visible loss. Dedupe is
|
|
51
51
|
* by resolved observation, not by index — two items can address one read.
|
|
52
52
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
resolveItem: (index: number) => StoryPlacementTarget | StoryPlacementLoss,
|
|
56
|
-
resolveRef?: StoryRefResolver,
|
|
57
|
-
): {
|
|
53
|
+
/** The resolved story: what each marker became and where each card sits. */
|
|
54
|
+
export interface StoryPlacements {
|
|
58
55
|
segments: StorySegment[];
|
|
59
56
|
byIndex: Map<number, StoryPlacementResolution>;
|
|
60
57
|
placedAt: Map<number, number>;
|
|
61
58
|
placedCount: number;
|
|
62
59
|
lostItems: number;
|
|
63
|
-
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveStoryPlacements(
|
|
63
|
+
report: string,
|
|
64
|
+
resolveItem: (index: number) => StoryPlacementTarget | StoryPlacementLoss,
|
|
65
|
+
resolveRef?: StoryRefResolver,
|
|
66
|
+
): StoryPlacements {
|
|
64
67
|
const { segments: written } = splitStory(report, resolveRef);
|
|
65
68
|
const byIndex = new Map<number, StoryPlacementResolution>();
|
|
66
69
|
// The segment position that renders each placed index's card; a repeated
|
|
@@ -312,9 +315,7 @@ const STORY_PROSE_CLASS =
|
|
|
312
315
|
* why the summary and the unresolved list live above it.
|
|
313
316
|
*/
|
|
314
317
|
export function AnalysisStory({
|
|
315
|
-
|
|
316
|
-
resolveItem,
|
|
317
|
-
resolveRef,
|
|
318
|
+
placements: story,
|
|
318
319
|
renderPlacement,
|
|
319
320
|
onReveal,
|
|
320
321
|
onViewSource,
|
|
@@ -322,10 +323,8 @@ export function AnalysisStory({
|
|
|
322
323
|
openRequest = 0,
|
|
323
324
|
className,
|
|
324
325
|
}: {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
/** Maps a ledger ref an agent wrote as a marker to its evidence index. */
|
|
328
|
-
resolveRef?: StoryRefResolver;
|
|
326
|
+
/** Resolved once by the host, which also needs it for its own counts. */
|
|
327
|
+
placements: StoryPlacements;
|
|
329
328
|
/** Renders the card for a placed item; `compact` when it sits in the collapsed preview. */
|
|
330
329
|
renderPlacement: (
|
|
331
330
|
target: StoryPlacementTarget,
|
|
@@ -348,10 +347,6 @@ export function AnalysisStory({
|
|
|
348
347
|
if (openRequest > 0) setOpen(true);
|
|
349
348
|
}, [openRequest]);
|
|
350
349
|
const regionId = useId();
|
|
351
|
-
const story = useMemo(
|
|
352
|
-
() => resolveStoryPlacements(report, resolveItem, resolveRef),
|
|
353
|
-
[report, resolveItem, resolveRef],
|
|
354
|
-
);
|
|
355
350
|
const firstPlacedAt = story.segments.findIndex(
|
|
356
351
|
(segment) =>
|
|
357
352
|
segment.kind === "placement" &&
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CheckCircle2,
|
|
4
|
+
AlertTriangle,
|
|
5
|
+
ShieldCheck,
|
|
6
|
+
Wrench,
|
|
7
|
+
Sparkles,
|
|
8
|
+
RefreshCw,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { DialogPortal } from "@skyhook-io/k8s-ui/components/ui/DialogPortal";
|
|
11
|
+
import { parseContextName } from "../../utils/context-name";
|
|
12
|
+
import type { Diagnosis, ApplyMutationOutcome } from "../../api/diagnose";
|
|
13
|
+
import { AIMarkdown, CopyButton } from "./AIMarkdown";
|
|
14
|
+
|
|
15
|
+
// The Apply confirmation — wider than a generic confirm so the recommended fix
|
|
16
|
+
// (rendered markdown) is legible, making it unambiguous what the one click does.
|
|
17
|
+
export function ApplyDialog({
|
|
18
|
+
open,
|
|
19
|
+
onClose,
|
|
20
|
+
onConfirm,
|
|
21
|
+
agentLabel,
|
|
22
|
+
resourceLabel,
|
|
23
|
+
context,
|
|
24
|
+
fix,
|
|
25
|
+
reason,
|
|
26
|
+
precondition,
|
|
27
|
+
managedBy,
|
|
28
|
+
confidence,
|
|
29
|
+
}: {
|
|
30
|
+
open: boolean;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onConfirm: () => void;
|
|
33
|
+
agentLabel: string;
|
|
34
|
+
resourceLabel: string;
|
|
35
|
+
context: string;
|
|
36
|
+
fix?: string;
|
|
37
|
+
/** The agent's one-clause case for this step, repeated at the decision. */
|
|
38
|
+
reason?: string;
|
|
39
|
+
/** The condition the agent attached to this step; shown before the operator confirms. */
|
|
40
|
+
precondition?: string;
|
|
41
|
+
managedBy?: string; // GitOps/Helm owner of the resource, if any
|
|
42
|
+
confidence?: number;
|
|
43
|
+
}) {
|
|
44
|
+
const fixText = fix?.trim();
|
|
45
|
+
const lowConfidence = confidence != null && confidence < 0.5;
|
|
46
|
+
// A GitOps/Helm-managed resource needs an explicit acknowledgment before applying
|
|
47
|
+
// a direct change — it's the canonical footgun (the controller reverts it). Gating
|
|
48
|
+
// (not just warning) makes the user opt into "yes, I know this may be undone."
|
|
49
|
+
// TODO(SKY-1075): once Radar can connect the user's SCM (GitHub/GitLab/…), replace
|
|
50
|
+
// direct apply on managed resources with "open a PR against the Git source"
|
|
51
|
+
// instead — the durable fix. See Linear SKY-1075.
|
|
52
|
+
const [acked, setAcked] = useState(false);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (open) setAcked(false);
|
|
55
|
+
}, [open]);
|
|
56
|
+
const applyBlocked = !!managedBy && !acked;
|
|
57
|
+
return (
|
|
58
|
+
<DialogPortal
|
|
59
|
+
open={open}
|
|
60
|
+
onClose={onClose}
|
|
61
|
+
className="flex max-h-[calc(100dvh-2rem)] w-full max-w-2xl flex-col overflow-hidden"
|
|
62
|
+
>
|
|
63
|
+
<div className="flex shrink-0 items-start gap-3 border-b border-theme-border p-4">
|
|
64
|
+
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
|
|
65
|
+
<AlertTriangle className="h-5 w-5 text-amber-500" />
|
|
66
|
+
</div>
|
|
67
|
+
<div className="min-w-0 flex-1">
|
|
68
|
+
<h3 className="text-lg font-semibold text-theme-text-primary">
|
|
69
|
+
Apply this fix?
|
|
70
|
+
</h3>
|
|
71
|
+
<p className="mt-1 text-sm text-theme-text-secondary">
|
|
72
|
+
Let {agentLabel} apply the recommended change to{" "}
|
|
73
|
+
<span className="font-medium text-theme-text-primary">
|
|
74
|
+
{resourceLabel}
|
|
75
|
+
</span>
|
|
76
|
+
.
|
|
77
|
+
</p>
|
|
78
|
+
<p className="mt-2 text-sm text-theme-text-secondary">
|
|
79
|
+
Cluster:{" "}
|
|
80
|
+
<span className="font-medium text-theme-text-primary">
|
|
81
|
+
{parseContextName(context).clusterName}
|
|
82
|
+
</span>
|
|
83
|
+
</p>
|
|
84
|
+
{context !== parseContextName(context).clusterName && (
|
|
85
|
+
<p className="mt-0.5 break-all font-mono text-xs text-theme-text-tertiary">
|
|
86
|
+
{context}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="min-h-0 overflow-y-auto">
|
|
93
|
+
{fixText && (
|
|
94
|
+
<div className="border-b border-theme-border p-4">
|
|
95
|
+
<div className="mb-1.5 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-accent">
|
|
96
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
97
|
+
Proposed change
|
|
98
|
+
</div>
|
|
99
|
+
<AIMarkdown className="text-sm text-theme-text-primary [overflow-wrap:anywhere] [&_code]:font-normal [&_p]:my-0 [&_p]:text-theme-text-primary [&_pre]:my-1.5 [&_pre]:whitespace-pre-wrap [&_pre_code]:whitespace-pre-wrap">
|
|
100
|
+
{fixText}
|
|
101
|
+
</AIMarkdown>
|
|
102
|
+
{reason ? (
|
|
103
|
+
<p
|
|
104
|
+
data-apply-reason
|
|
105
|
+
className="mt-2 text-sm text-theme-text-secondary"
|
|
106
|
+
>
|
|
107
|
+
<span className="font-medium text-theme-text-primary">
|
|
108
|
+
Why this step:
|
|
109
|
+
</span>{" "}
|
|
110
|
+
{reason}
|
|
111
|
+
</p>
|
|
112
|
+
) : null}
|
|
113
|
+
{precondition ? (
|
|
114
|
+
<p
|
|
115
|
+
data-apply-precondition
|
|
116
|
+
className="mt-2 flex items-start gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/5 px-2.5 py-1.5 text-xs text-theme-text-primary"
|
|
117
|
+
>
|
|
118
|
+
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
|
|
119
|
+
<span>
|
|
120
|
+
<span className="font-medium">
|
|
121
|
+
The agent said this applies only if:
|
|
122
|
+
</span>{" "}
|
|
123
|
+
{precondition}. Radar has not checked that condition.
|
|
124
|
+
</span>
|
|
125
|
+
</p>
|
|
126
|
+
) : null}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
<div className="shrink-0 space-y-2 p-4">
|
|
131
|
+
{/* The star warning: when we KNOW a controller owns this resource, a live
|
|
132
|
+
change reverts on the next reconcile — say so authoritatively. */}
|
|
133
|
+
{managedBy && (
|
|
134
|
+
<div className="space-y-2 rounded border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-theme-text-primary">
|
|
135
|
+
<div className="flex items-start gap-2">
|
|
136
|
+
<RefreshCw className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
|
|
137
|
+
<span>
|
|
138
|
+
<span className="font-medium">Managed by {managedBy}.</span>{" "}
|
|
139
|
+
Unless you turn off auto-sync, a direct change here will be
|
|
140
|
+
undone within minutes when {managedBy} re-syncs from Git — the
|
|
141
|
+
durable fix is to change it in Git (the {managedBy} source).
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
<label className="flex cursor-pointer items-center gap-2 pl-6 text-xs text-theme-text-secondary">
|
|
145
|
+
<input
|
|
146
|
+
type="checkbox"
|
|
147
|
+
checked={acked}
|
|
148
|
+
onChange={(e) => setAcked(e.target.checked)}
|
|
149
|
+
className="h-3.5 w-3.5 accent-amber-500"
|
|
150
|
+
/>
|
|
151
|
+
I understand {managedBy} may revert this — apply anyway.
|
|
152
|
+
</label>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
{lowConfidence && (
|
|
156
|
+
<div className="flex items-start gap-2 rounded border border-theme-border bg-theme-elevated p-3 text-sm text-theme-text-secondary">
|
|
157
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
158
|
+
<span>
|
|
159
|
+
The agent had <span className="font-medium">low confidence</span>{" "}
|
|
160
|
+
in this conclusion — consider asking a follow-up to verify before
|
|
161
|
+
applying.
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
<div className="flex items-start gap-2 rounded border border-theme-border bg-theme-base/50 p-3 text-sm text-theme-text-secondary">
|
|
166
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
|
|
167
|
+
<span>
|
|
168
|
+
{agentLabel} will change your cluster using your kubeconfig
|
|
169
|
+
credentials. Review the change above; if you're not sure, ask a
|
|
170
|
+
follow-up first.
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex shrink-0 items-center justify-end gap-3 border-t border-theme-border p-4">
|
|
175
|
+
<button
|
|
176
|
+
onClick={onClose}
|
|
177
|
+
className="rounded-lg px-4 py-2 text-sm font-medium text-theme-text-secondary transition-colors hover:bg-theme-elevated hover:text-theme-text-primary"
|
|
178
|
+
>
|
|
179
|
+
Cancel
|
|
180
|
+
</button>
|
|
181
|
+
<button
|
|
182
|
+
onClick={onConfirm}
|
|
183
|
+
disabled={applyBlocked}
|
|
184
|
+
className="flex items-center gap-1.5 rounded-lg btn-brand px-4 py-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
|
|
185
|
+
>
|
|
186
|
+
<Wrench className="h-4 w-4" />
|
|
187
|
+
Apply fix
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
</DialogPortal>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The result of an apply turn. Mutation truth comes from Radar write-tool
|
|
195
|
+
// results, not the agent's prose or process exit. Missing outcome metadata is
|
|
196
|
+
// therefore fail-closed as unknown; only explicit confirmation renders green.
|
|
197
|
+
export function ApplyOutcomeCard({
|
|
198
|
+
diagnosis,
|
|
199
|
+
error,
|
|
200
|
+
applyOutcome,
|
|
201
|
+
onCheckStatus,
|
|
202
|
+
animate,
|
|
203
|
+
}: {
|
|
204
|
+
diagnosis: Diagnosis | null;
|
|
205
|
+
error?: string | null;
|
|
206
|
+
applyOutcome?: ApplyMutationOutcome;
|
|
207
|
+
onCheckStatus?: () => void;
|
|
208
|
+
animate: boolean;
|
|
209
|
+
}) {
|
|
210
|
+
const outcome = diagnosis?.report || diagnosis?.rootCause;
|
|
211
|
+
const status = applyOutcome ?? "unknown";
|
|
212
|
+
const confirmed = status === "confirmed";
|
|
213
|
+
const failed = status === "failed";
|
|
214
|
+
const heading = confirmed
|
|
215
|
+
? "Applied"
|
|
216
|
+
: failed
|
|
217
|
+
? "Not applied"
|
|
218
|
+
: "Outcome unknown";
|
|
219
|
+
const detail = error || outcome;
|
|
220
|
+
const fallbackDetail = confirmed
|
|
221
|
+
? "Radar confirmed that the change was applied. Check the current state to confirm its effect."
|
|
222
|
+
: failed
|
|
223
|
+
? "Radar could not confirm that the change was applied. Review Activity before retrying."
|
|
224
|
+
: "Radar could not confirm whether the change was applied. Check the current state before retrying.";
|
|
225
|
+
const containerClass = confirmed
|
|
226
|
+
? "border-emerald-500/30 bg-emerald-500/5"
|
|
227
|
+
: failed
|
|
228
|
+
? "border-red-500/30 bg-red-500/5"
|
|
229
|
+
: "border-amber-500/40 bg-amber-500/10";
|
|
230
|
+
const accentClass = confirmed
|
|
231
|
+
? "text-emerald-500"
|
|
232
|
+
: failed
|
|
233
|
+
? "text-red-400"
|
|
234
|
+
: "text-amber-400";
|
|
235
|
+
const buttonClass = confirmed
|
|
236
|
+
? "border-emerald-500/40 text-emerald-500 hover:bg-emerald-500/10"
|
|
237
|
+
: "border-amber-500/40 text-amber-400 hover:bg-amber-500/10";
|
|
238
|
+
const provenance = confirmed
|
|
239
|
+
? error
|
|
240
|
+
? "Radar confirmed the change — the agent report is incomplete"
|
|
241
|
+
: "Radar confirmed the change was applied"
|
|
242
|
+
: failed
|
|
243
|
+
? "Radar did not confirm that the change was applied"
|
|
244
|
+
: "Radar cannot confirm whether the change was applied";
|
|
245
|
+
return (
|
|
246
|
+
<div className={`mt-3 space-y-2 ${animate ? "animate-result-in" : ""}`}>
|
|
247
|
+
<div className={`rounded-lg border p-3 ${containerClass}`}>
|
|
248
|
+
<div className="mb-1 flex items-center justify-between gap-2">
|
|
249
|
+
<div
|
|
250
|
+
className={`flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide ${accentClass}`}
|
|
251
|
+
>
|
|
252
|
+
{confirmed ? (
|
|
253
|
+
<CheckCircle2 className="h-3.5 w-3.5" />
|
|
254
|
+
) : (
|
|
255
|
+
<AlertTriangle className="h-3.5 w-3.5" />
|
|
256
|
+
)}
|
|
257
|
+
{heading}
|
|
258
|
+
</div>
|
|
259
|
+
{detail && <CopyButton text={detail} label="Copy apply result" />}
|
|
260
|
+
</div>
|
|
261
|
+
<AIMarkdown className="text-sm text-theme-text-primary [overflow-wrap:anywhere] [&_code]:font-normal [&_li]:text-theme-text-primary [&_p]:my-1 [&_p]:text-theme-text-primary [&_p:first-child]:mt-0 [&_p:last-child]:mb-0">
|
|
262
|
+
{detail || fallbackDetail}
|
|
263
|
+
</AIMarkdown>
|
|
264
|
+
{!confirmed && !failed && (
|
|
265
|
+
<div className="mt-2 flex items-start gap-1.5 text-xs font-medium text-amber-300">
|
|
266
|
+
<RefreshCw className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
267
|
+
<span>
|
|
268
|
+
Check the current state before trying to apply this change again.
|
|
269
|
+
</span>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
{onCheckStatus && !failed && (
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={onCheckStatus}
|
|
276
|
+
className={`mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg border py-2 text-sm font-medium ${buttonClass}`}
|
|
277
|
+
>
|
|
278
|
+
<RefreshCw className="h-4 w-4" />
|
|
279
|
+
Check current status
|
|
280
|
+
</button>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
<div className="flex items-center gap-1 px-0.5 text-[11px] text-theme-text-tertiary">
|
|
284
|
+
<ShieldCheck className="h-3 w-3 shrink-0" />
|
|
285
|
+
<span className="truncate">{provenance}</span>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|