@skyhook-io/radar-app 1.13.1 → 1.13.2
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/App.tsx +19 -1
- package/src/RadarApp.tsx +2 -2
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +72 -9
- package/src/components/diagnose/AISettings.tsx +1 -1
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +12 -17
- package/src/components/diagnose/DiagnoseSurface.test.tsx +211 -16
- package/src/components/diagnose/DiagnoseSurface.tsx +464 -133
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +289 -119
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
- package/src/components/diagnose/InvestigationView.test.ts +17 -0
- package/src/components/diagnose/InvestigationView.tsx +1900 -393
- package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
- package/src/components/diagnose/agentCatalog.ts +1 -1
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
- package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
- package/src/components/diagnose/investigationEvidence.ts +3492 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
- package/src/components/diagnose/investigationExplanation.test.ts +63 -0
- package/src/components/diagnose/investigationExplanation.ts +22 -0
- package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
- package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
- package/src/components/diagnose/investigationSourceFocus.ts +98 -0
- package/src/components/diagnose/investigationState.test.ts +695 -0
- package/src/components/diagnose/investigationState.ts +451 -0
- package/src/components/diagnose/parts.test.tsx +864 -3
- package/src/components/diagnose/parts.tsx +1337 -541
- package/src/components/diagnose/target.test.ts +39 -0
- package/src/components/diagnose/target.ts +36 -0
- package/src/components/diagnose/useDisclosureReveal.ts +117 -0
- package/src/components/home/MCPSetupDialog.tsx +2 -2
- package/src/components/home/mcpToolCatalog.test.ts +22 -0
- package/src/components/home/mcpToolCatalog.ts +3 -2
- package/src/components/issues/IssuesPane.tsx +5 -1
- package/src/components/settings/SettingsDialog.tsx +11 -13
- package/src/components/workload/WorkloadView.tsx +1 -1
- package/src/context/DiagnoseCustomization.tsx +11 -8
- package/src/index.css +63 -79
- package/src/index.ts +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { formatInvestigationTarget, runTargetKey } from "./target";
|
|
4
|
+
|
|
5
|
+
describe("investigation target identity", () => {
|
|
6
|
+
it("uses one running key for singular Kinds and plural resource names", () => {
|
|
7
|
+
expect(runTargetKey("Deployment", "prod", "api", "apps")).toBe(
|
|
8
|
+
runTargetKey("deployments", "prod", "api", "apps"),
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("keeps same-named resources in different API groups distinct", () => {
|
|
13
|
+
expect(runTargetKey("Service", "prod", "api", "")).not.toBe(
|
|
14
|
+
runTargetKey("services", "prod", "api", "serving.knative.dev"),
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("formats a non-core target with its Kubernetes-qualified Kind", () => {
|
|
19
|
+
expect(
|
|
20
|
+
formatInvestigationTarget({
|
|
21
|
+
kind: "Rollout",
|
|
22
|
+
group: "argoproj.io",
|
|
23
|
+
namespace: "prod",
|
|
24
|
+
name: "checkout",
|
|
25
|
+
}),
|
|
26
|
+
).toBe("Rollout.argoproj.io prod/checkout");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("keeps the core target label compact", () => {
|
|
30
|
+
expect(
|
|
31
|
+
formatInvestigationTarget({
|
|
32
|
+
kind: "Service",
|
|
33
|
+
group: "",
|
|
34
|
+
namespace: "prod",
|
|
35
|
+
name: "api",
|
|
36
|
+
}),
|
|
37
|
+
).toBe("Service prod/api");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resourceKey } from "@skyhook-io/k8s-ui";
|
|
2
|
+
|
|
3
|
+
import { kindToPluralWithGroup } from "../../utils/navigation";
|
|
4
|
+
|
|
5
|
+
export interface InvestigationTargetIdentity {
|
|
6
|
+
kind: string;
|
|
7
|
+
/** Kubernetes API group; empty means core. */
|
|
8
|
+
group: string;
|
|
9
|
+
namespace: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Normalize the Kind/resource-name boundary before comparing a run returned by
|
|
14
|
+
// the API (singular Kind) with a UI action (often a plural resource name).
|
|
15
|
+
export function runTargetKey(
|
|
16
|
+
kind: string,
|
|
17
|
+
namespace: string,
|
|
18
|
+
name: string,
|
|
19
|
+
group: string,
|
|
20
|
+
): string {
|
|
21
|
+
return resourceKey(
|
|
22
|
+
group,
|
|
23
|
+
kindToPluralWithGroup(kind, group),
|
|
24
|
+
namespace,
|
|
25
|
+
name,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Keep the UI aligned with the CLI and the prompts: a non-core target is shown
|
|
30
|
+
// as Kind.api.group, which is the Kubernetes-qualified identity users can copy.
|
|
31
|
+
export function formatInvestigationTarget(
|
|
32
|
+
target: InvestigationTargetIdentity,
|
|
33
|
+
): string {
|
|
34
|
+
const kind = target.group ? `${target.kind}.${target.group}` : target.kind;
|
|
35
|
+
return `${kind} ${target.namespace ? `${target.namespace}/` : ""}${target.name}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
// Shared Collapse uses a 200 ms grid-row transition. Keep a small paint margin
|
|
4
|
+
// before moving focus so the destination is stationary. Reduced-motion users get
|
|
5
|
+
// an immediate disclosure and focus hand-off because Collapse disables motion.
|
|
6
|
+
export const INVESTIGATION_DISCLOSURE_SETTLE_MS = 220;
|
|
7
|
+
export function prefersReducedMotion(): boolean {
|
|
8
|
+
return (
|
|
9
|
+
typeof window !== "undefined" &&
|
|
10
|
+
typeof window.matchMedia === "function" &&
|
|
11
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function investigationDisclosureSettleDelay(
|
|
16
|
+
reducedMotion: boolean,
|
|
17
|
+
): number {
|
|
18
|
+
return reducedMotion ? 0 : INVESTIGATION_DISCLOSURE_SETTLE_MS;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function investigationDisclosureScrollTop({
|
|
22
|
+
scrollTop,
|
|
23
|
+
viewportTop,
|
|
24
|
+
viewportBottom,
|
|
25
|
+
disclosureTop,
|
|
26
|
+
disclosureBottom,
|
|
27
|
+
inset = 8,
|
|
28
|
+
}: {
|
|
29
|
+
scrollTop: number;
|
|
30
|
+
viewportTop: number;
|
|
31
|
+
viewportBottom: number;
|
|
32
|
+
disclosureTop: number;
|
|
33
|
+
disclosureBottom: number;
|
|
34
|
+
inset?: number;
|
|
35
|
+
}): number | undefined {
|
|
36
|
+
const visibleTop = viewportTop + inset;
|
|
37
|
+
const visibleBottom = viewportBottom - inset;
|
|
38
|
+
if (disclosureTop >= visibleTop && disclosureBottom <= visibleBottom) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (disclosureTop < visibleTop) {
|
|
42
|
+
return Math.max(0, scrollTop + disclosureTop - visibleTop);
|
|
43
|
+
}
|
|
44
|
+
const viewportHeight = visibleBottom - visibleTop;
|
|
45
|
+
const disclosureHeight = disclosureBottom - disclosureTop;
|
|
46
|
+
if (disclosureHeight <= viewportHeight) {
|
|
47
|
+
return Math.max(0, scrollTop + disclosureBottom - visibleBottom);
|
|
48
|
+
}
|
|
49
|
+
return Math.max(0, scrollTop + disclosureTop - visibleTop);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useDisclosureReveal<T extends HTMLElement>() {
|
|
53
|
+
const elementRef = useRef<T>(null);
|
|
54
|
+
const timerRef = useRef<number | undefined>(undefined);
|
|
55
|
+
useLayoutEffect(
|
|
56
|
+
() => () => {
|
|
57
|
+
if (timerRef.current !== undefined) {
|
|
58
|
+
window.clearTimeout(timerRef.current);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const revealAfterToggle = useCallback((opening: boolean) => {
|
|
65
|
+
if (timerRef.current !== undefined) {
|
|
66
|
+
window.clearTimeout(timerRef.current);
|
|
67
|
+
timerRef.current = undefined;
|
|
68
|
+
}
|
|
69
|
+
if (!opening) return;
|
|
70
|
+
const settleDelay = investigationDisclosureSettleDelay(
|
|
71
|
+
prefersReducedMotion(),
|
|
72
|
+
);
|
|
73
|
+
const initialScroller = elementRef.current?.closest<HTMLElement>(
|
|
74
|
+
"[data-investigation-findings-scroll], [data-investigation-activity-scroll]",
|
|
75
|
+
);
|
|
76
|
+
const initialScrollTop = initialScroller?.scrollTop;
|
|
77
|
+
// Even reduced-motion disclosures need one task boundary so React can
|
|
78
|
+
// commit the open layout before it is measured.
|
|
79
|
+
timerRef.current = window.setTimeout(() => {
|
|
80
|
+
timerRef.current = undefined;
|
|
81
|
+
const element = elementRef.current;
|
|
82
|
+
if (!element) return;
|
|
83
|
+
const scroller = element.closest<HTMLElement>(
|
|
84
|
+
"[data-investigation-findings-scroll], [data-investigation-activity-scroll]",
|
|
85
|
+
);
|
|
86
|
+
// This component lives in a bounded Diagnose surface. Never fall back to
|
|
87
|
+
// scrolling the document (or an overflow-hidden ancestor), which can move
|
|
88
|
+
// the entire workspace and expose blank space below it.
|
|
89
|
+
if (!scroller) return;
|
|
90
|
+
// Expansion is delayed until Collapse has settled. If the reader scrolls
|
|
91
|
+
// during that interval, their newer intent wins over the automatic reveal.
|
|
92
|
+
if (
|
|
93
|
+
scroller === initialScroller &&
|
|
94
|
+
initialScrollTop !== undefined &&
|
|
95
|
+
Math.abs(scroller.scrollTop - initialScrollTop) > 2
|
|
96
|
+
) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const viewport = scroller.getBoundingClientRect();
|
|
100
|
+
const disclosure = element.getBoundingClientRect();
|
|
101
|
+
const top = investigationDisclosureScrollTop({
|
|
102
|
+
scrollTop: scroller.scrollTop,
|
|
103
|
+
viewportTop: viewport.top,
|
|
104
|
+
viewportBottom: viewport.bottom,
|
|
105
|
+
disclosureTop: disclosure.top,
|
|
106
|
+
disclosureBottom: disclosure.bottom,
|
|
107
|
+
});
|
|
108
|
+
if (top === undefined) return;
|
|
109
|
+
scroller.scrollTo({
|
|
110
|
+
top,
|
|
111
|
+
behavior: settleDelay === 0 ? "auto" : "smooth",
|
|
112
|
+
});
|
|
113
|
+
}, settleDelay);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
return { elementRef, revealAfterToggle };
|
|
117
|
+
}
|
|
@@ -204,14 +204,14 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
|
|
|
204
204
|
<a href="https://modelcontextprotocol.io" target="_blank" rel="noopener noreferrer" className="text-purple-400 hover:text-purple-300 underline underline-offset-2">
|
|
205
205
|
Model Context Protocol
|
|
206
206
|
</a>{' '}
|
|
207
|
-
(MCP) server that lets AI agents inspect,
|
|
207
|
+
(MCP) server that lets AI agents inspect, investigate, and operate your cluster through Radar.
|
|
208
208
|
Unlike raw kubectl access, Radar gives your AI pre-processed, enriched data —
|
|
209
209
|
topology graphs, health assessments, deduplicated events, filtered logs — so it
|
|
210
210
|
can understand your cluster state quickly without burning through context on
|
|
211
211
|
verbose YAML output.
|
|
212
212
|
</p>
|
|
213
213
|
<p className="text-sm text-theme-text-secondary leading-relaxed">
|
|
214
|
-
Most read tools do not change cluster state.
|
|
214
|
+
Most read tools do not change cluster state. An in-cluster route probe can create up
|
|
215
215
|
to five self-deleting probe pods when explicitly requested. Write tools (restart,
|
|
216
216
|
scale, sync, apply, node drain) are annotated as destructive so your AI client
|
|
217
217
|
can flag them and prompt before running.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { MCP_TOOL_CATALOG } from './mcpToolCatalog'
|
|
3
|
+
|
|
4
|
+
describe('diagnose catalog copy', () => {
|
|
5
|
+
const description = MCP_TOOL_CATALOG.find((tool) => tool.name === 'diagnose')?.desc
|
|
6
|
+
|
|
7
|
+
it('describes a bounded evidence bundle without promising complete sources or a verdict', () => {
|
|
8
|
+
expect(description).toContain('Bounded, point-in-time evidence bundle')
|
|
9
|
+
expect(description).toContain('not an agent run')
|
|
10
|
+
expect(description).toContain('not an authoritative root-cause verdict')
|
|
11
|
+
expect(description).toContain('attempts selected, capped current and previous logs where available')
|
|
12
|
+
expect(description).toContain('a capped warning-event sample')
|
|
13
|
+
expect(description).toContain('only when the evidence establishes one')
|
|
14
|
+
expect(description).not.toContain('One-call root-cause bundle')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('documents group-qualified Argo Rollout evidence', () => {
|
|
18
|
+
const diagnose = MCP_TOOL_CATALOG.find((tool) => tool.name === 'diagnose')
|
|
19
|
+
expect(diagnose?.desc).toContain('Argo Rollout')
|
|
20
|
+
expect(diagnose?.params.find((param) => param.arg === 'group')?.desc).toContain('argoproj.io')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -109,9 +109,10 @@ export const MCP_TOOL_CATALOG: MCPToolInfo[] = [
|
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
name: 'diagnose',
|
|
112
|
-
desc: '
|
|
112
|
+
desc: 'Bounded, point-in-time evidence bundle for one narrowed target — not an agent run and not an authoritative root-cause verdict. For workloads, including Argo Rollout, Radar attempts selected, capped current and previous logs where available, alongside resource context, a capped warning-event sample, and startup blockers; GitOps reconcilers, including Flux HelmRelease, get status summary + parsed related issues; network entry kinds (Service / Ingress / HTTPRoute / GRPCRoute / Gateway) get a coverage-honest path trace that identifies a broken hop only when the evidence establishes one, with an optional one-shot reachability test.',
|
|
113
113
|
params: [
|
|
114
|
-
{ arg: 'kind', required: true, desc: 'pod, deployment, statefulset, daemonset, application, kustomization, Flux HelmRelease, service, ingress, httproute, grpcroute, or gateway' },
|
|
114
|
+
{ arg: 'kind', required: true, desc: 'pod, deployment, statefulset, daemonset, Argo Rollout, application, kustomization, Flux HelmRelease, service, ingress, httproute, grpcroute, or gateway' },
|
|
115
|
+
{ arg: 'group', desc: 'API group for CRDs or kind collisions (for example argoproj.io for Rollout); built-ins are inferred' },
|
|
115
116
|
{ arg: 'namespace', required: true, desc: 'resource namespace' },
|
|
116
117
|
{ arg: 'name', required: true, desc: 'resource name' },
|
|
117
118
|
{ arg: 'probe', desc: 'network kinds only: add active DNS/TCP/TLS/HTTP probes against the declared path (0-3s wall time)' },
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import { useNavigate } from "react-router-dom";
|
|
3
3
|
import { useIssues } from "../../api/client";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useAPIResources,
|
|
6
|
+
karpenterCapacityAvailable,
|
|
7
|
+
} from "../../api/apiResources";
|
|
5
8
|
import { useCapabilitiesContext } from "../../contexts/CapabilitiesContext";
|
|
6
9
|
import { useConnection } from "../../context/ConnectionContext";
|
|
7
10
|
import type { SelectedResource } from "../../types";
|
|
@@ -242,6 +245,7 @@ export function IssuesPane({
|
|
|
242
245
|
)}
|
|
243
246
|
<IssueDiagnoseButton
|
|
244
247
|
kind={issue.kind}
|
|
248
|
+
group={issue.group}
|
|
245
249
|
namespace={issue.namespace ?? ""}
|
|
246
250
|
name={issue.name}
|
|
247
251
|
/>
|
|
@@ -107,7 +107,7 @@ interface SettingsDialogProps {
|
|
|
107
107
|
// the owner-gated footer and applied after restart.
|
|
108
108
|
// • Live integrations (Prometheus, cost source, Argo CD) — their own Apply/Connect endpoints
|
|
109
109
|
// re-point the running server; effect immediately, NOT part of footer dirty.
|
|
110
|
-
// • Self-saving preferences (cost currency, AI
|
|
110
|
+
// • Self-saving preferences (cost currency, AI investigations) — applied immediately.
|
|
111
111
|
// Integration fields (prometheusUrl, argoCdUrl, argoCdInsecureTls) apply through
|
|
112
112
|
// their own controls and are excluded here. Every field is normalized so
|
|
113
113
|
// unset≡default doesn't read as a change.
|
|
@@ -139,7 +139,7 @@ export function SettingsDialog({
|
|
|
139
139
|
const { data: versionInfo } = useVersionCheck()
|
|
140
140
|
// Radar configuration (kubeconfig, port, integrations…) is host-level and
|
|
141
141
|
// affects every user of this instance, so it's gated to owners. Personal
|
|
142
|
-
// sections (My permissions, AI
|
|
142
|
+
// sections (My permissions, AI investigations) stay usable by everyone. Non-Cloud
|
|
143
143
|
// callers (OSS, OIDC, kubectl plugin) have no role and pass — single-user
|
|
144
144
|
// laptops are never locked out of their own config. Backend enforces this too.
|
|
145
145
|
const { canAtLeast } = useCloudRole()
|
|
@@ -163,9 +163,7 @@ export function SettingsDialog({
|
|
|
163
163
|
open && section === 'argocd'
|
|
164
164
|
)
|
|
165
165
|
|
|
166
|
-
// AI
|
|
167
|
-
// section has its own Save that commits the draft to DiagnoseContext, so it's
|
|
168
|
-
// independent of the owner-gated footer. The draft is snapshotted on open.
|
|
166
|
+
// Local AI preferences save independently of the owner-gated server settings.
|
|
169
167
|
const diag = useDiagnose()
|
|
170
168
|
const aiAvailable = diag.available && diag.agents.length > 0
|
|
171
169
|
const [aiDraft, setAiDraft] = useState<AIDraft>({
|
|
@@ -443,7 +441,7 @@ export function SettingsDialog({
|
|
|
443
441
|
// Flat, un-grouped nav ordered as a narrative — at-a-glance, then you, then how
|
|
444
442
|
// Radar connects, then data integrations, then AI, then advanced. The
|
|
445
443
|
// per-section captions carry the restart-vs-live semantics, so group labels
|
|
446
|
-
// would only add visual weight. AI
|
|
444
|
+
// would only add visual weight. AI investigations is always shown (the section
|
|
447
445
|
// explains how to enable it when no agent CLI is installed).
|
|
448
446
|
const navItems: NavItemDef[] = [
|
|
449
447
|
{ id: 'overview', label: 'Overview', icon: LayoutDashboard, ownerOnly: false, dirty: false },
|
|
@@ -452,7 +450,7 @@ export function SettingsDialog({
|
|
|
452
450
|
{ id: 'prometheus', label: 'Metrics', icon: Activity, ownerOnly: true, dirty: false },
|
|
453
451
|
{ id: 'cost', label: 'Cost', icon: Coins, ownerOnly: true, dirty: costIntegrationDirty },
|
|
454
452
|
{ id: 'argocd', label: 'Argo CD', icon: GitBranch, ownerOnly: true, dirty: false },
|
|
455
|
-
{ id: 'ai', label: 'AI
|
|
453
|
+
{ id: 'ai', label: 'AI investigations', icon: Sparkles, ownerOnly: false, dirty: aiDirty },
|
|
456
454
|
{ id: 'advanced', label: 'Advanced', icon: SlidersHorizontal, ownerOnly: true, dirty: advancedDirty },
|
|
457
455
|
]
|
|
458
456
|
|
|
@@ -722,16 +720,16 @@ export function SettingsDialog({
|
|
|
722
720
|
/>
|
|
723
721
|
</SectionPane>
|
|
724
722
|
|
|
725
|
-
{/* AI
|
|
723
|
+
{/* AI investigations — self-saving, usable by everyone. Same heading block
|
|
726
724
|
as every other tab; the body is the agent controls (when a CLI is
|
|
727
725
|
installed) or an enable explainer (when not). */}
|
|
728
726
|
<div className={clsx(section !== 'ai' && 'hidden')} role="tabpanel" inert={section !== 'ai' || undefined}>
|
|
729
727
|
<div className="mb-4">
|
|
730
|
-
<h3 className="text-base font-semibold text-theme-text-primary">AI
|
|
728
|
+
<h3 className="text-base font-semibold text-theme-text-primary">AI investigations</h3>
|
|
731
729
|
<p className="mt-0.5 text-xs text-theme-text-tertiary">
|
|
732
730
|
{diag.hosted
|
|
733
|
-
? `Investigate incidents with ${diag.agentLabel} — reading logs, events, and topology to
|
|
734
|
-
: "Investigate incidents with an AI agent that runs on your own machine — reading logs, events, and topology to
|
|
731
|
+
? `Investigate incidents with ${diag.agentLabel} — reading logs, events, and topology to understand what's happening.`
|
|
732
|
+
: "Investigate incidents with an AI agent that runs on your own machine — reading logs, events, and topology to understand what's happening. No Radar cloud, no API key."}
|
|
735
733
|
</p>
|
|
736
734
|
</div>
|
|
737
735
|
{aiAvailable ? (
|
|
@@ -1140,7 +1138,7 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
|
|
|
1140
1138
|
copyable: mcpOn,
|
|
1141
1139
|
},
|
|
1142
1140
|
{
|
|
1143
|
-
id: 'ai', icon: Sparkles, label: 'AI
|
|
1141
|
+
id: 'ai', icon: Sparkles, label: 'AI investigations',
|
|
1144
1142
|
tone: aiAvailable ? 'ok' : 'off',
|
|
1145
1143
|
value: aiAvailable ? 'Ready' : 'No agent CLI',
|
|
1146
1144
|
detail: aiAvailable ? agentLabel : undefined,
|
|
@@ -1220,7 +1218,7 @@ function OverviewStatus({ tone }: { tone: OverviewTone }) {
|
|
|
1220
1218
|
return <span className={clsx('w-2 h-2 rounded-full shrink-0', cls)} />
|
|
1221
1219
|
}
|
|
1222
1220
|
|
|
1223
|
-
// AIUnavailableNotice is the body of the AI
|
|
1221
|
+
// AIUnavailableNotice is the body of the AI investigations tab when no supported agent
|
|
1224
1222
|
// CLI is installed — the heading/description are provided by the tab itself, so
|
|
1225
1223
|
// this is just the enable explainer (keeping the feature discoverable to whoever
|
|
1226
1224
|
// would set it up).
|
|
@@ -1795,7 +1795,7 @@ function DiagnoseFromWorkloadHint({
|
|
|
1795
1795
|
}) {
|
|
1796
1796
|
if (services.length === 0) return null
|
|
1797
1797
|
return (
|
|
1798
|
-
<Section title="
|
|
1798
|
+
<Section title="Trace network path">
|
|
1799
1799
|
<div className="flex items-start gap-2 text-xs text-theme-text-secondary">
|
|
1800
1800
|
<Stethoscope className="w-4 h-4 mt-0.5 shrink-0 text-theme-text-tertiary" aria-hidden />
|
|
1801
1801
|
<div className="flex-1 min-w-0">
|
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
// Slot-based injection of a resource-level "
|
|
1
|
+
// Slot-based injection of a resource-level "Investigate" action, and of the
|
|
2
2
|
// consent card's trust copy.
|
|
3
3
|
//
|
|
4
|
-
// Lets an embedding host (e.g. Radar Hub) inject
|
|
4
|
+
// Lets an embedding host (e.g. Radar Hub) inject an "Investigate with AI" button
|
|
5
5
|
// into every resource detail action bar — without forking WorkloadView or the
|
|
6
6
|
// shared ResourceActionsBar. The host returns whatever node should render in
|
|
7
7
|
// the action bar's right-aligned universal-actions area, given the resource
|
|
8
8
|
// context.
|
|
9
9
|
//
|
|
10
|
-
// Default (no provider): Radar renders no
|
|
10
|
+
// Default (no provider): Radar renders no Investigate button — OSS stays
|
|
11
11
|
// agent-free.
|
|
12
|
-
import { createContext, useContext, useMemo } from
|
|
13
|
-
import type { ReactNode } from
|
|
12
|
+
import { createContext, useContext, useMemo } from "react";
|
|
13
|
+
import type { ReactNode } from "react";
|
|
14
14
|
|
|
15
|
-
/** Render prop for the resource-level
|
|
15
|
+
/** Render prop for the resource-level Investigate action. */
|
|
16
16
|
export type RenderDiagnoseAction = (ctx: {
|
|
17
17
|
kind: string;
|
|
18
|
+
/** Kubernetes API group; empty means core. */
|
|
19
|
+
group?: string;
|
|
18
20
|
namespace: string;
|
|
19
21
|
name: string;
|
|
20
22
|
/** Coarse health of the resource (from its status badge), so the entry point can
|
|
21
|
-
* adapt: an urgent "
|
|
23
|
+
* adapt: an urgent "Investigate" on a problem vs. a quiet "ask AI" when fine/unknown. */
|
|
22
24
|
health?: "problem" | "healthy" | "unknown";
|
|
23
25
|
}) => ReactNode;
|
|
24
26
|
|
|
@@ -62,7 +64,8 @@ const DEFAULTS: DiagnoseCustomization = {
|
|
|
62
64
|
onOpenSettings: undefined,
|
|
63
65
|
};
|
|
64
66
|
|
|
65
|
-
const DiagnoseCustomizationContext =
|
|
67
|
+
const DiagnoseCustomizationContext =
|
|
68
|
+
createContext<DiagnoseCustomization>(DEFAULTS);
|
|
66
69
|
|
|
67
70
|
export function DiagnoseCustomizationProvider({
|
|
68
71
|
value,
|
package/src/index.css
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
+
|
|
2
3
|
/*
|
|
3
4
|
* Tailwind v4 JIT scans only sources under this CSS file's package (web/).
|
|
4
5
|
* k8s-ui is a workspace package whose TSX files also use Tailwind classes;
|
|
@@ -15,6 +16,25 @@
|
|
|
15
16
|
@import "@fontsource-variable/dm-sans";
|
|
16
17
|
@import "@fontsource/dm-mono";
|
|
17
18
|
|
|
19
|
+
@container investigation (min-width: 1000px) {
|
|
20
|
+
.investigation-split-enabled .investigation-evidence-jump { display: none; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@layer components {
|
|
24
|
+
.investigation-assessment {
|
|
25
|
+
background: var(--color-investigation-assessment);
|
|
26
|
+
border-color: var(--color-investigation-assessment-border);
|
|
27
|
+
}
|
|
28
|
+
.investigation-evidence {
|
|
29
|
+
background: var(--color-investigation-evidence);
|
|
30
|
+
border-color: var(--color-investigation-evidence-border);
|
|
31
|
+
}
|
|
32
|
+
.investigation-next-steps {
|
|
33
|
+
background: var(--color-investigation-next-steps);
|
|
34
|
+
border-color: var(--color-investigation-next-steps-border);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/*
|
|
19
39
|
* Tailwind v4 theme tokens. Migrated from the legacy v3 tailwind.config.js
|
|
20
40
|
* to CSS-first @theme so library consumers (e.g. radar-hub-web) don't need
|
|
@@ -22,6 +42,12 @@
|
|
|
22
42
|
* when they consume this package from npm.
|
|
23
43
|
*/
|
|
24
44
|
@theme {
|
|
45
|
+
--color-investigation-assessment: light-dark(#edf3fc, #182237);
|
|
46
|
+
--color-investigation-assessment-border: light-dark(#d3dff0, #2b3b56);
|
|
47
|
+
--color-investigation-evidence: light-dark(#f3f0fa, #211e30);
|
|
48
|
+
--color-investigation-evidence-border: light-dark(#e0d8ee, #39314b);
|
|
49
|
+
--color-investigation-next-steps: light-dark(#edf6f5, #172a2c);
|
|
50
|
+
--color-investigation-next-steps-border: light-dark(#cfe4e0, #2a4546);
|
|
25
51
|
/* Fonts — DM Sans for UI, DM Mono for code/metric panes. */
|
|
26
52
|
--font-sans: "DM Sans Variable", "DM Sans", system-ui, sans-serif;
|
|
27
53
|
--font-mono: "DM Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
@@ -44,8 +70,8 @@
|
|
|
44
70
|
--color-accent: var(--accent);
|
|
45
71
|
|
|
46
72
|
/* Border radius — rounder than Tailwind defaults to match the brand. */
|
|
47
|
-
--radius: 0.375rem;
|
|
48
|
-
--radius-md: 0.5rem;
|
|
73
|
+
--radius: 0.375rem; /* default (was 0.25rem) */
|
|
74
|
+
--radius-md: 0.5rem; /* was 0.375rem */
|
|
49
75
|
--radius-lg: 0.625rem; /* was 0.5rem */
|
|
50
76
|
--radius-xl: 0.875rem; /* was 0.75rem */
|
|
51
77
|
|
|
@@ -54,12 +80,26 @@
|
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
@keyframes fadeInOut {
|
|
57
|
-
0%,
|
|
58
|
-
|
|
83
|
+
0%,
|
|
84
|
+
100% {
|
|
85
|
+
opacity: 0;
|
|
86
|
+
transform: translateY(-8px);
|
|
87
|
+
}
|
|
88
|
+
15%,
|
|
89
|
+
85% {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
transform: translateY(0);
|
|
92
|
+
}
|
|
59
93
|
}
|
|
60
94
|
|
|
61
95
|
/* Global: pointer cursor for all interactive elements */
|
|
62
|
-
button,
|
|
96
|
+
button,
|
|
97
|
+
[role="button"],
|
|
98
|
+
a,
|
|
99
|
+
label,
|
|
100
|
+
select,
|
|
101
|
+
summary,
|
|
102
|
+
[tabindex]:not([tabindex="-1"]) {
|
|
63
103
|
cursor: pointer;
|
|
64
104
|
}
|
|
65
105
|
|
|
@@ -69,7 +109,7 @@ button, [role="button"], a, label, select, summary, [tabindex]:not([tabindex="-1
|
|
|
69
109
|
|
|
70
110
|
:root {
|
|
71
111
|
/* Font settings */
|
|
72
|
-
font-family:
|
|
112
|
+
font-family: "DM Sans Variable", "DM Sans", system-ui, sans-serif;
|
|
73
113
|
line-height: 1.5;
|
|
74
114
|
font-weight: 400;
|
|
75
115
|
}
|
|
@@ -91,7 +131,6 @@ body {
|
|
|
91
131
|
flex-direction: column;
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
|
|
95
134
|
/* ============================================
|
|
96
135
|
REACT FLOW CUSTOMIZATIONS
|
|
97
136
|
============================================ */
|
|
@@ -218,8 +257,14 @@ body {
|
|
|
218
257
|
}
|
|
219
258
|
|
|
220
259
|
@keyframes slide-out-to-right {
|
|
221
|
-
from {
|
|
222
|
-
|
|
260
|
+
from {
|
|
261
|
+
transform: translateX(0);
|
|
262
|
+
opacity: 1;
|
|
263
|
+
}
|
|
264
|
+
to {
|
|
265
|
+
transform: translateX(100%);
|
|
266
|
+
opacity: 0;
|
|
267
|
+
}
|
|
223
268
|
}
|
|
224
269
|
|
|
225
270
|
.animate-out {
|
|
@@ -271,8 +316,12 @@ body {
|
|
|
271
316
|
}
|
|
272
317
|
|
|
273
318
|
@keyframes status-enter {
|
|
274
|
-
from {
|
|
275
|
-
|
|
319
|
+
from {
|
|
320
|
+
opacity: 0;
|
|
321
|
+
}
|
|
322
|
+
to {
|
|
323
|
+
opacity: 1;
|
|
324
|
+
}
|
|
276
325
|
}
|
|
277
326
|
.animate-status-enter {
|
|
278
327
|
animation: status-enter 140ms ease-out both;
|
|
@@ -315,24 +364,8 @@ body {
|
|
|
315
364
|
|
|
316
365
|
/* Synthesis "thinking" dot — a calm breathing pulse (not a spinner) for the staged
|
|
317
366
|
pre-verdict beats. */
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
100% {
|
|
321
|
-
opacity: 0.35;
|
|
322
|
-
transform: scale(0.85);
|
|
323
|
-
}
|
|
324
|
-
50% {
|
|
325
|
-
opacity: 1;
|
|
326
|
-
transform: scale(1.15);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
.animate-synth-pulse {
|
|
330
|
-
animation: synth-pulse 1.1s ease-in-out infinite;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/* Result-reveal choreography. The card eases in; the root-cause label gets an accent
|
|
334
|
-
underline that sweeps out; the whole card pulses its accent border once to mark
|
|
335
|
-
"diagnosis ready." Sequenced via animation-delay relative to one mount. */
|
|
367
|
+
/* Results enter with the same short spatial motion as new transcript/evidence rows.
|
|
368
|
+
Avoid pulsing glows or staged pauses: the evidence itself should carry emphasis. */
|
|
336
369
|
@keyframes result-in {
|
|
337
370
|
from {
|
|
338
371
|
opacity: 0;
|
|
@@ -346,51 +379,6 @@ body {
|
|
|
346
379
|
.animate-result-in {
|
|
347
380
|
animation: result-in 320ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
348
381
|
}
|
|
349
|
-
@keyframes underline-sweep {
|
|
350
|
-
from {
|
|
351
|
-
transform: scaleX(0);
|
|
352
|
-
}
|
|
353
|
-
to {
|
|
354
|
-
transform: scaleX(1);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
.animate-underline-sweep {
|
|
358
|
-
transform-origin: left;
|
|
359
|
-
animation: underline-sweep 460ms cubic-bezier(0.16, 1, 0.3, 1) 120ms both;
|
|
360
|
-
}
|
|
361
|
-
/* Richer verdict reveal — a colored ring around the border plus an UNEVEN, breathing
|
|
362
|
-
outer glow (two soft peaks), held longer (~2.4s). Tinted per verdict via --glow
|
|
363
|
-
(amber root-cause, emerald healthy, accent remediation, slate inconclusive). */
|
|
364
|
-
@keyframes verdict-reveal {
|
|
365
|
-
0% {
|
|
366
|
-
box-shadow:
|
|
367
|
-
0 0 0 0 color-mix(in oklab, var(--glow, var(--accent)) 0%, transparent),
|
|
368
|
-
0 0 0 0 transparent;
|
|
369
|
-
}
|
|
370
|
-
18% {
|
|
371
|
-
box-shadow:
|
|
372
|
-
0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 60%, transparent),
|
|
373
|
-
0 0 20px 2px color-mix(in oklab, var(--glow, var(--accent)) 42%, transparent);
|
|
374
|
-
}
|
|
375
|
-
44% {
|
|
376
|
-
box-shadow:
|
|
377
|
-
0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 32%, transparent),
|
|
378
|
-
0 0 10px 1px color-mix(in oklab, var(--glow, var(--accent)) 20%, transparent);
|
|
379
|
-
}
|
|
380
|
-
68% {
|
|
381
|
-
box-shadow:
|
|
382
|
-
0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 50%, transparent),
|
|
383
|
-
0 0 24px 3px color-mix(in oklab, var(--glow, var(--accent)) 38%, transparent);
|
|
384
|
-
}
|
|
385
|
-
100% {
|
|
386
|
-
box-shadow:
|
|
387
|
-
0 0 0 0 transparent,
|
|
388
|
-
0 0 0 0 transparent;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
.animate-verdict-reveal {
|
|
392
|
-
animation: verdict-reveal 2400ms cubic-bezier(0.32, 0.72, 0, 1) 120ms both;
|
|
393
|
-
}
|
|
394
382
|
@media (prefers-reduced-motion: reduce) {
|
|
395
383
|
.ai-shimmer {
|
|
396
384
|
animation: none;
|
|
@@ -398,10 +386,7 @@ body {
|
|
|
398
386
|
-webkit-text-fill-color: currentColor;
|
|
399
387
|
color: var(--text-secondary);
|
|
400
388
|
}
|
|
401
|
-
.animate-
|
|
402
|
-
.animate-result-in,
|
|
403
|
-
.animate-underline-sweep,
|
|
404
|
-
.animate-verdict-reveal {
|
|
389
|
+
.animate-result-in {
|
|
405
390
|
animation: none;
|
|
406
391
|
}
|
|
407
392
|
}
|
|
@@ -428,7 +413,6 @@ body {
|
|
|
428
413
|
animation: slide-in-from-right 0.2s ease-out;
|
|
429
414
|
}
|
|
430
415
|
|
|
431
|
-
|
|
432
416
|
/* ============================================
|
|
433
417
|
XTERM TERMINAL STYLES
|
|
434
418
|
============================================ */
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type {
|
|
|
31
31
|
DiagnoseConsentCopy,
|
|
32
32
|
} from './context/DiagnoseCustomization';
|
|
33
33
|
|
|
34
|
-
// Standalone AI
|
|
34
|
+
// Standalone AI investigation surface — mount the investigation panel outside a
|
|
35
35
|
// full <RadarApp>. No router dependency, no client-side cluster state: the
|
|
36
36
|
// backend set via setApiBase() picks the cluster, so hosts remount
|
|
37
37
|
// <DiagnoseProvider key={cluster}> to switch. Mount order: ThemeProvider >
|