@skyhook-io/radar-app 1.13.3 → 1.13.5
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 +6 -6
- package/src/App.tsx +68 -23
- package/src/RadarApp.tsx +17 -1
- package/src/api/client.ts +28 -10
- package/src/api/diagnose.ts +61 -15
- package/src/components/ConnectionErrorView.test.tsx +30 -0
- package/src/components/ConnectionErrorView.tsx +31 -19
- package/src/components/diagnose/AgentCase.tsx +131 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +95 -0
- package/src/components/diagnose/DiagnoseContext.tsx +402 -77
- package/src/components/diagnose/DiagnoseSurface.test.tsx +53 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +198 -286
- package/src/components/diagnose/Home.test.tsx +23 -1
- package/src/components/diagnose/Home.tsx +49 -3
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +1446 -5
- package/src/components/diagnose/InvestigationEvidencePane.tsx +1373 -126
- package/src/components/diagnose/InvestigationView.tsx +227 -5
- package/src/components/diagnose/LocalDiagnoseAction.tsx +2 -3
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
- package/src/components/diagnose/investigationCase.test.tsx +1568 -0
- package/src/components/diagnose/investigationCase.ts +439 -0
- package/src/components/diagnose/investigationEvidence.test.ts +3525 -17
- package/src/components/diagnose/investigationEvidence.ts +2246 -166
- package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
- package/src/components/diagnose/investigationMetrics.test.ts +712 -0
- package/src/components/diagnose/investigationMetrics.ts +393 -0
- package/src/components/diagnose/investigationSourceFocus.ts +6 -0
- package/src/components/diagnose/investigationState.test.ts +322 -0
- package/src/components/diagnose/investigationState.ts +147 -25
- package/src/components/diagnose/parts.test.tsx +537 -1
- package/src/components/diagnose/parts.tsx +486 -42
- package/src/components/resource/HPACharts.render.test.tsx +77 -0
- package/src/components/resource/HPACharts.tsx +32 -25
- package/src/components/resource/PrometheusChartsGrid.tsx +181 -79
- package/src/components/resources/PodFilePreview.test.tsx +131 -0
- package/src/components/resources/PodFilePreview.tsx +394 -0
- package/src/components/resources/PodFilesystemModal.tsx +157 -67
- package/src/components/resources/ResourcesView.tsx +27 -2
- package/src/context/DiagnoseCustomization.test.tsx +26 -0
- package/src/context/DiagnoseCustomization.tsx +14 -2
- package/src/index.ts +8 -5
- package/src/utils/shell-safe.test.ts +25 -1
- package/src/utils/shell-safe.ts +16 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { Sparkles } from "lucide-react";
|
|
3
|
+
import { Badge } from "@skyhook-io/k8s-ui";
|
|
4
|
+
|
|
5
|
+
import type { DiagnosisEvidenceRole } from "../../api/diagnose";
|
|
6
|
+
import { Tooltip } from "../ui/Tooltip";
|
|
7
|
+
|
|
8
|
+
export const AGENT_ROLE_LABELS: Readonly<
|
|
9
|
+
Record<DiagnosisEvidenceRole, string>
|
|
10
|
+
> = {
|
|
11
|
+
cause: "Cause",
|
|
12
|
+
symptom: "Symptom",
|
|
13
|
+
context: "Context",
|
|
14
|
+
benign: "Not a problem",
|
|
15
|
+
demoted: "Less relevant",
|
|
16
|
+
rules_out: "Rules out",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The agent's framing of a Radar fact. Always the agent tone, never a
|
|
21
|
+
* severity tone: a chip must not read as a Radar finding.
|
|
22
|
+
*/
|
|
23
|
+
export function AgentRoleChip({ role }: { role: DiagnosisEvidenceRole }) {
|
|
24
|
+
return (
|
|
25
|
+
<Tooltip
|
|
26
|
+
content={`The agent labelled this evidence "${AGENT_ROLE_LABELS[role]}". Radar recorded the fact; the label is the agent's reading of it.`}
|
|
27
|
+
// The label has to share the sentence's baseline. Centring the badge box
|
|
28
|
+
// drops it ~1px (padding and border make the box taller than the line),
|
|
29
|
+
// and the badge's own baseline comes from the sparkle, landing 1px high.
|
|
30
|
+
wrapperClassName="shrink-0 align-[-1px]"
|
|
31
|
+
>
|
|
32
|
+
<Badge tone="agent" size="sm">
|
|
33
|
+
<Sparkles className="h-2.5 w-2.5 shrink-0" aria-hidden />
|
|
34
|
+
{AGENT_ROLE_LABELS[role]}
|
|
35
|
+
</Badge>
|
|
36
|
+
</Tooltip>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One sentence from the agent about a Radar fact. Everything the agent
|
|
42
|
+
* contributed to a card lives in this one row — the sparkle, the role, and the
|
|
43
|
+
* sentence — so the register is unmistakable: the fact above is Radar's, this
|
|
44
|
+
* line is the agent's reading of it. The role travelled with the card title
|
|
45
|
+
* once, where it sat in the same slot as Radar's own badges and could be read
|
|
46
|
+
* as Radar having established it.
|
|
47
|
+
*
|
|
48
|
+
* A `rules_out` note carries the hypothesis it excludes, as text beside the
|
|
49
|
+
* chip rather than inside it: "Rules out" alone forces the reader to find the
|
|
50
|
+
* hypothesis in a block further down and match it back, and a hypothesis long
|
|
51
|
+
* enough to be useful makes a badge that swallows the row.
|
|
52
|
+
*/
|
|
53
|
+
export function AgentClaimNote({
|
|
54
|
+
claim,
|
|
55
|
+
role,
|
|
56
|
+
excludes,
|
|
57
|
+
subject,
|
|
58
|
+
className,
|
|
59
|
+
}: {
|
|
60
|
+
claim: string;
|
|
61
|
+
role?: DiagnosisEvidenceRole;
|
|
62
|
+
/**
|
|
63
|
+
* The hypothesis this item excludes. Rendered only for `rules_out`, where
|
|
64
|
+
* the chip frames it as rejected: a hypothesis is a claim the agent
|
|
65
|
+
* DISPROVED, so on any other card it would read as a statement of fact.
|
|
66
|
+
*/
|
|
67
|
+
excludes?: string;
|
|
68
|
+
/** The observation the note was bound to, when it is not on that card. */
|
|
69
|
+
subject?: string;
|
|
70
|
+
className?: string;
|
|
71
|
+
}) {
|
|
72
|
+
if (!claim && !role) return null;
|
|
73
|
+
return (
|
|
74
|
+
<p
|
|
75
|
+
data-agent-claim
|
|
76
|
+
className={clsx(
|
|
77
|
+
"flex min-w-0 items-start gap-2 border-t border-dashed border-theme-border/70 text-xs leading-relaxed text-theme-text-secondary",
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<span className="min-w-0 flex-1 [overflow-wrap:anywhere]">
|
|
82
|
+
{/* The chip and its sparkle carry this visually; the word is what
|
|
83
|
+
survives when colour and iconography do not. "Agent" rather than
|
|
84
|
+
"Agent's note" because a role can arrive with no sentence. */}
|
|
85
|
+
<span className="sr-only">Agent: </span>
|
|
86
|
+
{role ? (
|
|
87
|
+
<>
|
|
88
|
+
<AgentRoleChip role={role} />{" "}
|
|
89
|
+
</>
|
|
90
|
+
) : null}
|
|
91
|
+
{excludes && role === "rules_out" ? (
|
|
92
|
+
<>
|
|
93
|
+
<span className="italic text-theme-text-tertiary">{excludes}</span>
|
|
94
|
+
{" — "}
|
|
95
|
+
</>
|
|
96
|
+
) : null}
|
|
97
|
+
{claim ? (
|
|
98
|
+
<>
|
|
99
|
+
{subject ? (
|
|
100
|
+
<span className="text-theme-text-tertiary">{subject} · </span>
|
|
101
|
+
) : null}
|
|
102
|
+
{renderClaim(claim)}
|
|
103
|
+
</>
|
|
104
|
+
) : null}
|
|
105
|
+
</span>
|
|
106
|
+
</p>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Claims are one sentence of prose in which the agent marks identifiers with
|
|
112
|
+
* backticks. Only that inline-code span is honoured: a full Markdown render
|
|
113
|
+
* would wrap the sentence in a block and break the shared baseline with the
|
|
114
|
+
* role chip and the lead-in.
|
|
115
|
+
*/
|
|
116
|
+
function renderClaim(claim: string) {
|
|
117
|
+
const parts = claim.split("`");
|
|
118
|
+
if (parts.length < 3) return claim;
|
|
119
|
+
return parts.map((part, i) =>
|
|
120
|
+
i % 2 === 1 && i < parts.length - 1 ? (
|
|
121
|
+
<code
|
|
122
|
+
key={i}
|
|
123
|
+
className="inline-code rounded border border-theme-border/60 bg-theme-base px-1 py-px font-mono text-[11px] font-normal text-theme-text-primary"
|
|
124
|
+
>
|
|
125
|
+
{part}
|
|
126
|
+
</code>
|
|
127
|
+
) : (
|
|
128
|
+
part
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
investigationWorkspaceNavigationState,
|
|
4
|
+
investigationWorkspacePath,
|
|
5
|
+
investigationWorkspaceRestorePath,
|
|
6
|
+
investigationWorkspaceSearch,
|
|
7
|
+
isInvestigationWorkspacePath,
|
|
8
|
+
shouldExitUnavailableWorkspace,
|
|
9
|
+
workspaceRunIDFromPath,
|
|
10
|
+
} from "./DiagnoseContext";
|
|
11
|
+
|
|
12
|
+
describe("investigation workspace routes", () => {
|
|
13
|
+
it("recognizes the base workspace and selected-run routes", () => {
|
|
14
|
+
expect(isInvestigationWorkspacePath("/investigations")).toBe(true);
|
|
15
|
+
expect(isInvestigationWorkspacePath("/investigations/run_123")).toBe(true);
|
|
16
|
+
expect(workspaceRunIDFromPath("/investigations/run_123")).toBe("run_123");
|
|
17
|
+
expect(workspaceRunIDFromPath("/investigations/run%20one")).toBe("run one");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("keeps only cross-view scope in workspace URLs", () => {
|
|
21
|
+
expect(
|
|
22
|
+
investigationWorkspaceSearch(
|
|
23
|
+
"?org=o1&namespaces=prod%2Cstage&ai-run=r1&resource=pod",
|
|
24
|
+
),
|
|
25
|
+
).toBe("?org=o1&namespaces=prod%2Cstage");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("uses Home for global entry and an explicit route for drawer expansion", () => {
|
|
29
|
+
expect(investigationWorkspacePath(null)).toBe("/investigations");
|
|
30
|
+
expect(investigationWorkspacePath("run 123")).toBe(
|
|
31
|
+
"/investigations/run%20123",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("builds opaque navigation state for a synthetic drawer return", () => {
|
|
36
|
+
expect(
|
|
37
|
+
investigationWorkspaceNavigationState({
|
|
38
|
+
returnPath: "/?ai-run=r1",
|
|
39
|
+
drawerOrigin: true,
|
|
40
|
+
closeHistorySteps: 2,
|
|
41
|
+
}),
|
|
42
|
+
).toEqual({
|
|
43
|
+
investigationWorkspaceReturn: "/?ai-run=r1",
|
|
44
|
+
investigationReturnHistorySteps: "2",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("restores the currently focused run into the original dock location", () => {
|
|
49
|
+
expect(
|
|
50
|
+
investigationWorkspaceRestorePath(
|
|
51
|
+
{ investigationWorkspaceReturn: "/resources?kind=pods&ai-run=old#row" },
|
|
52
|
+
"new run",
|
|
53
|
+
"https://radar.local",
|
|
54
|
+
),
|
|
55
|
+
).toBe("/resources?kind=pods&ai-run=new+run#row");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("falls back safely when a malformed return path normalizes cross-origin", () => {
|
|
59
|
+
expect(
|
|
60
|
+
investigationWorkspaceRestorePath(
|
|
61
|
+
{ investigationWorkspaceReturn: "/\\\\evil.example" },
|
|
62
|
+
"run-1",
|
|
63
|
+
"https://radar.local",
|
|
64
|
+
),
|
|
65
|
+
).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("exits only resolved AI-off workspace routes", () => {
|
|
69
|
+
expect(
|
|
70
|
+
shouldExitUnavailableWorkspace("/investigations/run-1", true, false),
|
|
71
|
+
).toBe(true);
|
|
72
|
+
expect(
|
|
73
|
+
shouldExitUnavailableWorkspace("/investigations/run-1", false, false),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
expect(shouldExitUnavailableWorkspace("/?ai-run=run-1", true, false)).toBe(
|
|
76
|
+
false,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("builds history-only return state for cross-tree workspace entry", () => {
|
|
81
|
+
expect(
|
|
82
|
+
investigationWorkspaceNavigationState({ closeHistorySteps: 1 }),
|
|
83
|
+
).toEqual({ investigationReturnHistorySteps: "1" });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not claim unrelated paths and rejects malformed run ids", () => {
|
|
87
|
+
expect(isInvestigationWorkspacePath("/resources/pods")).toBe(false);
|
|
88
|
+
expect(isInvestigationWorkspacePath("/investigations/run/extra")).toBe(
|
|
89
|
+
true,
|
|
90
|
+
);
|
|
91
|
+
expect(workspaceRunIDFromPath("/investigations")).toBeNull();
|
|
92
|
+
expect(workspaceRunIDFromPath("/investigations/%2Fbad")).toBeNull();
|
|
93
|
+
expect(workspaceRunIDFromPath("/investigations/%ZZ")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|