@skyhook-io/radar-app 1.14.0 → 1.14.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 +6 -4
- package/src/api/client.ts +3 -0
- package/src/api/diagnose.ts +31 -1
- package/src/components/CloudConnectFlow.tsx +173 -13
- package/src/components/CloudFunnelButton.tsx +4 -2
- package/src/components/ConnectionErrorView.tsx +9 -10
- package/src/components/DebugOverlay.tsx +10 -6
- package/src/components/cloudConnectHandoff.test.ts +109 -4
- package/src/components/cloudConnectHandoff.ts +154 -2
- package/src/components/curl/ServiceCurlButton.tsx +19 -26
- package/src/components/diagnose/AgentCase.tsx +18 -5
- package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
- package/src/components/diagnose/AnalysisStory.tsx +564 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
- package/src/components/diagnose/DiagnoseContext.tsx +25 -8
- package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
- package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
- package/src/components/diagnose/InvestigationView.tsx +493 -529
- package/src/components/diagnose/investigationCase.test.tsx +267 -129
- package/src/components/diagnose/investigationCase.ts +122 -77
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +27 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +28 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +11 -2
- package/src/components/diagnose/investigationEvidence/identity.test.ts +25 -7
- package/src/components/diagnose/investigationEvidence/identity.ts +4 -4
- package/src/components/diagnose/investigationEvidence/observations.ts +24 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +36 -3
- package/src/components/diagnose/investigationEvidence/types.ts +2 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
- package/src/components/diagnose/investigationState.test.ts +316 -58
- package/src/components/diagnose/investigationState.ts +257 -44
- package/src/components/diagnose/investigationStory.test.ts +161 -0
- package/src/components/diagnose/investigationStory.ts +207 -0
- package/src/components/diagnose/parts.test.tsx +9 -6
- package/src/components/diagnose/parts.tsx +977 -193
- package/src/components/diagnose/storyParts.test.tsx +426 -0
- package/src/components/diagnose/toolCallLabel.test.ts +51 -0
- package/src/components/diagnose/toolCallLabel.ts +135 -0
- package/src/components/diagnose/useDisclosureReveal.ts +10 -11
- package/src/components/helm/HelmCompareRoute.tsx +18 -4
- package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
- package/src/components/helm/InstallWizard.tsx +8 -3
- package/src/components/home/MCPSetupDialog.tsx +15 -9
- package/src/components/portforward/PortForwardManager.tsx +6 -13
- package/src/components/resource/PrometheusChartsGrid.tsx +3 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +3 -3
- package/src/components/resource/WorkloadMetricsSection.tsx +15 -21
- package/src/components/resources/PodFilePreview.tsx +3 -3
- package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
- package/src/components/settings/SettingsDialog.tsx +4 -2
- package/src/components/ui/CommandPalette.tsx +10 -6
- package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
- package/src/components/ui/Disclosure.tsx +47 -0
- package/src/components/ui/Markdown.tsx +23 -11
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
- package/src/components/ui/UpdateNotification.tsx +18 -2
- package/src/index.css +19 -6
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
useEffect,
|
|
6
6
|
useId,
|
|
7
7
|
useLayoutEffect,
|
|
8
|
+
useMemo,
|
|
8
9
|
useRef,
|
|
9
10
|
useState,
|
|
10
11
|
type ReactNode,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
DiffViewer,
|
|
32
33
|
StatusDot,
|
|
33
34
|
TerminalBlock,
|
|
35
|
+
TerminalBlockLabel,
|
|
34
36
|
defaultConditionTone,
|
|
35
37
|
displayKind,
|
|
36
38
|
formatRelativeAgeTime,
|
|
@@ -49,6 +51,16 @@ import {
|
|
|
49
51
|
type TimeSeries,
|
|
50
52
|
} from "@skyhook-io/k8s-ui/components/charts";
|
|
51
53
|
import { apiVersionToGroup } from "../../utils/navigation";
|
|
54
|
+
import {
|
|
55
|
+
AnalysisStory,
|
|
56
|
+
resolveStoryPlacements,
|
|
57
|
+
type StoryPlacementLoss,
|
|
58
|
+
type StoryPlacementTarget,
|
|
59
|
+
} from "./AnalysisStory";
|
|
60
|
+
import type {
|
|
61
|
+
DiagnosisEvidenceItem,
|
|
62
|
+
DiagnosisEvidenceSubject,
|
|
63
|
+
} from "../../api/diagnose";
|
|
52
64
|
import { parseLogLine } from "../../utils/log-format";
|
|
53
65
|
import {
|
|
54
66
|
metricsChangeCoverage,
|
|
@@ -67,6 +79,7 @@ import {
|
|
|
67
79
|
investigationSourceArgs,
|
|
68
80
|
type InvestigationEvidenceData,
|
|
69
81
|
type InvestigationEvidenceGroup,
|
|
82
|
+
type InvestigationResourceSummary,
|
|
70
83
|
type InvestigationEvidenceObservation,
|
|
71
84
|
type InvestigationEvidenceProjection,
|
|
72
85
|
type InvestigationRootCauseEvidenceResolution,
|
|
@@ -88,6 +101,7 @@ import {
|
|
|
88
101
|
investigationCaseObservationKey,
|
|
89
102
|
type InvestigationCaseItem,
|
|
90
103
|
type InvestigationCaseResolution,
|
|
104
|
+
sameKind,
|
|
91
105
|
} from "./investigationCase";
|
|
92
106
|
import type { DiagnosisEvidenceRole } from "../../api/diagnose";
|
|
93
107
|
|
|
@@ -197,16 +211,11 @@ export function partitionInvestigationEvidence(
|
|
|
197
211
|
resolution?: InvestigationRootCauseEvidenceResolution,
|
|
198
212
|
investigationCase?: InvestigationCaseResolution,
|
|
199
213
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* one: an id alone selects the group but leaves the broader gate below
|
|
204
|
-
* unable to find who cited it, and the card stays withheld.
|
|
214
|
+
* Under a story every captured result about the target belongs in the one
|
|
215
|
+
* inventory the reader opens to see what Radar recorded; nothing about the
|
|
216
|
+
* target is folded into a second tier. Broader results stay cited-only.
|
|
205
217
|
*/
|
|
206
|
-
|
|
207
|
-
groupId: string;
|
|
208
|
-
source: InvestigationEvidenceSource;
|
|
209
|
-
}[],
|
|
218
|
+
inventory = false,
|
|
210
219
|
) {
|
|
211
220
|
// Selection: legacy root-cause links plus every placed agent item, of any
|
|
212
221
|
// role (placement promotes a group into main the way a citation does; the
|
|
@@ -218,7 +227,6 @@ export function partitionInvestigationEvidence(
|
|
|
218
227
|
? resolution.links.map((link) => link.originalGroupId)
|
|
219
228
|
: []),
|
|
220
229
|
...caseByGroup.keys(),
|
|
221
|
-
...(alsoSelected ?? []).map((entry) => entry.groupId),
|
|
222
230
|
]);
|
|
223
231
|
const collections: Record<EvidenceCollection, InvestigationEvidenceGroup[]> =
|
|
224
232
|
{
|
|
@@ -251,9 +259,7 @@ export function partitionInvestigationEvidence(
|
|
|
251
259
|
if (broader && !namedByAgent) {
|
|
252
260
|
const citingSource =
|
|
253
261
|
resolution?.links.find((item) => item.originalGroupId === group.id)
|
|
254
|
-
?.source ??
|
|
255
|
-
caseByGroup.get(group.id)?.[0]?.source ??
|
|
256
|
-
alsoSelected?.find((entry) => entry.groupId === group.id)?.source;
|
|
262
|
+
?.source ?? caseByGroup.get(group.id)?.[0]?.source;
|
|
257
263
|
const focused = evidenceKindIsFocused(group.latest.data.type);
|
|
258
264
|
const sourceGroups = citingSource
|
|
259
265
|
? groups.filter(
|
|
@@ -279,7 +285,8 @@ export function partitionInvestigationEvidence(
|
|
|
279
285
|
!group.historical &&
|
|
280
286
|
(selected.has(group.id) ||
|
|
281
287
|
(!broader &&
|
|
282
|
-
(
|
|
288
|
+
(inventory ||
|
|
289
|
+
group.latest.tier === "key" ||
|
|
283
290
|
(group.latest.tier === "supporting" && adverse(group)))));
|
|
284
291
|
const collection = main
|
|
285
292
|
? "main"
|
|
@@ -358,8 +365,8 @@ export function investigationEvidenceRevealCollection(
|
|
|
358
365
|
export function InvestigationEvidencePane({
|
|
359
366
|
projection,
|
|
360
367
|
rootCauseEvidence,
|
|
361
|
-
alsoSelectedGroupIds,
|
|
362
368
|
investigationCase,
|
|
369
|
+
story,
|
|
363
370
|
collecting,
|
|
364
371
|
animateGroupIds,
|
|
365
372
|
onViewSource,
|
|
@@ -367,19 +374,24 @@ export function InvestigationEvidencePane({
|
|
|
367
374
|
onOpenResource,
|
|
368
375
|
onOpenTimeline,
|
|
369
376
|
afterEvidence,
|
|
377
|
+
recordNotes,
|
|
378
|
+
storyShell = false,
|
|
370
379
|
revealRequest,
|
|
371
380
|
onRevealReady,
|
|
372
381
|
}: {
|
|
373
382
|
projection: InvestigationEvidenceProjection;
|
|
374
383
|
/** Server-validated links for the current root cause; absent without one. */
|
|
375
384
|
rootCauseEvidence?: InvestigationRootCauseEvidenceResolution;
|
|
376
|
-
/** What a later turn cited; adds to the assessment's selection. */
|
|
377
|
-
alsoSelectedGroupIds?: readonly {
|
|
378
|
-
groupId: string;
|
|
379
|
-
source: InvestigationEvidenceSource;
|
|
380
|
-
}[];
|
|
381
385
|
/** The current assessment's agent case, resolved against this projection. */
|
|
382
386
|
investigationCase?: InvestigationCaseResolution;
|
|
387
|
+
/**
|
|
388
|
+
* The assessment's story: the agent's prose with `[[radar:evidence=N]]`
|
|
389
|
+
* placements resolved against `investigationCase` and the verdict's
|
|
390
|
+
* `evidence` array. Absent on backends and runs without the story contract,
|
|
391
|
+
* which keep the previous layout.
|
|
392
|
+
*/
|
|
393
|
+
story?: { report: string; evidence?: DiagnosisEvidenceItem[] };
|
|
394
|
+
/** Open the story fully instead of the bounded preview — when the layout has room for it. */
|
|
383
395
|
collecting: boolean;
|
|
384
396
|
animateGroupIds: ReadonlySet<string>;
|
|
385
397
|
onViewSource: (
|
|
@@ -392,8 +404,12 @@ export function InvestigationEvidencePane({
|
|
|
392
404
|
onOpenResource?: (ref: DiagnosisResourceRef) => void;
|
|
393
405
|
/** Opens Radar's Timeline filtered to the resource a changes card is about. */
|
|
394
406
|
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
395
|
-
/**
|
|
407
|
+
/** Next steps. Under a story they precede the inventory of captured results; otherwise they follow the evidence section. */
|
|
396
408
|
afterEvidence?: ReactNode;
|
|
409
|
+
/** The agent's notes that reached no card, shown inside Captured results. */
|
|
410
|
+
recordNotes?: ReactNode;
|
|
411
|
+
/** No verdict yet, but the story shape is coming: keep its cards in place. */
|
|
412
|
+
storyShell?: boolean;
|
|
397
413
|
/** Explicit Activity → Findings navigation, including repeat clicks. */
|
|
398
414
|
revealRequest?: { sourceId: string; requestId: number };
|
|
399
415
|
onRevealReady?: (sourceId: string) => void;
|
|
@@ -413,13 +429,17 @@ export function InvestigationEvidencePane({
|
|
|
413
429
|
const [coverageOpen, setCoverageOpen] = useState(false);
|
|
414
430
|
const [workloadOpen, setWorkloadOpen] = useState(false);
|
|
415
431
|
const [earlierOpen, setEarlierOpen] = useState(false);
|
|
432
|
+
// Null until the reader touches the fold: with nothing placed above it, a
|
|
433
|
+
// finished story opens Captured results by itself, and it stays theirs to close.
|
|
434
|
+
const [resultsOpen, setResultsOpen] = useState<boolean | null>(null);
|
|
416
435
|
const handledRevealRequestRef = useRef<number | undefined>(undefined);
|
|
417
436
|
const openingForRevealRequestRef = useRef<number | undefined>(undefined);
|
|
437
|
+
const storyMode = story !== undefined || storyShell;
|
|
418
438
|
const partition = partitionInvestigationEvidence(
|
|
419
439
|
projection.groups,
|
|
420
440
|
rootCauseEvidence,
|
|
421
441
|
investigationCase,
|
|
422
|
-
|
|
442
|
+
storyMode,
|
|
423
443
|
);
|
|
424
444
|
const hasCurrentEvidence =
|
|
425
445
|
partition.main.length + partition.workload.length > 0;
|
|
@@ -449,6 +469,10 @@ export function InvestigationEvidencePane({
|
|
|
449
469
|
// nested "Previous observations" collection; open the way to it first.
|
|
450
470
|
const collection = collectionByGroup.get(groupId);
|
|
451
471
|
let settle = 0;
|
|
472
|
+
if (storyMode && !resultsOpen) {
|
|
473
|
+
setResultsOpen(true);
|
|
474
|
+
settle = investigationDisclosureSettleDelay(prefersReducedMotion());
|
|
475
|
+
}
|
|
452
476
|
if (collection === "workload" || collection === "earlier") {
|
|
453
477
|
setWorkloadOpen(true);
|
|
454
478
|
settle = investigationDisclosureSettleDelay(prefersReducedMotion());
|
|
@@ -467,7 +491,7 @@ export function InvestigationEvidencePane({
|
|
|
467
491
|
});
|
|
468
492
|
}, settle);
|
|
469
493
|
},
|
|
470
|
-
[onGroupOpenChange, collectionByGroup],
|
|
494
|
+
[onGroupOpenChange, collectionByGroup, storyMode, resultsOpen],
|
|
471
495
|
);
|
|
472
496
|
const revealCollection = revealRequest
|
|
473
497
|
? investigationEvidenceRevealCollection(
|
|
@@ -501,6 +525,11 @@ export function InvestigationEvidencePane({
|
|
|
501
525
|
setEarlierOpen(true);
|
|
502
526
|
return;
|
|
503
527
|
}
|
|
528
|
+
if (storyMode && revealCollection && !resultsOpen) {
|
|
529
|
+
openingForRevealRequestRef.current = requestId;
|
|
530
|
+
setResultsOpen(true);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
504
533
|
if (revealCollection === "coverage" && !coverageOpen) {
|
|
505
534
|
openingForRevealRequestRef.current = requestId;
|
|
506
535
|
setCoverageOpen(true);
|
|
@@ -535,42 +564,311 @@ export function InvestigationEvidencePane({
|
|
|
535
564
|
earlierOpen,
|
|
536
565
|
coverageOpen,
|
|
537
566
|
workloadOpen,
|
|
567
|
+
storyMode,
|
|
568
|
+
resultsOpen,
|
|
538
569
|
]);
|
|
539
570
|
|
|
540
571
|
const coverageGroups = groupEvidenceCoverage(projection.limitations);
|
|
572
|
+
// A caveat belongs on the claim it bounds: the "does not cover" line shows
|
|
573
|
+
// under the cause card. A verdict with no cause card (healthy) keeps it on
|
|
574
|
+
// every placed card, since that is where its scope lives.
|
|
575
|
+
const storyGapRoles = useMemo(
|
|
576
|
+
() =>
|
|
577
|
+
investigationCase?.items.some(
|
|
578
|
+
(item) => item.role === "cause" && item.placement === "card",
|
|
579
|
+
)
|
|
580
|
+
? new Set(["cause"])
|
|
581
|
+
: undefined,
|
|
582
|
+
[investigationCase],
|
|
583
|
+
);
|
|
541
584
|
const limitationSummary = coverageGroups
|
|
542
585
|
.map((group) => `${group.label}: ${group.summary}`)
|
|
543
586
|
.join(" · ");
|
|
587
|
+
// Resolves a story placement to the agent item it names, or says why it
|
|
588
|
+
// cannot: an index Radar does not know, an item the server could not bind,
|
|
589
|
+
// or a bound item that matches no single observation. Only the last two
|
|
590
|
+
// distinguish "the agent was wrong" from "Radar could not tell".
|
|
591
|
+
const resolveStoryItem = useCallback(
|
|
592
|
+
(index: number): StoryPlacementTarget | StoryPlacementLoss => {
|
|
593
|
+
const evidence = story?.evidence ?? [];
|
|
594
|
+
if (!Number.isInteger(index) || index < 0 || index >= evidence.length)
|
|
595
|
+
return "invalid";
|
|
596
|
+
if (evidence[index]?.status === "unlinked") return "unlinked";
|
|
597
|
+
const item = investigationCase?.items.find(
|
|
598
|
+
(candidate) => candidate.index === index,
|
|
599
|
+
);
|
|
600
|
+
if (!item) return "unplaced";
|
|
601
|
+
if (item.groupId && item.observation)
|
|
602
|
+
return { item, domId: `story-${item.groupId}` };
|
|
603
|
+
// A subject-less citation of a call that fans out into several cards
|
|
604
|
+
// (the diagnose bundle above all) names the call, not one of its
|
|
605
|
+
// results. Placing the call's primary card is not guessing which claim
|
|
606
|
+
// the agent meant: it is rendering the headline result of what it
|
|
607
|
+
// cited, the same card Activity's "Show evidence" opens for that call.
|
|
608
|
+
if (item.subject === undefined && item.source.primaryGroupId) {
|
|
609
|
+
const group = projection.groups.find(
|
|
610
|
+
(candidate) => candidate.id === item.source.primaryGroupId,
|
|
611
|
+
);
|
|
612
|
+
// The card is the one the cited call itself produced. A later read of
|
|
613
|
+
// the same thing lives on the same group; substituting it would show
|
|
614
|
+
// newer evidence under an older claim.
|
|
615
|
+
const observation = group?.observations.find(
|
|
616
|
+
(candidate) => candidate.source.id === item.source.id,
|
|
617
|
+
);
|
|
618
|
+
if (group && observation)
|
|
619
|
+
return {
|
|
620
|
+
item: {
|
|
621
|
+
...item,
|
|
622
|
+
groupId: group.id,
|
|
623
|
+
observation,
|
|
624
|
+
placement: observation === group.latest ? "card" : "revision",
|
|
625
|
+
},
|
|
626
|
+
domId: `story-${group.id}`,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
// A bound call that produced no card at all (a search, a metrics
|
|
630
|
+
// table) is not ambiguous, it is unrendered; its raw result is in
|
|
631
|
+
// Activity and the note at the claim says so.
|
|
632
|
+
const hasCard = projection.groups.some((group) =>
|
|
633
|
+
group.observations.some(
|
|
634
|
+
(observation) => observation.source.id === item.source.id,
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
return hasCard
|
|
638
|
+
? "unplaced"
|
|
639
|
+
: { kind: "nocard", sourceId: item.source.id };
|
|
640
|
+
},
|
|
641
|
+
[story?.evidence, investigationCase, projection.groups],
|
|
642
|
+
);
|
|
643
|
+
const groupsById = new Map(
|
|
644
|
+
projection.groups.map((group) => [group.id, group] as const),
|
|
645
|
+
);
|
|
646
|
+
const renderStoryPlacement = useCallback(
|
|
647
|
+
(target: StoryPlacementTarget, { compact }: { compact: boolean }) => {
|
|
648
|
+
const group = target.item.groupId
|
|
649
|
+
? groupsById.get(target.item.groupId)
|
|
650
|
+
: undefined;
|
|
651
|
+
if (!group || !target.item.observation) return null;
|
|
652
|
+
return (
|
|
653
|
+
<EvidenceCard
|
|
654
|
+
group={group}
|
|
655
|
+
observation={target.item.observation}
|
|
656
|
+
domId={target.domId}
|
|
657
|
+
animateArrival={false}
|
|
658
|
+
onViewSource={onViewSource}
|
|
659
|
+
spanFullRow
|
|
660
|
+
compact={compact}
|
|
661
|
+
noteMode="chip"
|
|
662
|
+
gapRoles={storyGapRoles}
|
|
663
|
+
/>
|
|
664
|
+
);
|
|
665
|
+
},
|
|
666
|
+
// groupsById is rebuilt per render from projection.groups.
|
|
667
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
668
|
+
[projection.groups, onViewSource, storyGapRoles],
|
|
669
|
+
);
|
|
670
|
+
// A card behind the story's fold stays mounted but inert once the fold has
|
|
671
|
+
// been opened and closed; a reveal has to open the fold first, then land.
|
|
672
|
+
const [storyOpenRequest, setStoryOpenRequest] = useState(0);
|
|
673
|
+
// What the story actually placed, not what the case could have: a cited
|
|
674
|
+
// card the agent never placed is inventory, and the inventory says so.
|
|
675
|
+
// A marker written in the ledger's ref form names the item carrying that ref.
|
|
676
|
+
const storyEvidence = story?.evidence;
|
|
677
|
+
const resolveStoryRef = useCallback(
|
|
678
|
+
(ref: string): number | undefined => {
|
|
679
|
+
const matches = (storyEvidence ?? [])
|
|
680
|
+
.map((item, index) => (item.ref === ref ? index : -1))
|
|
681
|
+
.filter((index) => index >= 0);
|
|
682
|
+
// Two items on one ref name no single card; the renderer flags it.
|
|
683
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
684
|
+
},
|
|
685
|
+
[storyEvidence],
|
|
686
|
+
);
|
|
687
|
+
const storyReport = story?.report;
|
|
688
|
+
const storyPlacements = useMemo(
|
|
689
|
+
() =>
|
|
690
|
+
storyReport === undefined
|
|
691
|
+
? undefined
|
|
692
|
+
: resolveStoryPlacements(
|
|
693
|
+
storyReport,
|
|
694
|
+
resolveStoryItem,
|
|
695
|
+
resolveStoryRef,
|
|
696
|
+
),
|
|
697
|
+
[storyReport, resolveStoryItem, resolveStoryRef],
|
|
698
|
+
);
|
|
699
|
+
const placedCount = storyPlacements?.placedCount ?? 0;
|
|
700
|
+
// A card behind the fold may be mounted and inert, or not mounted at all
|
|
701
|
+
// before the fold first opens; either way the fold opens first and the
|
|
702
|
+
// card is found once it has settled.
|
|
703
|
+
const revealStoryTarget = useCallback(
|
|
704
|
+
(target: StoryPlacementTarget) => {
|
|
705
|
+
const land = (placed: HTMLElement) => {
|
|
706
|
+
placed.scrollIntoView({
|
|
707
|
+
block: "center",
|
|
708
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth",
|
|
709
|
+
});
|
|
710
|
+
placed.focus({ preventScroll: true });
|
|
711
|
+
};
|
|
712
|
+
const placed = document.getElementById(target.domId);
|
|
713
|
+
// A twin's chip shares its sibling's card, so the card's position, not
|
|
714
|
+
// the item's own kind, says whether the story holds it.
|
|
715
|
+
const placedInStory =
|
|
716
|
+
storyPlacements?.placedAt.has(target.item.index) ?? false;
|
|
717
|
+
if (placed && !placed.closest("[inert]")) {
|
|
718
|
+
land(placed);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (placed || placedInStory) {
|
|
722
|
+
setStoryOpenRequest((n) => n + 1);
|
|
723
|
+
window.setTimeout(() => {
|
|
724
|
+
const settled = document.getElementById(target.domId);
|
|
725
|
+
if (settled) land(settled);
|
|
726
|
+
else revealCaseItem(target.item);
|
|
727
|
+
}, investigationDisclosureSettleDelay(prefersReducedMotion()));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
revealCaseItem(target.item);
|
|
731
|
+
},
|
|
732
|
+
[revealCaseItem, storyPlacements],
|
|
733
|
+
);
|
|
734
|
+
const resultsShown =
|
|
735
|
+
resultsOpen ?? (placedCount === 0 && !collecting && !!story);
|
|
736
|
+
const placedGroupIds = new Set(
|
|
737
|
+
[...(storyPlacements?.byIndex.values() ?? [])].flatMap((entry) =>
|
|
738
|
+
entry.kind === "placed" && entry.target.item.groupId
|
|
739
|
+
? [entry.target.item.groupId]
|
|
740
|
+
: [],
|
|
741
|
+
),
|
|
742
|
+
);
|
|
743
|
+
const capturedTotal =
|
|
744
|
+
partition.main.length +
|
|
745
|
+
partition.workload.length +
|
|
746
|
+
partition.earlier.length;
|
|
747
|
+
const resultsSummary = [
|
|
748
|
+
partition.hiddenBroader > 0
|
|
749
|
+
? `${partition.hiddenBroader} about other resources not shown`
|
|
750
|
+
: undefined,
|
|
751
|
+
partition.hiddenMetrics > 0
|
|
752
|
+
? `${partition.hiddenMetrics} broader metric ${partition.hiddenMetrics === 1 ? "result" : "results"} not shown`
|
|
753
|
+
: undefined,
|
|
754
|
+
projection.limitations.length > 0
|
|
755
|
+
? `${projection.limitations.length} ${projection.limitations.length === 1 ? "check" : "checks"} limited`
|
|
756
|
+
: undefined,
|
|
757
|
+
]
|
|
758
|
+
.filter((part): part is string => Boolean(part))
|
|
759
|
+
.join(" · ");
|
|
760
|
+
const mainCards = (
|
|
761
|
+
<>
|
|
762
|
+
<div className="grid items-start gap-2.5">
|
|
763
|
+
{partition.main.map((group) => (
|
|
764
|
+
<EvidenceCard
|
|
765
|
+
key={group.id}
|
|
766
|
+
group={group}
|
|
767
|
+
spanFullRow
|
|
768
|
+
animateArrival={animateGroupIds.has(group.id)}
|
|
769
|
+
onViewSource={onViewSource}
|
|
770
|
+
placedInStory={placedGroupIds.has(group.id)}
|
|
771
|
+
/>
|
|
772
|
+
))}
|
|
773
|
+
</div>
|
|
774
|
+
|
|
775
|
+
{partition.hiddenBroader > 0 ? (
|
|
776
|
+
<p
|
|
777
|
+
className="text-xs text-theme-text-tertiary"
|
|
778
|
+
data-testid="investigation-hidden-evidence"
|
|
779
|
+
>
|
|
780
|
+
{partition.hiddenBroader === 1
|
|
781
|
+
? "1 result about another resource is not shown; it appears here when the assessment cites it."
|
|
782
|
+
: `${partition.hiddenBroader} results about other resources are not shown; they appear here when the assessment cites them.`}
|
|
783
|
+
</p>
|
|
784
|
+
) : null}
|
|
785
|
+
|
|
786
|
+
{partition.hiddenMetrics > 0 ? (
|
|
787
|
+
<p
|
|
788
|
+
className="text-xs text-theme-text-tertiary"
|
|
789
|
+
data-testid="investigation-hidden-metrics"
|
|
790
|
+
>
|
|
791
|
+
{partition.hiddenMetrics === 1
|
|
792
|
+
? "1 broader metric result is not shown; it appears here when the assessment cites it."
|
|
793
|
+
: `${partition.hiddenMetrics} broader metric results are not shown; they appear here when the assessment cites them.`}
|
|
794
|
+
</p>
|
|
795
|
+
) : null}
|
|
796
|
+
|
|
797
|
+
{!hasCurrentEvidence ? (
|
|
798
|
+
<EmptyCollection
|
|
799
|
+
collecting={collecting}
|
|
800
|
+
hasEarlierEvidence={partition.earlier.length > 0}
|
|
801
|
+
onViewActivity={onViewActivity}
|
|
802
|
+
/>
|
|
803
|
+
) : null}
|
|
804
|
+
|
|
805
|
+
<CollapsedEvidenceCollection
|
|
806
|
+
id="investigation-workload-evidence"
|
|
807
|
+
title="More evidence about this resource"
|
|
808
|
+
description=""
|
|
809
|
+
groups={partition.workload}
|
|
810
|
+
totalCount={partition.workload.length + partition.earlier.length}
|
|
811
|
+
animateGroupIds={animateGroupIds}
|
|
812
|
+
onViewSource={onViewSource}
|
|
813
|
+
open={workloadOpen}
|
|
814
|
+
onOpenChange={setWorkloadOpen}
|
|
815
|
+
>
|
|
816
|
+
<CollapsedEvidenceCollection
|
|
817
|
+
id="investigation-earlier-evidence"
|
|
818
|
+
title="Previous observations"
|
|
819
|
+
description="Earlier does not mean resolved."
|
|
820
|
+
groups={partition.earlier}
|
|
821
|
+
animateGroupIds={animateGroupIds}
|
|
822
|
+
onViewSource={onViewSource}
|
|
823
|
+
open={earlierOpen}
|
|
824
|
+
onOpenChange={setEarlierOpen}
|
|
825
|
+
/>
|
|
826
|
+
</CollapsedEvidenceCollection>
|
|
827
|
+
</>
|
|
828
|
+
);
|
|
544
829
|
const content = (
|
|
545
830
|
<section
|
|
546
831
|
aria-labelledby="investigation-radar-evidence"
|
|
547
|
-
className=
|
|
832
|
+
className={
|
|
833
|
+
// In the story shape this is no card: the analysis, the steps and the
|
|
834
|
+
// record are siblings. The element stays only as the container the
|
|
835
|
+
// cards' queries measure.
|
|
836
|
+
storyMode
|
|
837
|
+
? "@container/evidence space-y-4"
|
|
838
|
+
: "investigation-evidence @container/evidence space-y-3 rounded-xl border p-3"
|
|
839
|
+
}
|
|
548
840
|
>
|
|
549
841
|
<span className="sr-only" role="status" aria-live="polite">
|
|
550
842
|
{projection.limitations.length > 0
|
|
551
843
|
? `Evidence coverage update: ${limitationSummary}`
|
|
552
844
|
: ""}
|
|
553
845
|
</span>
|
|
554
|
-
|
|
555
|
-
<div className="min-w-0">
|
|
556
|
-
<div className="
|
|
557
|
-
<
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
<span className="
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
846
|
+
{!storyMode ? (
|
|
847
|
+
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
848
|
+
<div className="min-w-0">
|
|
849
|
+
<div className="flex items-center gap-2">
|
|
850
|
+
<h2
|
|
851
|
+
id="investigation-radar-evidence"
|
|
852
|
+
className="text-lg font-semibold text-theme-text-primary"
|
|
853
|
+
>
|
|
854
|
+
Evidence
|
|
855
|
+
</h2>
|
|
856
|
+
{collecting ? (
|
|
857
|
+
<span className="inline-flex items-center gap-1.5 text-xs text-accent-text">
|
|
858
|
+
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-accent" />
|
|
859
|
+
collecting
|
|
860
|
+
</span>
|
|
861
|
+
) : null}
|
|
862
|
+
</div>
|
|
569
863
|
</div>
|
|
570
864
|
</div>
|
|
571
|
-
|
|
865
|
+
) : (
|
|
866
|
+
<h2 id="investigation-radar-evidence" className="sr-only">
|
|
867
|
+
Analysis and evidence
|
|
868
|
+
</h2>
|
|
869
|
+
)}
|
|
572
870
|
|
|
573
|
-
{projection.limitations.length > 0 ? (
|
|
871
|
+
{projection.limitations.length > 0 && !storyMode ? (
|
|
574
872
|
<CoverageStrip
|
|
575
873
|
groups={coverageGroups}
|
|
576
874
|
visibleGroupIds={new Set(partition.collectionByGroup.keys())}
|
|
@@ -589,77 +887,103 @@ export function InvestigationEvidencePane({
|
|
|
589
887
|
/>
|
|
590
888
|
) : null}
|
|
591
889
|
|
|
592
|
-
{
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
) : null}
|
|
598
|
-
|
|
599
|
-
<div className="grid items-start gap-2.5">
|
|
600
|
-
{partition.main.map((group) => (
|
|
601
|
-
<EvidenceCard
|
|
602
|
-
key={group.id}
|
|
603
|
-
group={group}
|
|
604
|
-
spanFullRow
|
|
605
|
-
animateArrival={animateGroupIds.has(group.id)}
|
|
606
|
-
onViewSource={onViewSource}
|
|
607
|
-
/>
|
|
608
|
-
))}
|
|
609
|
-
</div>
|
|
610
|
-
|
|
611
|
-
{partition.hiddenBroader > 0 ? (
|
|
612
|
-
<p
|
|
613
|
-
className="text-xs text-theme-text-tertiary"
|
|
614
|
-
data-testid="investigation-hidden-evidence"
|
|
890
|
+
{storyMode ? (
|
|
891
|
+
<div
|
|
892
|
+
data-story-card
|
|
893
|
+
data-story-shell={story ? undefined : ""}
|
|
894
|
+
className="investigation-evidence rounded-xl border p-4"
|
|
615
895
|
>
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
896
|
+
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-theme-text-secondary">
|
|
897
|
+
Why
|
|
898
|
+
<span className="font-normal text-theme-text-tertiary">
|
|
899
|
+
· Agent analysis
|
|
900
|
+
</span>
|
|
901
|
+
{collecting ? (
|
|
902
|
+
<span className="ml-auto inline-flex items-center gap-1.5 text-xs font-normal text-accent-text">
|
|
903
|
+
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-accent" />
|
|
904
|
+
collecting
|
|
905
|
+
</span>
|
|
906
|
+
) : null}
|
|
907
|
+
</div>
|
|
908
|
+
{story ? (
|
|
909
|
+
<AnalysisStory
|
|
910
|
+
report={story.report}
|
|
911
|
+
resolveItem={resolveStoryItem}
|
|
912
|
+
resolveRef={resolveStoryRef}
|
|
913
|
+
trailing={
|
|
914
|
+
visibleRuledOut.length > 0 ? (
|
|
915
|
+
<RuledOutBlock
|
|
916
|
+
entries={visibleRuledOut}
|
|
917
|
+
onReveal={revealCaseItem}
|
|
918
|
+
/>
|
|
919
|
+
) : undefined
|
|
920
|
+
}
|
|
921
|
+
renderPlacement={renderStoryPlacement}
|
|
922
|
+
onReveal={revealStoryTarget}
|
|
923
|
+
openRequest={storyOpenRequest}
|
|
924
|
+
onViewSource={(sourceId) => onViewSource(sourceId)}
|
|
925
|
+
/>
|
|
926
|
+
) : (
|
|
927
|
+
<p className="text-sm text-theme-text-tertiary">
|
|
928
|
+
The analysis appears when the investigation finishes.
|
|
929
|
+
</p>
|
|
930
|
+
)}
|
|
931
|
+
</div>
|
|
620
932
|
) : null}
|
|
621
933
|
|
|
622
|
-
{
|
|
623
|
-
<p
|
|
624
|
-
className="text-xs text-theme-text-tertiary"
|
|
625
|
-
data-testid="investigation-hidden-metrics"
|
|
626
|
-
>
|
|
627
|
-
{partition.hiddenMetrics === 1
|
|
628
|
-
? "1 broader metric result is not shown; it appears here when the assessment cites it."
|
|
629
|
-
: `${partition.hiddenMetrics} broader metric results are not shown; they appear here when the assessment cites them.`}
|
|
630
|
-
</p>
|
|
631
|
-
) : null}
|
|
934
|
+
{storyMode ? afterEvidence : null}
|
|
632
935
|
|
|
633
|
-
{!
|
|
634
|
-
<
|
|
635
|
-
collecting={collecting}
|
|
636
|
-
hasEarlierEvidence={partition.earlier.length > 0}
|
|
637
|
-
onViewActivity={onViewActivity}
|
|
638
|
-
/>
|
|
936
|
+
{!storyMode && visibleRuledOut.length > 0 ? (
|
|
937
|
+
<RuledOutBlock entries={visibleRuledOut} onReveal={revealCaseItem} />
|
|
639
938
|
) : null}
|
|
640
939
|
|
|
641
|
-
|
|
642
|
-
id="investigation-workload-evidence"
|
|
643
|
-
title="More evidence about this workload"
|
|
644
|
-
description=""
|
|
645
|
-
groups={partition.workload}
|
|
646
|
-
totalCount={partition.workload.length + partition.earlier.length}
|
|
647
|
-
animateGroupIds={animateGroupIds}
|
|
648
|
-
onViewSource={onViewSource}
|
|
649
|
-
open={workloadOpen}
|
|
650
|
-
onOpenChange={setWorkloadOpen}
|
|
651
|
-
>
|
|
940
|
+
{storyMode ? (
|
|
652
941
|
<CollapsedEvidenceCollection
|
|
653
|
-
id="investigation-
|
|
654
|
-
title="
|
|
655
|
-
description=
|
|
656
|
-
|
|
942
|
+
id="investigation-captured-results"
|
|
943
|
+
title="Captured results"
|
|
944
|
+
description={[
|
|
945
|
+
placedCount > 0
|
|
946
|
+
? `${placedCount} shown above`
|
|
947
|
+
: collecting
|
|
948
|
+
? `${capturedTotal} collected so far`
|
|
949
|
+
: "None shown above",
|
|
950
|
+
resultsSummary,
|
|
951
|
+
]
|
|
952
|
+
.filter(Boolean)
|
|
953
|
+
.join(" · ")}
|
|
954
|
+
groups={[]}
|
|
955
|
+
totalCount={capturedTotal}
|
|
956
|
+
keepWhenEmpty
|
|
657
957
|
animateGroupIds={animateGroupIds}
|
|
658
958
|
onViewSource={onViewSource}
|
|
659
|
-
open={
|
|
660
|
-
onOpenChange={
|
|
661
|
-
|
|
662
|
-
|
|
959
|
+
open={resultsShown}
|
|
960
|
+
onOpenChange={setResultsOpen}
|
|
961
|
+
>
|
|
962
|
+
<div className="space-y-4">
|
|
963
|
+
{projection.limitations.length > 0 ? (
|
|
964
|
+
<CoverageStrip
|
|
965
|
+
groups={coverageGroups}
|
|
966
|
+
visibleGroupIds={new Set(partition.collectionByGroup.keys())}
|
|
967
|
+
summary={limitationSummary}
|
|
968
|
+
onViewSource={onViewSource}
|
|
969
|
+
open={coverageOpen}
|
|
970
|
+
onOpenChange={setCoverageOpen}
|
|
971
|
+
/>
|
|
972
|
+
) : null}
|
|
973
|
+
{mainCards}
|
|
974
|
+
{recordNotes ? (
|
|
975
|
+
<div data-record-notes>
|
|
976
|
+
<div className="mb-1 text-xs font-semibold text-theme-text-secondary">
|
|
977
|
+
Notes without a card
|
|
978
|
+
</div>
|
|
979
|
+
{recordNotes}
|
|
980
|
+
</div>
|
|
981
|
+
) : null}
|
|
982
|
+
</div>
|
|
983
|
+
</CollapsedEvidenceCollection>
|
|
984
|
+
) : (
|
|
985
|
+
mainCards
|
|
986
|
+
)}
|
|
663
987
|
</div>
|
|
664
988
|
</section>
|
|
665
989
|
);
|
|
@@ -704,7 +1028,7 @@ export function InvestigationEvidencePane({
|
|
|
704
1028
|
}}
|
|
705
1029
|
>
|
|
706
1030
|
{content}
|
|
707
|
-
{afterEvidence}
|
|
1031
|
+
{storyMode ? null : afterEvidence}
|
|
708
1032
|
</EvidenceNavigationContext.Provider>
|
|
709
1033
|
);
|
|
710
1034
|
}
|
|
@@ -732,7 +1056,7 @@ function RuledOutBlock({
|
|
|
732
1056
|
id={headingId}
|
|
733
1057
|
className="flex items-center gap-2 text-xs font-semibold text-theme-text-secondary"
|
|
734
1058
|
>
|
|
735
|
-
|
|
1059
|
+
Also checked
|
|
736
1060
|
<span className="text-[10px] font-semibold uppercase tracking-wide text-accent-text">
|
|
737
1061
|
Agent
|
|
738
1062
|
</span>
|
|
@@ -856,6 +1180,7 @@ function CollapsedEvidenceCollection({
|
|
|
856
1180
|
open,
|
|
857
1181
|
onOpenChange,
|
|
858
1182
|
totalCount = groups.length,
|
|
1183
|
+
keepWhenEmpty = false,
|
|
859
1184
|
children,
|
|
860
1185
|
}: {
|
|
861
1186
|
id: string;
|
|
@@ -870,10 +1195,12 @@ function CollapsedEvidenceCollection({
|
|
|
870
1195
|
open: boolean;
|
|
871
1196
|
onOpenChange: (open: boolean) => void;
|
|
872
1197
|
totalCount?: number;
|
|
1198
|
+
/** Render at zero: the children carry the empty state and the withheld-results note. */
|
|
1199
|
+
keepWhenEmpty?: boolean;
|
|
873
1200
|
children?: ReactNode;
|
|
874
1201
|
}) {
|
|
875
1202
|
const { elementRef, revealAfterToggle } = useDisclosureReveal<HTMLElement>();
|
|
876
|
-
if (totalCount === 0) return null;
|
|
1203
|
+
if (totalCount === 0 && !keepWhenEmpty) return null;
|
|
877
1204
|
const fullRowFlags = investigationEvidenceFullRowFlags(
|
|
878
1205
|
groups.map((group) => group.latest.data.type),
|
|
879
1206
|
);
|
|
@@ -1106,7 +1433,7 @@ function CoverageGroupRow({
|
|
|
1106
1433
|
</p>
|
|
1107
1434
|
{single.sources.at(-1) ? (
|
|
1108
1435
|
<SourceButton
|
|
1109
|
-
ariaLabel={`View
|
|
1436
|
+
ariaLabel={`View result for ${group.label}`}
|
|
1110
1437
|
buttonLabel={
|
|
1111
1438
|
single.sources.length > 1 ? "View latest in Activity" : undefined
|
|
1112
1439
|
}
|
|
@@ -1186,7 +1513,7 @@ function CoverageGroupRow({
|
|
|
1186
1513
|
</p>
|
|
1187
1514
|
{limitation.sources.at(-1) ? (
|
|
1188
1515
|
<SourceButton
|
|
1189
|
-
ariaLabel={`View
|
|
1516
|
+
ariaLabel={`View result for ${limitation.source}`}
|
|
1190
1517
|
buttonLabel={
|
|
1191
1518
|
limitation.sources.length > 1
|
|
1192
1519
|
? "View latest in Activity"
|
|
@@ -1259,13 +1586,24 @@ export function investigationEvidenceShouldRevealHistory(
|
|
|
1259
1586
|
|
|
1260
1587
|
function EvidenceCard({
|
|
1261
1588
|
group,
|
|
1589
|
+
observation: observationOverride,
|
|
1262
1590
|
domId = group.id,
|
|
1263
1591
|
animateArrival,
|
|
1264
1592
|
onViewSource,
|
|
1265
1593
|
spanFullRow = false,
|
|
1266
1594
|
prominence = "primary",
|
|
1595
|
+
compact = false,
|
|
1596
|
+
placedInStory = false,
|
|
1597
|
+
gapRoles,
|
|
1598
|
+
noteMode = "full",
|
|
1267
1599
|
}: {
|
|
1268
1600
|
group: InvestigationEvidenceGroup;
|
|
1601
|
+
/**
|
|
1602
|
+
* Render this exact observation instead of the group's current one — a
|
|
1603
|
+
* story places the read the agent cited, which may since have been
|
|
1604
|
+
* superseded, and says so rather than swapping in today's.
|
|
1605
|
+
*/
|
|
1606
|
+
observation?: InvestigationEvidenceObservation;
|
|
1269
1607
|
/** Stable layout/scroll identity when an existing card changes section. */
|
|
1270
1608
|
domId?: string;
|
|
1271
1609
|
animateArrival: boolean;
|
|
@@ -1276,6 +1614,21 @@ function EvidenceCard({
|
|
|
1276
1614
|
/** Fill both columns when this card has no compact row partner. */
|
|
1277
1615
|
spanFullRow?: boolean;
|
|
1278
1616
|
prominence?: "primary" | "supporting" | "secondary";
|
|
1617
|
+
/**
|
|
1618
|
+
* Header and summary only, so the collapsed story preview never slices a
|
|
1619
|
+
* card. A story log card keeps its cited lines: they are the card.
|
|
1620
|
+
*/
|
|
1621
|
+
compact?: boolean;
|
|
1622
|
+
/** This card also appears inside the story above. */
|
|
1623
|
+
placedInStory?: boolean;
|
|
1624
|
+
/** Roles whose "does not cover" line is shown; undefined shows every gap. */
|
|
1625
|
+
gapRoles?: ReadonlySet<string>;
|
|
1626
|
+
/**
|
|
1627
|
+
* Inside the story the prose above the card is the agent's reading of it,
|
|
1628
|
+
* so the note row keeps only the role chip (and an excluded hypothesis);
|
|
1629
|
+
* repeating the sentence under the card doubled the text for nothing.
|
|
1630
|
+
*/
|
|
1631
|
+
noteMode?: "full" | "chip";
|
|
1279
1632
|
}) {
|
|
1280
1633
|
const {
|
|
1281
1634
|
onOpenResource,
|
|
@@ -1295,18 +1648,52 @@ function EvidenceCard({
|
|
|
1295
1648
|
[onGroupOpenChange, group.id],
|
|
1296
1649
|
);
|
|
1297
1650
|
const { elementRef, revealAfterToggle } = useDisclosureReveal<HTMLElement>();
|
|
1298
|
-
const observation = group.latest;
|
|
1651
|
+
const observation = observationOverride ?? group.latest;
|
|
1652
|
+
const supersededRead =
|
|
1653
|
+
observationOverride !== undefined && observationOverride !== group.latest;
|
|
1299
1654
|
const displaySummary =
|
|
1300
1655
|
observation.data.type === "crash" && observation.summary
|
|
1301
1656
|
? parseLogLine(observation.summary).content
|
|
1302
1657
|
: observation.summary;
|
|
1303
1658
|
const citedOrder = citedOrderByGroup?.get(group.id);
|
|
1304
1659
|
const caseItems = caseByGroup?.get(group.id) ?? [];
|
|
1305
|
-
|
|
1660
|
+
// Annotations belong to the observation on screen: a card rendering an
|
|
1661
|
+
// earlier read shows the note the agent attached to that read, not the
|
|
1662
|
+
// latest one's.
|
|
1663
|
+
const cardItems = caseItems.filter((item) =>
|
|
1664
|
+
observationOverride
|
|
1665
|
+
? item.observation !== undefined &&
|
|
1666
|
+
item.observation.source.id === observation.source.id &&
|
|
1667
|
+
item.observation.revision === observation.revision
|
|
1668
|
+
: item.placement === "card",
|
|
1669
|
+
);
|
|
1306
1670
|
const revisionItems = caseItems.filter(
|
|
1307
1671
|
(item) => item.placement === "revision",
|
|
1308
1672
|
);
|
|
1309
1673
|
const agentCause = cardItems.some((item) => item.role === "cause");
|
|
1674
|
+
// A card placed in the story: the prose carries the claim, so the card
|
|
1675
|
+
// shows its role beside the title, its scope line if any, and for a log
|
|
1676
|
+
// stream the decisive lines themselves; the rest waits behind the chevron.
|
|
1677
|
+
const storyCard = noteMode === "chip";
|
|
1678
|
+
const storyGaps = storyCard
|
|
1679
|
+
? cardItems
|
|
1680
|
+
.map((item) =>
|
|
1681
|
+
item.gap && (!gapRoles || gapRoles.has(item.role)) ? item.gap : "",
|
|
1682
|
+
)
|
|
1683
|
+
.filter(Boolean)
|
|
1684
|
+
: [];
|
|
1685
|
+
// A compact card keeps its single row; the agent's clause on what the
|
|
1686
|
+
// result does not cover rides on it after the status, cut to the row, whole
|
|
1687
|
+
// on hover. The clause is written to stand alone, so it carries no label.
|
|
1688
|
+
const gapOnRow = compact && storyGaps.length > 0;
|
|
1689
|
+
// What a rules_out card excludes is the point of placing it; it stays on
|
|
1690
|
+
// the story card as its one line.
|
|
1691
|
+
const storyExcludes = storyCard
|
|
1692
|
+
? cardItems
|
|
1693
|
+
.filter((item) => item.role === "rules_out")
|
|
1694
|
+
.map((item) => excludedByItem?.get(investigationCaseItemKey(item)))
|
|
1695
|
+
.filter((entry): entry is string => Boolean(entry))
|
|
1696
|
+
: [];
|
|
1310
1697
|
const previousObservations = previousDifferentObservations(
|
|
1311
1698
|
group,
|
|
1312
1699
|
citedOrder,
|
|
@@ -1325,7 +1712,41 @@ function EvidenceCard({
|
|
|
1325
1712
|
observation.data,
|
|
1326
1713
|
observation.summary,
|
|
1327
1714
|
);
|
|
1328
|
-
const
|
|
1715
|
+
const storyLogLines =
|
|
1716
|
+
storyCard && observation.data.type === "logs"
|
|
1717
|
+
? (observation.data.logs?.lines ?? [])
|
|
1718
|
+
.slice(-VISIBLE_LOG_EVIDENCE_LINES)
|
|
1719
|
+
.map((line) => stripAnsi(line))
|
|
1720
|
+
: [];
|
|
1721
|
+
// A story log card shows the newest two selected lines always — the ones a
|
|
1722
|
+
// story cites from a tail. Expanding appends the earlier lines below them,
|
|
1723
|
+
// under a divider, so nothing above them moves.
|
|
1724
|
+
const storyLogHead = storyLogLines.slice(-2);
|
|
1725
|
+
const storyLogRest = storyLogLines.slice(0, -2);
|
|
1726
|
+
// A story log card carries its cited lines inline, so the only thing left to
|
|
1727
|
+
// expand is the earlier lines; the record keeps the full logs body.
|
|
1728
|
+
const inlineLogs = storyCard && observation.data.type === "logs";
|
|
1729
|
+
// A chart placed as the cause or the symptom is the card: it shows inline,
|
|
1730
|
+
// and the fold has nothing left to add.
|
|
1731
|
+
const inlineMetrics =
|
|
1732
|
+
storyCard &&
|
|
1733
|
+
!compact &&
|
|
1734
|
+
observation.data.type === "metrics" &&
|
|
1735
|
+
cardItems.some((item) => item.role === "cause" || item.role === "symptom");
|
|
1736
|
+
// Each row a citation names is the part of the listing the sentence rests
|
|
1737
|
+
// on; an entry the listing does not hold is often the point, so the card
|
|
1738
|
+
// says so rather than showing another row.
|
|
1739
|
+
const storyInventoryRows =
|
|
1740
|
+
storyCard && !compact && observation.data.type === "inventory"
|
|
1741
|
+
? namedInventoryRows(
|
|
1742
|
+
observation.data.resources,
|
|
1743
|
+
cardItems,
|
|
1744
|
+
listingScopeNamespace(observation.source),
|
|
1745
|
+
)
|
|
1746
|
+
: [];
|
|
1747
|
+
const hasExpandableBody =
|
|
1748
|
+
(hasEvidenceDetails && !inlineLogs && !inlineMetrics) || meaningfulHistory;
|
|
1749
|
+
const canExpand = !compact && (hasExpandableBody || storyLogLines.length > 2);
|
|
1329
1750
|
const revealHistory = investigationEvidenceShouldRevealHistory(
|
|
1330
1751
|
group,
|
|
1331
1752
|
revealSourceId,
|
|
@@ -1361,6 +1782,11 @@ function EvidenceCard({
|
|
|
1361
1782
|
<span className="text-sm font-semibold leading-snug text-theme-text-primary">
|
|
1362
1783
|
{observation.title}
|
|
1363
1784
|
</span>
|
|
1785
|
+
{storyCard
|
|
1786
|
+
? [...new Set(cardItems.map((item) => item.role))].map((role) =>
|
|
1787
|
+
role ? <AgentRoleChip key={role} role={role} /> : null,
|
|
1788
|
+
)
|
|
1789
|
+
: null}
|
|
1364
1790
|
</span>
|
|
1365
1791
|
{observation.relevance === "broader" &&
|
|
1366
1792
|
resourceRef &&
|
|
@@ -1370,7 +1796,20 @@ function EvidenceCard({
|
|
|
1370
1796
|
{resourceIdentity} · {resourceRef.kind}
|
|
1371
1797
|
</span>
|
|
1372
1798
|
) : null}
|
|
1373
|
-
{
|
|
1799
|
+
{gapOnRow ? (
|
|
1800
|
+
<Tooltip
|
|
1801
|
+
content={storyGaps.join(" · ")}
|
|
1802
|
+
wrapperClassName="mt-0.5 w-full min-w-0"
|
|
1803
|
+
>
|
|
1804
|
+
<span className="min-w-0 flex-1 truncate text-xs leading-relaxed text-theme-text-secondary">
|
|
1805
|
+
{displaySummary}
|
|
1806
|
+
{displaySummary ? " · " : null}
|
|
1807
|
+
<span data-agent-gap-hint className="text-theme-text-tertiary">
|
|
1808
|
+
{storyGaps.join(" · ")}
|
|
1809
|
+
</span>
|
|
1810
|
+
</span>
|
|
1811
|
+
</Tooltip>
|
|
1812
|
+
) : displaySummary ? (
|
|
1374
1813
|
<span
|
|
1375
1814
|
className={clsx(
|
|
1376
1815
|
"mt-0.5 block text-xs leading-relaxed text-theme-text-secondary",
|
|
@@ -1381,7 +1820,16 @@ function EvidenceCard({
|
|
|
1381
1820
|
{displaySummary}
|
|
1382
1821
|
</span>
|
|
1383
1822
|
) : null}
|
|
1384
|
-
{
|
|
1823
|
+
{supersededRead ? (
|
|
1824
|
+
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
1825
|
+
Captured in turn {observation.source.turnIndex + 1} · a newer read
|
|
1826
|
+
of this exists in Captured results
|
|
1827
|
+
</span>
|
|
1828
|
+
) : placedInStory ? (
|
|
1829
|
+
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
1830
|
+
In the analysis
|
|
1831
|
+
</span>
|
|
1832
|
+
) : group.historical ? (
|
|
1385
1833
|
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
1386
1834
|
Previous observation · not confirmed by the latest check
|
|
1387
1835
|
</span>
|
|
@@ -1468,7 +1916,7 @@ function EvidenceCard({
|
|
|
1468
1916
|
<EvidenceCaveat data={observation.data} />
|
|
1469
1917
|
) : null}
|
|
1470
1918
|
<SourceButton
|
|
1471
|
-
ariaLabel={`View
|
|
1919
|
+
ariaLabel={`View result for ${observation.title}`}
|
|
1472
1920
|
compact
|
|
1473
1921
|
onClick={() =>
|
|
1474
1922
|
onViewSource(
|
|
@@ -1490,7 +1938,104 @@ function EvidenceCard({
|
|
|
1490
1938
|
) : null}
|
|
1491
1939
|
</div>
|
|
1492
1940
|
</div>
|
|
1493
|
-
{
|
|
1941
|
+
{storyCard ? (
|
|
1942
|
+
<>
|
|
1943
|
+
{(storyGaps.length > 0 || storyExcludes.length > 0) && !compact ? (
|
|
1944
|
+
<div
|
|
1945
|
+
className={clsx(
|
|
1946
|
+
"space-y-0.5 text-[11px] text-theme-text-tertiary",
|
|
1947
|
+
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2",
|
|
1948
|
+
)}
|
|
1949
|
+
>
|
|
1950
|
+
{storyExcludes.map((hypothesis) => (
|
|
1951
|
+
<p key={hypothesis} data-agent-excludes>
|
|
1952
|
+
Rules out: {hypothesis}
|
|
1953
|
+
</p>
|
|
1954
|
+
))}
|
|
1955
|
+
{storyGaps.map((gap) => (
|
|
1956
|
+
<p key={gap} data-agent-gap>
|
|
1957
|
+
{gap}
|
|
1958
|
+
</p>
|
|
1959
|
+
))}
|
|
1960
|
+
</div>
|
|
1961
|
+
) : null}
|
|
1962
|
+
{storyLogHead.length > 0 ? (
|
|
1963
|
+
<div
|
|
1964
|
+
data-story-log-lines
|
|
1965
|
+
className={
|
|
1966
|
+
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2"
|
|
1967
|
+
}
|
|
1968
|
+
>
|
|
1969
|
+
<TerminalBlock
|
|
1970
|
+
footer={
|
|
1971
|
+
storyLogRest.length > 0 ? (
|
|
1972
|
+
<Collapse open={open && canExpand}>
|
|
1973
|
+
<TerminalBlockLabel divider>
|
|
1974
|
+
Earlier lines
|
|
1975
|
+
</TerminalBlockLabel>
|
|
1976
|
+
<pre className="overflow-x-auto whitespace-pre-wrap break-words px-3 pb-2.5 font-mono text-xs leading-relaxed text-[var(--terminal-text)]">
|
|
1977
|
+
{storyLogRest.join("\n")}
|
|
1978
|
+
</pre>
|
|
1979
|
+
</Collapse>
|
|
1980
|
+
) : null
|
|
1981
|
+
}
|
|
1982
|
+
>
|
|
1983
|
+
{storyLogHead.join("\n")}
|
|
1984
|
+
</TerminalBlock>
|
|
1985
|
+
</div>
|
|
1986
|
+
) : null}
|
|
1987
|
+
{storyInventoryRows.length > 0 ? (
|
|
1988
|
+
<div
|
|
1989
|
+
data-story-inventory-rows
|
|
1990
|
+
className={
|
|
1991
|
+
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2"
|
|
1992
|
+
}
|
|
1993
|
+
>
|
|
1994
|
+
<div className="rounded-md border border-theme-border">
|
|
1995
|
+
{storyInventoryRows.map((entry, index) =>
|
|
1996
|
+
entry.row ? (
|
|
1997
|
+
<InventoryRow
|
|
1998
|
+
key={entry.key}
|
|
1999
|
+
resource={entry.row}
|
|
2000
|
+
divider={index > 0}
|
|
2001
|
+
/>
|
|
2002
|
+
) : (
|
|
2003
|
+
<p
|
|
2004
|
+
key={entry.key}
|
|
2005
|
+
data-story-inventory-absent={
|
|
2006
|
+
entry.matches === 0 ? "" : undefined
|
|
2007
|
+
}
|
|
2008
|
+
className={clsx(
|
|
2009
|
+
"px-2.5 py-1.5 font-mono text-xs text-theme-text-secondary",
|
|
2010
|
+
index > 0 && "border-t border-theme-border/60",
|
|
2011
|
+
)}
|
|
2012
|
+
>
|
|
2013
|
+
{entry.matches === 0
|
|
2014
|
+
? `Not in this listing: ${entry.label}`
|
|
2015
|
+
: `${entry.label}: ${entry.matches} entries match in this listing`}
|
|
2016
|
+
</p>
|
|
2017
|
+
),
|
|
2018
|
+
)}
|
|
2019
|
+
</div>
|
|
2020
|
+
</div>
|
|
2021
|
+
) : null}
|
|
2022
|
+
{inlineMetrics && observation.data.type === "metrics" ? (
|
|
2023
|
+
<div
|
|
2024
|
+
data-story-metrics
|
|
2025
|
+
className={
|
|
2026
|
+
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2"
|
|
2027
|
+
}
|
|
2028
|
+
>
|
|
2029
|
+
<MetricsBody
|
|
2030
|
+
data={observation.data}
|
|
2031
|
+
changeCoverage={metricsMarkersBySource?.get(
|
|
2032
|
+
observation.source.id,
|
|
2033
|
+
)}
|
|
2034
|
+
/>
|
|
2035
|
+
</div>
|
|
2036
|
+
) : null}
|
|
2037
|
+
</>
|
|
2038
|
+
) : cardItems.some((item) => item.claim || item.role) ? (
|
|
1494
2039
|
<div
|
|
1495
2040
|
className={clsx(
|
|
1496
2041
|
"space-y-1.5",
|
|
@@ -1500,15 +2045,20 @@ function EvidenceCard({
|
|
|
1500
2045
|
{cardItems.map((item) => (
|
|
1501
2046
|
<AgentClaimNote
|
|
1502
2047
|
key={item.index}
|
|
1503
|
-
claim={item.claim}
|
|
2048
|
+
claim={compact ? "" : item.claim}
|
|
1504
2049
|
role={item.role}
|
|
2050
|
+
gap={
|
|
2051
|
+
compact || (gapRoles && !gapRoles.has(item.role))
|
|
2052
|
+
? undefined
|
|
2053
|
+
: item.gap
|
|
2054
|
+
}
|
|
1505
2055
|
excludes={excludedByItem?.get(investigationCaseItemKey(item))}
|
|
1506
2056
|
className="pt-1.5"
|
|
1507
2057
|
/>
|
|
1508
2058
|
))}
|
|
1509
2059
|
</div>
|
|
1510
2060
|
) : null}
|
|
1511
|
-
{canExpand ? (
|
|
2061
|
+
{canExpand && hasExpandableBody ? (
|
|
1512
2062
|
<div id={bodyId}>
|
|
1513
2063
|
<Collapse open={open}>
|
|
1514
2064
|
<div
|
|
@@ -1517,9 +2067,10 @@ function EvidenceCard({
|
|
|
1517
2067
|
prominence === "primary" ? "px-3 py-3" : "px-2.5 py-2.5",
|
|
1518
2068
|
)}
|
|
1519
2069
|
>
|
|
1520
|
-
{hasEvidenceDetails ? (
|
|
2070
|
+
{hasEvidenceDetails && !inlineLogs && !inlineMetrics ? (
|
|
1521
2071
|
<EvidenceBody
|
|
1522
2072
|
data={observation.data}
|
|
2073
|
+
condensed={storyCard}
|
|
1523
2074
|
cardSummary={observation.summary}
|
|
1524
2075
|
changeCoverage={metricsMarkersBySource?.get(
|
|
1525
2076
|
observation.source.id,
|
|
@@ -1633,11 +2184,14 @@ function EvidenceBody({
|
|
|
1633
2184
|
data,
|
|
1634
2185
|
cardSummary,
|
|
1635
2186
|
changeCoverage,
|
|
2187
|
+
condensed = false,
|
|
1636
2188
|
}: {
|
|
1637
2189
|
data: InvestigationEvidenceData;
|
|
1638
2190
|
cardSummary?: string;
|
|
1639
2191
|
/** Change markers for a metrics chart; derived by the pane, never by data. */
|
|
1640
2192
|
changeCoverage?: MetricsChangeCoverage;
|
|
2193
|
+
/** The card's title already names the stream: skip the repeated badges. */
|
|
2194
|
+
condensed?: boolean;
|
|
1641
2195
|
}) {
|
|
1642
2196
|
switch (data.type) {
|
|
1643
2197
|
case "issue":
|
|
@@ -1649,7 +2203,7 @@ function EvidenceBody({
|
|
|
1649
2203
|
case "resource":
|
|
1650
2204
|
return <ResourceBody data={data} />;
|
|
1651
2205
|
case "logs":
|
|
1652
|
-
return <LogsBody data={data} />;
|
|
2206
|
+
return <LogsBody data={data} condensed={condensed} />;
|
|
1653
2207
|
case "events":
|
|
1654
2208
|
return <EventsBody data={data} />;
|
|
1655
2209
|
case "changes":
|
|
@@ -2089,7 +2643,13 @@ function conditionStatusTone(condition: {
|
|
|
2089
2643
|
}
|
|
2090
2644
|
}
|
|
2091
2645
|
|
|
2092
|
-
function LogsBody({
|
|
2646
|
+
function LogsBody({
|
|
2647
|
+
data,
|
|
2648
|
+
condensed = false,
|
|
2649
|
+
}: {
|
|
2650
|
+
data: EvidenceDataOf<"logs">;
|
|
2651
|
+
condensed?: boolean;
|
|
2652
|
+
}) {
|
|
2093
2653
|
const lines = data.logs?.lines ?? [];
|
|
2094
2654
|
const visibleLines = lines
|
|
2095
2655
|
.slice(-VISIBLE_LOG_EVIDENCE_LINES)
|
|
@@ -2098,25 +2658,35 @@ function LogsBody({ data }: { data: EvidenceDataOf<"logs"> }) {
|
|
|
2098
2658
|
return (
|
|
2099
2659
|
<div className="space-y-2">
|
|
2100
2660
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2661
|
+
{condensed ? null : (
|
|
2662
|
+
<>
|
|
2663
|
+
<Badge tone="structural" size="sm">
|
|
2664
|
+
{data.pod} / {data.container}
|
|
2665
|
+
</Badge>
|
|
2666
|
+
<Badge severity="neutral" size="sm">
|
|
2667
|
+
{data.previous ? "previous instance" : "current instance"}
|
|
2668
|
+
</Badge>
|
|
2669
|
+
</>
|
|
2670
|
+
)}
|
|
2107
2671
|
{data.logs?.fallback ? (
|
|
2108
|
-
<Badge severity="
|
|
2109
|
-
|
|
2672
|
+
<Badge severity="neutral" size="sm">
|
|
2673
|
+
Log tail · filter matched nothing
|
|
2110
2674
|
</Badge>
|
|
2111
2675
|
) : null}
|
|
2112
2676
|
{data.logs ? (
|
|
2113
2677
|
<span className="text-xs text-theme-text-tertiary">
|
|
2114
|
-
{data.logs.matchedLines}
|
|
2115
|
-
|
|
2678
|
+
{data.logs.matchedLines} of {data.logs.totalLines} read lines
|
|
2679
|
+
matched the filter
|
|
2116
2680
|
</span>
|
|
2117
2681
|
) : null}
|
|
2118
2682
|
</div>
|
|
2119
|
-
{
|
|
2683
|
+
{condensed ? (
|
|
2684
|
+
omittedLines > 0 ? (
|
|
2685
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
2686
|
+
Showing the last {visibleLines.length} of {lines.length} lines.
|
|
2687
|
+
</p>
|
|
2688
|
+
) : null
|
|
2689
|
+
) : visibleLines.length > 0 ? (
|
|
2120
2690
|
<TerminalBlock
|
|
2121
2691
|
label={
|
|
2122
2692
|
omittedLines > 0
|
|
@@ -2754,46 +3324,117 @@ function MetricsBody({
|
|
|
2754
3324
|
);
|
|
2755
3325
|
}
|
|
2756
3326
|
|
|
3327
|
+
// The rows the citations on a card name, in citation order: one row when the
|
|
3328
|
+
// name picks exactly one entry, otherwise a line saying the entry is absent
|
|
3329
|
+
// or which entries the name could mean.
|
|
3330
|
+
function listingScopeNamespace(
|
|
3331
|
+
source: InvestigationEvidenceSource,
|
|
3332
|
+
): string | undefined {
|
|
3333
|
+
const namespace = investigationSourceArgs(source)?.namespace;
|
|
3334
|
+
return typeof namespace === "string" && namespace ? namespace : undefined;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
export function namedInventoryRows(
|
|
3338
|
+
resources: InvestigationResourceSummary[],
|
|
3339
|
+
items: readonly { subject?: DiagnosisEvidenceSubject }[],
|
|
3340
|
+
/** The namespace the listing call was scoped to; rows omit it then. */
|
|
3341
|
+
scopeNamespace?: string,
|
|
3342
|
+
): {
|
|
3343
|
+
key: string;
|
|
3344
|
+
label: string;
|
|
3345
|
+
matches: number;
|
|
3346
|
+
row?: InvestigationResourceSummary;
|
|
3347
|
+
}[] {
|
|
3348
|
+
const seen = new Set<string>();
|
|
3349
|
+
const out: {
|
|
3350
|
+
key: string;
|
|
3351
|
+
label: string;
|
|
3352
|
+
matches: number;
|
|
3353
|
+
row?: InvestigationResourceSummary;
|
|
3354
|
+
}[] = [];
|
|
3355
|
+
for (const item of items) {
|
|
3356
|
+
const subject = item.subject;
|
|
3357
|
+
if (!subject?.name) continue;
|
|
3358
|
+
// Agents write namespace "" for a cluster-scoped kind; that states none.
|
|
3359
|
+
const namespace = subject.namespace || undefined;
|
|
3360
|
+
const key = `${namespace ?? ""}/${subject.name}`;
|
|
3361
|
+
if (seen.has(key)) continue;
|
|
3362
|
+
seen.add(key);
|
|
3363
|
+
// A row without a namespace lives in the listing's scope, or in none
|
|
3364
|
+
// (a cluster-scoped kind); neither satisfies a namespace the citation
|
|
3365
|
+
// states unless it is the scope itself.
|
|
3366
|
+
const matches = resources.filter(
|
|
3367
|
+
(resource) =>
|
|
3368
|
+
resource.name === subject.name &&
|
|
3369
|
+
sameKind(resource.kind, subject.kind) &&
|
|
3370
|
+
(namespace === undefined ||
|
|
3371
|
+
(resource.namespace ?? scopeNamespace) === namespace),
|
|
3372
|
+
);
|
|
3373
|
+
out.push({
|
|
3374
|
+
key,
|
|
3375
|
+
label: namespace ? `${namespace}/${subject.name}` : subject.name,
|
|
3376
|
+
matches: matches.length,
|
|
3377
|
+
row: matches.length === 1 ? matches[0] : undefined,
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
return out;
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
function InventoryRow({
|
|
3384
|
+
resource,
|
|
3385
|
+
divider = false,
|
|
3386
|
+
}: {
|
|
3387
|
+
resource: InvestigationResourceSummary;
|
|
3388
|
+
divider?: boolean;
|
|
3389
|
+
}) {
|
|
3390
|
+
return (
|
|
3391
|
+
<div
|
|
3392
|
+
className={clsx(
|
|
3393
|
+
"flex min-w-0 items-center gap-2 px-2.5 py-1.5",
|
|
3394
|
+
divider && "border-t border-theme-border/60",
|
|
3395
|
+
)}
|
|
3396
|
+
>
|
|
3397
|
+
<StatusDot
|
|
3398
|
+
tone={mapHealthToTone(resource.summaryContext?.health ?? "")}
|
|
3399
|
+
className="shrink-0"
|
|
3400
|
+
/>
|
|
3401
|
+
<Badge tone="structural" size="sm">
|
|
3402
|
+
{resource.kind}
|
|
3403
|
+
</Badge>
|
|
3404
|
+
<span className="min-w-0 flex-1">
|
|
3405
|
+
<span className="block truncate font-mono text-xs text-theme-text-secondary">
|
|
3406
|
+
{resource.namespace ? `${resource.namespace}/` : ""}
|
|
3407
|
+
{resource.name}
|
|
3408
|
+
</span>
|
|
3409
|
+
{resource.issue ? (
|
|
3410
|
+
<span className="block truncate text-xs text-warning-text">
|
|
3411
|
+
{resource.issue}
|
|
3412
|
+
</span>
|
|
3413
|
+
) : null}
|
|
3414
|
+
</span>
|
|
3415
|
+
{resource.ready || resource.status ? (
|
|
3416
|
+
<span className="ml-auto shrink-0 font-mono text-xs text-theme-text-tertiary">
|
|
3417
|
+
{resource.ready || resource.status}
|
|
3418
|
+
</span>
|
|
3419
|
+
) : null}
|
|
3420
|
+
{(resource.summaryContext?.issueCount ?? 0) > 0 ? (
|
|
3421
|
+
<Badge severity="warning" size="sm">
|
|
3422
|
+
{resource.summaryContext?.issueCount} issues
|
|
3423
|
+
</Badge>
|
|
3424
|
+
) : null}
|
|
3425
|
+
</div>
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
|
|
2757
3429
|
function InventoryBody({ data }: { data: EvidenceDataOf<"inventory"> }) {
|
|
2758
3430
|
return (
|
|
2759
3431
|
<div className="max-h-72 overflow-y-auto rounded-md border border-theme-border">
|
|
2760
3432
|
{data.resources.map((resource, index) => (
|
|
2761
|
-
<
|
|
3433
|
+
<InventoryRow
|
|
2762
3434
|
key={`${resource.kind}-${resource.namespace ?? ""}-${resource.name}`}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
)}
|
|
2767
|
-
>
|
|
2768
|
-
<StatusDot
|
|
2769
|
-
tone={mapHealthToTone(resource.summaryContext?.health ?? "")}
|
|
2770
|
-
className="shrink-0"
|
|
2771
|
-
/>
|
|
2772
|
-
<Badge tone="structural" size="sm">
|
|
2773
|
-
{resource.kind}
|
|
2774
|
-
</Badge>
|
|
2775
|
-
<span className="min-w-0 flex-1">
|
|
2776
|
-
<span className="block truncate font-mono text-xs text-theme-text-secondary">
|
|
2777
|
-
{resource.namespace ? `${resource.namespace}/` : ""}
|
|
2778
|
-
{resource.name}
|
|
2779
|
-
</span>
|
|
2780
|
-
{resource.issue ? (
|
|
2781
|
-
<span className="block truncate text-xs text-warning-text">
|
|
2782
|
-
{resource.issue}
|
|
2783
|
-
</span>
|
|
2784
|
-
) : null}
|
|
2785
|
-
</span>
|
|
2786
|
-
{resource.ready || resource.status ? (
|
|
2787
|
-
<span className="ml-auto shrink-0 font-mono text-xs text-theme-text-tertiary">
|
|
2788
|
-
{resource.ready || resource.status}
|
|
2789
|
-
</span>
|
|
2790
|
-
) : null}
|
|
2791
|
-
{(resource.summaryContext?.issueCount ?? 0) > 0 ? (
|
|
2792
|
-
<Badge severity="warning" size="sm">
|
|
2793
|
-
{resource.summaryContext?.issueCount} issues
|
|
2794
|
-
</Badge>
|
|
2795
|
-
) : null}
|
|
2796
|
-
</div>
|
|
3435
|
+
resource={resource}
|
|
3436
|
+
divider={index > 0}
|
|
3437
|
+
/>
|
|
2797
3438
|
))}
|
|
2798
3439
|
</div>
|
|
2799
3440
|
);
|
|
@@ -3338,7 +3979,7 @@ function RevisionHistory({
|
|
|
3338
3979
|
)}
|
|
3339
3980
|
</div>
|
|
3340
3981
|
<SourceButton
|
|
3341
|
-
ariaLabel={`View
|
|
3982
|
+
ariaLabel={`View result for ${phaseLabel(observation.source.phase).toLowerCase()} observation of ${observation.title}`}
|
|
3342
3983
|
onClick={() =>
|
|
3343
3984
|
onViewSource(
|
|
3344
3985
|
observation.source.id,
|
|
@@ -3412,7 +4053,7 @@ function EvidenceIcon({
|
|
|
3412
4053
|
|
|
3413
4054
|
function SourceButton({
|
|
3414
4055
|
ariaLabel,
|
|
3415
|
-
buttonLabel = "View
|
|
4056
|
+
buttonLabel = "View result",
|
|
3416
4057
|
compact = false,
|
|
3417
4058
|
onClick,
|
|
3418
4059
|
}: {
|