@skyhook-io/radar-app 1.14.1 → 1.14.3
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 +30 -0
- 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 +1560 -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 +11 -28
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +631 -2
- package/src/components/diagnose/investigationCase.ts +132 -24
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
- 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 +134 -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 +11 -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/parts.test.tsx +3 -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/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/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +33 -10
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -1,366 +1,61 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createContext,
|
|
3
2
|
useCallback,
|
|
4
|
-
useContext,
|
|
5
3
|
useEffect,
|
|
6
|
-
useId,
|
|
7
4
|
useLayoutEffect,
|
|
8
5
|
useMemo,
|
|
9
6
|
useRef,
|
|
10
7
|
useState,
|
|
11
8
|
type ReactNode,
|
|
12
9
|
} from "react";
|
|
13
|
-
import { clsx } from "clsx";
|
|
14
|
-
import {
|
|
15
|
-
EVIDENCE_KIND_TRAITS,
|
|
16
|
-
evidenceKindIsFocused,
|
|
17
|
-
} from "./investigationEvidenceKinds";
|
|
18
|
-
import {
|
|
19
|
-
Activity,
|
|
20
|
-
AlertTriangle,
|
|
21
|
-
CircleAlert,
|
|
22
|
-
FileClock,
|
|
23
|
-
FileSearch,
|
|
24
|
-
Info,
|
|
25
|
-
SearchCheck,
|
|
26
|
-
SquareArrowOutUpRight,
|
|
27
|
-
} from "lucide-react";
|
|
28
|
-
import {
|
|
29
|
-
Badge,
|
|
30
|
-
Collapse,
|
|
31
|
-
CollapseChevron,
|
|
32
|
-
DiffViewer,
|
|
33
|
-
StatusDot,
|
|
34
|
-
TerminalBlock,
|
|
35
|
-
TerminalBlockLabel,
|
|
36
|
-
defaultConditionTone,
|
|
37
|
-
displayKind,
|
|
38
|
-
formatRelativeAgeTime,
|
|
39
|
-
HPADiagnosisSummary,
|
|
40
|
-
mapHealthToTone,
|
|
41
|
-
ResourceLink,
|
|
42
|
-
stripAnsi,
|
|
43
|
-
} from "@skyhook-io/k8s-ui";
|
|
44
|
-
import {
|
|
45
|
-
AreaChart,
|
|
46
|
-
SERIES_COLORS,
|
|
47
|
-
SeriesLegend,
|
|
48
|
-
formatMetricValue,
|
|
49
|
-
seriesDisplayLabels,
|
|
50
|
-
seriesFill,
|
|
51
|
-
type TimeSeries,
|
|
52
|
-
} from "@skyhook-io/k8s-ui/components/charts";
|
|
53
|
-
import { apiVersionToGroup } from "../../utils/navigation";
|
|
54
10
|
import {
|
|
55
11
|
AnalysisStory,
|
|
56
12
|
resolveStoryPlacements,
|
|
57
13
|
type StoryPlacementLoss,
|
|
58
14
|
type StoryPlacementTarget,
|
|
59
15
|
} from "./AnalysisStory";
|
|
16
|
+
import type { DiagnosisEvidenceItem } from "../../api/diagnose";
|
|
17
|
+
import { metricsChangeCoverage } from "./investigationMetrics";
|
|
18
|
+
import { groupEvidenceCoverage } from "./investigationEvidencePresentation";
|
|
60
19
|
import type {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
} from "../../api/diagnose";
|
|
64
|
-
import { parseLogLine } from "../../utils/log-format";
|
|
65
|
-
import {
|
|
66
|
-
metricsChangeCoverage,
|
|
67
|
-
metricsDomain,
|
|
68
|
-
type MetricsChangeCoverage,
|
|
69
|
-
} from "./investigationMetrics";
|
|
70
|
-
import {
|
|
71
|
-
evidenceDisplaySnapshot,
|
|
72
|
-
groupEvidenceCoverage,
|
|
73
|
-
type EvidenceCoverageGroup,
|
|
74
|
-
} from "./investigationEvidencePresentation";
|
|
75
|
-
|
|
76
|
-
import {
|
|
77
|
-
investigationEvidenceSubjectRef,
|
|
78
|
-
investigationEvidenceSourceDomId,
|
|
79
|
-
investigationSourceArgs,
|
|
80
|
-
type InvestigationEvidenceData,
|
|
81
|
-
type InvestigationEvidenceGroup,
|
|
82
|
-
type InvestigationResourceSummary,
|
|
83
|
-
type InvestigationEvidenceObservation,
|
|
84
|
-
type InvestigationEvidenceProjection,
|
|
85
|
-
type InvestigationRootCauseEvidenceResolution,
|
|
86
|
-
type InvestigationEvidenceSource,
|
|
87
|
-
type InvestigationEvidenceTier,
|
|
20
|
+
InvestigationEvidenceProjection,
|
|
21
|
+
InvestigationRootCauseEvidenceResolution,
|
|
88
22
|
} from "./investigationEvidence";
|
|
89
|
-
import type {
|
|
90
|
-
DiagnosisResourceContext,
|
|
91
|
-
DiagnosisResourceRef,
|
|
92
|
-
DiagnosisScalerRef,
|
|
93
|
-
} from "./diagnoseEvidenceTypes";
|
|
94
|
-
import { InvestigationResourceEvidence } from "./InvestigationResourceEvidence";
|
|
95
|
-
import { investigationResourceEvidenceHasDetails } from "./investigationResourceEvidenceModel";
|
|
23
|
+
import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
|
|
96
24
|
import type { InvestigationSourceExcerpt } from "./investigationSourceFocus";
|
|
97
|
-
import {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
import {
|
|
101
|
-
investigationCaseObservationKey,
|
|
102
|
-
type InvestigationCaseItem,
|
|
103
|
-
type InvestigationCaseResolution,
|
|
104
|
-
sameKind,
|
|
25
|
+
import type {
|
|
26
|
+
InvestigationCaseItem,
|
|
27
|
+
InvestigationCaseResolution,
|
|
105
28
|
} from "./investigationCase";
|
|
106
|
-
import type { DiagnosisEvidenceRole } from "../../api/diagnose";
|
|
107
|
-
|
|
108
29
|
import {
|
|
109
30
|
investigationDisclosureSettleDelay,
|
|
110
31
|
prefersReducedMotion,
|
|
111
|
-
useDisclosureReveal,
|
|
112
32
|
} from "./useDisclosureReveal";
|
|
33
|
+
import {
|
|
34
|
+
EvidenceNavigationContext,
|
|
35
|
+
type InvestigationTimelineScope,
|
|
36
|
+
} from "./investigationEvidence/navigation";
|
|
37
|
+
import {
|
|
38
|
+
investigationCaseByGroup,
|
|
39
|
+
investigationCaseItemKey,
|
|
40
|
+
investigationEvidenceRevealCollection,
|
|
41
|
+
partitionInvestigationEvidence,
|
|
42
|
+
} from "./investigationEvidencePartition";
|
|
43
|
+
import { EvidenceCard } from "./EvidenceCard";
|
|
44
|
+
import {
|
|
45
|
+
AssessmentEvidenceQualification,
|
|
46
|
+
CollapsedEvidenceCollection,
|
|
47
|
+
CoverageStrip,
|
|
48
|
+
EmptyCollection,
|
|
49
|
+
RuledOutBlock,
|
|
50
|
+
} from "./InvestigationEvidenceSections";
|
|
51
|
+
|
|
113
52
|
export {
|
|
114
53
|
INVESTIGATION_DISCLOSURE_SETTLE_MS,
|
|
115
54
|
investigationDisclosureSettleDelay,
|
|
116
55
|
investigationDisclosureScrollTop,
|
|
117
56
|
} from "./useDisclosureReveal";
|
|
118
|
-
export const VISIBLE_LOG_EVIDENCE_LINES = 12;
|
|
119
|
-
|
|
120
|
-
/** The Timeline scope a changes card can open: one resource name in one namespace. */
|
|
121
|
-
export interface InvestigationTimelineScope {
|
|
122
|
-
namespace?: string;
|
|
123
|
-
name: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const EvidenceNavigationContext = createContext<{
|
|
127
|
-
onOpenResource?: (ref: DiagnosisResourceRef) => void;
|
|
128
|
-
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
129
|
-
revealSourceId?: string;
|
|
130
|
-
revealRequestId?: number;
|
|
131
|
-
expandedGroupIds?: ReadonlySet<string>;
|
|
132
|
-
onGroupOpenChange?: (id: string, open: boolean) => void;
|
|
133
|
-
citedOrderByGroup?: ReadonlyMap<string, number>;
|
|
134
|
-
/** Change markers for each metrics observation, keyed by its source id. */
|
|
135
|
-
metricsMarkersBySource?: ReadonlyMap<string, MetricsChangeCoverage>;
|
|
136
|
-
/** Card- and revision-placed agent items, keyed by group id. */
|
|
137
|
-
caseByGroup?: ReadonlyMap<string, InvestigationCaseItem[]>;
|
|
138
|
-
/** The hypothesis each `rules_out` item excludes, keyed by item. */
|
|
139
|
-
excludedByItem?: ReadonlyMap<string, string>;
|
|
140
|
-
}>({});
|
|
141
|
-
|
|
142
|
-
/** Item identity for the excluded-hypothesis lookup; index alone repeats across turns. */
|
|
143
|
-
export function investigationCaseItemKey(item: InvestigationCaseItem): string {
|
|
144
|
-
return `${item.source.id}\u0000${item.index}`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const EVIDENCE_ROLE_RANK: Readonly<Record<DiagnosisEvidenceRole, number>> = {
|
|
148
|
-
cause: 0,
|
|
149
|
-
symptom: 1,
|
|
150
|
-
// A ruled-out card keeps Radar's place in the list; the role only labels it.
|
|
151
|
-
rules_out: 2,
|
|
152
|
-
context: 3,
|
|
153
|
-
benign: 4,
|
|
154
|
-
demoted: 5,
|
|
155
|
-
};
|
|
156
|
-
const UNLABELLED_RANK = 2;
|
|
157
|
-
|
|
158
|
-
function sourceNamesOneResource(source: InvestigationEvidenceSource): boolean {
|
|
159
|
-
const args = investigationSourceArgs(source);
|
|
160
|
-
return (
|
|
161
|
-
typeof args?.kind === "string" &&
|
|
162
|
-
args.kind !== "" &&
|
|
163
|
-
typeof args.name === "string" &&
|
|
164
|
-
args.name !== ""
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function investigationCaseByGroup(
|
|
169
|
-
investigationCase?: InvestigationCaseResolution,
|
|
170
|
-
): Map<string, InvestigationCaseItem[]> {
|
|
171
|
-
const byGroup = new Map<string, InvestigationCaseItem[]>();
|
|
172
|
-
for (const item of investigationCase?.items ?? []) {
|
|
173
|
-
if (!item.groupId || item.placement === "source") continue;
|
|
174
|
-
const items = byGroup.get(item.groupId) ?? [];
|
|
175
|
-
items.push(item);
|
|
176
|
-
byGroup.set(item.groupId, items);
|
|
177
|
-
}
|
|
178
|
-
return byGroup;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function evidenceTypePrefersFullRow(
|
|
182
|
-
type: InvestigationEvidenceData["type"],
|
|
183
|
-
): boolean {
|
|
184
|
-
return EVIDENCE_KIND_TRAITS[type].fullRow;
|
|
185
|
-
}
|
|
186
57
|
|
|
187
|
-
|
|
188
|
-
// Keep compact cards paired only with an adjacent compact card. Otherwise a
|
|
189
|
-
// full-row card between them strands a conspicuous empty half-row (and makes the
|
|
190
|
-
// visual order look accidental), as does an odd card at the end of a run.
|
|
191
|
-
export function investigationEvidenceFullRowFlags(
|
|
192
|
-
types: readonly InvestigationEvidenceData["type"][],
|
|
193
|
-
): boolean[] {
|
|
194
|
-
const fullRow = types.map(evidenceTypePrefersFullRow);
|
|
195
|
-
let compactRunStart = 0;
|
|
196
|
-
|
|
197
|
-
for (let index = 0; index <= types.length; index += 1) {
|
|
198
|
-
if (index < types.length && !fullRow[index]) continue;
|
|
199
|
-
const compactRunLength = index - compactRunStart;
|
|
200
|
-
if (compactRunLength % 2 === 1) fullRow[index - 1] = true;
|
|
201
|
-
compactRunStart = index + 1;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return fullRow;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
type EvidenceCollection = "main" | "workload" | "earlier";
|
|
208
|
-
|
|
209
|
-
export function partitionInvestigationEvidence(
|
|
210
|
-
groups: InvestigationEvidenceGroup[],
|
|
211
|
-
resolution?: InvestigationRootCauseEvidenceResolution,
|
|
212
|
-
investigationCase?: InvestigationCaseResolution,
|
|
213
|
-
/**
|
|
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.
|
|
217
|
-
*/
|
|
218
|
-
inventory = false,
|
|
219
|
-
) {
|
|
220
|
-
// Selection: legacy root-cause links plus every placed agent item, of any
|
|
221
|
-
// role (placement promotes a group into main the way a citation does; the
|
|
222
|
-
// role does not matter here). Ordering below is the only place roles act,
|
|
223
|
-
// and nothing the agent sends can remove a group from a collection.
|
|
224
|
-
const caseByGroup = investigationCaseByGroup(investigationCase);
|
|
225
|
-
const selected = new Set([
|
|
226
|
-
...(resolution?.status === "linked"
|
|
227
|
-
? resolution.links.map((link) => link.originalGroupId)
|
|
228
|
-
: []),
|
|
229
|
-
...caseByGroup.keys(),
|
|
230
|
-
]);
|
|
231
|
-
const collections: Record<EvidenceCollection, InvestigationEvidenceGroup[]> =
|
|
232
|
-
{
|
|
233
|
-
main: [],
|
|
234
|
-
workload: [],
|
|
235
|
-
earlier: [],
|
|
236
|
-
};
|
|
237
|
-
const collectionByGroup = new Map<string, EvidenceCollection>();
|
|
238
|
-
const adverse = (group: InvestigationEvidenceGroup) =>
|
|
239
|
-
["error", "alert", "warning"].includes(group.latest.tone);
|
|
240
|
-
// Broader cards are facts about something other than the target. They are
|
|
241
|
-
// withheld unless cited, and the pane says how many were withheld so a
|
|
242
|
-
// reader knows the agent looked at things it did not build its case on.
|
|
243
|
-
let hiddenBroader = 0;
|
|
244
|
-
// Broader metrics are facts about something other than the target. They are
|
|
245
|
-
// withheld unless cited, and the pane says how many were withheld so a
|
|
246
|
-
// reader knows the agent ran queries it did not build its case on.
|
|
247
|
-
let hiddenMetrics = 0;
|
|
248
|
-
for (const group of groups) {
|
|
249
|
-
const broader = group.latest.relevance === "broader";
|
|
250
|
-
// Citations select tool results, not individual rows in a broad search.
|
|
251
|
-
// Only a focused, unambiguous fact can be promoted by a source citation.
|
|
252
|
-
// An agent item names this observation when its subject does, or when the
|
|
253
|
-
// call it cites was itself scoped to one resource; a subject-less item on
|
|
254
|
-
// a broad query only proves the query returned one row and is gated like
|
|
255
|
-
// a citation of its source.
|
|
256
|
-
const namedByAgent = (caseByGroup.get(group.id) ?? []).some(
|
|
257
|
-
(item) => item.subject || sourceNamesOneResource(item.source),
|
|
258
|
-
);
|
|
259
|
-
if (broader && !namedByAgent) {
|
|
260
|
-
const citingSource =
|
|
261
|
-
resolution?.links.find((item) => item.originalGroupId === group.id)
|
|
262
|
-
?.source ?? caseByGroup.get(group.id)?.[0]?.source;
|
|
263
|
-
const focused = evidenceKindIsFocused(group.latest.data.type);
|
|
264
|
-
const sourceGroups = citingSource
|
|
265
|
-
? groups.filter(
|
|
266
|
-
(candidate) =>
|
|
267
|
-
evidenceKindIsFocused(candidate.latest.data.type) &&
|
|
268
|
-
candidate.observations.some(
|
|
269
|
-
(observation) => observation.source.id === citingSource.id,
|
|
270
|
-
),
|
|
271
|
-
)
|
|
272
|
-
: [];
|
|
273
|
-
if (!selected.has(group.id) || !focused || sourceGroups.length !== 1) {
|
|
274
|
-
// The two counts head separate lines in the pane, so they have to be
|
|
275
|
-
// disjoint: a withheld chart announced by both would read as two
|
|
276
|
-
// withheld results.
|
|
277
|
-
if (!group.historical) {
|
|
278
|
-
if (group.latest.data.type === "metrics") hiddenMetrics += 1;
|
|
279
|
-
else hiddenBroader += 1;
|
|
280
|
-
}
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const main =
|
|
285
|
-
!group.historical &&
|
|
286
|
-
(selected.has(group.id) ||
|
|
287
|
-
(!broader &&
|
|
288
|
-
(inventory ||
|
|
289
|
-
group.latest.tier === "key" ||
|
|
290
|
-
(group.latest.tier === "supporting" && adverse(group)))));
|
|
291
|
-
const collection = main
|
|
292
|
-
? "main"
|
|
293
|
-
: group.historical
|
|
294
|
-
? "earlier"
|
|
295
|
-
: "workload";
|
|
296
|
-
collections[collection].push(group);
|
|
297
|
-
collectionByGroup.set(group.id, collection);
|
|
298
|
-
}
|
|
299
|
-
// A group takes its strongest role (lowest rank); within one role the
|
|
300
|
-
// agent's own item order decides. Unlabelled and ruled-out groups keep
|
|
301
|
-
// Radar's order among themselves.
|
|
302
|
-
const roleOrder = (group: InvestigationEvidenceGroup) => {
|
|
303
|
-
let rank: number | undefined;
|
|
304
|
-
let itemIndex = Number.POSITIVE_INFINITY;
|
|
305
|
-
for (const item of caseByGroup.get(group.id) ?? []) {
|
|
306
|
-
const itemRank = EVIDENCE_ROLE_RANK[item.role];
|
|
307
|
-
if (rank === undefined || itemRank < rank) {
|
|
308
|
-
rank = itemRank;
|
|
309
|
-
itemIndex = item.index;
|
|
310
|
-
} else if (itemRank === rank) {
|
|
311
|
-
itemIndex = Math.min(itemIndex, item.index);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (rank === undefined || rank === UNLABELLED_RANK) {
|
|
315
|
-
return { rank: UNLABELLED_RANK, itemIndex: Number.POSITIVE_INFINITY };
|
|
316
|
-
}
|
|
317
|
-
return { rank, itemIndex };
|
|
318
|
-
};
|
|
319
|
-
collections.main.sort((left, right) => {
|
|
320
|
-
const leftRole = roleOrder(left);
|
|
321
|
-
const rightRole = roleOrder(right);
|
|
322
|
-
const agentOrder =
|
|
323
|
-
Number.isFinite(leftRole.itemIndex) ||
|
|
324
|
-
Number.isFinite(rightRole.itemIndex)
|
|
325
|
-
? leftRole.itemIndex - rightRole.itemIndex
|
|
326
|
-
: 0;
|
|
327
|
-
return (
|
|
328
|
-
leftRole.rank - rightRole.rank ||
|
|
329
|
-
agentOrder ||
|
|
330
|
-
Number(right.latest.tier === "key") -
|
|
331
|
-
Number(left.latest.tier === "key") ||
|
|
332
|
-
Number(adverse(right)) - Number(adverse(left)) ||
|
|
333
|
-
left.firstOrder - right.firstOrder
|
|
334
|
-
);
|
|
335
|
-
});
|
|
336
|
-
// A healthy workload's collection opens with several "nothing here"
|
|
337
|
-
// receipts; the cards that carry facts (its vitals above all) come first,
|
|
338
|
-
// and the receipts keep their order among themselves.
|
|
339
|
-
collections.workload.sort(
|
|
340
|
-
(left, right) =>
|
|
341
|
-
Number(left.kind === "receipt") - Number(right.kind === "receipt"),
|
|
342
|
-
);
|
|
343
|
-
return { ...collections, collectionByGroup, hiddenBroader, hiddenMetrics };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
export function investigationEvidenceRevealCollection(
|
|
347
|
-
projection: InvestigationEvidenceProjection,
|
|
348
|
-
sourceId: string,
|
|
349
|
-
partition = partitionInvestigationEvidence(projection.groups),
|
|
350
|
-
): Exclude<EvidenceCollection, "main"> | "coverage" | undefined {
|
|
351
|
-
const source = projection.sources.find((item) => item.id === sourceId);
|
|
352
|
-
const collection = source?.primaryGroupId
|
|
353
|
-
? partition.collectionByGroup.get(source.primaryGroupId)
|
|
354
|
-
: undefined;
|
|
355
|
-
if (collection) return collection === "main" ? undefined : collection;
|
|
356
|
-
if (
|
|
357
|
-
projection.limitations.some((limitation) =>
|
|
358
|
-
limitation.sources.some((source) => source.id === sourceId),
|
|
359
|
-
)
|
|
360
|
-
)
|
|
361
|
-
return "coverage";
|
|
362
|
-
return undefined;
|
|
363
|
-
}
|
|
58
|
+
export type { InvestigationTimelineScope } from "./investigationEvidence/navigation";
|
|
364
59
|
|
|
365
60
|
export function InvestigationEvidencePane({
|
|
366
61
|
projection,
|
|
@@ -905,11 +600,9 @@ export function InvestigationEvidencePane({
|
|
|
905
600
|
</span>
|
|
906
601
|
) : null}
|
|
907
602
|
</div>
|
|
908
|
-
{story ? (
|
|
603
|
+
{story && storyPlacements ? (
|
|
909
604
|
<AnalysisStory
|
|
910
|
-
|
|
911
|
-
resolveItem={resolveStoryItem}
|
|
912
|
-
resolveRef={resolveStoryRef}
|
|
605
|
+
placements={storyPlacements}
|
|
913
606
|
trailing={
|
|
914
607
|
visibleRuledOut.length > 0 ? (
|
|
915
608
|
<RuledOutBlock
|
|
@@ -1032,3110 +725,3 @@ export function InvestigationEvidencePane({
|
|
|
1032
725
|
</EvidenceNavigationContext.Provider>
|
|
1033
726
|
);
|
|
1034
727
|
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Hypotheses the agent dropped, each pointing at the Radar observation whose
|
|
1038
|
-
* result contradicted it. Entries whose item could not be placed on an
|
|
1039
|
-
* observation are filtered out by the resolver and never rendered here.
|
|
1040
|
-
*/
|
|
1041
|
-
function RuledOutBlock({
|
|
1042
|
-
entries,
|
|
1043
|
-
onReveal,
|
|
1044
|
-
}: {
|
|
1045
|
-
entries: InvestigationCaseResolution["ruledOut"];
|
|
1046
|
-
onReveal: (item: InvestigationCaseItem) => void;
|
|
1047
|
-
}) {
|
|
1048
|
-
const headingId = useId();
|
|
1049
|
-
return (
|
|
1050
|
-
<section
|
|
1051
|
-
aria-labelledby={headingId}
|
|
1052
|
-
data-testid="investigation-ruled-out"
|
|
1053
|
-
className="rounded-lg border border-theme-border/80 bg-theme-base/20 px-3 py-2.5"
|
|
1054
|
-
>
|
|
1055
|
-
<h3
|
|
1056
|
-
id={headingId}
|
|
1057
|
-
className="flex items-center gap-2 text-xs font-semibold text-theme-text-secondary"
|
|
1058
|
-
>
|
|
1059
|
-
Also checked
|
|
1060
|
-
<span className="text-[10px] font-semibold uppercase tracking-wide text-accent-text">
|
|
1061
|
-
Agent
|
|
1062
|
-
</span>
|
|
1063
|
-
</h3>
|
|
1064
|
-
<ul className="mt-1.5 space-y-1.5">
|
|
1065
|
-
{entries.map((entry, position) => {
|
|
1066
|
-
const observation = entry.item.observation!;
|
|
1067
|
-
return (
|
|
1068
|
-
<li
|
|
1069
|
-
key={position}
|
|
1070
|
-
className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs"
|
|
1071
|
-
>
|
|
1072
|
-
<span className="min-w-0 text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
1073
|
-
{entry.hypothesis}
|
|
1074
|
-
</span>
|
|
1075
|
-
<button
|
|
1076
|
-
type="button"
|
|
1077
|
-
onClick={() => onReveal(entry.item)}
|
|
1078
|
-
className="shrink-0 rounded text-accent-text hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
1079
|
-
>
|
|
1080
|
-
See {observation.title}
|
|
1081
|
-
{entry.item.placement === "revision"
|
|
1082
|
-
? " (earlier observation)"
|
|
1083
|
-
: ""}
|
|
1084
|
-
</button>
|
|
1085
|
-
</li>
|
|
1086
|
-
);
|
|
1087
|
-
})}
|
|
1088
|
-
</ul>
|
|
1089
|
-
</section>
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
function AssessmentEvidenceQualification({
|
|
1094
|
-
resolution,
|
|
1095
|
-
onViewActivity,
|
|
1096
|
-
}: {
|
|
1097
|
-
resolution: InvestigationRootCauseEvidenceResolution;
|
|
1098
|
-
onViewActivity: () => void;
|
|
1099
|
-
}) {
|
|
1100
|
-
return (
|
|
1101
|
-
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2.5">
|
|
1102
|
-
<AlertTriangle
|
|
1103
|
-
className="mt-0.5 h-4 w-4 shrink-0 text-amber-500"
|
|
1104
|
-
aria-hidden
|
|
1105
|
-
/>
|
|
1106
|
-
<div className="min-w-0 flex-1">
|
|
1107
|
-
<p className="text-xs font-medium text-theme-text-primary">
|
|
1108
|
-
{resolution.status === "invalid"
|
|
1109
|
-
? "Assessment references could not be matched"
|
|
1110
|
-
: "Assessment does not cite specific Radar evidence"}
|
|
1111
|
-
</p>
|
|
1112
|
-
<p className="mt-0.5 text-xs leading-relaxed text-theme-text-tertiary">
|
|
1113
|
-
{resolution.status === "invalid"
|
|
1114
|
-
? "Radar could not match the assessment’s references to this investigation. Review Activity before acting."
|
|
1115
|
-
: "Review Activity and the Radar evidence below before acting on the agent’s conclusion."}
|
|
1116
|
-
</p>
|
|
1117
|
-
</div>
|
|
1118
|
-
<button
|
|
1119
|
-
type="button"
|
|
1120
|
-
onClick={onViewActivity}
|
|
1121
|
-
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"
|
|
1122
|
-
>
|
|
1123
|
-
View Activity
|
|
1124
|
-
</button>
|
|
1125
|
-
</div>
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
function EmptyCollection({
|
|
1130
|
-
collecting,
|
|
1131
|
-
hasEarlierEvidence,
|
|
1132
|
-
onViewActivity,
|
|
1133
|
-
}: {
|
|
1134
|
-
collecting: boolean;
|
|
1135
|
-
hasEarlierEvidence: boolean;
|
|
1136
|
-
onViewActivity: () => void;
|
|
1137
|
-
}) {
|
|
1138
|
-
return (
|
|
1139
|
-
<div className="rounded-lg border border-dashed border-theme-border px-4 py-5 text-center">
|
|
1140
|
-
{collecting ? (
|
|
1141
|
-
<Activity className="mx-auto h-5 w-5 text-accent" aria-hidden />
|
|
1142
|
-
) : (
|
|
1143
|
-
<SearchCheck
|
|
1144
|
-
className="mx-auto h-5 w-5 text-theme-text-tertiary"
|
|
1145
|
-
aria-hidden
|
|
1146
|
-
/>
|
|
1147
|
-
)}
|
|
1148
|
-
<p className="mt-2 text-sm font-medium text-theme-text-secondary">
|
|
1149
|
-
{collecting
|
|
1150
|
-
? "Evidence will appear here"
|
|
1151
|
-
: hasEarlierEvidence
|
|
1152
|
-
? "No current evidence was captured during verification"
|
|
1153
|
-
: "No relevant evidence to show yet"}
|
|
1154
|
-
</p>
|
|
1155
|
-
<p className="mx-auto mt-1 max-w-md text-xs leading-relaxed text-theme-text-tertiary">
|
|
1156
|
-
{collecting
|
|
1157
|
-
? "The Activity pane remains the live record while the agent investigates."
|
|
1158
|
-
: hasEarlierEvidence
|
|
1159
|
-
? "Earlier observations remain below. This does not prove that those conditions resolved."
|
|
1160
|
-
: "Investigation details are still available in Activity. This does not mean the resource is healthy."}
|
|
1161
|
-
</p>
|
|
1162
|
-
<button
|
|
1163
|
-
type="button"
|
|
1164
|
-
onClick={onViewActivity}
|
|
1165
|
-
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"
|
|
1166
|
-
>
|
|
1167
|
-
View Activity
|
|
1168
|
-
</button>
|
|
1169
|
-
</div>
|
|
1170
|
-
);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function CollapsedEvidenceCollection({
|
|
1174
|
-
id,
|
|
1175
|
-
title,
|
|
1176
|
-
description,
|
|
1177
|
-
groups,
|
|
1178
|
-
animateGroupIds,
|
|
1179
|
-
onViewSource,
|
|
1180
|
-
open,
|
|
1181
|
-
onOpenChange,
|
|
1182
|
-
totalCount = groups.length,
|
|
1183
|
-
keepWhenEmpty = false,
|
|
1184
|
-
children,
|
|
1185
|
-
}: {
|
|
1186
|
-
id: string;
|
|
1187
|
-
title: string;
|
|
1188
|
-
description: string;
|
|
1189
|
-
groups: InvestigationEvidenceGroup[];
|
|
1190
|
-
animateGroupIds: ReadonlySet<string>;
|
|
1191
|
-
onViewSource: (
|
|
1192
|
-
sourceId: string,
|
|
1193
|
-
excerpt?: InvestigationSourceExcerpt,
|
|
1194
|
-
) => void;
|
|
1195
|
-
open: boolean;
|
|
1196
|
-
onOpenChange: (open: boolean) => void;
|
|
1197
|
-
totalCount?: number;
|
|
1198
|
-
/** Render at zero: the children carry the empty state and the withheld-results note. */
|
|
1199
|
-
keepWhenEmpty?: boolean;
|
|
1200
|
-
children?: ReactNode;
|
|
1201
|
-
}) {
|
|
1202
|
-
const { elementRef, revealAfterToggle } = useDisclosureReveal<HTMLElement>();
|
|
1203
|
-
if (totalCount === 0 && !keepWhenEmpty) return null;
|
|
1204
|
-
const fullRowFlags = investigationEvidenceFullRowFlags(
|
|
1205
|
-
groups.map((group) => group.latest.data.type),
|
|
1206
|
-
);
|
|
1207
|
-
return (
|
|
1208
|
-
<section
|
|
1209
|
-
ref={elementRef}
|
|
1210
|
-
className="overflow-hidden rounded-lg border border-theme-border/80 bg-theme-base/20"
|
|
1211
|
-
>
|
|
1212
|
-
<button
|
|
1213
|
-
type="button"
|
|
1214
|
-
aria-expanded={open}
|
|
1215
|
-
aria-controls={id}
|
|
1216
|
-
onClick={() => {
|
|
1217
|
-
onOpenChange(!open);
|
|
1218
|
-
revealAfterToggle(!open);
|
|
1219
|
-
}}
|
|
1220
|
-
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"
|
|
1221
|
-
>
|
|
1222
|
-
<span className="min-w-0 flex-1">
|
|
1223
|
-
<span className="block text-xs font-semibold text-theme-text-secondary">
|
|
1224
|
-
{title}
|
|
1225
|
-
</span>
|
|
1226
|
-
{description ? (
|
|
1227
|
-
<span className="block text-xs text-theme-text-tertiary">
|
|
1228
|
-
{description}
|
|
1229
|
-
</span>
|
|
1230
|
-
) : null}
|
|
1231
|
-
</span>
|
|
1232
|
-
<span className="shrink-0 font-mono text-xs text-theme-text-tertiary">
|
|
1233
|
-
{totalCount}
|
|
1234
|
-
</span>
|
|
1235
|
-
<CollapseChevron open={open} className="h-4 w-4" />
|
|
1236
|
-
</button>
|
|
1237
|
-
<div id={id}>
|
|
1238
|
-
<Collapse open={open}>
|
|
1239
|
-
<div
|
|
1240
|
-
className={clsx(
|
|
1241
|
-
"grid items-start gap-2.5 @min-[760px]/evidence:grid-cols-2",
|
|
1242
|
-
"border-t border-theme-border/60 p-2.5",
|
|
1243
|
-
)}
|
|
1244
|
-
>
|
|
1245
|
-
{groups.map((group, index) => (
|
|
1246
|
-
<EvidenceCard
|
|
1247
|
-
key={group.id}
|
|
1248
|
-
group={group}
|
|
1249
|
-
|
|
1250
|
-
animateArrival={animateGroupIds.has(group.id)}
|
|
1251
|
-
onViewSource={onViewSource}
|
|
1252
|
-
spanFullRow={fullRowFlags[index]}
|
|
1253
|
-
prominence="secondary"
|
|
1254
|
-
/>
|
|
1255
|
-
))}
|
|
1256
|
-
{children ? <div className="col-span-full">{children}</div> : null}
|
|
1257
|
-
</div>
|
|
1258
|
-
</Collapse>
|
|
1259
|
-
</div>
|
|
1260
|
-
</section>
|
|
1261
|
-
);
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
function CoverageStrip({
|
|
1265
|
-
groups,
|
|
1266
|
-
visibleGroupIds,
|
|
1267
|
-
summary,
|
|
1268
|
-
onViewSource,
|
|
1269
|
-
open,
|
|
1270
|
-
onOpenChange,
|
|
1271
|
-
}: {
|
|
1272
|
-
groups: EvidenceCoverageGroup[];
|
|
1273
|
-
visibleGroupIds: ReadonlySet<string>;
|
|
1274
|
-
summary: string;
|
|
1275
|
-
onViewSource: (
|
|
1276
|
-
sourceId: string,
|
|
1277
|
-
excerpt?: InvestigationSourceExcerpt,
|
|
1278
|
-
) => void;
|
|
1279
|
-
open: boolean;
|
|
1280
|
-
onOpenChange: (open: boolean) => void;
|
|
1281
|
-
}) {
|
|
1282
|
-
const hasError = groups.some((group) => group.hasError);
|
|
1283
|
-
const historyOnly = groups.every((group) => group.historyOnly);
|
|
1284
|
-
const quiet = historyOnly;
|
|
1285
|
-
const { elementRef, revealAfterToggle } =
|
|
1286
|
-
useDisclosureReveal<HTMLDivElement>();
|
|
1287
|
-
const regionId = "investigation-evidence-coverage";
|
|
1288
|
-
const anchoredSources = new Set<string>();
|
|
1289
|
-
return (
|
|
1290
|
-
<div
|
|
1291
|
-
ref={elementRef}
|
|
1292
|
-
className="overflow-hidden rounded-lg border border-theme-border/80 bg-theme-base/20"
|
|
1293
|
-
>
|
|
1294
|
-
<button
|
|
1295
|
-
type="button"
|
|
1296
|
-
aria-expanded={open}
|
|
1297
|
-
aria-controls={regionId}
|
|
1298
|
-
onClick={() => {
|
|
1299
|
-
onOpenChange(!open);
|
|
1300
|
-
revealAfterToggle(!open);
|
|
1301
|
-
}}
|
|
1302
|
-
className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left hover:bg-theme-hover/50"
|
|
1303
|
-
>
|
|
1304
|
-
{quiet ? (
|
|
1305
|
-
<Info
|
|
1306
|
-
className="h-4 w-4 shrink-0 text-theme-text-tertiary"
|
|
1307
|
-
aria-hidden
|
|
1308
|
-
/>
|
|
1309
|
-
) : hasError ? (
|
|
1310
|
-
<CircleAlert className="h-4 w-4 shrink-0 text-red-400" aria-hidden />
|
|
1311
|
-
) : (
|
|
1312
|
-
<AlertTriangle
|
|
1313
|
-
className="h-4 w-4 shrink-0 text-amber-500"
|
|
1314
|
-
aria-hidden
|
|
1315
|
-
/>
|
|
1316
|
-
)}
|
|
1317
|
-
<span className="min-w-0 flex-1">
|
|
1318
|
-
<span
|
|
1319
|
-
className={clsx(
|
|
1320
|
-
"block text-xs",
|
|
1321
|
-
quiet
|
|
1322
|
-
? "font-medium text-theme-text-secondary"
|
|
1323
|
-
: "font-semibold text-theme-text-primary",
|
|
1324
|
-
)}
|
|
1325
|
-
>
|
|
1326
|
-
{historyOnly
|
|
1327
|
-
? "Change history is limited"
|
|
1328
|
-
: "Evidence coverage is incomplete"}
|
|
1329
|
-
</span>
|
|
1330
|
-
{!historyOnly && !open ? (
|
|
1331
|
-
<span className="line-clamp-2 text-xs text-theme-text-tertiary [overflow-wrap:anywhere]">
|
|
1332
|
-
{summary}
|
|
1333
|
-
</span>
|
|
1334
|
-
) : null}
|
|
1335
|
-
</span>
|
|
1336
|
-
<CollapseChevron open={open} className="h-4 w-4" />
|
|
1337
|
-
</button>
|
|
1338
|
-
<div id={regionId}>
|
|
1339
|
-
<Collapse open={open}>
|
|
1340
|
-
<div className="divide-y divide-theme-border/60 border-t border-theme-border/60">
|
|
1341
|
-
{groups.map((group) => {
|
|
1342
|
-
const sourceIds = group.limitations
|
|
1343
|
-
.flatMap((item) => item.sources)
|
|
1344
|
-
.filter((source) => {
|
|
1345
|
-
if (
|
|
1346
|
-
(source.primaryGroupId &&
|
|
1347
|
-
visibleGroupIds.has(source.primaryGroupId)) ||
|
|
1348
|
-
anchoredSources.has(source.id)
|
|
1349
|
-
)
|
|
1350
|
-
return false;
|
|
1351
|
-
anchoredSources.add(source.id);
|
|
1352
|
-
return true;
|
|
1353
|
-
})
|
|
1354
|
-
.map((source) => source.id);
|
|
1355
|
-
return (
|
|
1356
|
-
<CoverageGroupRow
|
|
1357
|
-
key={group.label}
|
|
1358
|
-
group={group}
|
|
1359
|
-
sourceIds={sourceIds}
|
|
1360
|
-
onViewSource={onViewSource}
|
|
1361
|
-
/>
|
|
1362
|
-
);
|
|
1363
|
-
})}
|
|
1364
|
-
</div>
|
|
1365
|
-
</Collapse>
|
|
1366
|
-
</div>
|
|
1367
|
-
</div>
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
function CoverageGroupRow({
|
|
1372
|
-
group,
|
|
1373
|
-
sourceIds,
|
|
1374
|
-
onViewSource,
|
|
1375
|
-
}: {
|
|
1376
|
-
group: EvidenceCoverageGroup;
|
|
1377
|
-
sourceIds: string[];
|
|
1378
|
-
onViewSource: (
|
|
1379
|
-
sourceId: string,
|
|
1380
|
-
excerpt?: InvestigationSourceExcerpt,
|
|
1381
|
-
) => void;
|
|
1382
|
-
}) {
|
|
1383
|
-
const [open, setOpen] = useState(false);
|
|
1384
|
-
const regionId = useId();
|
|
1385
|
-
const { elementRef, revealAfterToggle } =
|
|
1386
|
-
useDisclosureReveal<HTMLDivElement>();
|
|
1387
|
-
// One limitation whose message is the whole summary would repeat itself as
|
|
1388
|
-
// its own detail row; show its source link on the summary row instead.
|
|
1389
|
-
const single =
|
|
1390
|
-
group.limitations.length === 1 &&
|
|
1391
|
-
group.limitations[0].message === group.summary
|
|
1392
|
-
? group.limitations[0]
|
|
1393
|
-
: undefined;
|
|
1394
|
-
const anchors = sourceIds.map((id) => (
|
|
1395
|
-
<span
|
|
1396
|
-
key={id}
|
|
1397
|
-
id={investigationEvidenceSourceDomId(id)}
|
|
1398
|
-
className="sr-only scroll-mt-14"
|
|
1399
|
-
aria-hidden
|
|
1400
|
-
/>
|
|
1401
|
-
));
|
|
1402
|
-
if (single) {
|
|
1403
|
-
return (
|
|
1404
|
-
<div
|
|
1405
|
-
ref={elementRef}
|
|
1406
|
-
data-evidence-source-container
|
|
1407
|
-
tabIndex={-1}
|
|
1408
|
-
aria-label={`Evidence limitation for ${group.label}: ${group.summary}`}
|
|
1409
|
-
className="flex items-center gap-2 px-3 py-2.5 text-xs outline-none focus:ring-2 focus:ring-accent/40"
|
|
1410
|
-
>
|
|
1411
|
-
{anchors}
|
|
1412
|
-
{group.hasError ? (
|
|
1413
|
-
<CircleAlert
|
|
1414
|
-
className="h-3.5 w-3.5 shrink-0 text-red-400"
|
|
1415
|
-
aria-hidden
|
|
1416
|
-
/>
|
|
1417
|
-
) : single.kind === "truncated" ? (
|
|
1418
|
-
<AlertTriangle
|
|
1419
|
-
className="h-3.5 w-3.5 shrink-0 text-amber-500"
|
|
1420
|
-
aria-hidden
|
|
1421
|
-
/>
|
|
1422
|
-
) : (
|
|
1423
|
-
<Info
|
|
1424
|
-
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
1425
|
-
aria-hidden
|
|
1426
|
-
/>
|
|
1427
|
-
)}
|
|
1428
|
-
<p className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
|
|
1429
|
-
<span className="font-medium text-theme-text-primary">
|
|
1430
|
-
{group.label}:
|
|
1431
|
-
</span>{" "}
|
|
1432
|
-
{group.summary}
|
|
1433
|
-
</p>
|
|
1434
|
-
{single.sources.at(-1) ? (
|
|
1435
|
-
<SourceButton
|
|
1436
|
-
ariaLabel={`View result for ${group.label}`}
|
|
1437
|
-
buttonLabel={
|
|
1438
|
-
single.sources.length > 1 ? "View latest in Activity" : undefined
|
|
1439
|
-
}
|
|
1440
|
-
onClick={() => onViewSource(single.sources.at(-1)!.id)}
|
|
1441
|
-
/>
|
|
1442
|
-
) : null}
|
|
1443
|
-
</div>
|
|
1444
|
-
);
|
|
1445
|
-
}
|
|
1446
|
-
return (
|
|
1447
|
-
<div
|
|
1448
|
-
ref={elementRef}
|
|
1449
|
-
data-evidence-source-container
|
|
1450
|
-
tabIndex={-1}
|
|
1451
|
-
aria-label={`Evidence limitation for ${group.label}: ${group.summary}`}
|
|
1452
|
-
className="outline-none focus:ring-2 focus:ring-accent/40"
|
|
1453
|
-
>
|
|
1454
|
-
{anchors}
|
|
1455
|
-
<button
|
|
1456
|
-
type="button"
|
|
1457
|
-
aria-expanded={open}
|
|
1458
|
-
aria-controls={regionId}
|
|
1459
|
-
onClick={() => {
|
|
1460
|
-
setOpen(!open);
|
|
1461
|
-
revealAfterToggle(!open);
|
|
1462
|
-
}}
|
|
1463
|
-
className="flex w-full items-center gap-2 px-3 py-2.5 text-left text-xs hover:bg-theme-hover/50"
|
|
1464
|
-
>
|
|
1465
|
-
{group.hasError ? (
|
|
1466
|
-
<CircleAlert
|
|
1467
|
-
className="h-3.5 w-3.5 shrink-0 text-red-400"
|
|
1468
|
-
aria-hidden
|
|
1469
|
-
/>
|
|
1470
|
-
) : (
|
|
1471
|
-
<Info
|
|
1472
|
-
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
1473
|
-
aria-hidden
|
|
1474
|
-
/>
|
|
1475
|
-
)}
|
|
1476
|
-
<span className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
|
|
1477
|
-
<span className="font-medium text-theme-text-primary">
|
|
1478
|
-
{group.label}:{" "}
|
|
1479
|
-
</span>
|
|
1480
|
-
{group.summary}
|
|
1481
|
-
</span>
|
|
1482
|
-
<CollapseChevron open={open} className="h-3.5 w-3.5 shrink-0" />
|
|
1483
|
-
</button>
|
|
1484
|
-
<div id={regionId}>
|
|
1485
|
-
<Collapse open={open}>
|
|
1486
|
-
<ul className="space-y-2 border-t border-theme-border/60 px-3 py-2.5">
|
|
1487
|
-
{group.limitations.map((limitation, index) => (
|
|
1488
|
-
<li
|
|
1489
|
-
key={index}
|
|
1490
|
-
className="flex min-w-0 items-start gap-2 text-xs"
|
|
1491
|
-
>
|
|
1492
|
-
{limitation.kind === "error" ? (
|
|
1493
|
-
<CircleAlert
|
|
1494
|
-
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-400"
|
|
1495
|
-
aria-hidden
|
|
1496
|
-
/>
|
|
1497
|
-
) : limitation.kind === "truncated" ? (
|
|
1498
|
-
<AlertTriangle
|
|
1499
|
-
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500"
|
|
1500
|
-
aria-hidden
|
|
1501
|
-
/>
|
|
1502
|
-
) : (
|
|
1503
|
-
<Info
|
|
1504
|
-
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
1505
|
-
aria-hidden
|
|
1506
|
-
/>
|
|
1507
|
-
)}
|
|
1508
|
-
<p className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
|
|
1509
|
-
<span className="font-medium text-theme-text-primary">
|
|
1510
|
-
{limitation.source}:
|
|
1511
|
-
</span>{" "}
|
|
1512
|
-
{limitation.message}
|
|
1513
|
-
</p>
|
|
1514
|
-
{limitation.sources.at(-1) ? (
|
|
1515
|
-
<SourceButton
|
|
1516
|
-
ariaLabel={`View result for ${limitation.source}`}
|
|
1517
|
-
buttonLabel={
|
|
1518
|
-
limitation.sources.length > 1
|
|
1519
|
-
? "View latest in Activity"
|
|
1520
|
-
: undefined
|
|
1521
|
-
}
|
|
1522
|
-
onClick={() => onViewSource(limitation.sources.at(-1)!.id)}
|
|
1523
|
-
/>
|
|
1524
|
-
) : null}
|
|
1525
|
-
</li>
|
|
1526
|
-
))}
|
|
1527
|
-
</ul>
|
|
1528
|
-
</Collapse>
|
|
1529
|
-
</div>
|
|
1530
|
-
</div>
|
|
1531
|
-
);
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
function previousDifferentObservations(
|
|
1535
|
-
group: InvestigationEvidenceGroup,
|
|
1536
|
-
citedOrder?: number,
|
|
1537
|
-
boundOrders: ReadonlySet<number> = new Set(),
|
|
1538
|
-
) {
|
|
1539
|
-
const seen = new Set([evidenceDisplaySnapshot(group.latest)]);
|
|
1540
|
-
const pinned = (observation: InvestigationEvidenceObservation) =>
|
|
1541
|
-
observation.source.order === citedOrder ||
|
|
1542
|
-
boundOrders.has(observation.source.order);
|
|
1543
|
-
return [...group.observations].reverse().filter((observation) => {
|
|
1544
|
-
if (observation === group.latest) return false;
|
|
1545
|
-
// An observation bound to an agent item always keeps its own row: display
|
|
1546
|
-
// equivalence is a folding rule for unpinned rechecks, never a reason to
|
|
1547
|
-
// move a claim onto different content.
|
|
1548
|
-
if (boundOrders.has(observation.source.order)) return true;
|
|
1549
|
-
// A pinned observation wins over an unpinned twin with the same display
|
|
1550
|
-
// content.
|
|
1551
|
-
if (
|
|
1552
|
-
!pinned(observation) &&
|
|
1553
|
-
group.observations.some(
|
|
1554
|
-
(other) =>
|
|
1555
|
-
pinned(other) &&
|
|
1556
|
-
evidenceDisplaySnapshot(other) ===
|
|
1557
|
-
evidenceDisplaySnapshot(observation),
|
|
1558
|
-
)
|
|
1559
|
-
)
|
|
1560
|
-
return false;
|
|
1561
|
-
const snapshot = evidenceDisplaySnapshot(observation);
|
|
1562
|
-
if (seen.has(snapshot)) return false;
|
|
1563
|
-
seen.add(snapshot);
|
|
1564
|
-
return true;
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
export function investigationEvidenceShouldRevealHistory(
|
|
1569
|
-
group: InvestigationEvidenceGroup,
|
|
1570
|
-
sourceId?: string,
|
|
1571
|
-
/** Sources whose observation carries an agent item and so always has a row. */
|
|
1572
|
-
boundSourceIds: ReadonlySet<string> = new Set(),
|
|
1573
|
-
): boolean {
|
|
1574
|
-
return (
|
|
1575
|
-
Boolean(sourceId) &&
|
|
1576
|
-
group.latest.source.id !== sourceId &&
|
|
1577
|
-
group.observations.some(
|
|
1578
|
-
(observation) =>
|
|
1579
|
-
observation.source.id === sourceId &&
|
|
1580
|
-
(boundSourceIds.has(observation.source.id) ||
|
|
1581
|
-
evidenceDisplaySnapshot(observation) !==
|
|
1582
|
-
evidenceDisplaySnapshot(group.latest)),
|
|
1583
|
-
)
|
|
1584
|
-
);
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
function EvidenceCard({
|
|
1588
|
-
group,
|
|
1589
|
-
observation: observationOverride,
|
|
1590
|
-
domId = group.id,
|
|
1591
|
-
animateArrival,
|
|
1592
|
-
onViewSource,
|
|
1593
|
-
spanFullRow = false,
|
|
1594
|
-
prominence = "primary",
|
|
1595
|
-
compact = false,
|
|
1596
|
-
placedInStory = false,
|
|
1597
|
-
gapRoles,
|
|
1598
|
-
noteMode = "full",
|
|
1599
|
-
}: {
|
|
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;
|
|
1607
|
-
/** Stable layout/scroll identity when an existing card changes section. */
|
|
1608
|
-
domId?: string;
|
|
1609
|
-
animateArrival: boolean;
|
|
1610
|
-
onViewSource: (
|
|
1611
|
-
sourceId: string,
|
|
1612
|
-
excerpt?: InvestigationSourceExcerpt,
|
|
1613
|
-
) => void;
|
|
1614
|
-
/** Fill both columns when this card has no compact row partner. */
|
|
1615
|
-
spanFullRow?: boolean;
|
|
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";
|
|
1632
|
-
}) {
|
|
1633
|
-
const {
|
|
1634
|
-
onOpenResource,
|
|
1635
|
-
onOpenTimeline,
|
|
1636
|
-
revealSourceId,
|
|
1637
|
-
revealRequestId,
|
|
1638
|
-
citedOrderByGroup,
|
|
1639
|
-
expandedGroupIds,
|
|
1640
|
-
onGroupOpenChange,
|
|
1641
|
-
metricsMarkersBySource,
|
|
1642
|
-
caseByGroup,
|
|
1643
|
-
excludedByItem,
|
|
1644
|
-
} = useContext(EvidenceNavigationContext);
|
|
1645
|
-
const open = expandedGroupIds?.has(group.id) ?? false;
|
|
1646
|
-
const setOpen = useCallback(
|
|
1647
|
-
(value: boolean) => onGroupOpenChange?.(group.id, value),
|
|
1648
|
-
[onGroupOpenChange, group.id],
|
|
1649
|
-
);
|
|
1650
|
-
const { elementRef, revealAfterToggle } = useDisclosureReveal<HTMLElement>();
|
|
1651
|
-
const observation = observationOverride ?? group.latest;
|
|
1652
|
-
const supersededRead =
|
|
1653
|
-
observationOverride !== undefined && observationOverride !== group.latest;
|
|
1654
|
-
const displaySummary =
|
|
1655
|
-
observation.data.type === "crash" && observation.summary
|
|
1656
|
-
? parseLogLine(observation.summary).content
|
|
1657
|
-
: observation.summary;
|
|
1658
|
-
const citedOrder = citedOrderByGroup?.get(group.id);
|
|
1659
|
-
const caseItems = caseByGroup?.get(group.id) ?? [];
|
|
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
|
-
);
|
|
1670
|
-
const revisionItems = caseItems.filter(
|
|
1671
|
-
(item) => item.placement === "revision",
|
|
1672
|
-
);
|
|
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
|
-
: [];
|
|
1697
|
-
const previousObservations = previousDifferentObservations(
|
|
1698
|
-
group,
|
|
1699
|
-
citedOrder,
|
|
1700
|
-
new Set(revisionItems.map((item) => item.observation!.source.order)),
|
|
1701
|
-
);
|
|
1702
|
-
const meaningfulHistory = previousObservations.length > 0;
|
|
1703
|
-
const citedObservation = group.observations.find(
|
|
1704
|
-
(item) => item.source.order === citedOrder,
|
|
1705
|
-
);
|
|
1706
|
-
const differsFromAssessment =
|
|
1707
|
-
citedObservation &&
|
|
1708
|
-
evidenceDisplaySnapshot(citedObservation) !==
|
|
1709
|
-
evidenceDisplaySnapshot(observation);
|
|
1710
|
-
const bodyId = `${domId}-body`;
|
|
1711
|
-
const hasEvidenceDetails = evidenceHasDetails(
|
|
1712
|
-
observation.data,
|
|
1713
|
-
observation.summary,
|
|
1714
|
-
);
|
|
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);
|
|
1750
|
-
const revealHistory = investigationEvidenceShouldRevealHistory(
|
|
1751
|
-
group,
|
|
1752
|
-
revealSourceId,
|
|
1753
|
-
new Set(revisionItems.map((item) => item.observation!.source.id)),
|
|
1754
|
-
);
|
|
1755
|
-
useLayoutEffect(() => {
|
|
1756
|
-
const destination = revealSourceId
|
|
1757
|
-
? document.getElementById(
|
|
1758
|
-
investigationEvidenceSourceDomId(revealSourceId),
|
|
1759
|
-
)
|
|
1760
|
-
: null;
|
|
1761
|
-
if (
|
|
1762
|
-
revealHistory ||
|
|
1763
|
-
(destination && elementRef.current?.contains(destination))
|
|
1764
|
-
)
|
|
1765
|
-
setOpen(true);
|
|
1766
|
-
}, [revealHistory, revealSourceId, revealRequestId, elementRef, setOpen]);
|
|
1767
|
-
const wide = spanFullRow || evidenceTypePrefersFullRow(observation.data.type);
|
|
1768
|
-
const resourceRef = investigationEvidenceSubjectRef(observation.data);
|
|
1769
|
-
const resourceIdentity = resourceRef
|
|
1770
|
-
? `${resourceRef.namespace ? `${resourceRef.namespace}/` : ""}${resourceRef.name}`
|
|
1771
|
-
: undefined;
|
|
1772
|
-
const primarySources = uniquePrimarySources(group);
|
|
1773
|
-
const inlineSecretKeys =
|
|
1774
|
-
observation.data.type === "resource" &&
|
|
1775
|
-
observation.data.resource.kind.toLowerCase() === "secret" &&
|
|
1776
|
-
!canExpand;
|
|
1777
|
-
const headerContent = (
|
|
1778
|
-
<>
|
|
1779
|
-
<EvidenceIcon observation={observation} prominence={prominence} />
|
|
1780
|
-
<span className="min-w-0 flex-1">
|
|
1781
|
-
<span className="flex flex-wrap items-center gap-1.5">
|
|
1782
|
-
<span className="text-sm font-semibold leading-snug text-theme-text-primary">
|
|
1783
|
-
{observation.title}
|
|
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}
|
|
1790
|
-
</span>
|
|
1791
|
-
{observation.relevance === "broader" &&
|
|
1792
|
-
resourceRef &&
|
|
1793
|
-
resourceIdentity &&
|
|
1794
|
-
!observation.title.includes(resourceIdentity) ? (
|
|
1795
|
-
<span className="mt-0.5 block text-xs text-theme-text-secondary">
|
|
1796
|
-
{resourceIdentity} · {resourceRef.kind}
|
|
1797
|
-
</span>
|
|
1798
|
-
) : null}
|
|
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 ? (
|
|
1813
|
-
<span
|
|
1814
|
-
className={clsx(
|
|
1815
|
-
"mt-0.5 block text-xs leading-relaxed text-theme-text-secondary",
|
|
1816
|
-
!open && !inlineSecretKeys && "line-clamp-2",
|
|
1817
|
-
"[overflow-wrap:anywhere]",
|
|
1818
|
-
)}
|
|
1819
|
-
>
|
|
1820
|
-
{displaySummary}
|
|
1821
|
-
</span>
|
|
1822
|
-
) : null}
|
|
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 ? (
|
|
1833
|
-
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
1834
|
-
Previous observation · not confirmed by the latest check
|
|
1835
|
-
</span>
|
|
1836
|
-
) : differsFromAssessment &&
|
|
1837
|
-
citedOrder != null &&
|
|
1838
|
-
observation.source.order !== citedOrder ? (
|
|
1839
|
-
<span className="mt-0.5 block text-xs text-theme-text-tertiary">
|
|
1840
|
-
{observation.source.order > citedOrder
|
|
1841
|
-
? "Observed after the assessment’s source"
|
|
1842
|
-
: "Earlier observation retained from a more direct source"}
|
|
1843
|
-
</span>
|
|
1844
|
-
) : null}
|
|
1845
|
-
</span>
|
|
1846
|
-
{canExpand ? (
|
|
1847
|
-
<CollapseChevron open={open} className="h-4 w-4 self-center" />
|
|
1848
|
-
) : null}
|
|
1849
|
-
</>
|
|
1850
|
-
);
|
|
1851
|
-
return (
|
|
1852
|
-
<article
|
|
1853
|
-
data-evidence-source={observation.source.id}
|
|
1854
|
-
ref={elementRef}
|
|
1855
|
-
id={domId}
|
|
1856
|
-
data-evidence-card
|
|
1857
|
-
tabIndex={-1}
|
|
1858
|
-
aria-label={`${observation.title} evidence`}
|
|
1859
|
-
className={clsx(
|
|
1860
|
-
"@container/card scroll-mt-14 overflow-hidden outline-none focus:ring-2 focus:ring-accent/50 data-[source-related]:ring-2 data-[source-related]:ring-accent/35",
|
|
1861
|
-
"rounded-lg border",
|
|
1862
|
-
// The card the agent calls the cause sorts first and is often the
|
|
1863
|
-
// calmest thing on screen: a cause is frequently a Secret or a
|
|
1864
|
-
// ConfigMap that Radar has no reason to colour, while every card
|
|
1865
|
-
// echoing the failure below it carries red. Lifting the surface marks
|
|
1866
|
-
// it without borrowing a severity hue, and without touching the left
|
|
1867
|
-
// rule (Radar's severity) or the ring (the source you are viewing).
|
|
1868
|
-
agentCause ? "bg-theme-elevated shadow-theme-sm" : "bg-theme-surface",
|
|
1869
|
-
toneBorder(observation.tone, observation.tier, prominence),
|
|
1870
|
-
wide && "@min-[760px]/evidence:col-span-2",
|
|
1871
|
-
animateArrival && "animate-transcript-enter",
|
|
1872
|
-
)}
|
|
1873
|
-
>
|
|
1874
|
-
{primarySources.map((source) => (
|
|
1875
|
-
<span
|
|
1876
|
-
key={source.id}
|
|
1877
|
-
id={investigationEvidenceSourceDomId(source.id)}
|
|
1878
|
-
className="block scroll-mt-14"
|
|
1879
|
-
aria-hidden
|
|
1880
|
-
/>
|
|
1881
|
-
))}
|
|
1882
|
-
<div className="flex min-w-0 items-stretch">
|
|
1883
|
-
{canExpand ? (
|
|
1884
|
-
<button
|
|
1885
|
-
type="button"
|
|
1886
|
-
aria-expanded={open}
|
|
1887
|
-
aria-controls={bodyId}
|
|
1888
|
-
onClick={() => {
|
|
1889
|
-
const opening = !open;
|
|
1890
|
-
setOpen(opening);
|
|
1891
|
-
revealAfterToggle(opening);
|
|
1892
|
-
}}
|
|
1893
|
-
className={clsx(
|
|
1894
|
-
"flex min-w-0 flex-1 items-start text-left hover:bg-theme-hover/60",
|
|
1895
|
-
prominence === "primary"
|
|
1896
|
-
? "gap-2.5 px-3 py-2.5"
|
|
1897
|
-
: "gap-2 px-2.5 py-2",
|
|
1898
|
-
)}
|
|
1899
|
-
>
|
|
1900
|
-
{headerContent}
|
|
1901
|
-
</button>
|
|
1902
|
-
) : (
|
|
1903
|
-
<div
|
|
1904
|
-
className={clsx(
|
|
1905
|
-
"flex min-w-0 flex-1 items-start text-left",
|
|
1906
|
-
prominence === "primary"
|
|
1907
|
-
? "gap-2.5 px-3 py-2.5"
|
|
1908
|
-
: "gap-2 px-2.5 py-2",
|
|
1909
|
-
)}
|
|
1910
|
-
>
|
|
1911
|
-
{headerContent}
|
|
1912
|
-
</div>
|
|
1913
|
-
)}
|
|
1914
|
-
<div className="flex shrink-0 items-center gap-0.5 px-2">
|
|
1915
|
-
{observation.data.type === "changes" ? (
|
|
1916
|
-
<EvidenceCaveat data={observation.data} />
|
|
1917
|
-
) : null}
|
|
1918
|
-
<SourceButton
|
|
1919
|
-
ariaLabel={`View result for ${observation.title}`}
|
|
1920
|
-
compact
|
|
1921
|
-
onClick={() =>
|
|
1922
|
-
onViewSource(
|
|
1923
|
-
observation.source.id,
|
|
1924
|
-
evidenceSourceExcerpt(observation.data),
|
|
1925
|
-
)
|
|
1926
|
-
}
|
|
1927
|
-
/>
|
|
1928
|
-
<OpenResourceButton
|
|
1929
|
-
resourceRef={resourceRef}
|
|
1930
|
-
onOpenResource={onOpenResource}
|
|
1931
|
-
compact
|
|
1932
|
-
/>
|
|
1933
|
-
{observation.data.type === "changes" && observation.data.subject ? (
|
|
1934
|
-
<OpenTimelineButton
|
|
1935
|
-
scope={observation.data.subject}
|
|
1936
|
-
onOpenTimeline={onOpenTimeline}
|
|
1937
|
-
/>
|
|
1938
|
-
) : null}
|
|
1939
|
-
</div>
|
|
1940
|
-
</div>
|
|
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) ? (
|
|
2039
|
-
<div
|
|
2040
|
-
className={clsx(
|
|
2041
|
-
"space-y-1.5",
|
|
2042
|
-
prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2",
|
|
2043
|
-
)}
|
|
2044
|
-
>
|
|
2045
|
-
{cardItems.map((item) => (
|
|
2046
|
-
<AgentClaimNote
|
|
2047
|
-
key={item.index}
|
|
2048
|
-
claim={compact ? "" : item.claim}
|
|
2049
|
-
role={item.role}
|
|
2050
|
-
gap={
|
|
2051
|
-
compact || (gapRoles && !gapRoles.has(item.role))
|
|
2052
|
-
? undefined
|
|
2053
|
-
: item.gap
|
|
2054
|
-
}
|
|
2055
|
-
excludes={excludedByItem?.get(investigationCaseItemKey(item))}
|
|
2056
|
-
className="pt-1.5"
|
|
2057
|
-
/>
|
|
2058
|
-
))}
|
|
2059
|
-
</div>
|
|
2060
|
-
) : null}
|
|
2061
|
-
{canExpand && hasExpandableBody ? (
|
|
2062
|
-
<div id={bodyId}>
|
|
2063
|
-
<Collapse open={open}>
|
|
2064
|
-
<div
|
|
2065
|
-
className={clsx(
|
|
2066
|
-
"space-y-3 border-t border-theme-border/60",
|
|
2067
|
-
prominence === "primary" ? "px-3 py-3" : "px-2.5 py-2.5",
|
|
2068
|
-
)}
|
|
2069
|
-
>
|
|
2070
|
-
{hasEvidenceDetails && !inlineLogs && !inlineMetrics ? (
|
|
2071
|
-
<EvidenceBody
|
|
2072
|
-
data={observation.data}
|
|
2073
|
-
condensed={storyCard}
|
|
2074
|
-
cardSummary={observation.summary}
|
|
2075
|
-
changeCoverage={metricsMarkersBySource?.get(
|
|
2076
|
-
observation.source.id,
|
|
2077
|
-
)}
|
|
2078
|
-
/>
|
|
2079
|
-
) : null}
|
|
2080
|
-
{meaningfulHistory ? (
|
|
2081
|
-
<RevisionHistory
|
|
2082
|
-
observations={previousObservations}
|
|
2083
|
-
citedOrder={citedOrder}
|
|
2084
|
-
caseItems={revisionItems}
|
|
2085
|
-
reveal={revealHistory}
|
|
2086
|
-
revealRequestId={revealRequestId}
|
|
2087
|
-
onViewSource={onViewSource}
|
|
2088
|
-
/>
|
|
2089
|
-
) : null}
|
|
2090
|
-
{observation.data.type !== "changes" ? (
|
|
2091
|
-
<EvidenceCaveat data={observation.data} />
|
|
2092
|
-
) : null}
|
|
2093
|
-
</div>
|
|
2094
|
-
</Collapse>
|
|
2095
|
-
</div>
|
|
2096
|
-
) : null}
|
|
2097
|
-
</article>
|
|
2098
|
-
);
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
function OpenResourceButton({
|
|
2102
|
-
resourceRef,
|
|
2103
|
-
onOpenResource,
|
|
2104
|
-
compact = false,
|
|
2105
|
-
}: {
|
|
2106
|
-
resourceRef?: DiagnosisResourceRef;
|
|
2107
|
-
onOpenResource?: (ref: DiagnosisResourceRef) => void;
|
|
2108
|
-
compact?: boolean;
|
|
2109
|
-
}) {
|
|
2110
|
-
if (!resourceRef || !onOpenResource) return null;
|
|
2111
|
-
const identity = `${resourceRef.namespace ? `${resourceRef.namespace}/` : ""}${resourceRef.name}`;
|
|
2112
|
-
const label = `Open current ${displayKind(resourceRef.kind)} ${identity} in Radar`;
|
|
2113
|
-
return (
|
|
2114
|
-
<Tooltip
|
|
2115
|
-
content={label}
|
|
2116
|
-
delay={350}
|
|
2117
|
-
position="left"
|
|
2118
|
-
wrapperClassName="flex shrink-0"
|
|
2119
|
-
>
|
|
2120
|
-
<button
|
|
2121
|
-
type="button"
|
|
2122
|
-
aria-label={label}
|
|
2123
|
-
onClick={() => onOpenResource(resourceRef)}
|
|
2124
|
-
className={clsx(
|
|
2125
|
-
"flex h-7 shrink-0 items-center justify-center rounded text-theme-text-tertiary transition-colors hover:bg-theme-hover hover:text-accent-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent",
|
|
2126
|
-
"gap-1 px-2 text-xs font-medium",
|
|
2127
|
-
)}
|
|
2128
|
-
>
|
|
2129
|
-
{compact ? null : (
|
|
2130
|
-
<span>Open current {displayKind(resourceRef.kind)}</span>
|
|
2131
|
-
)}
|
|
2132
|
-
<SquareArrowOutUpRight className="h-3.5 w-3.5" aria-hidden />
|
|
2133
|
-
</button>
|
|
2134
|
-
</Tooltip>
|
|
2135
|
-
);
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
function OpenTimelineButton({
|
|
2139
|
-
scope,
|
|
2140
|
-
onOpenTimeline,
|
|
2141
|
-
}: {
|
|
2142
|
-
scope: InvestigationTimelineScope;
|
|
2143
|
-
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
2144
|
-
}) {
|
|
2145
|
-
if (!onOpenTimeline) return null;
|
|
2146
|
-
const identity = `${scope.namespace ? `${scope.namespace}/` : ""}${scope.name}`;
|
|
2147
|
-
const label = `Open the Timeline for ${identity}`;
|
|
2148
|
-
return (
|
|
2149
|
-
<Tooltip
|
|
2150
|
-
content={label}
|
|
2151
|
-
delay={350}
|
|
2152
|
-
position="left"
|
|
2153
|
-
wrapperClassName="flex shrink-0"
|
|
2154
|
-
>
|
|
2155
|
-
<button
|
|
2156
|
-
type="button"
|
|
2157
|
-
aria-label={label}
|
|
2158
|
-
onClick={() => onOpenTimeline(scope)}
|
|
2159
|
-
className="flex h-7 shrink-0 items-center justify-center gap-1 rounded px-2 text-xs font-medium text-theme-text-tertiary transition-colors hover:bg-theme-hover hover:text-accent-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
|
2160
|
-
>
|
|
2161
|
-
<FileClock className="h-3.5 w-3.5" aria-hidden />
|
|
2162
|
-
<span className="hidden @min-[540px]/card:inline">Timeline</span>
|
|
2163
|
-
</button>
|
|
2164
|
-
</Tooltip>
|
|
2165
|
-
);
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
function uniquePrimarySources(
|
|
2169
|
-
group: InvestigationEvidenceGroup,
|
|
2170
|
-
): InvestigationEvidenceSource[] {
|
|
2171
|
-
// One tool call can contribute repeated revisions to the same semantic group.
|
|
2172
|
-
// It still owns one navigation destination, and DOM ids must remain unique.
|
|
2173
|
-
const sources = new Map<string, InvestigationEvidenceSource>();
|
|
2174
|
-
for (const observation of group.observations) {
|
|
2175
|
-
const { source } = observation;
|
|
2176
|
-
if (source.primaryGroupId === group.id && !sources.has(source.id)) {
|
|
2177
|
-
sources.set(source.id, source);
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
return [...sources.values()];
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
function EvidenceBody({
|
|
2184
|
-
data,
|
|
2185
|
-
cardSummary,
|
|
2186
|
-
changeCoverage,
|
|
2187
|
-
condensed = false,
|
|
2188
|
-
}: {
|
|
2189
|
-
data: InvestigationEvidenceData;
|
|
2190
|
-
cardSummary?: string;
|
|
2191
|
-
/** Change markers for a metrics chart; derived by the pane, never by data. */
|
|
2192
|
-
changeCoverage?: MetricsChangeCoverage;
|
|
2193
|
-
/** The card's title already names the stream: skip the repeated badges. */
|
|
2194
|
-
condensed?: boolean;
|
|
2195
|
-
}) {
|
|
2196
|
-
switch (data.type) {
|
|
2197
|
-
case "issue":
|
|
2198
|
-
return <IssueBody data={data} cardSummary={cardSummary} />;
|
|
2199
|
-
case "startup":
|
|
2200
|
-
return <StartupBody data={data} />;
|
|
2201
|
-
case "crash":
|
|
2202
|
-
return <CrashBody data={data} />;
|
|
2203
|
-
case "resource":
|
|
2204
|
-
return <ResourceBody data={data} />;
|
|
2205
|
-
case "logs":
|
|
2206
|
-
return <LogsBody data={data} condensed={condensed} />;
|
|
2207
|
-
case "events":
|
|
2208
|
-
return <EventsBody data={data} />;
|
|
2209
|
-
case "changes":
|
|
2210
|
-
return <ChangesBody data={data} />;
|
|
2211
|
-
case "dns":
|
|
2212
|
-
return <DNSBody data={data} />;
|
|
2213
|
-
case "network":
|
|
2214
|
-
return <NetworkBody data={data} />;
|
|
2215
|
-
case "relationships":
|
|
2216
|
-
return <RelationshipsBody data={data} />;
|
|
2217
|
-
case "topology":
|
|
2218
|
-
return <TopologyBody data={data} />;
|
|
2219
|
-
case "inventory":
|
|
2220
|
-
return <InventoryBody data={data} />;
|
|
2221
|
-
case "receipt":
|
|
2222
|
-
// The title and the scope above it are the answer. A body appears only
|
|
2223
|
-
// when it adds the reason or the limit, so an absent one renders nothing
|
|
2224
|
-
// rather than an empty line.
|
|
2225
|
-
return data.message ? (
|
|
2226
|
-
<p className="text-xs text-theme-text-secondary">{data.message}</p>
|
|
2227
|
-
) : null;
|
|
2228
|
-
case "alerts":
|
|
2229
|
-
return <AlertsBody data={data} />;
|
|
2230
|
-
case "helm":
|
|
2231
|
-
return <HelmBody data={data} />;
|
|
2232
|
-
case "permissions":
|
|
2233
|
-
return <PermissionsBody data={data} />;
|
|
2234
|
-
case "metrics":
|
|
2235
|
-
return <MetricsBody data={data} changeCoverage={changeCoverage} />;
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
function evidenceHasDetails(
|
|
2240
|
-
data: InvestigationEvidenceData,
|
|
2241
|
-
cardSummary?: string,
|
|
2242
|
-
): boolean {
|
|
2243
|
-
switch (data.type) {
|
|
2244
|
-
case "issue": {
|
|
2245
|
-
const summary = cardSummary?.trim();
|
|
2246
|
-
const cause = data.issue.cause?.trim();
|
|
2247
|
-
const message = data.issue.message?.trim();
|
|
2248
|
-
return Boolean(
|
|
2249
|
-
(cause && cause !== summary) ||
|
|
2250
|
-
(message && message !== summary && message !== cause) ||
|
|
2251
|
-
data.pods?.length,
|
|
2252
|
-
);
|
|
2253
|
-
}
|
|
2254
|
-
case "startup":
|
|
2255
|
-
return (data.pods?.length ?? 0) > 1;
|
|
2256
|
-
case "receipt":
|
|
2257
|
-
return false;
|
|
2258
|
-
case "resource": {
|
|
2259
|
-
const replicas = data.resourceContext?.workloadSummary?.replicas;
|
|
2260
|
-
return Boolean(
|
|
2261
|
-
investigationResourceEvidenceHasDetails(data.resource) ||
|
|
2262
|
-
replicas?.desired !== undefined ||
|
|
2263
|
-
data.resourceContext?.statusSummary?.conditions?.length ||
|
|
2264
|
-
diagnosedScalers(data.resourceContext).length ||
|
|
2265
|
-
data.gitOpsDiagnosis ||
|
|
2266
|
-
data.warnings.length,
|
|
2267
|
-
);
|
|
2268
|
-
}
|
|
2269
|
-
case "logs":
|
|
2270
|
-
return (data.logs?.lines?.length ?? 0) > 0 || Boolean(data.error);
|
|
2271
|
-
case "changes":
|
|
2272
|
-
return data.changes.length > 0 || Boolean(data.changeContext?.evidence);
|
|
2273
|
-
default:
|
|
2274
|
-
return true;
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
type EvidenceDataOf<T extends InvestigationEvidenceData["type"]> = Extract<
|
|
2279
|
-
InvestigationEvidenceData,
|
|
2280
|
-
{ type: T }
|
|
2281
|
-
>;
|
|
2282
|
-
|
|
2283
|
-
function IssueBody({
|
|
2284
|
-
data,
|
|
2285
|
-
cardSummary,
|
|
2286
|
-
}: {
|
|
2287
|
-
data: EvidenceDataOf<"issue">;
|
|
2288
|
-
cardSummary?: string;
|
|
2289
|
-
}) {
|
|
2290
|
-
const issue = data.issue;
|
|
2291
|
-
const showCause =
|
|
2292
|
-
Boolean(issue.cause) && issue.cause?.trim() !== cardSummary?.trim();
|
|
2293
|
-
const showMessage =
|
|
2294
|
-
Boolean(issue.message) &&
|
|
2295
|
-
issue.message?.trim() !== cardSummary?.trim() &&
|
|
2296
|
-
issue.message?.trim() !== issue.cause?.trim();
|
|
2297
|
-
return (
|
|
2298
|
-
<div className="space-y-2">
|
|
2299
|
-
<div className="flex flex-wrap gap-1.5">
|
|
2300
|
-
<Badge
|
|
2301
|
-
severity={issue.severity === "critical" ? "error" : "warning"}
|
|
2302
|
-
size="sm"
|
|
2303
|
-
>
|
|
2304
|
-
{issue.severity}
|
|
2305
|
-
</Badge>
|
|
2306
|
-
<Badge tone="structural" size="sm">
|
|
2307
|
-
{issue.kind}
|
|
2308
|
-
</Badge>
|
|
2309
|
-
<Badge tone="structural" size="sm">
|
|
2310
|
-
{issue.namespace ? `${issue.namespace}/` : ""}
|
|
2311
|
-
{issue.name}
|
|
2312
|
-
</Badge>
|
|
2313
|
-
{data.pods && data.pods.length > 0 ? (
|
|
2314
|
-
<>
|
|
2315
|
-
<Badge tone="structural" size="sm">
|
|
2316
|
-
{data.pods.length === 1 ? "1 Pod" : `${data.pods.length} Pods`}
|
|
2317
|
-
</Badge>
|
|
2318
|
-
{data.pods.map((pod) => (
|
|
2319
|
-
<Badge key={pod} tone="structural" size="sm">
|
|
2320
|
-
{pod}
|
|
2321
|
-
</Badge>
|
|
2322
|
-
))}
|
|
2323
|
-
</>
|
|
2324
|
-
) : null}
|
|
2325
|
-
</div>
|
|
2326
|
-
{showCause ? (
|
|
2327
|
-
<p className="text-sm font-medium leading-relaxed text-theme-text-primary">
|
|
2328
|
-
{issue.cause}
|
|
2329
|
-
</p>
|
|
2330
|
-
) : null}
|
|
2331
|
-
{showMessage ? (
|
|
2332
|
-
<p className="text-xs leading-relaxed text-theme-text-secondary">
|
|
2333
|
-
{issue.message}
|
|
2334
|
-
</p>
|
|
2335
|
-
) : null}
|
|
2336
|
-
</div>
|
|
2337
|
-
);
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
function StartupBody({ data }: { data: EvidenceDataOf<"startup"> }) {
|
|
2341
|
-
const blocker = data.blocker;
|
|
2342
|
-
const pods = data.pods && data.pods.length > 1 ? data.pods : [blocker.name];
|
|
2343
|
-
return (
|
|
2344
|
-
<div className="space-y-2">
|
|
2345
|
-
<div className="flex flex-wrap gap-1.5">
|
|
2346
|
-
<Badge tone="structural" size="sm">
|
|
2347
|
-
{pods.length > 1 ? `${pods.length} ${blocker.kind}s` : blocker.kind}
|
|
2348
|
-
</Badge>
|
|
2349
|
-
{pods.map((pod) => (
|
|
2350
|
-
<Badge key={pod} tone="structural" size="sm">
|
|
2351
|
-
{pod}
|
|
2352
|
-
</Badge>
|
|
2353
|
-
))}
|
|
2354
|
-
<Badge severity={severityBadge(blocker.severity)} size="sm">
|
|
2355
|
-
{blocker.severity}
|
|
2356
|
-
</Badge>
|
|
2357
|
-
</div>
|
|
2358
|
-
<p className="text-sm leading-relaxed text-theme-text-primary">
|
|
2359
|
-
{blocker.message}
|
|
2360
|
-
</p>
|
|
2361
|
-
</div>
|
|
2362
|
-
);
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
function CrashBody({ data }: { data: EvidenceDataOf<"crash"> }) {
|
|
2366
|
-
const crash = data.crash;
|
|
2367
|
-
return (
|
|
2368
|
-
<div className="space-y-2.5">
|
|
2369
|
-
<div className="flex flex-wrap gap-1.5">
|
|
2370
|
-
<Badge severity="error" size="sm">
|
|
2371
|
-
{crash.reason || crash.state}
|
|
2372
|
-
</Badge>
|
|
2373
|
-
<Badge tone="structural" size="sm">
|
|
2374
|
-
exit {crash.exitCode}
|
|
2375
|
-
</Badge>
|
|
2376
|
-
<Badge tone="structural" size="sm">
|
|
2377
|
-
{crash.container}
|
|
2378
|
-
</Badge>
|
|
2379
|
-
</div>
|
|
2380
|
-
<p className="text-xs text-theme-text-tertiary">
|
|
2381
|
-
{crash.pods.join(", ")} · {crash.logSource.replaceAll("_", " ")}
|
|
2382
|
-
</p>
|
|
2383
|
-
<TerminalBlock label="Selected crash line">{crash.logLine}</TerminalBlock>
|
|
2384
|
-
</div>
|
|
2385
|
-
);
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
function ResourceBody({ data }: { data: EvidenceDataOf<"resource"> }) {
|
|
2389
|
-
const replicas = data.resourceContext?.workloadSummary?.replicas;
|
|
2390
|
-
// SealedSecret's dedicated body renders the resource's conditions beside
|
|
2391
|
-
// its controller state, so repeating the derived summary here adds noise.
|
|
2392
|
-
const conditions =
|
|
2393
|
-
data.resource.kind.toLowerCase() === "sealedsecret"
|
|
2394
|
-
? []
|
|
2395
|
-
: (data.resourceContext?.statusSummary?.conditions ?? []);
|
|
2396
|
-
const desired = replicas?.desired;
|
|
2397
|
-
const ready = replicas ? (replicas.ready ?? 0) : undefined;
|
|
2398
|
-
const shortfall =
|
|
2399
|
-
desired !== undefined && ready !== undefined && ready < desired;
|
|
2400
|
-
const scalers = diagnosedScalers(data.resourceContext);
|
|
2401
|
-
return (
|
|
2402
|
-
<div className="space-y-3">
|
|
2403
|
-
<InvestigationResourceEvidence resource={data.resource} />
|
|
2404
|
-
{data.gitOpsDiagnosis ? (
|
|
2405
|
-
<GitOpsStatusBody status={data.gitOpsDiagnosis} />
|
|
2406
|
-
) : null}
|
|
2407
|
-
{desired !== undefined && ready !== undefined ? (
|
|
2408
|
-
<dl className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
|
|
2409
|
-
<div>
|
|
2410
|
-
<dt className="text-theme-text-tertiary">Ready replicas</dt>
|
|
2411
|
-
<dd
|
|
2412
|
-
className={clsx(
|
|
2413
|
-
"font-mono font-semibold tabular-nums",
|
|
2414
|
-
shortfall ? "text-warning-text" : "text-theme-text-primary",
|
|
2415
|
-
)}
|
|
2416
|
-
>
|
|
2417
|
-
{ready}/{desired}
|
|
2418
|
-
</dd>
|
|
2419
|
-
</div>
|
|
2420
|
-
<ResourceFact label="Available" value={replicas?.available} />
|
|
2421
|
-
<ResourceFact label="Updated" value={replicas?.updated} />
|
|
2422
|
-
<ResourceFact label="Unavailable" value={replicas?.unavailable} />
|
|
2423
|
-
<ResourceFact
|
|
2424
|
-
label="Phase"
|
|
2425
|
-
value={data.resourceContext?.statusSummary?.phase}
|
|
2426
|
-
/>
|
|
2427
|
-
</dl>
|
|
2428
|
-
) : null}
|
|
2429
|
-
{conditions.length > 0 ? (
|
|
2430
|
-
<div>
|
|
2431
|
-
<div className="mb-1.5 text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
2432
|
-
Conditions
|
|
2433
|
-
</div>
|
|
2434
|
-
<div className="max-h-52 space-y-1.5 overflow-y-auto pr-1">
|
|
2435
|
-
{conditions.map((condition) => (
|
|
2436
|
-
<div
|
|
2437
|
-
key={`${condition.type}-${condition.status}-${condition.reason ?? ""}`}
|
|
2438
|
-
className="flex min-w-0 items-start gap-2 text-xs"
|
|
2439
|
-
>
|
|
2440
|
-
<StatusDot
|
|
2441
|
-
tone={conditionStatusTone(condition)}
|
|
2442
|
-
className="mt-1 shrink-0"
|
|
2443
|
-
/>
|
|
2444
|
-
<p className="min-w-0 text-theme-text-secondary">
|
|
2445
|
-
<span className="font-medium text-theme-text-primary">
|
|
2446
|
-
{condition.type}={condition.status}
|
|
2447
|
-
</span>
|
|
2448
|
-
{condition.reason ? ` · ${condition.reason}` : ""}
|
|
2449
|
-
{condition.message ? ` — ${condition.message}` : ""}
|
|
2450
|
-
</p>
|
|
2451
|
-
</div>
|
|
2452
|
-
))}
|
|
2453
|
-
</div>
|
|
2454
|
-
</div>
|
|
2455
|
-
) : null}
|
|
2456
|
-
{scalers.length > 0 ? (
|
|
2457
|
-
<ScaledBySection
|
|
2458
|
-
scalers={scalers}
|
|
2459
|
-
allScalers={data.resourceContext?.scaledBy ?? []}
|
|
2460
|
-
/>
|
|
2461
|
-
) : null}
|
|
2462
|
-
{data.warnings.length > 0 ? (
|
|
2463
|
-
<ul className="space-y-1 text-xs text-theme-text-secondary">
|
|
2464
|
-
{data.warnings.map((warning) => (
|
|
2465
|
-
<li key={warning} className="flex items-start gap-1.5">
|
|
2466
|
-
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
|
|
2467
|
-
<span>{warning}</span>
|
|
2468
|
-
</li>
|
|
2469
|
-
))}
|
|
2470
|
-
</ul>
|
|
2471
|
-
) : null}
|
|
2472
|
-
</div>
|
|
2473
|
-
);
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
type DiagnosedScaler = DiagnosisScalerRef &
|
|
2477
|
-
Required<Pick<DiagnosisScalerRef, "hpaSummary">>;
|
|
2478
|
-
|
|
2479
|
-
function diagnosedScalers(
|
|
2480
|
-
context: DiagnosisResourceContext | undefined,
|
|
2481
|
-
): DiagnosedScaler[] {
|
|
2482
|
-
return (context?.scaledBy ?? []).filter(
|
|
2483
|
-
(scaler): scaler is DiagnosedScaler => scaler.hpaSummary !== undefined,
|
|
2484
|
-
);
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
function ScaledBySection({
|
|
2488
|
-
scalers,
|
|
2489
|
-
allScalers,
|
|
2490
|
-
}: {
|
|
2491
|
-
scalers: DiagnosedScaler[];
|
|
2492
|
-
allScalers: DiagnosisScalerRef[];
|
|
2493
|
-
}) {
|
|
2494
|
-
const { onOpenResource } = useContext(EvidenceNavigationContext);
|
|
2495
|
-
// The ScaledObject is only a link when Radar listed it as a scaler too; a
|
|
2496
|
-
// name read off the HPA's owner reference alone is not a resource we hold.
|
|
2497
|
-
const listedScaler = (ref: DiagnosisResourceRef) =>
|
|
2498
|
-
allScalers.some(
|
|
2499
|
-
(scaler) =>
|
|
2500
|
-
scaler.kind === ref.kind &&
|
|
2501
|
-
scaler.name === ref.name &&
|
|
2502
|
-
(scaler.namespace ?? "") === (ref.namespace ?? ""),
|
|
2503
|
-
);
|
|
2504
|
-
return (
|
|
2505
|
-
<div>
|
|
2506
|
-
<div className="mb-1.5 text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
2507
|
-
Scaled by
|
|
2508
|
-
</div>
|
|
2509
|
-
<div className="space-y-2">
|
|
2510
|
-
{scalers.map((scaler) => (
|
|
2511
|
-
<HPADiagnosisSummary
|
|
2512
|
-
key={`${scaler.namespace ?? ""}/${scaler.name}`}
|
|
2513
|
-
diagnosis={scaler.hpaSummary}
|
|
2514
|
-
variant="inline"
|
|
2515
|
-
header={
|
|
2516
|
-
<>
|
|
2517
|
-
<span className="font-medium text-theme-text-primary">
|
|
2518
|
-
{displayKind(scaler.kind)} {scaler.name}
|
|
2519
|
-
</span>
|
|
2520
|
-
{scaler.managedBy ? (
|
|
2521
|
-
<span className="text-theme-text-tertiary">
|
|
2522
|
-
managed by KEDA {displayKind(scaler.managedBy.kind)}{" "}
|
|
2523
|
-
<ResourceLink
|
|
2524
|
-
name={scaler.managedBy.name}
|
|
2525
|
-
kind={scaler.managedBy.kind}
|
|
2526
|
-
namespace={scaler.managedBy.namespace ?? ""}
|
|
2527
|
-
group={scaler.managedBy.group}
|
|
2528
|
-
onNavigate={
|
|
2529
|
-
onOpenResource && listedScaler(scaler.managedBy)
|
|
2530
|
-
? (ref) => onOpenResource(ref)
|
|
2531
|
-
: undefined
|
|
2532
|
-
}
|
|
2533
|
-
/>
|
|
2534
|
-
</span>
|
|
2535
|
-
) : null}
|
|
2536
|
-
</>
|
|
2537
|
-
}
|
|
2538
|
-
/>
|
|
2539
|
-
))}
|
|
2540
|
-
</div>
|
|
2541
|
-
</div>
|
|
2542
|
-
);
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
function GitOpsStatusBody({
|
|
2546
|
-
status,
|
|
2547
|
-
}: {
|
|
2548
|
-
status: NonNullable<EvidenceDataOf<"resource">["gitOpsDiagnosis"]>;
|
|
2549
|
-
}) {
|
|
2550
|
-
const fields = [
|
|
2551
|
-
["Sync", status.sync],
|
|
2552
|
-
["Health", status.health],
|
|
2553
|
-
["Operation", status.operationPhase],
|
|
2554
|
-
["Ready", status.ready],
|
|
2555
|
-
] as const;
|
|
2556
|
-
return (
|
|
2557
|
-
<div className="rounded-md border border-theme-border bg-theme-base/40 p-2.5">
|
|
2558
|
-
<div className="mb-2 flex flex-wrap items-center gap-1.5">
|
|
2559
|
-
<span className="text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
2560
|
-
GitOps controller status
|
|
2561
|
-
</span>
|
|
2562
|
-
<Badge tone="note" size="sm">
|
|
2563
|
-
{status.tool === "argocd" ? "Argo CD" : "Flux"}
|
|
2564
|
-
</Badge>
|
|
2565
|
-
{status.suspended ? (
|
|
2566
|
-
<Badge severity="info" size="sm">
|
|
2567
|
-
Suspended
|
|
2568
|
-
</Badge>
|
|
2569
|
-
) : null}
|
|
2570
|
-
</div>
|
|
2571
|
-
<div className="flex flex-wrap gap-1.5">
|
|
2572
|
-
{fields.map(([label, value]) =>
|
|
2573
|
-
value ? (
|
|
2574
|
-
<Badge
|
|
2575
|
-
key={label}
|
|
2576
|
-
severity={gitOpsValueSeverity(label, value)}
|
|
2577
|
-
size="sm"
|
|
2578
|
-
>
|
|
2579
|
-
{label}: {value}
|
|
2580
|
-
</Badge>
|
|
2581
|
-
) : null,
|
|
2582
|
-
)}
|
|
2583
|
-
{status.appliedRevision ? (
|
|
2584
|
-
<Badge tone="structural" size="sm">
|
|
2585
|
-
{status.appliedRevision}
|
|
2586
|
-
</Badge>
|
|
2587
|
-
) : null}
|
|
2588
|
-
</div>
|
|
2589
|
-
</div>
|
|
2590
|
-
);
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
function gitOpsValueSeverity(label: string, value: string) {
|
|
2594
|
-
const normalized = value.toLowerCase();
|
|
2595
|
-
if (
|
|
2596
|
-
normalized === "healthy" ||
|
|
2597
|
-
normalized === "synced" ||
|
|
2598
|
-
normalized === "succeeded" ||
|
|
2599
|
-
(label === "Ready" && normalized.startsWith("true"))
|
|
2600
|
-
) {
|
|
2601
|
-
return "success" as const;
|
|
2602
|
-
}
|
|
2603
|
-
if (
|
|
2604
|
-
normalized === "degraded" ||
|
|
2605
|
-
normalized === "missing" ||
|
|
2606
|
-
normalized === "failed" ||
|
|
2607
|
-
normalized === "error" ||
|
|
2608
|
-
(label === "Ready" && normalized.startsWith("false"))
|
|
2609
|
-
) {
|
|
2610
|
-
return "error" as const;
|
|
2611
|
-
}
|
|
2612
|
-
if (normalized === "outofsync") return "warning" as const;
|
|
2613
|
-
if (normalized === "progressing" || normalized === "running")
|
|
2614
|
-
return "info" as const;
|
|
2615
|
-
return "neutral" as const;
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
function ResourceFact({ label, value }: { label: string; value: unknown }) {
|
|
2619
|
-
if (value === undefined || value === null || value === "") return null;
|
|
2620
|
-
return (
|
|
2621
|
-
<div className="flex justify-between gap-2 @min-[560px]/evidence:block">
|
|
2622
|
-
<dt className="text-theme-text-tertiary">{label}</dt>
|
|
2623
|
-
<dd className="font-mono font-medium text-theme-text-primary">
|
|
2624
|
-
{String(value)}
|
|
2625
|
-
</dd>
|
|
2626
|
-
</div>
|
|
2627
|
-
);
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
function conditionStatusTone(condition: {
|
|
2631
|
-
type: string;
|
|
2632
|
-
status: string;
|
|
2633
|
-
}): "healthy" | "degraded" | "unhealthy" | "unknown" {
|
|
2634
|
-
switch (defaultConditionTone(condition)) {
|
|
2635
|
-
case "ok":
|
|
2636
|
-
return "healthy";
|
|
2637
|
-
case "warning":
|
|
2638
|
-
return "degraded";
|
|
2639
|
-
case "fail":
|
|
2640
|
-
return "unhealthy";
|
|
2641
|
-
case "unknown":
|
|
2642
|
-
return "unknown";
|
|
2643
|
-
}
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
function LogsBody({
|
|
2647
|
-
data,
|
|
2648
|
-
condensed = false,
|
|
2649
|
-
}: {
|
|
2650
|
-
data: EvidenceDataOf<"logs">;
|
|
2651
|
-
condensed?: boolean;
|
|
2652
|
-
}) {
|
|
2653
|
-
const lines = data.logs?.lines ?? [];
|
|
2654
|
-
const visibleLines = lines
|
|
2655
|
-
.slice(-VISIBLE_LOG_EVIDENCE_LINES)
|
|
2656
|
-
.map((line) => stripAnsi(line));
|
|
2657
|
-
const omittedLines = lines.length - visibleLines.length;
|
|
2658
|
-
return (
|
|
2659
|
-
<div className="space-y-2">
|
|
2660
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
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
|
-
)}
|
|
2671
|
-
{data.logs?.fallback ? (
|
|
2672
|
-
<Badge severity="neutral" size="sm">
|
|
2673
|
-
Log tail · filter matched nothing
|
|
2674
|
-
</Badge>
|
|
2675
|
-
) : null}
|
|
2676
|
-
{data.logs ? (
|
|
2677
|
-
<span className="text-xs text-theme-text-tertiary">
|
|
2678
|
-
{data.logs.matchedLines} of {data.logs.totalLines} read lines
|
|
2679
|
-
matched the filter
|
|
2680
|
-
</span>
|
|
2681
|
-
) : null}
|
|
2682
|
-
</div>
|
|
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 ? (
|
|
2690
|
-
<TerminalBlock
|
|
2691
|
-
label={
|
|
2692
|
-
omittedLines > 0
|
|
2693
|
-
? `Selected log excerpt · last ${visibleLines.length} of ${lines.length} lines`
|
|
2694
|
-
: "Selected log excerpt"
|
|
2695
|
-
}
|
|
2696
|
-
>
|
|
2697
|
-
{visibleLines.join("\n")}
|
|
2698
|
-
</TerminalBlock>
|
|
2699
|
-
) : (
|
|
2700
|
-
<p className="text-xs italic text-theme-text-tertiary">
|
|
2701
|
-
No lines were captured from this stream.
|
|
2702
|
-
</p>
|
|
2703
|
-
)}
|
|
2704
|
-
{data.error ? (
|
|
2705
|
-
<p className="text-xs leading-relaxed text-red-400">{data.error}</p>
|
|
2706
|
-
) : null}
|
|
2707
|
-
{data.warnings.map((warning) => (
|
|
2708
|
-
<p key={warning} className="text-xs text-warning-text">
|
|
2709
|
-
{warning}
|
|
2710
|
-
</p>
|
|
2711
|
-
))}
|
|
2712
|
-
</div>
|
|
2713
|
-
);
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
function EventsBody({ data }: { data: EvidenceDataOf<"events"> }) {
|
|
2717
|
-
return (
|
|
2718
|
-
<div>
|
|
2719
|
-
<p className="mb-2 text-xs text-theme-text-tertiary">{data.scope}</p>
|
|
2720
|
-
<ol className="max-h-[28rem] space-y-0 overflow-y-auto pr-1">
|
|
2721
|
-
{data.events.map((event, index) => (
|
|
2722
|
-
<li
|
|
2723
|
-
key={`${event.reason}-${event.lastTimestamp}-${index}`}
|
|
2724
|
-
className="relative flex gap-3 pb-3 last:pb-0"
|
|
2725
|
-
>
|
|
2726
|
-
{index < data.events.length - 1 ? (
|
|
2727
|
-
<span className="absolute bottom-0 left-[5px] top-3 w-px bg-theme-border" />
|
|
2728
|
-
) : null}
|
|
2729
|
-
<span className="relative mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full border-2 border-amber-500 bg-theme-surface" />
|
|
2730
|
-
<div className="min-w-0 flex-1">
|
|
2731
|
-
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
|
2732
|
-
<span className="text-xs font-semibold text-theme-text-primary">
|
|
2733
|
-
{event.reason}
|
|
2734
|
-
{event.count > 1 ? (
|
|
2735
|
-
<span className="ml-1.5 font-mono text-[11px] font-normal text-theme-text-tertiary">
|
|
2736
|
-
×{event.count}
|
|
2737
|
-
</span>
|
|
2738
|
-
) : null}
|
|
2739
|
-
</span>
|
|
2740
|
-
<Tooltip
|
|
2741
|
-
content={new Date(event.lastTimestamp).toLocaleString()}
|
|
2742
|
-
delay={150}
|
|
2743
|
-
position="left"
|
|
2744
|
-
>
|
|
2745
|
-
<time
|
|
2746
|
-
dateTime={event.lastTimestamp}
|
|
2747
|
-
className="text-xs text-theme-text-tertiary"
|
|
2748
|
-
>
|
|
2749
|
-
{formatRelativeAgeTime(event.lastTimestamp)}
|
|
2750
|
-
</time>
|
|
2751
|
-
</Tooltip>
|
|
2752
|
-
</div>
|
|
2753
|
-
<p className="mt-0.5 text-xs leading-relaxed text-theme-text-secondary">
|
|
2754
|
-
{event.message}
|
|
2755
|
-
</p>
|
|
2756
|
-
</div>
|
|
2757
|
-
</li>
|
|
2758
|
-
))}
|
|
2759
|
-
</ol>
|
|
2760
|
-
</div>
|
|
2761
|
-
);
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
function ChangesBody({ data }: { data: EvidenceDataOf<"changes"> }) {
|
|
2765
|
-
const { onOpenResource } = useContext(EvidenceNavigationContext);
|
|
2766
|
-
return (
|
|
2767
|
-
<div className="space-y-2.5">
|
|
2768
|
-
{data.changeContext?.evidence ? (
|
|
2769
|
-
<p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
2770
|
-
{data.changeContext.evidence}
|
|
2771
|
-
</p>
|
|
2772
|
-
) : null}
|
|
2773
|
-
{data.changes.map((change, index) => (
|
|
2774
|
-
<div
|
|
2775
|
-
key={`${change.kind}-${change.namespace ?? ""}-${change.name}-${change.timestamp}-${index}`}
|
|
2776
|
-
className="rounded-md border border-theme-border/70 bg-theme-base/30 p-2.5"
|
|
2777
|
-
>
|
|
2778
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
2779
|
-
<Badge tone="structural" size="sm">
|
|
2780
|
-
{change.kind}
|
|
2781
|
-
</Badge>
|
|
2782
|
-
<span className="font-mono text-xs">
|
|
2783
|
-
<ResourceLink
|
|
2784
|
-
name={change.name}
|
|
2785
|
-
kind={change.kind}
|
|
2786
|
-
namespace={change.namespace ?? ""}
|
|
2787
|
-
group={
|
|
2788
|
-
change.apiVersion
|
|
2789
|
-
? apiVersionToGroup(change.apiVersion)
|
|
2790
|
-
: undefined
|
|
2791
|
-
}
|
|
2792
|
-
label={`${change.namespace ? `${change.namespace}/` : ""}${change.name}`}
|
|
2793
|
-
onNavigate={
|
|
2794
|
-
change.apiVersion && onOpenResource
|
|
2795
|
-
? (ref) => onOpenResource(ref)
|
|
2796
|
-
: undefined
|
|
2797
|
-
}
|
|
2798
|
-
/>
|
|
2799
|
-
</span>
|
|
2800
|
-
<Badge tone="note" size="sm">
|
|
2801
|
-
{change.changeType.replaceAll("_", " ")}
|
|
2802
|
-
</Badge>
|
|
2803
|
-
<Tooltip
|
|
2804
|
-
content={new Date(change.timestamp).toLocaleString()}
|
|
2805
|
-
delay={150}
|
|
2806
|
-
position="left"
|
|
2807
|
-
wrapperClassName="ml-auto"
|
|
2808
|
-
>
|
|
2809
|
-
<time
|
|
2810
|
-
dateTime={change.timestamp}
|
|
2811
|
-
className="text-xs text-theme-text-tertiary"
|
|
2812
|
-
>
|
|
2813
|
-
{formatRelativeAgeTime(change.timestamp)}
|
|
2814
|
-
</time>
|
|
2815
|
-
</Tooltip>
|
|
2816
|
-
</div>
|
|
2817
|
-
{change.summary ? (
|
|
2818
|
-
<p className="mt-1.5 text-xs text-theme-text-secondary">
|
|
2819
|
-
{change.summary}
|
|
2820
|
-
</p>
|
|
2821
|
-
) : null}
|
|
2822
|
-
{change.fields?.length ? (
|
|
2823
|
-
<div className="mt-2">
|
|
2824
|
-
<DiffViewer
|
|
2825
|
-
diff={{
|
|
2826
|
-
summary: `${change.fields.length} changed ${change.fields.length === 1 ? "field" : "fields"}`,
|
|
2827
|
-
fields: change.fields.map((field) => ({
|
|
2828
|
-
path: field.path,
|
|
2829
|
-
oldValue: field.oldValue ?? null,
|
|
2830
|
-
newValue: field.newValue ?? null,
|
|
2831
|
-
})),
|
|
2832
|
-
}}
|
|
2833
|
-
/>
|
|
2834
|
-
</div>
|
|
2835
|
-
) : null}
|
|
2836
|
-
</div>
|
|
2837
|
-
))}
|
|
2838
|
-
</div>
|
|
2839
|
-
);
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2842
|
-
function DNSBody({ data }: { data: EvidenceDataOf<"dns"> }) {
|
|
2843
|
-
const { onOpenResource } = useContext(EvidenceNavigationContext);
|
|
2844
|
-
return (
|
|
2845
|
-
<div className="space-y-2">
|
|
2846
|
-
{(data.dns.signals ?? []).map((signal) => (
|
|
2847
|
-
<p
|
|
2848
|
-
key={signal}
|
|
2849
|
-
className="text-xs leading-relaxed text-theme-text-secondary"
|
|
2850
|
-
>
|
|
2851
|
-
{signal}
|
|
2852
|
-
</p>
|
|
2853
|
-
))}
|
|
2854
|
-
{(data.dns.coreDNSFindings ?? []).map((finding) => (
|
|
2855
|
-
<div
|
|
2856
|
-
key={`${finding.kind}-${finding.namespace}-${finding.name}-${finding.reason}`}
|
|
2857
|
-
className="rounded-md border border-theme-border bg-theme-base/40 p-2"
|
|
2858
|
-
>
|
|
2859
|
-
<div className="flex flex-wrap gap-1.5">
|
|
2860
|
-
<Badge tone="structural" size="sm">
|
|
2861
|
-
{finding.kind}
|
|
2862
|
-
</Badge>
|
|
2863
|
-
<span className="font-mono text-xs">
|
|
2864
|
-
<ResourceLink
|
|
2865
|
-
name={finding.name}
|
|
2866
|
-
kind={finding.kind}
|
|
2867
|
-
namespace={finding.namespace}
|
|
2868
|
-
group=""
|
|
2869
|
-
label={`${finding.namespace}/${finding.name}`}
|
|
2870
|
-
onNavigate={
|
|
2871
|
-
finding.kind.toLowerCase() === "configmap" && onOpenResource
|
|
2872
|
-
? (ref) => onOpenResource(ref)
|
|
2873
|
-
: undefined
|
|
2874
|
-
}
|
|
2875
|
-
/>
|
|
2876
|
-
</span>
|
|
2877
|
-
<Badge severity={severityBadge(finding.severity)} size="sm">
|
|
2878
|
-
{finding.severity}
|
|
2879
|
-
</Badge>
|
|
2880
|
-
</div>
|
|
2881
|
-
<p className="mt-1 text-xs text-theme-text-secondary">
|
|
2882
|
-
{finding.reason}
|
|
2883
|
-
{finding.message ? ` — ${finding.message}` : ""}
|
|
2884
|
-
</p>
|
|
2885
|
-
</div>
|
|
2886
|
-
))}
|
|
2887
|
-
</div>
|
|
2888
|
-
);
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
function NetworkBody({ data }: { data: EvidenceDataOf<"network"> }) {
|
|
2892
|
-
const { network } = data;
|
|
2893
|
-
const stats = [
|
|
2894
|
-
["Tested", network.summary.tested],
|
|
2895
|
-
["Passed", network.summary.passed],
|
|
2896
|
-
["Failed", network.summary.failed],
|
|
2897
|
-
["Inferred", network.summary.derived ?? 0],
|
|
2898
|
-
["Skipped", network.summary.skipped],
|
|
2899
|
-
] as const;
|
|
2900
|
-
return (
|
|
2901
|
-
<div className="space-y-3">
|
|
2902
|
-
<div className="grid grid-cols-3 gap-1.5 @min-[620px]/evidence:grid-cols-5">
|
|
2903
|
-
{stats.map(([label, value]) => (
|
|
2904
|
-
<div
|
|
2905
|
-
key={label}
|
|
2906
|
-
className="rounded-md border border-theme-border bg-theme-base/40 px-2 py-1.5 text-center"
|
|
2907
|
-
>
|
|
2908
|
-
<div className="font-mono text-sm font-semibold text-theme-text-primary">
|
|
2909
|
-
{value}
|
|
2910
|
-
</div>
|
|
2911
|
-
<div className="text-xs uppercase tracking-wide text-theme-text-tertiary">
|
|
2912
|
-
{label}
|
|
2913
|
-
</div>
|
|
2914
|
-
</div>
|
|
2915
|
-
))}
|
|
2916
|
-
</div>
|
|
2917
|
-
{network.diagnosis ? (
|
|
2918
|
-
<div className="rounded-md border border-theme-border bg-theme-base/40 px-2.5 py-2">
|
|
2919
|
-
<p className="text-xs font-medium leading-relaxed text-theme-text-primary">
|
|
2920
|
-
{network.diagnosis.summary}
|
|
2921
|
-
</p>
|
|
2922
|
-
{network.diagnosis.nextAction ? (
|
|
2923
|
-
<p className="mt-1 border-l-2 border-accent/50 pl-2 text-xs leading-relaxed text-theme-text-secondary">
|
|
2924
|
-
Next check: {network.diagnosis.nextAction}
|
|
2925
|
-
</p>
|
|
2926
|
-
) : null}
|
|
2927
|
-
</div>
|
|
2928
|
-
) : (
|
|
2929
|
-
<p className="text-xs leading-relaxed text-theme-text-secondary">
|
|
2930
|
-
{network.summary.headline}
|
|
2931
|
-
</p>
|
|
2932
|
-
)}
|
|
2933
|
-
{network.routes.length > 0 ? (
|
|
2934
|
-
<ol className="max-h-64 space-y-1.5 overflow-y-auto pr-1">
|
|
2935
|
-
{network.routes.map((route, index) => (
|
|
2936
|
-
<li
|
|
2937
|
-
key={`${route.route}-${route.target ?? ""}-${index}`}
|
|
2938
|
-
className="flex min-w-0 items-start gap-2 rounded-md border border-theme-border/70 bg-theme-base/30 px-2.5 py-2"
|
|
2939
|
-
>
|
|
2940
|
-
<StatusDot
|
|
2941
|
-
tone={networkOutcomeTone(route.outcome, route.benign)}
|
|
2942
|
-
className="mt-1 shrink-0"
|
|
2943
|
-
/>
|
|
2944
|
-
<span className="min-w-0 flex-1">
|
|
2945
|
-
<span className="block truncate font-mono text-xs text-theme-text-primary">
|
|
2946
|
-
{route.route}
|
|
2947
|
-
{route.target ? ` → ${route.target}` : ""}
|
|
2948
|
-
</span>
|
|
2949
|
-
{route.evidence ? (
|
|
2950
|
-
<span className="mt-0.5 block text-xs leading-relaxed text-theme-text-secondary">
|
|
2951
|
-
{route.evidence}
|
|
2952
|
-
</span>
|
|
2953
|
-
) : null}
|
|
2954
|
-
</span>
|
|
2955
|
-
<Badge
|
|
2956
|
-
severity={networkOutcomeSeverity(route.outcome, route.benign)}
|
|
2957
|
-
size="sm"
|
|
2958
|
-
>
|
|
2959
|
-
{route.benign
|
|
2960
|
-
? "intentional"
|
|
2961
|
-
: route.outcome.replaceAll("_", " ")}
|
|
2962
|
-
</Badge>
|
|
2963
|
-
</li>
|
|
2964
|
-
))}
|
|
2965
|
-
</ol>
|
|
2966
|
-
) : null}
|
|
2967
|
-
</div>
|
|
2968
|
-
);
|
|
2969
|
-
}
|
|
2970
|
-
|
|
2971
|
-
function networkOutcomeTone(
|
|
2972
|
-
outcome: string,
|
|
2973
|
-
benign?: boolean,
|
|
2974
|
-
): "healthy" | "degraded" | "unhealthy" | "unknown" {
|
|
2975
|
-
if (benign) return "degraded";
|
|
2976
|
-
const normalized = outcome.toLowerCase();
|
|
2977
|
-
if (normalized.includes("verified") || normalized.includes("reached"))
|
|
2978
|
-
return "healthy";
|
|
2979
|
-
if (normalized.includes("fail") || normalized.includes("unreachable"))
|
|
2980
|
-
return "unhealthy";
|
|
2981
|
-
if (normalized.includes("skip") || normalized.includes("not"))
|
|
2982
|
-
return "unknown";
|
|
2983
|
-
return "degraded";
|
|
2984
|
-
}
|
|
2985
|
-
|
|
2986
|
-
function networkOutcomeSeverity(outcome: string, benign?: boolean) {
|
|
2987
|
-
switch (networkOutcomeTone(outcome, benign)) {
|
|
2988
|
-
case "healthy":
|
|
2989
|
-
return "success" as const;
|
|
2990
|
-
case "degraded":
|
|
2991
|
-
return "warning" as const;
|
|
2992
|
-
case "unhealthy":
|
|
2993
|
-
return "error" as const;
|
|
2994
|
-
case "unknown":
|
|
2995
|
-
return "neutral" as const;
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
function RelationshipsBody({
|
|
3000
|
-
data,
|
|
3001
|
-
}: {
|
|
3002
|
-
data: EvidenceDataOf<"relationships">;
|
|
3003
|
-
}) {
|
|
3004
|
-
return (
|
|
3005
|
-
<div className="space-y-2.5">
|
|
3006
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
3007
|
-
<Badge tone="structural" size="sm">
|
|
3008
|
-
{data.root.kind}
|
|
3009
|
-
</Badge>
|
|
3010
|
-
<span className="font-mono text-xs text-theme-text-secondary">
|
|
3011
|
-
{data.root.namespace ? `${data.root.namespace}/` : ""}
|
|
3012
|
-
{data.root.name}
|
|
3013
|
-
</span>
|
|
3014
|
-
<span className="text-xs text-theme-text-tertiary">
|
|
3015
|
-
{data.nodes.length} resources · {data.edges.length} direct
|
|
3016
|
-
relationships
|
|
3017
|
-
</span>
|
|
3018
|
-
</div>
|
|
3019
|
-
<div className="max-h-44 overflow-y-auto pr-1">
|
|
3020
|
-
<div className="flex flex-wrap gap-1.5">
|
|
3021
|
-
{data.nodes.map((node) => (
|
|
3022
|
-
<span
|
|
3023
|
-
key={node.id}
|
|
3024
|
-
className="inline-flex items-center gap-1 rounded-md border border-theme-border bg-theme-base px-2 py-1 text-xs"
|
|
3025
|
-
>
|
|
3026
|
-
<Badge tone="structural" size="sm">
|
|
3027
|
-
{node.kind}
|
|
3028
|
-
</Badge>
|
|
3029
|
-
<span className="font-mono text-theme-text-secondary">
|
|
3030
|
-
{node.name}
|
|
3031
|
-
</span>
|
|
3032
|
-
</span>
|
|
3033
|
-
))}
|
|
3034
|
-
</div>
|
|
3035
|
-
</div>
|
|
3036
|
-
{data.edges.length > 0 ? (
|
|
3037
|
-
<div className="grid max-h-52 gap-1 overflow-y-auto pr-1 @min-[680px]/evidence:grid-cols-2">
|
|
3038
|
-
{data.edges.map((edge, index) => (
|
|
3039
|
-
<div
|
|
3040
|
-
key={
|
|
3041
|
-
edge.id ?? `${edge.source}-${edge.target}-${edge.type}-${index}`
|
|
3042
|
-
}
|
|
3043
|
-
className="flex min-w-0 items-center gap-1.5 rounded bg-theme-base/50 px-2 py-1.5 font-mono text-xs text-theme-text-secondary"
|
|
3044
|
-
>
|
|
3045
|
-
<span className="truncate">{edge.source}</span>
|
|
3046
|
-
<span className="shrink-0 text-theme-text-tertiary">→</span>
|
|
3047
|
-
<span className="truncate">{edge.target}</span>
|
|
3048
|
-
<Badge tone="structural" size="sm">
|
|
3049
|
-
{edge.label || edge.type}
|
|
3050
|
-
</Badge>
|
|
3051
|
-
</div>
|
|
3052
|
-
))}
|
|
3053
|
-
</div>
|
|
3054
|
-
) : null}
|
|
3055
|
-
</div>
|
|
3056
|
-
);
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
function TopologyBody({ data }: { data: EvidenceDataOf<"topology"> }) {
|
|
3060
|
-
return (
|
|
3061
|
-
<div className="space-y-2.5">
|
|
3062
|
-
<div className="grid grid-cols-2 gap-2">
|
|
3063
|
-
<TopologyStat label="Nodes" value={data.stats.nodes} />
|
|
3064
|
-
<TopologyStat label="Relationships" value={data.stats.edges} />
|
|
3065
|
-
</div>
|
|
3066
|
-
{data.problems.map((problem) => (
|
|
3067
|
-
<p
|
|
3068
|
-
key={problem}
|
|
3069
|
-
className="flex items-start gap-1.5 text-xs text-warning-text"
|
|
3070
|
-
>
|
|
3071
|
-
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
3072
|
-
{problem}
|
|
3073
|
-
</p>
|
|
3074
|
-
))}
|
|
3075
|
-
<div className="max-h-64 space-y-2 overflow-y-auto pr-1">
|
|
3076
|
-
{data.namespaces.map((namespace) => (
|
|
3077
|
-
<div key={namespace.namespace}>
|
|
3078
|
-
<div className="text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
3079
|
-
{namespace.namespace || "cluster-scoped"}
|
|
3080
|
-
</div>
|
|
3081
|
-
<ul className="mt-1 space-y-1 font-mono text-xs text-theme-text-secondary">
|
|
3082
|
-
{namespace.chains.map((chain) => (
|
|
3083
|
-
<li key={chain}>{chain}</li>
|
|
3084
|
-
))}
|
|
3085
|
-
</ul>
|
|
3086
|
-
</div>
|
|
3087
|
-
))}
|
|
3088
|
-
</div>
|
|
3089
|
-
{data.warnings.map((warning) => (
|
|
3090
|
-
<p key={warning} className="text-xs text-warning-text">
|
|
3091
|
-
{warning}
|
|
3092
|
-
</p>
|
|
3093
|
-
))}
|
|
3094
|
-
</div>
|
|
3095
|
-
);
|
|
3096
|
-
}
|
|
3097
|
-
|
|
3098
|
-
function TopologyStat({ label, value }: { label: string; value: number }) {
|
|
3099
|
-
return (
|
|
3100
|
-
<div className="rounded-md border border-theme-border bg-theme-base/40 px-3 py-2">
|
|
3101
|
-
<div className="font-mono text-lg font-semibold text-theme-text-primary">
|
|
3102
|
-
{value}
|
|
3103
|
-
</div>
|
|
3104
|
-
<div className="text-xs uppercase tracking-wide text-theme-text-tertiary">
|
|
3105
|
-
{label}
|
|
3106
|
-
</div>
|
|
3107
|
-
</div>
|
|
3108
|
-
);
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
const METRICS_AXIS_LABEL_MAX_CHARS = 72;
|
|
3112
|
-
|
|
3113
|
-
function finiteSamples(series: TimeSeries): TimeSeries["dataPoints"] {
|
|
3114
|
-
return series.dataPoints.filter((point) => typeof point.value === "number");
|
|
3115
|
-
}
|
|
3116
|
-
|
|
3117
|
-
function MetricsValueTable({
|
|
3118
|
-
series,
|
|
3119
|
-
labels,
|
|
3120
|
-
unit,
|
|
3121
|
-
withTime,
|
|
3122
|
-
}: {
|
|
3123
|
-
series: TimeSeries[];
|
|
3124
|
-
/** Display name per series, derived from the complete result. */
|
|
3125
|
-
labels: string[];
|
|
3126
|
-
unit: string;
|
|
3127
|
-
withTime: boolean;
|
|
3128
|
-
}) {
|
|
3129
|
-
return (
|
|
3130
|
-
<table className="w-full text-xs">
|
|
3131
|
-
<tbody>
|
|
3132
|
-
{series.map((item, index) => {
|
|
3133
|
-
const sample = finiteSamples(item).at(-1);
|
|
3134
|
-
return (
|
|
3135
|
-
<tr
|
|
3136
|
-
key={`${labels[index]}-${index}`}
|
|
3137
|
-
className="border-b border-theme-border/60 last:border-b-0"
|
|
3138
|
-
>
|
|
3139
|
-
<td className="py-1 pr-3 font-mono text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
3140
|
-
{labels[index]}
|
|
3141
|
-
</td>
|
|
3142
|
-
{withTime ? (
|
|
3143
|
-
<td className="py-1 pr-3 text-right text-theme-text-tertiary tabular-nums">
|
|
3144
|
-
{sample
|
|
3145
|
-
? new Date(sample.timestamp * 1000).toLocaleTimeString()
|
|
3146
|
-
: ""}
|
|
3147
|
-
</td>
|
|
3148
|
-
) : null}
|
|
3149
|
-
<td className="py-1 text-right font-mono tabular-nums text-theme-text-primary">
|
|
3150
|
-
{sample && typeof sample.value === "number"
|
|
3151
|
-
? formatMetricValue(sample.value, unit)
|
|
3152
|
-
: "no value"}
|
|
3153
|
-
</td>
|
|
3154
|
-
</tr>
|
|
3155
|
-
);
|
|
3156
|
-
})}
|
|
3157
|
-
</tbody>
|
|
3158
|
-
</table>
|
|
3159
|
-
);
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
function MetricsBody({
|
|
3163
|
-
data,
|
|
3164
|
-
changeCoverage,
|
|
3165
|
-
}: {
|
|
3166
|
-
data: EvidenceDataOf<"metrics">;
|
|
3167
|
-
changeCoverage?: MetricsChangeCoverage;
|
|
3168
|
-
}) {
|
|
3169
|
-
const annotations = changeCoverage?.markers;
|
|
3170
|
-
const unit = data.unit ?? "";
|
|
3171
|
-
const axisLabel = data.label ?? data.query;
|
|
3172
|
-
const axisTruncated = axisLabel.length > METRICS_AXIS_LABEL_MAX_CHARS;
|
|
3173
|
-
const axisText = axisTruncated
|
|
3174
|
-
? `${axisLabel.slice(0, METRICS_AXIS_LABEL_MAX_CHARS - 1)}…`
|
|
3175
|
-
: axisLabel;
|
|
3176
|
-
const domain = metricsDomain(data);
|
|
3177
|
-
const windowText = domain
|
|
3178
|
-
? `${new Date(domain.start * 1000).toLocaleString()} to ${new Date(domain.end * 1000).toLocaleString()}`
|
|
3179
|
-
: undefined;
|
|
3180
|
-
if (data.series.length === 0) {
|
|
3181
|
-
return (
|
|
3182
|
-
<div className="space-y-2">
|
|
3183
|
-
<p className="text-xs text-theme-text-secondary">
|
|
3184
|
-
No series matched this query
|
|
3185
|
-
{data.mode === "range" ? " in the window" : ""}.
|
|
3186
|
-
</p>
|
|
3187
|
-
<pre className="whitespace-pre-wrap break-all rounded-md border border-theme-border/70 bg-theme-base/30 p-2 font-mono text-xs text-theme-text-secondary">
|
|
3188
|
-
{data.query}
|
|
3189
|
-
</pre>
|
|
3190
|
-
{data.note ? (
|
|
3191
|
-
<p className="text-xs text-theme-text-tertiary">{data.note}</p>
|
|
3192
|
-
) : null}
|
|
3193
|
-
</div>
|
|
3194
|
-
);
|
|
3195
|
-
}
|
|
3196
|
-
if (data.mode === "instant") {
|
|
3197
|
-
return (
|
|
3198
|
-
<div className="space-y-2">
|
|
3199
|
-
<MetricsValueTable
|
|
3200
|
-
series={data.series}
|
|
3201
|
-
labels={seriesDisplayLabels(data.series)}
|
|
3202
|
-
unit={unit}
|
|
3203
|
-
withTime={false}
|
|
3204
|
-
/>
|
|
3205
|
-
<pre className="whitespace-pre-wrap break-all rounded-md border border-theme-border/70 bg-theme-base/30 p-2 font-mono text-xs text-theme-text-secondary">
|
|
3206
|
-
{data.query}
|
|
3207
|
-
</pre>
|
|
3208
|
-
{data.note ? (
|
|
3209
|
-
<p className="text-xs text-theme-text-tertiary">{data.note}</p>
|
|
3210
|
-
) : null}
|
|
3211
|
-
</div>
|
|
3212
|
-
);
|
|
3213
|
-
}
|
|
3214
|
-
// Names come from the whole result so two series that differ only by a
|
|
3215
|
-
// label the chart would hide stay distinguishable wherever they are listed.
|
|
3216
|
-
const labels = seriesDisplayLabels(data.series);
|
|
3217
|
-
const indexed = data.series.map((item, index) => ({
|
|
3218
|
-
item,
|
|
3219
|
-
label: labels[index],
|
|
3220
|
-
finite: finiteSamples(item).length,
|
|
3221
|
-
}));
|
|
3222
|
-
// A series with one finite sample has no line to draw, and one with none
|
|
3223
|
-
// (Prometheus serializes NaN and infinities as gaps) has nothing to show.
|
|
3224
|
-
// Listing both keeps what was captured readable and states what was not.
|
|
3225
|
-
const charted = indexed.filter((entry) => entry.finite >= 2);
|
|
3226
|
-
const single = indexed.filter((entry) => entry.finite === 1);
|
|
3227
|
-
const empty = indexed.filter((entry) => entry.finite === 0);
|
|
3228
|
-
return (
|
|
3229
|
-
<div className="space-y-2">
|
|
3230
|
-
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs">
|
|
3231
|
-
<Tooltip
|
|
3232
|
-
content={axisLabel}
|
|
3233
|
-
delay={200}
|
|
3234
|
-
position="top"
|
|
3235
|
-
disabled={!axisTruncated}
|
|
3236
|
-
>
|
|
3237
|
-
<span
|
|
3238
|
-
className="min-w-0 truncate font-mono text-theme-text-secondary"
|
|
3239
|
-
data-testid="investigation-metrics-axis-label"
|
|
3240
|
-
>
|
|
3241
|
-
{axisText}
|
|
3242
|
-
</span>
|
|
3243
|
-
</Tooltip>
|
|
3244
|
-
{unit ? (
|
|
3245
|
-
<span className="text-theme-text-tertiary">({unit})</span>
|
|
3246
|
-
) : null}
|
|
3247
|
-
{windowText ? (
|
|
3248
|
-
<span className="ml-auto text-theme-text-tertiary">{windowText}</span>
|
|
3249
|
-
) : null}
|
|
3250
|
-
</div>
|
|
3251
|
-
{charted.length > 0 ? (
|
|
3252
|
-
<div className="space-y-1.5 rounded-md border border-theme-border/70 bg-theme-base/30 p-2">
|
|
3253
|
-
<AreaChart
|
|
3254
|
-
series={charted.map((entry) => entry.item)}
|
|
3255
|
-
seriesLabels={charted.map((entry) => entry.label)}
|
|
3256
|
-
color={SERIES_COLORS[0]}
|
|
3257
|
-
fillColor={seriesFill(0, SERIES_COLORS[0])}
|
|
3258
|
-
unit={unit}
|
|
3259
|
-
annotations={annotations}
|
|
3260
|
-
domain={domain}
|
|
3261
|
-
layout="auto"
|
|
3262
|
-
/>
|
|
3263
|
-
{charted.length > 1 ? (
|
|
3264
|
-
<SeriesLegend
|
|
3265
|
-
series={charted.map((entry) => entry.item)}
|
|
3266
|
-
seriesLabels={charted.map((entry) => entry.label)}
|
|
3267
|
-
color={SERIES_COLORS[0]}
|
|
3268
|
-
/>
|
|
3269
|
-
) : null}
|
|
3270
|
-
</div>
|
|
3271
|
-
) : null}
|
|
3272
|
-
{single.length > 0 ? (
|
|
3273
|
-
<div
|
|
3274
|
-
className="space-y-1"
|
|
3275
|
-
data-testid="investigation-metrics-sparse-series"
|
|
3276
|
-
>
|
|
3277
|
-
<p className="text-xs text-theme-text-tertiary">
|
|
3278
|
-
{single.length === 1
|
|
3279
|
-
? "1 series has a single sample in this window, listed with its time:"
|
|
3280
|
-
: `${single.length} series have a single sample in this window, listed with their times:`}
|
|
3281
|
-
</p>
|
|
3282
|
-
<MetricsValueTable
|
|
3283
|
-
series={single.map((entry) => entry.item)}
|
|
3284
|
-
labels={single.map((entry) => entry.label)}
|
|
3285
|
-
unit={unit}
|
|
3286
|
-
withTime
|
|
3287
|
-
/>
|
|
3288
|
-
</div>
|
|
3289
|
-
) : null}
|
|
3290
|
-
{empty.length > 0 ? (
|
|
3291
|
-
<p
|
|
3292
|
-
className="text-xs text-theme-text-tertiary [overflow-wrap:anywhere]"
|
|
3293
|
-
data-testid="investigation-metrics-empty-series"
|
|
3294
|
-
>
|
|
3295
|
-
{empty.length === 1
|
|
3296
|
-
? "1 series had no usable samples in this window: "
|
|
3297
|
-
: `${empty.length} series had no usable samples in this window: `}
|
|
3298
|
-
<span className="font-mono">
|
|
3299
|
-
{empty.map((entry) => entry.label).join("; ")}
|
|
3300
|
-
</span>
|
|
3301
|
-
</p>
|
|
3302
|
-
) : null}
|
|
3303
|
-
{changeCoverage?.checked && charted.length > 0 ? (
|
|
3304
|
-
<p className="text-xs text-theme-text-tertiary">
|
|
3305
|
-
{annotations?.length === 1
|
|
3306
|
-
? "1 change recorded in this window is marked on the chart."
|
|
3307
|
-
: annotations?.length
|
|
3308
|
-
? `${annotations.length} changes recorded in this window are marked on the chart.`
|
|
3309
|
-
: // Only that the changes Radar read miss this window — not that
|
|
3310
|
-
// the window was fully covered. The change lookup has its own
|
|
3311
|
-
// window, which need not span the chart's.
|
|
3312
|
-
"None of the changes Radar read fall in this window."}
|
|
3313
|
-
</p>
|
|
3314
|
-
) : null}
|
|
3315
|
-
{axisTruncated ? (
|
|
3316
|
-
<pre className="whitespace-pre-wrap break-all rounded-md border border-theme-border/70 bg-theme-base/30 p-2 font-mono text-xs text-theme-text-secondary">
|
|
3317
|
-
{data.query}
|
|
3318
|
-
</pre>
|
|
3319
|
-
) : null}
|
|
3320
|
-
{data.note ? (
|
|
3321
|
-
<p className="text-xs text-theme-text-tertiary">{data.note}</p>
|
|
3322
|
-
) : null}
|
|
3323
|
-
</div>
|
|
3324
|
-
);
|
|
3325
|
-
}
|
|
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
|
-
|
|
3429
|
-
function InventoryBody({ data }: { data: EvidenceDataOf<"inventory"> }) {
|
|
3430
|
-
return (
|
|
3431
|
-
<div className="max-h-72 overflow-y-auto rounded-md border border-theme-border">
|
|
3432
|
-
{data.resources.map((resource, index) => (
|
|
3433
|
-
<InventoryRow
|
|
3434
|
-
key={`${resource.kind}-${resource.namespace ?? ""}-${resource.name}`}
|
|
3435
|
-
resource={resource}
|
|
3436
|
-
divider={index > 0}
|
|
3437
|
-
/>
|
|
3438
|
-
))}
|
|
3439
|
-
</div>
|
|
3440
|
-
);
|
|
3441
|
-
}
|
|
3442
|
-
|
|
3443
|
-
const VISIBLE_ALERT_LABELS = 6;
|
|
3444
|
-
|
|
3445
|
-
function alertStateSeverity(state: string | undefined) {
|
|
3446
|
-
switch ((state ?? "").toLowerCase()) {
|
|
3447
|
-
case "firing":
|
|
3448
|
-
return "error" as const;
|
|
3449
|
-
case "pending":
|
|
3450
|
-
return "warning" as const;
|
|
3451
|
-
case "inactive":
|
|
3452
|
-
return "success" as const;
|
|
3453
|
-
default:
|
|
3454
|
-
return "neutral" as const;
|
|
3455
|
-
}
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
|
-
function AlertsBody({ data }: { data: EvidenceDataOf<"alerts"> }) {
|
|
3459
|
-
const { rule, instances, annotations } = data;
|
|
3460
|
-
const health = rule.health?.toLowerCase();
|
|
3461
|
-
// Target instances lead; the rest stay visible in producer order.
|
|
3462
|
-
const ordered = [...instances].sort(
|
|
3463
|
-
(left, right) => Number(right.namesTarget) - Number(left.namesTarget),
|
|
3464
|
-
);
|
|
3465
|
-
const annotationEntries = Object.entries(annotations);
|
|
3466
|
-
return (
|
|
3467
|
-
<div className="space-y-2.5">
|
|
3468
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
3469
|
-
{rule.state ? (
|
|
3470
|
-
<Badge severity={alertStateSeverity(rule.state)} size="sm">
|
|
3471
|
-
{rule.state}
|
|
3472
|
-
</Badge>
|
|
3473
|
-
) : null}
|
|
3474
|
-
{rule.labels.severity ? (
|
|
3475
|
-
<Badge severity={severityBadge(rule.labels.severity)} size="sm">
|
|
3476
|
-
severity {rule.labels.severity}
|
|
3477
|
-
</Badge>
|
|
3478
|
-
) : null}
|
|
3479
|
-
<Badge tone="structural" size="sm">
|
|
3480
|
-
{rule.group}
|
|
3481
|
-
</Badge>
|
|
3482
|
-
{health && health !== "ok" ? (
|
|
3483
|
-
<Badge severity={health === "err" ? "error" : "neutral"} size="sm">
|
|
3484
|
-
health {health}
|
|
3485
|
-
</Badge>
|
|
3486
|
-
) : null}
|
|
3487
|
-
</div>
|
|
3488
|
-
{ordered.length > 0 ? (
|
|
3489
|
-
<ol className="max-h-64 space-y-1.5 overflow-y-auto pr-1">
|
|
3490
|
-
{ordered.map((instance, index) => {
|
|
3491
|
-
const labels = Object.entries(instance.labels).filter(
|
|
3492
|
-
([key]) => key !== "alertname" && key !== "severity",
|
|
3493
|
-
);
|
|
3494
|
-
const hidden = labels.length - VISIBLE_ALERT_LABELS;
|
|
3495
|
-
return (
|
|
3496
|
-
<li
|
|
3497
|
-
key={`${instance.state}-${index}-${labels.map(([k, v]) => `${k}=${v}`).join(",")}`}
|
|
3498
|
-
className="rounded-md border border-theme-border/70 bg-theme-base/30 px-2.5 py-2"
|
|
3499
|
-
>
|
|
3500
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
3501
|
-
<Badge
|
|
3502
|
-
severity={alertStateSeverity(instance.state)}
|
|
3503
|
-
size="sm"
|
|
3504
|
-
>
|
|
3505
|
-
{instance.state}
|
|
3506
|
-
</Badge>
|
|
3507
|
-
{instance.namesTarget ? (
|
|
3508
|
-
<Badge severity="info" size="sm">
|
|
3509
|
-
names this resource
|
|
3510
|
-
</Badge>
|
|
3511
|
-
) : null}
|
|
3512
|
-
{instance.value ? (
|
|
3513
|
-
<span className="font-mono text-xs text-theme-text-secondary">
|
|
3514
|
-
value {instance.value}
|
|
3515
|
-
</span>
|
|
3516
|
-
) : null}
|
|
3517
|
-
{instance.activeAt ? (
|
|
3518
|
-
<Tooltip
|
|
3519
|
-
content={new Date(instance.activeAt).toLocaleString()}
|
|
3520
|
-
delay={150}
|
|
3521
|
-
position="left"
|
|
3522
|
-
wrapperClassName="ml-auto"
|
|
3523
|
-
>
|
|
3524
|
-
<time
|
|
3525
|
-
dateTime={instance.activeAt}
|
|
3526
|
-
className="text-xs text-theme-text-tertiary"
|
|
3527
|
-
>
|
|
3528
|
-
active {formatRelativeAgeTime(instance.activeAt)}
|
|
3529
|
-
</time>
|
|
3530
|
-
</Tooltip>
|
|
3531
|
-
) : null}
|
|
3532
|
-
</div>
|
|
3533
|
-
{labels.length > 0 ? (
|
|
3534
|
-
<div className="mt-1.5 flex flex-wrap gap-1 font-mono text-[11px] text-theme-text-secondary">
|
|
3535
|
-
{labels
|
|
3536
|
-
.slice(0, VISIBLE_ALERT_LABELS)
|
|
3537
|
-
.map(([key, value]) => (
|
|
3538
|
-
<span
|
|
3539
|
-
key={key}
|
|
3540
|
-
className="rounded bg-theme-base px-1.5 py-0.5 [overflow-wrap:anywhere]"
|
|
3541
|
-
>
|
|
3542
|
-
{key}={value}
|
|
3543
|
-
</span>
|
|
3544
|
-
))}
|
|
3545
|
-
{hidden > 0 ? (
|
|
3546
|
-
<span className="px-1 py-0.5 text-theme-text-tertiary">
|
|
3547
|
-
+{hidden} more
|
|
3548
|
-
</span>
|
|
3549
|
-
) : null}
|
|
3550
|
-
</div>
|
|
3551
|
-
) : null}
|
|
3552
|
-
</li>
|
|
3553
|
-
);
|
|
3554
|
-
})}
|
|
3555
|
-
</ol>
|
|
3556
|
-
) : (
|
|
3557
|
-
<p className="text-xs italic text-theme-text-tertiary">
|
|
3558
|
-
No active instances were reported for this rule.
|
|
3559
|
-
</p>
|
|
3560
|
-
)}
|
|
3561
|
-
{annotationEntries.length > 0 ? (
|
|
3562
|
-
<dl className="space-y-1 text-xs">
|
|
3563
|
-
{annotationEntries.map(([key, value]) => (
|
|
3564
|
-
<div key={key} className="flex min-w-0 gap-2">
|
|
3565
|
-
<dt className="shrink-0 text-theme-text-tertiary">{key}</dt>
|
|
3566
|
-
<dd className="min-w-0 leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
3567
|
-
{value}
|
|
3568
|
-
</dd>
|
|
3569
|
-
</div>
|
|
3570
|
-
))}
|
|
3571
|
-
</dl>
|
|
3572
|
-
) : null}
|
|
3573
|
-
{rule.query ? (
|
|
3574
|
-
<TerminalBlock label="Rule expression">{rule.query}</TerminalBlock>
|
|
3575
|
-
) : null}
|
|
3576
|
-
</div>
|
|
3577
|
-
);
|
|
3578
|
-
}
|
|
3579
|
-
|
|
3580
|
-
function helmStatusSeverity(status: string) {
|
|
3581
|
-
const normalized = status.toLowerCase();
|
|
3582
|
-
if (normalized === "deployed") return "success" as const;
|
|
3583
|
-
if (normalized.includes("failed")) return "error" as const;
|
|
3584
|
-
if (normalized.startsWith("pending") || normalized === "uninstalling")
|
|
3585
|
-
return "info" as const;
|
|
3586
|
-
return "warning" as const;
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
function HelmBody({ data }: { data: EvidenceDataOf<"helm"> }) {
|
|
3590
|
-
const { onOpenResource } = useContext(EvidenceNavigationContext);
|
|
3591
|
-
const { release } = data;
|
|
3592
|
-
const operation = release.lastOperation;
|
|
3593
|
-
return (
|
|
3594
|
-
<div className="space-y-3">
|
|
3595
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
3596
|
-
<Badge severity={helmStatusSeverity(release.status)} size="sm">
|
|
3597
|
-
{release.status}
|
|
3598
|
-
</Badge>
|
|
3599
|
-
<Badge tone="structural" size="sm">
|
|
3600
|
-
{release.chart}
|
|
3601
|
-
{release.chartVersion ? ` ${release.chartVersion}` : ""}
|
|
3602
|
-
</Badge>
|
|
3603
|
-
<Badge tone="structural" size="sm">
|
|
3604
|
-
revision {release.revision}
|
|
3605
|
-
</Badge>
|
|
3606
|
-
{release.appVersion ? (
|
|
3607
|
-
<Badge tone="note" size="sm">
|
|
3608
|
-
app {release.appVersion}
|
|
3609
|
-
</Badge>
|
|
3610
|
-
) : null}
|
|
3611
|
-
{release.resourceHealth ? (
|
|
3612
|
-
<Badge
|
|
3613
|
-
severity={
|
|
3614
|
-
mapHealthToTone(release.resourceHealth) === "healthy"
|
|
3615
|
-
? "success"
|
|
3616
|
-
: mapHealthToTone(release.resourceHealth) === "unhealthy"
|
|
3617
|
-
? "error"
|
|
3618
|
-
: "warning"
|
|
3619
|
-
}
|
|
3620
|
-
size="sm"
|
|
3621
|
-
>
|
|
3622
|
-
resources {release.resourceHealth}
|
|
3623
|
-
</Badge>
|
|
3624
|
-
) : null}
|
|
3625
|
-
<Tooltip
|
|
3626
|
-
content={new Date(release.updated).toLocaleString()}
|
|
3627
|
-
delay={150}
|
|
3628
|
-
position="left"
|
|
3629
|
-
wrapperClassName="ml-auto"
|
|
3630
|
-
>
|
|
3631
|
-
<time
|
|
3632
|
-
dateTime={release.updated}
|
|
3633
|
-
className="text-xs text-theme-text-tertiary"
|
|
3634
|
-
>
|
|
3635
|
-
updated {formatRelativeAgeTime(release.updated)}
|
|
3636
|
-
</time>
|
|
3637
|
-
</Tooltip>
|
|
3638
|
-
</div>
|
|
3639
|
-
{release.healthIssue ? (
|
|
3640
|
-
<p className="text-xs leading-relaxed text-warning-text">
|
|
3641
|
-
{release.healthIssue}
|
|
3642
|
-
</p>
|
|
3643
|
-
) : null}
|
|
3644
|
-
{release.healthSummary &&
|
|
3645
|
-
release.healthSummary !== release.healthIssue ? (
|
|
3646
|
-
<p className="text-xs leading-relaxed text-theme-text-secondary">
|
|
3647
|
-
{release.healthSummary}
|
|
3648
|
-
</p>
|
|
3649
|
-
) : null}
|
|
3650
|
-
{operation ? (
|
|
3651
|
-
<div className="rounded-md border border-theme-border bg-theme-base/40 px-2.5 py-2">
|
|
3652
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
3653
|
-
<span className="text-xs font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
3654
|
-
Last operation
|
|
3655
|
-
</span>
|
|
3656
|
-
<Badge tone="note" size="sm">
|
|
3657
|
-
{operation.kind.replaceAll("_", " ")}
|
|
3658
|
-
</Badge>
|
|
3659
|
-
<Badge
|
|
3660
|
-
severity={
|
|
3661
|
-
operation.status === "completed"
|
|
3662
|
-
? "success"
|
|
3663
|
-
: operation.status === "failed"
|
|
3664
|
-
? "error"
|
|
3665
|
-
: "warning"
|
|
3666
|
-
}
|
|
3667
|
-
size="sm"
|
|
3668
|
-
>
|
|
3669
|
-
{operation.status.replaceAll("_", " ")}
|
|
3670
|
-
</Badge>
|
|
3671
|
-
</div>
|
|
3672
|
-
{operation.message ? (
|
|
3673
|
-
<p className="mt-1 text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
3674
|
-
{operation.message}
|
|
3675
|
-
</p>
|
|
3676
|
-
) : null}
|
|
3677
|
-
</div>
|
|
3678
|
-
) : null}
|
|
3679
|
-
{release.description ? (
|
|
3680
|
-
<p className="text-xs text-theme-text-tertiary [overflow-wrap:anywhere]">
|
|
3681
|
-
{release.description}
|
|
3682
|
-
</p>
|
|
3683
|
-
) : null}
|
|
3684
|
-
{release.storageNamespace &&
|
|
3685
|
-
release.storageNamespace !== release.namespace ? (
|
|
3686
|
-
<p className="text-xs text-theme-text-tertiary">
|
|
3687
|
-
Release metadata stored in namespace {release.storageNamespace}
|
|
3688
|
-
</p>
|
|
3689
|
-
) : null}
|
|
3690
|
-
{release.managedByFluxHelmRelease ? (
|
|
3691
|
-
<p className="text-xs text-theme-text-tertiary">
|
|
3692
|
-
Managed by Flux HelmRelease {release.managedByFluxHelmRelease}
|
|
3693
|
-
</p>
|
|
3694
|
-
) : null}
|
|
3695
|
-
{release.resources.length > 0 ? (
|
|
3696
|
-
<div className="max-h-52 overflow-y-auto rounded-md border border-theme-border">
|
|
3697
|
-
{release.resources.map((owned, index) => (
|
|
3698
|
-
<div
|
|
3699
|
-
key={`${owned.kind}-${owned.namespace}-${owned.name}`}
|
|
3700
|
-
className={clsx(
|
|
3701
|
-
"flex min-w-0 items-center gap-2 px-2.5 py-1.5",
|
|
3702
|
-
index > 0 && "border-t border-theme-border/60",
|
|
3703
|
-
)}
|
|
3704
|
-
>
|
|
3705
|
-
<StatusDot
|
|
3706
|
-
tone={mapHealthToTone(
|
|
3707
|
-
owned.issue ? "unhealthy" : (owned.status ?? ""),
|
|
3708
|
-
)}
|
|
3709
|
-
className="shrink-0"
|
|
3710
|
-
/>
|
|
3711
|
-
<Badge tone="structural" size="sm">
|
|
3712
|
-
{owned.kind}
|
|
3713
|
-
</Badge>
|
|
3714
|
-
<span className="min-w-0 flex-1">
|
|
3715
|
-
<span className="block truncate font-mono text-xs">
|
|
3716
|
-
<ResourceLink
|
|
3717
|
-
name={owned.name}
|
|
3718
|
-
kind={owned.kind}
|
|
3719
|
-
namespace={owned.namespace}
|
|
3720
|
-
group={
|
|
3721
|
-
owned.apiVersion
|
|
3722
|
-
? apiVersionToGroup(owned.apiVersion)
|
|
3723
|
-
: undefined
|
|
3724
|
-
}
|
|
3725
|
-
label={`${owned.namespace ? `${owned.namespace}/` : ""}${owned.name}`}
|
|
3726
|
-
onNavigate={
|
|
3727
|
-
owned.apiVersion && onOpenResource
|
|
3728
|
-
? (ref) => onOpenResource(ref)
|
|
3729
|
-
: undefined
|
|
3730
|
-
}
|
|
3731
|
-
/>
|
|
3732
|
-
</span>
|
|
3733
|
-
{owned.issue ? (
|
|
3734
|
-
<span className="block truncate text-xs text-warning-text">
|
|
3735
|
-
{owned.issue}
|
|
3736
|
-
</span>
|
|
3737
|
-
) : owned.summary || owned.message ? (
|
|
3738
|
-
<span className="block truncate text-xs text-theme-text-tertiary">
|
|
3739
|
-
{owned.summary || owned.message}
|
|
3740
|
-
</span>
|
|
3741
|
-
) : null}
|
|
3742
|
-
</span>
|
|
3743
|
-
{owned.ready || owned.status ? (
|
|
3744
|
-
<span className="ml-auto shrink-0 font-mono text-xs text-theme-text-tertiary">
|
|
3745
|
-
{owned.ready || owned.status}
|
|
3746
|
-
</span>
|
|
3747
|
-
) : null}
|
|
3748
|
-
</div>
|
|
3749
|
-
))}
|
|
3750
|
-
</div>
|
|
3751
|
-
) : null}
|
|
3752
|
-
</div>
|
|
3753
|
-
);
|
|
3754
|
-
}
|
|
3755
|
-
|
|
3756
|
-
function PermissionsBody({ data }: { data: EvidenceDataOf<"permissions"> }) {
|
|
3757
|
-
const { subject, accessCheck } = data;
|
|
3758
|
-
const subjectBadges = (
|
|
3759
|
-
<>
|
|
3760
|
-
<Badge tone="structural" size="sm">
|
|
3761
|
-
{subject.kind}
|
|
3762
|
-
</Badge>
|
|
3763
|
-
<span className="font-mono text-xs text-theme-text-secondary">
|
|
3764
|
-
{subject.namespace ? `${subject.namespace}/` : ""}
|
|
3765
|
-
{subject.name}
|
|
3766
|
-
</span>
|
|
3767
|
-
</>
|
|
3768
|
-
);
|
|
3769
|
-
if (accessCheck) {
|
|
3770
|
-
const facts = [
|
|
3771
|
-
["Verb", accessCheck.verb],
|
|
3772
|
-
["Resource", accessCheck.resource],
|
|
3773
|
-
["Subresource", accessCheck.subresource],
|
|
3774
|
-
["API group", accessCheck.group || "core"],
|
|
3775
|
-
["Namespace", accessCheck.namespace || "cluster-wide"],
|
|
3776
|
-
["Name", accessCheck.resourceName],
|
|
3777
|
-
] as const;
|
|
3778
|
-
return (
|
|
3779
|
-
<div className="space-y-2.5">
|
|
3780
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
3781
|
-
{subjectBadges}
|
|
3782
|
-
<Badge
|
|
3783
|
-
severity={accessCheck.allowed ? "success" : "warning"}
|
|
3784
|
-
size="sm"
|
|
3785
|
-
>
|
|
3786
|
-
{accessCheck.allowed
|
|
3787
|
-
? "allowed"
|
|
3788
|
-
: accessCheck.denied
|
|
3789
|
-
? "denied"
|
|
3790
|
-
: "not allowed"}
|
|
3791
|
-
</Badge>
|
|
3792
|
-
</div>
|
|
3793
|
-
<dl className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
|
|
3794
|
-
{facts.map(([label, value]) => (
|
|
3795
|
-
<ResourceFact key={label} label={label} value={value} />
|
|
3796
|
-
))}
|
|
3797
|
-
</dl>
|
|
3798
|
-
{accessCheck.reason ? (
|
|
3799
|
-
<p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
3800
|
-
{accessCheck.reason}
|
|
3801
|
-
</p>
|
|
3802
|
-
) : null}
|
|
3803
|
-
{accessCheck.evaluationError ? (
|
|
3804
|
-
<p className="text-xs leading-relaxed text-semantic-error">
|
|
3805
|
-
{accessCheck.evaluationError}
|
|
3806
|
-
</p>
|
|
3807
|
-
) : null}
|
|
3808
|
-
</div>
|
|
3809
|
-
);
|
|
3810
|
-
}
|
|
3811
|
-
const bindings = data.bindings ?? [];
|
|
3812
|
-
const usedByPods = data.usedByPods ?? [];
|
|
3813
|
-
return (
|
|
3814
|
-
<div className="space-y-2.5">
|
|
3815
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
3816
|
-
{subjectBadges}
|
|
3817
|
-
<Badge tone="note" size="sm">
|
|
3818
|
-
{data.flatRulesCount ?? 0}
|
|
3819
|
-
{data.truncated ? "+" : ""} effective rules
|
|
3820
|
-
</Badge>
|
|
3821
|
-
{data.truncated ? (
|
|
3822
|
-
<Badge severity="warning" size="sm">
|
|
3823
|
-
rule list truncated
|
|
3824
|
-
</Badge>
|
|
3825
|
-
) : null}
|
|
3826
|
-
</div>
|
|
3827
|
-
{bindings.length > 0 ? (
|
|
3828
|
-
<div className="max-h-52 overflow-y-auto rounded-md border border-theme-border">
|
|
3829
|
-
{bindings.map((binding, index) => (
|
|
3830
|
-
<div
|
|
3831
|
-
key={`${binding.bindingKind}-${binding.bindingNamespace ?? ""}-${binding.bindingName}`}
|
|
3832
|
-
className={clsx(
|
|
3833
|
-
"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-2.5 py-1.5 text-xs",
|
|
3834
|
-
index > 0 && "border-t border-theme-border/60",
|
|
3835
|
-
)}
|
|
3836
|
-
>
|
|
3837
|
-
<Badge tone="structural" size="sm">
|
|
3838
|
-
{binding.bindingKind}
|
|
3839
|
-
</Badge>
|
|
3840
|
-
<span className="min-w-0 truncate font-mono text-theme-text-secondary">
|
|
3841
|
-
{binding.bindingNamespace ? `${binding.bindingNamespace}/` : ""}
|
|
3842
|
-
{binding.bindingName}
|
|
3843
|
-
</span>
|
|
3844
|
-
<span className="text-theme-text-tertiary">→</span>
|
|
3845
|
-
<Badge tone="structural" size="sm">
|
|
3846
|
-
{binding.roleKind}
|
|
3847
|
-
</Badge>
|
|
3848
|
-
<span className="min-w-0 truncate font-mono text-theme-text-primary">
|
|
3849
|
-
{binding.roleNamespace ? `${binding.roleNamespace}/` : ""}
|
|
3850
|
-
{binding.roleName}
|
|
3851
|
-
</span>
|
|
3852
|
-
<span className="ml-auto shrink-0 font-mono text-theme-text-tertiary">
|
|
3853
|
-
{binding.rulesCount} rule{binding.rulesCount === 1 ? "" : "s"}
|
|
3854
|
-
</span>
|
|
3855
|
-
{binding.inheritedFromGroup ? (
|
|
3856
|
-
<Badge tone="note" size="sm">
|
|
3857
|
-
via {binding.inheritedFromGroup}
|
|
3858
|
-
</Badge>
|
|
3859
|
-
) : null}
|
|
3860
|
-
</div>
|
|
3861
|
-
))}
|
|
3862
|
-
</div>
|
|
3863
|
-
) : (
|
|
3864
|
-
<p className="text-xs italic text-theme-text-tertiary">
|
|
3865
|
-
No RoleBinding or ClusterRoleBinding grants this subject anything.
|
|
3866
|
-
</p>
|
|
3867
|
-
)}
|
|
3868
|
-
{usedByPods.length > 0 ? (
|
|
3869
|
-
<p className="text-xs leading-relaxed text-theme-text-secondary [overflow-wrap:anywhere]">
|
|
3870
|
-
<span className="text-theme-text-tertiary">Used by pods: </span>
|
|
3871
|
-
<span className="font-mono">{usedByPods.join(", ")}</span>
|
|
3872
|
-
{data.podsTotal && data.podsTotal > usedByPods.length
|
|
3873
|
-
? ` and ${data.podsTotal - usedByPods.length} more`
|
|
3874
|
-
: ""}
|
|
3875
|
-
</p>
|
|
3876
|
-
) : null}
|
|
3877
|
-
</div>
|
|
3878
|
-
);
|
|
3879
|
-
}
|
|
3880
|
-
|
|
3881
|
-
function RevisionHistory({
|
|
3882
|
-
observations,
|
|
3883
|
-
citedOrder,
|
|
3884
|
-
caseItems = [],
|
|
3885
|
-
reveal,
|
|
3886
|
-
revealRequestId,
|
|
3887
|
-
onViewSource,
|
|
3888
|
-
}: {
|
|
3889
|
-
observations: InvestigationEvidenceObservation[];
|
|
3890
|
-
citedOrder?: number;
|
|
3891
|
-
/** Agent items bound to a superseded observation of this group. */
|
|
3892
|
-
caseItems?: InvestigationCaseItem[];
|
|
3893
|
-
reveal: boolean;
|
|
3894
|
-
revealRequestId?: number;
|
|
3895
|
-
onViewSource: (
|
|
3896
|
-
sourceId: string,
|
|
3897
|
-
excerpt?: InvestigationSourceExcerpt,
|
|
3898
|
-
) => void;
|
|
3899
|
-
}) {
|
|
3900
|
-
const [open, setOpen] = useState(false);
|
|
3901
|
-
const regionId = useId();
|
|
3902
|
-
const { metricsMarkersBySource } = useContext(EvidenceNavigationContext);
|
|
3903
|
-
const { elementRef, revealAfterToggle } =
|
|
3904
|
-
useDisclosureReveal<HTMLDivElement>();
|
|
3905
|
-
useLayoutEffect(() => {
|
|
3906
|
-
if (reveal) setOpen(true);
|
|
3907
|
-
}, [reveal, revealRequestId]);
|
|
3908
|
-
return (
|
|
3909
|
-
<div>
|
|
3910
|
-
<button
|
|
3911
|
-
type="button"
|
|
3912
|
-
aria-expanded={open}
|
|
3913
|
-
aria-controls={regionId}
|
|
3914
|
-
onClick={() => {
|
|
3915
|
-
setOpen(!open);
|
|
3916
|
-
revealAfterToggle(!open);
|
|
3917
|
-
}}
|
|
3918
|
-
className="flex items-center gap-1.5 rounded-md px-1 py-1 text-xs text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-secondary"
|
|
3919
|
-
>
|
|
3920
|
-
<CollapseChevron open={open} className="h-3.5 w-3.5" />
|
|
3921
|
-
Previous observations · {observations.length}
|
|
3922
|
-
</button>
|
|
3923
|
-
<div id={regionId} ref={elementRef}>
|
|
3924
|
-
<Collapse open={open}>
|
|
3925
|
-
<ol className="space-y-3 pt-2">
|
|
3926
|
-
{observations.map((observation) => {
|
|
3927
|
-
const key = investigationCaseObservationKey(observation);
|
|
3928
|
-
const rowItems = caseItems.filter(
|
|
3929
|
-
(item) =>
|
|
3930
|
-
item.observation &&
|
|
3931
|
-
investigationCaseObservationKey(item.observation) === key,
|
|
3932
|
-
);
|
|
3933
|
-
const rowRoles = [...new Set(rowItems.map((item) => item.role))];
|
|
3934
|
-
return (
|
|
3935
|
-
<li
|
|
3936
|
-
key={`${observation.source.id}-${observation.revision}`}
|
|
3937
|
-
data-case-observation={rowItems.length > 0 ? key : undefined}
|
|
3938
|
-
className="flex min-w-0 items-start gap-2 text-xs"
|
|
3939
|
-
>
|
|
3940
|
-
<span className="mt-0.5 shrink-0 text-theme-text-tertiary">
|
|
3941
|
-
<span className="font-medium text-theme-text-secondary">
|
|
3942
|
-
{observation.source.order === citedOrder
|
|
3943
|
-
? "Used for assessment"
|
|
3944
|
-
: phaseLabel(observation.source.phase)}
|
|
3945
|
-
</span>
|
|
3946
|
-
</span>
|
|
3947
|
-
<div className="min-w-0 flex-1 space-y-1 text-theme-text-secondary">
|
|
3948
|
-
{rowRoles.length > 0 ? (
|
|
3949
|
-
<p className="flex flex-wrap items-center gap-1.5">
|
|
3950
|
-
<span>{observation.summary || observation.title}</span>
|
|
3951
|
-
{rowRoles.map((role) => (
|
|
3952
|
-
<AgentRoleChip key={role} role={role} />
|
|
3953
|
-
))}
|
|
3954
|
-
</p>
|
|
3955
|
-
) : (
|
|
3956
|
-
<p>{observation.summary || observation.title}</p>
|
|
3957
|
-
)}
|
|
3958
|
-
{rowItems
|
|
3959
|
-
.filter((item) => item.claim)
|
|
3960
|
-
.map((item) => (
|
|
3961
|
-
<AgentClaimNote
|
|
3962
|
-
key={item.index}
|
|
3963
|
-
claim={item.claim}
|
|
3964
|
-
role={rowRoles.length > 1 ? item.role : undefined}
|
|
3965
|
-
className="pt-1"
|
|
3966
|
-
/>
|
|
3967
|
-
))}
|
|
3968
|
-
{evidenceHasDetails(
|
|
3969
|
-
observation.data,
|
|
3970
|
-
observation.summary,
|
|
3971
|
-
) && (
|
|
3972
|
-
<EvidenceBody
|
|
3973
|
-
data={observation.data}
|
|
3974
|
-
cardSummary={observation.summary}
|
|
3975
|
-
changeCoverage={metricsMarkersBySource?.get(
|
|
3976
|
-
observation.source.id,
|
|
3977
|
-
)}
|
|
3978
|
-
/>
|
|
3979
|
-
)}
|
|
3980
|
-
</div>
|
|
3981
|
-
<SourceButton
|
|
3982
|
-
ariaLabel={`View result for ${phaseLabel(observation.source.phase).toLowerCase()} observation of ${observation.title}`}
|
|
3983
|
-
onClick={() =>
|
|
3984
|
-
onViewSource(
|
|
3985
|
-
observation.source.id,
|
|
3986
|
-
evidenceSourceExcerpt(observation.data),
|
|
3987
|
-
)
|
|
3988
|
-
}
|
|
3989
|
-
/>
|
|
3990
|
-
</li>
|
|
3991
|
-
);
|
|
3992
|
-
})}
|
|
3993
|
-
</ol>
|
|
3994
|
-
</Collapse>
|
|
3995
|
-
</div>
|
|
3996
|
-
</div>
|
|
3997
|
-
);
|
|
3998
|
-
}
|
|
3999
|
-
|
|
4000
|
-
function EvidenceCaveat({ data }: { data: InvestigationEvidenceData }) {
|
|
4001
|
-
// Changes get a tooltip rather than a sentence because the wording depends
|
|
4002
|
-
// on whether the reported age was collected with the change.
|
|
4003
|
-
if (data.type === "changes") {
|
|
4004
|
-
return (
|
|
4005
|
-
<Tooltip
|
|
4006
|
-
content={`A change alone does not establish the cause.${data.changeContext?.when ? " The reported age is as of collection." : ""}`}
|
|
4007
|
-
position="left"
|
|
4008
|
-
className="pointer-events-none"
|
|
4009
|
-
wrapperClassName="flex shrink-0"
|
|
4010
|
-
>
|
|
4011
|
-
<button
|
|
4012
|
-
type="button"
|
|
4013
|
-
aria-label="About change evidence"
|
|
4014
|
-
className="flex h-7 w-7 items-center justify-center rounded-md text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
|
|
4015
|
-
>
|
|
4016
|
-
<Info className="h-3.5 w-3.5" aria-hidden />
|
|
4017
|
-
</button>
|
|
4018
|
-
</Tooltip>
|
|
4019
|
-
);
|
|
4020
|
-
}
|
|
4021
|
-
const text = EVIDENCE_KIND_TRAITS[data.type].caveat;
|
|
4022
|
-
if (!text) return null;
|
|
4023
|
-
return (
|
|
4024
|
-
<p className="flex items-start gap-1.5 text-xs leading-relaxed text-theme-text-tertiary">
|
|
4025
|
-
<Info className="mt-0.5 h-3 w-3 shrink-0" aria-hidden />
|
|
4026
|
-
{text}
|
|
4027
|
-
</p>
|
|
4028
|
-
);
|
|
4029
|
-
}
|
|
4030
|
-
|
|
4031
|
-
function EvidenceIcon({
|
|
4032
|
-
observation,
|
|
4033
|
-
prominence,
|
|
4034
|
-
}: {
|
|
4035
|
-
observation: InvestigationEvidenceObservation;
|
|
4036
|
-
prominence: "primary" | "supporting" | "secondary";
|
|
4037
|
-
}) {
|
|
4038
|
-
const Icon = evidenceIcon(observation.data.type);
|
|
4039
|
-
return (
|
|
4040
|
-
<span
|
|
4041
|
-
className={clsx(
|
|
4042
|
-
"flex shrink-0 items-center justify-center text-theme-text-tertiary",
|
|
4043
|
-
prominence === "primary" ? "h-7 w-7" : "h-6 w-6",
|
|
4044
|
-
)}
|
|
4045
|
-
>
|
|
4046
|
-
<Icon
|
|
4047
|
-
className={prominence === "primary" ? "h-4 w-4" : "h-3.5 w-3.5"}
|
|
4048
|
-
aria-hidden
|
|
4049
|
-
/>
|
|
4050
|
-
</span>
|
|
4051
|
-
);
|
|
4052
|
-
}
|
|
4053
|
-
|
|
4054
|
-
function SourceButton({
|
|
4055
|
-
ariaLabel,
|
|
4056
|
-
buttonLabel = "View result",
|
|
4057
|
-
compact = false,
|
|
4058
|
-
onClick,
|
|
4059
|
-
}: {
|
|
4060
|
-
ariaLabel?: string;
|
|
4061
|
-
buttonLabel?: string;
|
|
4062
|
-
compact?: boolean;
|
|
4063
|
-
onClick: () => void;
|
|
4064
|
-
}) {
|
|
4065
|
-
const tooltip = "Open the original tool result";
|
|
4066
|
-
return (
|
|
4067
|
-
<Tooltip
|
|
4068
|
-
content={tooltip}
|
|
4069
|
-
delay={350}
|
|
4070
|
-
position="left"
|
|
4071
|
-
wrapperClassName="flex shrink-0"
|
|
4072
|
-
>
|
|
4073
|
-
<button
|
|
4074
|
-
type="button"
|
|
4075
|
-
aria-label={ariaLabel ?? tooltip}
|
|
4076
|
-
onClick={onClick}
|
|
4077
|
-
className={clsx(
|
|
4078
|
-
"flex h-7 shrink-0 items-center justify-center rounded-md text-theme-text-tertiary hover:bg-theme-hover hover:text-accent-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50",
|
|
4079
|
-
"gap-1 px-2 text-xs font-medium",
|
|
4080
|
-
)}
|
|
4081
|
-
>
|
|
4082
|
-
<FileSearch className="h-3.5 w-3.5" aria-hidden />
|
|
4083
|
-
<span
|
|
4084
|
-
className={compact ? "hidden @min-[540px]/card:inline" : undefined}
|
|
4085
|
-
>
|
|
4086
|
-
{buttonLabel}
|
|
4087
|
-
</span>
|
|
4088
|
-
</button>
|
|
4089
|
-
</Tooltip>
|
|
4090
|
-
);
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
function evidenceIcon(type: InvestigationEvidenceData["type"]) {
|
|
4094
|
-
return EVIDENCE_KIND_TRAITS[type].icon;
|
|
4095
|
-
}
|
|
4096
|
-
|
|
4097
|
-
function severityBadge(value: string) {
|
|
4098
|
-
const tone = value.toLowerCase();
|
|
4099
|
-
if (tone === "error" || tone === "critical" || tone === "failed")
|
|
4100
|
-
return "error" as const;
|
|
4101
|
-
if (tone === "alert" || tone === "high") return "alert" as const;
|
|
4102
|
-
if (tone === "warning" || tone === "medium") return "warning" as const;
|
|
4103
|
-
if (tone === "info" || tone === "low") return "info" as const;
|
|
4104
|
-
return "neutral" as const;
|
|
4105
|
-
}
|
|
4106
|
-
|
|
4107
|
-
function phaseLabel(
|
|
4108
|
-
phase: InvestigationEvidenceObservation["source"]["phase"],
|
|
4109
|
-
): string {
|
|
4110
|
-
switch (phase) {
|
|
4111
|
-
case "initial":
|
|
4112
|
-
return "Initial";
|
|
4113
|
-
case "followup":
|
|
4114
|
-
return "Follow-up";
|
|
4115
|
-
case "verification":
|
|
4116
|
-
return "Verification";
|
|
4117
|
-
case "apply":
|
|
4118
|
-
return "Apply";
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
function toneBorder(
|
|
4123
|
-
tone: InvestigationEvidenceObservation["tone"],
|
|
4124
|
-
tier: InvestigationEvidenceTier,
|
|
4125
|
-
prominence: "primary" | "supporting" | "secondary",
|
|
4126
|
-
): string {
|
|
4127
|
-
if (prominence !== "primary") return "border-theme-border/70";
|
|
4128
|
-
// A supporting adverse card is what the healthy-conflict banner points at,
|
|
4129
|
-
// so it must be tellable from its neutral neighbours: a thin rule, lighter
|
|
4130
|
-
// than the Key tier's.
|
|
4131
|
-
if (tier === "supporting" && tone === "error")
|
|
4132
|
-
return "border-l-2 border-l-semantic-error border-theme-border";
|
|
4133
|
-
if (tier === "supporting" && (tone === "warning" || tone === "alert"))
|
|
4134
|
-
return "border-l-2 border-l-semantic-warning border-theme-border";
|
|
4135
|
-
if (tier !== "key") return "border-theme-border";
|
|
4136
|
-
if (tone === "error")
|
|
4137
|
-
return "border-l-[3px] border-l-red-500 border-theme-border";
|
|
4138
|
-
if (tone === "alert")
|
|
4139
|
-
return "border-l-[3px] border-l-orange-500 border-theme-border";
|
|
4140
|
-
return "border-l-[3px] border-l-amber-500 border-theme-border";
|
|
4141
|
-
}
|