@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
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { useId, useState, type ReactNode } from "react";
|
|
2
|
+
import { clsx } from "clsx";
|
|
3
|
+
import {
|
|
4
|
+
Activity,
|
|
5
|
+
AlertTriangle,
|
|
6
|
+
CircleAlert,
|
|
7
|
+
Info,
|
|
8
|
+
SearchCheck,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { Collapse, CollapseChevron } from "@skyhook-io/k8s-ui";
|
|
11
|
+
import type { EvidenceCoverageGroup } from "./investigationEvidencePresentation";
|
|
12
|
+
import {
|
|
13
|
+
investigationEvidenceSourceDomId,
|
|
14
|
+
type InvestigationEvidenceGroup,
|
|
15
|
+
type InvestigationRootCauseEvidenceResolution,
|
|
16
|
+
} from "./investigationEvidence";
|
|
17
|
+
import type { InvestigationSourceExcerpt } from "./investigationSourceFocus";
|
|
18
|
+
import type {
|
|
19
|
+
InvestigationCaseItem,
|
|
20
|
+
InvestigationCaseResolution,
|
|
21
|
+
} from "./investigationCase";
|
|
22
|
+
import { useDisclosureReveal } from "./useDisclosureReveal";
|
|
23
|
+
import { SourceButton } from "./investigationEvidence/cardParts";
|
|
24
|
+
import { investigationEvidenceFullRowFlags } from "./investigationEvidencePartition";
|
|
25
|
+
import { EvidenceCard } from "./EvidenceCard";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hypotheses the agent dropped, each pointing at the Radar observation whose
|
|
29
|
+
* result contradicted it. Entries whose item could not be placed on an
|
|
30
|
+
* observation are filtered out by the resolver and never rendered here.
|
|
31
|
+
*/
|
|
32
|
+
export function RuledOutBlock({
|
|
33
|
+
entries,
|
|
34
|
+
onReveal,
|
|
35
|
+
}: {
|
|
36
|
+
entries: InvestigationCaseResolution["ruledOut"];
|
|
37
|
+
onReveal: (item: InvestigationCaseItem) => void;
|
|
38
|
+
}) {
|
|
39
|
+
const headingId = useId();
|
|
40
|
+
return (
|
|
41
|
+
<section
|
|
42
|
+
aria-labelledby={headingId}
|
|
43
|
+
data-testid="investigation-ruled-out"
|
|
44
|
+
className="rounded-lg border border-theme-border/80 bg-theme-base/20 px-3 py-2.5"
|
|
45
|
+
>
|
|
46
|
+
<h3
|
|
47
|
+
id={headingId}
|
|
48
|
+
className="flex items-center gap-2 text-xs font-semibold text-theme-text-secondary"
|
|
49
|
+
>
|
|
50
|
+
Also checked
|
|
51
|
+
<span className="text-[10px] font-semibold uppercase tracking-wide text-accent-text">
|
|
52
|
+
Agent
|
|
53
|
+
</span>
|
|
54
|
+
</h3>
|
|
55
|
+
<ul className="mt-1.5 space-y-1.5">
|
|
56
|
+
{entries.map((entry, position) => {
|
|
57
|
+
const observation = entry.item.observation!;
|
|
58
|
+
return (
|
|
59
|
+
<li
|
|
60
|
+
key={position}
|
|
61
|
+
className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs"
|
|
62
|
+
>
|
|
63
|
+
<span className="min-w-0 text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
64
|
+
{entry.hypothesis}
|
|
65
|
+
</span>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => onReveal(entry.item)}
|
|
69
|
+
className="shrink-0 rounded text-accent-text hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
70
|
+
>
|
|
71
|
+
See {observation.title}
|
|
72
|
+
{entry.item.placement === "revision"
|
|
73
|
+
? " (earlier observation)"
|
|
74
|
+
: ""}
|
|
75
|
+
</button>
|
|
76
|
+
</li>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</ul>
|
|
80
|
+
</section>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function AssessmentEvidenceQualification({
|
|
85
|
+
resolution,
|
|
86
|
+
onViewActivity,
|
|
87
|
+
}: {
|
|
88
|
+
resolution: InvestigationRootCauseEvidenceResolution;
|
|
89
|
+
onViewActivity: () => void;
|
|
90
|
+
}) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2.5">
|
|
93
|
+
<AlertTriangle
|
|
94
|
+
className="mt-0.5 h-4 w-4 shrink-0 text-amber-500"
|
|
95
|
+
aria-hidden
|
|
96
|
+
/>
|
|
97
|
+
<div className="min-w-0 flex-1">
|
|
98
|
+
<p className="text-xs font-medium text-theme-text-primary">
|
|
99
|
+
{resolution.status === "invalid"
|
|
100
|
+
? "Assessment references could not be matched"
|
|
101
|
+
: "Assessment does not cite specific Radar evidence"}
|
|
102
|
+
</p>
|
|
103
|
+
<p className="mt-0.5 text-xs leading-relaxed text-theme-text-tertiary">
|
|
104
|
+
{resolution.status === "invalid"
|
|
105
|
+
? "Radar could not match the assessment’s references to this investigation. Review Activity before acting."
|
|
106
|
+
: "Review Activity and the Radar evidence below before acting on the agent’s conclusion."}
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={onViewActivity}
|
|
112
|
+
className="shrink-0 rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
113
|
+
>
|
|
114
|
+
View Activity
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function EmptyCollection({
|
|
121
|
+
collecting,
|
|
122
|
+
hasEarlierEvidence,
|
|
123
|
+
onViewActivity,
|
|
124
|
+
}: {
|
|
125
|
+
collecting: boolean;
|
|
126
|
+
hasEarlierEvidence: boolean;
|
|
127
|
+
onViewActivity: () => void;
|
|
128
|
+
}) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="rounded-lg border border-dashed border-theme-border px-4 py-5 text-center">
|
|
131
|
+
{collecting ? (
|
|
132
|
+
<Activity className="mx-auto h-5 w-5 text-accent" aria-hidden />
|
|
133
|
+
) : (
|
|
134
|
+
<SearchCheck
|
|
135
|
+
className="mx-auto h-5 w-5 text-theme-text-tertiary"
|
|
136
|
+
aria-hidden
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
<p className="mt-2 text-sm font-medium text-theme-text-secondary">
|
|
140
|
+
{collecting
|
|
141
|
+
? "Evidence will appear here"
|
|
142
|
+
: hasEarlierEvidence
|
|
143
|
+
? "No current evidence was captured during verification"
|
|
144
|
+
: "No relevant evidence to show yet"}
|
|
145
|
+
</p>
|
|
146
|
+
<p className="mx-auto mt-1 max-w-md text-xs leading-relaxed text-theme-text-tertiary">
|
|
147
|
+
{collecting
|
|
148
|
+
? "The Activity pane remains the live record while the agent investigates."
|
|
149
|
+
: hasEarlierEvidence
|
|
150
|
+
? "Earlier observations remain below. This does not prove that those conditions resolved."
|
|
151
|
+
: "Investigation details are still available in Activity. This does not mean the resource is healthy."}
|
|
152
|
+
</p>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={onViewActivity}
|
|
156
|
+
className="mt-2 rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
157
|
+
>
|
|
158
|
+
View Activity
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function CollapsedEvidenceCollection({
|
|
165
|
+
id,
|
|
166
|
+
title,
|
|
167
|
+
description,
|
|
168
|
+
groups,
|
|
169
|
+
animateGroupIds,
|
|
170
|
+
onViewSource,
|
|
171
|
+
open,
|
|
172
|
+
onOpenChange,
|
|
173
|
+
totalCount = groups.length,
|
|
174
|
+
keepWhenEmpty = false,
|
|
175
|
+
children,
|
|
176
|
+
}: {
|
|
177
|
+
id: string;
|
|
178
|
+
title: string;
|
|
179
|
+
description: string;
|
|
180
|
+
groups: InvestigationEvidenceGroup[];
|
|
181
|
+
animateGroupIds: ReadonlySet<string>;
|
|
182
|
+
onViewSource: (
|
|
183
|
+
sourceId: string,
|
|
184
|
+
excerpt?: InvestigationSourceExcerpt,
|
|
185
|
+
) => void;
|
|
186
|
+
open: boolean;
|
|
187
|
+
onOpenChange: (open: boolean) => void;
|
|
188
|
+
totalCount?: number;
|
|
189
|
+
/** Render at zero: the children carry the empty state and the withheld-results note. */
|
|
190
|
+
keepWhenEmpty?: boolean;
|
|
191
|
+
children?: ReactNode;
|
|
192
|
+
}) {
|
|
193
|
+
const { elementRef, revealAfterToggle } = useDisclosureReveal<HTMLElement>();
|
|
194
|
+
if (totalCount === 0 && !keepWhenEmpty) return null;
|
|
195
|
+
const fullRowFlags = investigationEvidenceFullRowFlags(
|
|
196
|
+
groups.map((group) => group.latest.data.type),
|
|
197
|
+
);
|
|
198
|
+
return (
|
|
199
|
+
<section
|
|
200
|
+
ref={elementRef}
|
|
201
|
+
className="overflow-hidden rounded-lg border border-theme-border/80 bg-theme-base/20"
|
|
202
|
+
>
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
aria-expanded={open}
|
|
206
|
+
aria-controls={id}
|
|
207
|
+
onClick={() => {
|
|
208
|
+
onOpenChange(!open);
|
|
209
|
+
revealAfterToggle(!open);
|
|
210
|
+
}}
|
|
211
|
+
className="flex w-full min-w-0 items-center gap-2 px-3 py-2.5 text-left hover:bg-theme-hover/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
212
|
+
>
|
|
213
|
+
<span className="min-w-0 flex-1">
|
|
214
|
+
<span className="block text-xs font-semibold text-theme-text-secondary">
|
|
215
|
+
{title}
|
|
216
|
+
</span>
|
|
217
|
+
{description ? (
|
|
218
|
+
<span className="block text-xs text-theme-text-tertiary">
|
|
219
|
+
{description}
|
|
220
|
+
</span>
|
|
221
|
+
) : null}
|
|
222
|
+
</span>
|
|
223
|
+
<span className="shrink-0 font-mono text-xs text-theme-text-tertiary">
|
|
224
|
+
{totalCount}
|
|
225
|
+
</span>
|
|
226
|
+
<CollapseChevron open={open} className="h-4 w-4" />
|
|
227
|
+
</button>
|
|
228
|
+
<div id={id}>
|
|
229
|
+
<Collapse open={open}>
|
|
230
|
+
<div
|
|
231
|
+
className={clsx(
|
|
232
|
+
"grid items-start gap-2.5 @min-[760px]/evidence:grid-cols-2",
|
|
233
|
+
"border-t border-theme-border/60 p-2.5",
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
{groups.map((group, index) => (
|
|
237
|
+
<EvidenceCard
|
|
238
|
+
key={group.id}
|
|
239
|
+
group={group}
|
|
240
|
+
|
|
241
|
+
animateArrival={animateGroupIds.has(group.id)}
|
|
242
|
+
onViewSource={onViewSource}
|
|
243
|
+
spanFullRow={fullRowFlags[index]}
|
|
244
|
+
prominence="secondary"
|
|
245
|
+
/>
|
|
246
|
+
))}
|
|
247
|
+
{children ? <div className="col-span-full">{children}</div> : null}
|
|
248
|
+
</div>
|
|
249
|
+
</Collapse>
|
|
250
|
+
</div>
|
|
251
|
+
</section>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function CoverageStrip({
|
|
256
|
+
groups,
|
|
257
|
+
visibleGroupIds,
|
|
258
|
+
summary,
|
|
259
|
+
onViewSource,
|
|
260
|
+
open,
|
|
261
|
+
onOpenChange,
|
|
262
|
+
}: {
|
|
263
|
+
groups: EvidenceCoverageGroup[];
|
|
264
|
+
visibleGroupIds: ReadonlySet<string>;
|
|
265
|
+
summary: string;
|
|
266
|
+
onViewSource: (
|
|
267
|
+
sourceId: string,
|
|
268
|
+
excerpt?: InvestigationSourceExcerpt,
|
|
269
|
+
) => void;
|
|
270
|
+
open: boolean;
|
|
271
|
+
onOpenChange: (open: boolean) => void;
|
|
272
|
+
}) {
|
|
273
|
+
const hasError = groups.some((group) => group.hasError);
|
|
274
|
+
const historyOnly = groups.every((group) => group.historyOnly);
|
|
275
|
+
const quiet = historyOnly;
|
|
276
|
+
const { elementRef, revealAfterToggle } =
|
|
277
|
+
useDisclosureReveal<HTMLDivElement>();
|
|
278
|
+
const regionId = "investigation-evidence-coverage";
|
|
279
|
+
const anchoredSources = new Set<string>();
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
ref={elementRef}
|
|
283
|
+
className="overflow-hidden rounded-lg border border-theme-border/80 bg-theme-base/20"
|
|
284
|
+
>
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
aria-expanded={open}
|
|
288
|
+
aria-controls={regionId}
|
|
289
|
+
onClick={() => {
|
|
290
|
+
onOpenChange(!open);
|
|
291
|
+
revealAfterToggle(!open);
|
|
292
|
+
}}
|
|
293
|
+
className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left hover:bg-theme-hover/50"
|
|
294
|
+
>
|
|
295
|
+
{quiet ? (
|
|
296
|
+
<Info
|
|
297
|
+
className="h-4 w-4 shrink-0 text-theme-text-tertiary"
|
|
298
|
+
aria-hidden
|
|
299
|
+
/>
|
|
300
|
+
) : hasError ? (
|
|
301
|
+
<CircleAlert className="h-4 w-4 shrink-0 text-red-400" aria-hidden />
|
|
302
|
+
) : (
|
|
303
|
+
<AlertTriangle
|
|
304
|
+
className="h-4 w-4 shrink-0 text-amber-500"
|
|
305
|
+
aria-hidden
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
<span className="min-w-0 flex-1">
|
|
309
|
+
<span
|
|
310
|
+
className={clsx(
|
|
311
|
+
"block text-xs",
|
|
312
|
+
quiet
|
|
313
|
+
? "font-medium text-theme-text-secondary"
|
|
314
|
+
: "font-semibold text-theme-text-primary",
|
|
315
|
+
)}
|
|
316
|
+
>
|
|
317
|
+
{historyOnly
|
|
318
|
+
? "Change history is limited"
|
|
319
|
+
: "Evidence coverage is incomplete"}
|
|
320
|
+
</span>
|
|
321
|
+
{!historyOnly && !open ? (
|
|
322
|
+
<span className="line-clamp-2 text-xs text-theme-text-tertiary [overflow-wrap:anywhere]">
|
|
323
|
+
{summary}
|
|
324
|
+
</span>
|
|
325
|
+
) : null}
|
|
326
|
+
</span>
|
|
327
|
+
<CollapseChevron open={open} className="h-4 w-4" />
|
|
328
|
+
</button>
|
|
329
|
+
<div id={regionId}>
|
|
330
|
+
<Collapse open={open}>
|
|
331
|
+
<div className="divide-y divide-theme-border/60 border-t border-theme-border/60">
|
|
332
|
+
{groups.map((group) => {
|
|
333
|
+
const sourceIds = group.limitations
|
|
334
|
+
.flatMap((item) => item.sources)
|
|
335
|
+
.filter((source) => {
|
|
336
|
+
if (
|
|
337
|
+
(source.primaryGroupId &&
|
|
338
|
+
visibleGroupIds.has(source.primaryGroupId)) ||
|
|
339
|
+
anchoredSources.has(source.id)
|
|
340
|
+
)
|
|
341
|
+
return false;
|
|
342
|
+
anchoredSources.add(source.id);
|
|
343
|
+
return true;
|
|
344
|
+
})
|
|
345
|
+
.map((source) => source.id);
|
|
346
|
+
return (
|
|
347
|
+
<CoverageGroupRow
|
|
348
|
+
key={group.label}
|
|
349
|
+
group={group}
|
|
350
|
+
sourceIds={sourceIds}
|
|
351
|
+
onViewSource={onViewSource}
|
|
352
|
+
/>
|
|
353
|
+
);
|
|
354
|
+
})}
|
|
355
|
+
</div>
|
|
356
|
+
</Collapse>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function CoverageGroupRow({
|
|
363
|
+
group,
|
|
364
|
+
sourceIds,
|
|
365
|
+
onViewSource,
|
|
366
|
+
}: {
|
|
367
|
+
group: EvidenceCoverageGroup;
|
|
368
|
+
sourceIds: string[];
|
|
369
|
+
onViewSource: (
|
|
370
|
+
sourceId: string,
|
|
371
|
+
excerpt?: InvestigationSourceExcerpt,
|
|
372
|
+
) => void;
|
|
373
|
+
}) {
|
|
374
|
+
const [open, setOpen] = useState(false);
|
|
375
|
+
const regionId = useId();
|
|
376
|
+
const { elementRef, revealAfterToggle } =
|
|
377
|
+
useDisclosureReveal<HTMLDivElement>();
|
|
378
|
+
// One limitation whose message is the whole summary would repeat itself as
|
|
379
|
+
// its own detail row; show its source link on the summary row instead.
|
|
380
|
+
const single =
|
|
381
|
+
group.limitations.length === 1 &&
|
|
382
|
+
group.limitations[0].message === group.summary
|
|
383
|
+
? group.limitations[0]
|
|
384
|
+
: undefined;
|
|
385
|
+
const anchors = sourceIds.map((id) => (
|
|
386
|
+
<span
|
|
387
|
+
key={id}
|
|
388
|
+
id={investigationEvidenceSourceDomId(id)}
|
|
389
|
+
className="sr-only scroll-mt-14"
|
|
390
|
+
aria-hidden
|
|
391
|
+
/>
|
|
392
|
+
));
|
|
393
|
+
if (single) {
|
|
394
|
+
return (
|
|
395
|
+
<div
|
|
396
|
+
ref={elementRef}
|
|
397
|
+
data-evidence-source-container
|
|
398
|
+
tabIndex={-1}
|
|
399
|
+
aria-label={`Evidence limitation for ${group.label}: ${group.summary}`}
|
|
400
|
+
className="flex items-center gap-2 px-3 py-2.5 text-xs outline-none focus:ring-2 focus:ring-accent/40"
|
|
401
|
+
>
|
|
402
|
+
{anchors}
|
|
403
|
+
{group.hasError ? (
|
|
404
|
+
<CircleAlert
|
|
405
|
+
className="h-3.5 w-3.5 shrink-0 text-red-400"
|
|
406
|
+
aria-hidden
|
|
407
|
+
/>
|
|
408
|
+
) : single.kind === "truncated" ? (
|
|
409
|
+
<AlertTriangle
|
|
410
|
+
className="h-3.5 w-3.5 shrink-0 text-amber-500"
|
|
411
|
+
aria-hidden
|
|
412
|
+
/>
|
|
413
|
+
) : (
|
|
414
|
+
<Info
|
|
415
|
+
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
416
|
+
aria-hidden
|
|
417
|
+
/>
|
|
418
|
+
)}
|
|
419
|
+
<p className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
|
|
420
|
+
<span className="font-medium text-theme-text-primary">
|
|
421
|
+
{group.label}:
|
|
422
|
+
</span>{" "}
|
|
423
|
+
{group.summary}
|
|
424
|
+
</p>
|
|
425
|
+
{single.sources.at(-1) ? (
|
|
426
|
+
<SourceButton
|
|
427
|
+
ariaLabel={`View result for ${group.label}`}
|
|
428
|
+
buttonLabel={
|
|
429
|
+
single.sources.length > 1 ? "View latest in Activity" : undefined
|
|
430
|
+
}
|
|
431
|
+
onClick={() => onViewSource(single.sources.at(-1)!.id)}
|
|
432
|
+
/>
|
|
433
|
+
) : null}
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return (
|
|
438
|
+
<div
|
|
439
|
+
ref={elementRef}
|
|
440
|
+
data-evidence-source-container
|
|
441
|
+
tabIndex={-1}
|
|
442
|
+
aria-label={`Evidence limitation for ${group.label}: ${group.summary}`}
|
|
443
|
+
className="outline-none focus:ring-2 focus:ring-accent/40"
|
|
444
|
+
>
|
|
445
|
+
{anchors}
|
|
446
|
+
<button
|
|
447
|
+
type="button"
|
|
448
|
+
aria-expanded={open}
|
|
449
|
+
aria-controls={regionId}
|
|
450
|
+
onClick={() => {
|
|
451
|
+
setOpen(!open);
|
|
452
|
+
revealAfterToggle(!open);
|
|
453
|
+
}}
|
|
454
|
+
className="flex w-full items-center gap-2 px-3 py-2.5 text-left text-xs hover:bg-theme-hover/50"
|
|
455
|
+
>
|
|
456
|
+
{group.hasError ? (
|
|
457
|
+
<CircleAlert
|
|
458
|
+
className="h-3.5 w-3.5 shrink-0 text-red-400"
|
|
459
|
+
aria-hidden
|
|
460
|
+
/>
|
|
461
|
+
) : (
|
|
462
|
+
<Info
|
|
463
|
+
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
464
|
+
aria-hidden
|
|
465
|
+
/>
|
|
466
|
+
)}
|
|
467
|
+
<span className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
|
|
468
|
+
<span className="font-medium text-theme-text-primary">
|
|
469
|
+
{group.label}:{" "}
|
|
470
|
+
</span>
|
|
471
|
+
{group.summary}
|
|
472
|
+
</span>
|
|
473
|
+
<CollapseChevron open={open} className="h-3.5 w-3.5 shrink-0" />
|
|
474
|
+
</button>
|
|
475
|
+
<div id={regionId}>
|
|
476
|
+
<Collapse open={open}>
|
|
477
|
+
<ul className="space-y-2 border-t border-theme-border/60 px-3 py-2.5">
|
|
478
|
+
{group.limitations.map((limitation, index) => (
|
|
479
|
+
<li
|
|
480
|
+
key={index}
|
|
481
|
+
className="flex min-w-0 items-start gap-2 text-xs"
|
|
482
|
+
>
|
|
483
|
+
{limitation.kind === "error" ? (
|
|
484
|
+
<CircleAlert
|
|
485
|
+
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-400"
|
|
486
|
+
aria-hidden
|
|
487
|
+
/>
|
|
488
|
+
) : limitation.kind === "truncated" ? (
|
|
489
|
+
<AlertTriangle
|
|
490
|
+
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500"
|
|
491
|
+
aria-hidden
|
|
492
|
+
/>
|
|
493
|
+
) : (
|
|
494
|
+
<Info
|
|
495
|
+
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
496
|
+
aria-hidden
|
|
497
|
+
/>
|
|
498
|
+
)}
|
|
499
|
+
<p className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
|
|
500
|
+
<span className="font-medium text-theme-text-primary">
|
|
501
|
+
{limitation.source}:
|
|
502
|
+
</span>{" "}
|
|
503
|
+
{limitation.message}
|
|
504
|
+
</p>
|
|
505
|
+
{limitation.sources.at(-1) ? (
|
|
506
|
+
<SourceButton
|
|
507
|
+
ariaLabel={`View result for ${limitation.source}`}
|
|
508
|
+
buttonLabel={
|
|
509
|
+
limitation.sources.length > 1
|
|
510
|
+
? "View latest in Activity"
|
|
511
|
+
: undefined
|
|
512
|
+
}
|
|
513
|
+
onClick={() => onViewSource(limitation.sources.at(-1)!.id)}
|
|
514
|
+
/>
|
|
515
|
+
) : null}
|
|
516
|
+
</li>
|
|
517
|
+
))}
|
|
518
|
+
</ul>
|
|
519
|
+
</Collapse>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
}
|