@skyhook-io/radar-app 1.13.3 → 1.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/App.tsx +68 -23
- package/src/RadarApp.tsx +17 -1
- package/src/api/client.ts +28 -10
- package/src/api/diagnose.ts +61 -15
- package/src/components/ConnectionErrorView.test.tsx +30 -0
- package/src/components/ConnectionErrorView.tsx +31 -19
- package/src/components/diagnose/AgentCase.tsx +131 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +95 -0
- package/src/components/diagnose/DiagnoseContext.tsx +402 -77
- package/src/components/diagnose/DiagnoseSurface.test.tsx +53 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +198 -286
- package/src/components/diagnose/Home.test.tsx +23 -1
- package/src/components/diagnose/Home.tsx +49 -3
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +1446 -5
- package/src/components/diagnose/InvestigationEvidencePane.tsx +1373 -126
- package/src/components/diagnose/InvestigationView.tsx +227 -5
- package/src/components/diagnose/LocalDiagnoseAction.tsx +2 -3
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
- package/src/components/diagnose/investigationCase.test.tsx +1568 -0
- package/src/components/diagnose/investigationCase.ts +439 -0
- package/src/components/diagnose/investigationEvidence.test.ts +3525 -17
- package/src/components/diagnose/investigationEvidence.ts +2246 -166
- package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
- package/src/components/diagnose/investigationMetrics.test.ts +712 -0
- package/src/components/diagnose/investigationMetrics.ts +393 -0
- package/src/components/diagnose/investigationSourceFocus.ts +6 -0
- package/src/components/diagnose/investigationState.test.ts +322 -0
- package/src/components/diagnose/investigationState.ts +147 -25
- package/src/components/diagnose/parts.test.tsx +537 -1
- package/src/components/diagnose/parts.tsx +486 -42
- package/src/components/resource/HPACharts.render.test.tsx +77 -0
- package/src/components/resource/HPACharts.tsx +32 -25
- package/src/components/resource/PrometheusChartsGrid.tsx +181 -79
- package/src/components/resources/PodFilePreview.test.tsx +131 -0
- package/src/components/resources/PodFilePreview.tsx +394 -0
- package/src/components/resources/PodFilesystemModal.tsx +157 -67
- package/src/components/resources/ResourcesView.tsx +27 -2
- package/src/context/DiagnoseCustomization.test.tsx +26 -0
- package/src/context/DiagnoseCustomization.tsx +14 -2
- package/src/index.ts +8 -5
- package/src/utils/shell-safe.test.ts +25 -1
- package/src/utils/shell-safe.ts +16 -0
|
@@ -20,15 +20,10 @@ import {
|
|
|
20
20
|
TerminalSquare,
|
|
21
21
|
Copy,
|
|
22
22
|
Check,
|
|
23
|
-
|
|
23
|
+
RotateCcw,
|
|
24
24
|
PanelLeftOpen,
|
|
25
|
-
Link,
|
|
26
|
-
Lock,
|
|
27
|
-
Users,
|
|
28
25
|
} from "lucide-react";
|
|
29
26
|
import { Tooltip } from "../ui/Tooltip";
|
|
30
|
-
import { ConfirmDialog } from "../ui/ConfirmDialog";
|
|
31
|
-
import { Badge } from "@skyhook-io/k8s-ui/components/ui/Badge";
|
|
32
27
|
import { useAnimatedUnmount } from "../../hooks/useAnimatedUnmount";
|
|
33
28
|
import { TRANSITION_BACKDROP, TRANSITION_DRAWER } from "../../utils/animation";
|
|
34
29
|
import {
|
|
@@ -39,13 +34,19 @@ import {
|
|
|
39
34
|
type DiagnoseView,
|
|
40
35
|
} from "./DiagnoseContext";
|
|
41
36
|
import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
|
|
37
|
+
import { useNavCustomization } from "../../context/NavCustomization";
|
|
42
38
|
import { InvestigationView } from "./InvestigationView";
|
|
43
|
-
import {
|
|
39
|
+
import type { InvestigationTimelineScope } from "./InvestigationEvidencePane";
|
|
40
|
+
import {
|
|
41
|
+
InvestigationHome,
|
|
42
|
+
RecentList,
|
|
43
|
+
absoluteTime,
|
|
44
|
+
statusWord,
|
|
45
|
+
} from "./Home";
|
|
44
46
|
import { AgentSetupNotice } from "./AgentSetupNotice";
|
|
45
47
|
import { ConsentCard } from "./parts";
|
|
46
48
|
import { buildLaunchCommand, launchAgentLabel, openInTerminal } from "./launch";
|
|
47
49
|
import {
|
|
48
|
-
updateRunVisibility,
|
|
49
50
|
type RunSummary,
|
|
50
51
|
type ExecutionProfile,
|
|
51
52
|
} from "../../api/diagnose";
|
|
@@ -183,163 +184,14 @@ function InvestigationMenu({ run }: { run: RunSummary }) {
|
|
|
183
184
|
);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
export function canCopyRunLink(
|
|
187
|
-
run: RunSummary | null | undefined,
|
|
188
|
-
): run is RunSummary & { radarUrl: string } {
|
|
189
|
-
return typeof run?.radarUrl === "string" && run.radarUrl.length > 0;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function CopyRunLink({
|
|
193
|
-
radarUrl,
|
|
194
|
-
visibility,
|
|
195
|
-
}: {
|
|
196
|
-
radarUrl: string;
|
|
197
|
-
visibility: RunSummary["visibility"];
|
|
198
|
-
}) {
|
|
199
|
-
const label =
|
|
200
|
-
visibility === "private"
|
|
201
|
-
? "Copy private link (only you can open it)"
|
|
202
|
-
: "Copy investigation link";
|
|
203
|
-
const [copyState, setCopyState] = useState<"idle" | "copied" | "error">(
|
|
204
|
-
"idle",
|
|
205
|
-
);
|
|
206
|
-
const copy = async () => {
|
|
207
|
-
try {
|
|
208
|
-
if (!navigator.clipboard) throw new Error("clipboard unavailable");
|
|
209
|
-
await navigator.clipboard.writeText(
|
|
210
|
-
new URL(radarUrl, window.location.origin).href,
|
|
211
|
-
);
|
|
212
|
-
setCopyState("copied");
|
|
213
|
-
} catch {
|
|
214
|
-
setCopyState("error");
|
|
215
|
-
}
|
|
216
|
-
setTimeout(() => setCopyState("idle"), 1500);
|
|
217
|
-
};
|
|
218
|
-
return (
|
|
219
|
-
<Tooltip
|
|
220
|
-
content={
|
|
221
|
-
copyState === "copied"
|
|
222
|
-
? "Link copied"
|
|
223
|
-
: copyState === "error"
|
|
224
|
-
? "Couldn’t copy link"
|
|
225
|
-
: label
|
|
226
|
-
}
|
|
227
|
-
position="bottom"
|
|
228
|
-
>
|
|
229
|
-
<button
|
|
230
|
-
onClick={copy}
|
|
231
|
-
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
232
|
-
aria-label={label}
|
|
233
|
-
>
|
|
234
|
-
{copyState === "copied" ? (
|
|
235
|
-
<Check className="h-4 w-4 text-emerald-500" />
|
|
236
|
-
) : (
|
|
237
|
-
<Link className="h-4 w-4" />
|
|
238
|
-
)}
|
|
239
|
-
</button>
|
|
240
|
-
</Tooltip>
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function VisibilityControl({
|
|
245
|
-
run,
|
|
246
|
-
onChanged,
|
|
247
|
-
}: {
|
|
248
|
-
run: RunSummary;
|
|
249
|
-
onChanged: (run: RunSummary) => void;
|
|
250
|
-
}) {
|
|
251
|
-
const [busy, setBusy] = useState(false);
|
|
252
|
-
const [error, setError] = useState(false);
|
|
253
|
-
const [confirmShare, setConfirmShare] = useState(false);
|
|
254
|
-
if (!run.canManageVisibility) {
|
|
255
|
-
return run.visibility === "organization" ? (
|
|
256
|
-
<Tooltip content="Shared with your organization" position="bottom">
|
|
257
|
-
<Badge severity="neutral" size="sm" className="shrink-0">
|
|
258
|
-
<Users className="h-3 w-3" />
|
|
259
|
-
Organization
|
|
260
|
-
</Badge>
|
|
261
|
-
</Tooltip>
|
|
262
|
-
) : run.visibility === "private" ? (
|
|
263
|
-
<Tooltip
|
|
264
|
-
content="Only you can view this investigation. Other organization members don’t have access."
|
|
265
|
-
position="bottom"
|
|
266
|
-
>
|
|
267
|
-
<Badge severity="neutral" size="sm" className="shrink-0">
|
|
268
|
-
<Lock className="h-3 w-3" />
|
|
269
|
-
Private
|
|
270
|
-
</Badge>
|
|
271
|
-
</Tooltip>
|
|
272
|
-
) : null;
|
|
273
|
-
}
|
|
274
|
-
const shared = run.visibility === "organization";
|
|
275
|
-
const update = () => {
|
|
276
|
-
if (busy) return;
|
|
277
|
-
setBusy(true);
|
|
278
|
-
setError(false);
|
|
279
|
-
updateRunVisibility(run.id, shared ? "private" : "organization")
|
|
280
|
-
.then((updated) => {
|
|
281
|
-
onChanged(updated);
|
|
282
|
-
setConfirmShare(false);
|
|
283
|
-
})
|
|
284
|
-
.catch(() => setError(true))
|
|
285
|
-
.finally(() => setBusy(false));
|
|
286
|
-
};
|
|
287
|
-
const label = shared ? "Organization" : "Private";
|
|
288
|
-
return (
|
|
289
|
-
<>
|
|
290
|
-
<Tooltip
|
|
291
|
-
content={
|
|
292
|
-
error
|
|
293
|
-
? "Couldn't change sharing"
|
|
294
|
-
: shared
|
|
295
|
-
? "Shared with your organization — make private"
|
|
296
|
-
: "Private — click to let organization members access this investigation"
|
|
297
|
-
}
|
|
298
|
-
position="bottom"
|
|
299
|
-
>
|
|
300
|
-
<button
|
|
301
|
-
onClick={() => (shared ? update() : setConfirmShare(true))}
|
|
302
|
-
disabled={busy}
|
|
303
|
-
className="flex items-center gap-1 rounded-md border border-theme-border/70 px-1.5 py-1 text-[11px] font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:opacity-50"
|
|
304
|
-
aria-label={
|
|
305
|
-
shared
|
|
306
|
-
? "Shared with your organization — make private"
|
|
307
|
-
: "Private — click to let organization members access this investigation"
|
|
308
|
-
}
|
|
309
|
-
>
|
|
310
|
-
{shared ? (
|
|
311
|
-
<Users className="h-3.5 w-3.5" />
|
|
312
|
-
) : (
|
|
313
|
-
<Lock className="h-3.5 w-3.5" />
|
|
314
|
-
)}
|
|
315
|
-
{label}
|
|
316
|
-
</button>
|
|
317
|
-
</Tooltip>
|
|
318
|
-
<ConfirmDialog
|
|
319
|
-
open={confirmShare}
|
|
320
|
-
onClose={() => !busy && setConfirmShare(false)}
|
|
321
|
-
onConfirm={update}
|
|
322
|
-
title="Share this investigation?"
|
|
323
|
-
message="Everyone in your organization can read this entire investigation—including your questions and the logs and manifests Radar read—and can continue or stop it."
|
|
324
|
-
confirmLabel="Share with organization"
|
|
325
|
-
showWarning={false}
|
|
326
|
-
variant="warning"
|
|
327
|
-
isLoading={busy}
|
|
328
|
-
/>
|
|
329
|
-
</>
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
187
|
// The panel is an ABSOLUTE slot inside the app's body frame (the column under the
|
|
334
188
|
// header, right of the nav rail) — App renders it there and passes topInset (the
|
|
335
189
|
// header height; 0 in chromeless embeds). It shares that frame with the resource/
|
|
336
190
|
// Helm drawers, so it no longer floats viewport-fixed or DOM-measures the chrome.
|
|
337
|
-
// Whether the header offers a
|
|
191
|
+
// Whether the header offers a fresh re-run of the focused investigation.
|
|
338
192
|
// Every clause is a failure this button actually had:
|
|
339
193
|
//
|
|
340
|
-
// view
|
|
341
|
-
// the last focused run. Without this the button dispatches an
|
|
342
|
-
// agent — real tokens — from a screen showing an unrelated list.
|
|
194
|
+
// view Home is the fresh-entry surface, never a re-run action.
|
|
343
195
|
// run nothing to take a resource from.
|
|
344
196
|
// running/stopping human starts reuse the live run. Automatic investigations
|
|
345
197
|
// are immutable, so they retain a separate fresh human start.
|
|
@@ -347,7 +199,7 @@ function VisibilityControl({
|
|
|
347
199
|
// the resource may not exist in the active context. Do not
|
|
348
200
|
// silently start a different-cluster investigation from here.
|
|
349
201
|
// needsConsent the consent card owns the surface until it's answered.
|
|
350
|
-
export function
|
|
202
|
+
export function canRerunInvestigation(
|
|
351
203
|
view: DiagnoseView,
|
|
352
204
|
run: RunSummary | null,
|
|
353
205
|
needsConsent: boolean,
|
|
@@ -355,6 +207,8 @@ export function canStartNewInvestigation(
|
|
|
355
207
|
return (
|
|
356
208
|
view === "investigation" &&
|
|
357
209
|
!!run &&
|
|
210
|
+
!!run.kind &&
|
|
211
|
+
!!run.name &&
|
|
358
212
|
((run.status !== "running" && run.status !== "stopping") ||
|
|
359
213
|
run.trigger === "background") &&
|
|
360
214
|
run.status !== "stale" &&
|
|
@@ -366,12 +220,6 @@ export function canStartNewInvestigation(
|
|
|
366
220
|
// Keep these Tailwind literals aligned with the measured rail threshold.
|
|
367
221
|
// Detail uses one stable history toggle; below this width it opens an overlay.
|
|
368
222
|
export const INVESTIGATION_HISTORY_MIN_WIDTH = 1750;
|
|
369
|
-
export const MAXIMIZED_COMPACT_HISTORY_VISIBILITY_CLASS =
|
|
370
|
-
"@min-[1750px]/diagnose-surface:hidden";
|
|
371
|
-
export const MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS =
|
|
372
|
-
"hidden @min-[1750px]/diagnose-surface:flex";
|
|
373
|
-
export const MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS =
|
|
374
|
-
"hidden @min-[1750px]/diagnose-surface:block";
|
|
375
223
|
export const MAXIMIZED_RUN_META_VISIBILITY_CLASS =
|
|
376
224
|
"hidden @min-[1750px]/diagnose-surface:flex";
|
|
377
225
|
// The panel is a bounded absolute frame whose descendants own scrolling. If
|
|
@@ -382,6 +230,24 @@ export const MAXIMIZED_RUN_META_VISIBILITY_CLASS =
|
|
|
382
230
|
export const DIAGNOSE_SURFACE_FRAME_CLASS =
|
|
383
231
|
"@container/diagnose-surface absolute z-40 flex min-h-0 flex-col overflow-hidden border-l border-theme-border bg-theme-surface shadow-drawer";
|
|
384
232
|
|
|
233
|
+
export function unavailableInvestigationMessage(embedded: boolean): string {
|
|
234
|
+
return embedded
|
|
235
|
+
? "This investigation is unavailable. It may be private, your access may have changed, or its history may have been cleared. Check your account and organization, or ask the creator for access."
|
|
236
|
+
: "This investigation is unavailable. It may have been removed, or its saved history may have been cleared.";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function investigationHistoryIsPersistent(input: {
|
|
240
|
+
maximized: boolean;
|
|
241
|
+
view: DiagnoseView;
|
|
242
|
+
surfaceWidth: number;
|
|
243
|
+
}): boolean {
|
|
244
|
+
return (
|
|
245
|
+
input.maximized &&
|
|
246
|
+
(input.view === "home" ||
|
|
247
|
+
input.surfaceWidth >= INVESTIGATION_HISTORY_MIN_WIDTH)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
385
251
|
export function investigationHeaderPresentation(input: {
|
|
386
252
|
view: DiagnoseView;
|
|
387
253
|
maximized: boolean;
|
|
@@ -398,17 +264,10 @@ export function investigationHeaderPresentation(input: {
|
|
|
398
264
|
runActionsClass: input.hasVisibleRunDetail ? "" : null,
|
|
399
265
|
};
|
|
400
266
|
}
|
|
401
|
-
const hasWideRetainedDetail = input.maximized && input.hasVisibleRunDetail;
|
|
402
267
|
return {
|
|
403
|
-
genericIdentityClass:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
detailIdentityClass: hasWideRetainedDetail
|
|
407
|
-
? MAXIMIZED_HOME_RUN_HEADER_VISIBILITY_CLASS
|
|
408
|
-
: null,
|
|
409
|
-
runActionsClass: hasWideRetainedDetail
|
|
410
|
-
? MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS
|
|
411
|
-
: null,
|
|
268
|
+
genericIdentityClass: "",
|
|
269
|
+
detailIdentityClass: null,
|
|
270
|
+
runActionsClass: null,
|
|
412
271
|
};
|
|
413
272
|
}
|
|
414
273
|
|
|
@@ -431,69 +290,93 @@ function DiagnoseHeaderIdentity({
|
|
|
431
290
|
onOpenSettings: (() => void) | null;
|
|
432
291
|
}) {
|
|
433
292
|
return (
|
|
434
|
-
<div className={`min-w-0 ${className}`}>
|
|
435
|
-
<div className="
|
|
436
|
-
<div className="min-w-0
|
|
293
|
+
<div className={`flex min-w-0 items-center gap-3 ${className}`}>
|
|
294
|
+
<div className="min-w-0 flex-1">
|
|
295
|
+
<div className="min-w-0 truncate text-sm font-medium text-theme-text-primary">
|
|
437
296
|
{title}
|
|
438
297
|
</div>
|
|
439
|
-
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
{onOpenSettings && (
|
|
454
|
-
<Tooltip content="AI settings" position="bottom">
|
|
455
|
-
<button
|
|
456
|
-
onClick={onOpenSettings}
|
|
457
|
-
className="shrink-0 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
|
|
458
|
-
aria-label="AI settings"
|
|
459
|
-
>
|
|
460
|
-
<Settings2 className="h-3 w-3" />
|
|
461
|
-
</button>
|
|
462
|
-
</Tooltip>
|
|
463
|
-
)}
|
|
298
|
+
<div className="flex items-center gap-1 text-xs text-theme-text-tertiary">
|
|
299
|
+
<span className="truncate">{configLine}</span>
|
|
300
|
+
{onOpenSettings && (
|
|
301
|
+
<Tooltip content="AI settings" position="bottom">
|
|
302
|
+
<button
|
|
303
|
+
onClick={onOpenSettings}
|
|
304
|
+
className="shrink-0 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
|
|
305
|
+
aria-label="AI settings"
|
|
306
|
+
>
|
|
307
|
+
<Settings2 className="h-3 w-3" />
|
|
308
|
+
</button>
|
|
309
|
+
</Tooltip>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
464
312
|
</div>
|
|
313
|
+
{runMeta ? (
|
|
314
|
+
<div
|
|
315
|
+
className={`${MAXIMIZED_RUN_META_VISIBILITY_CLASS} shrink-0 items-center gap-1 whitespace-nowrap text-[11px] tabular-nums text-theme-text-tertiary`}
|
|
316
|
+
>
|
|
317
|
+
<span className={`font-medium ${runMeta.labelClass}`}>
|
|
318
|
+
{runMeta.label}
|
|
319
|
+
</span>
|
|
320
|
+
<span aria-hidden>·</span>
|
|
321
|
+
<time dateTime={runMeta.dateTime}>{runMeta.time}</time>
|
|
322
|
+
</div>
|
|
323
|
+
) : null}
|
|
465
324
|
</div>
|
|
466
325
|
);
|
|
467
326
|
}
|
|
468
327
|
|
|
469
328
|
export function openInvestigationEvidenceResource(
|
|
470
329
|
ref: DiagnosisResourceRef,
|
|
471
|
-
onOpenResource: (ref: DiagnosisResourceRef) => void,
|
|
330
|
+
onOpenResource: (ref: DiagnosisResourceRef, runID?: string | null) => void,
|
|
472
331
|
setMaximized: (maximized: boolean) => void,
|
|
473
|
-
|
|
332
|
+
dismissDiagnose: () => void,
|
|
333
|
+
dockedPanelWouldOverlay: boolean,
|
|
334
|
+
activeRunID?: string,
|
|
335
|
+
) {
|
|
336
|
+
revealInvestigationDestination(
|
|
337
|
+
setMaximized,
|
|
338
|
+
dismissDiagnose,
|
|
339
|
+
dockedPanelWouldOverlay,
|
|
340
|
+
);
|
|
341
|
+
if (dockedPanelWouldOverlay) onOpenResource(ref, null);
|
|
342
|
+
else if (activeRunID) onOpenResource(ref, activeRunID);
|
|
343
|
+
else onOpenResource(ref);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// A destination must be visible after the handoff. On a wide canvas, restoring
|
|
347
|
+
// the docked panel leaves Radar and the investigation side by side. At tighter
|
|
348
|
+
// widths that same panel overlays the host content, so close it before
|
|
349
|
+
// navigating instead of making the click appear to do nothing.
|
|
350
|
+
function revealInvestigationDestination(
|
|
351
|
+
setMaximized: (maximized: boolean) => void,
|
|
352
|
+
dismissDiagnose: () => void,
|
|
474
353
|
dockedPanelWouldOverlay: boolean,
|
|
475
354
|
) {
|
|
476
|
-
// A resource destination must be visible after the handoff. On a wide canvas,
|
|
477
|
-
// restoring the docked panel leaves Radar and the investigation side by side.
|
|
478
|
-
// At tighter widths that same panel overlays the host content, so close it
|
|
479
|
-
// before navigating instead of making the click appear to do nothing.
|
|
480
355
|
if (dockedPanelWouldOverlay) {
|
|
481
356
|
// Closing already exposes the destination; retain the user's maximized
|
|
482
357
|
// preference for the next time they open investigations.
|
|
483
|
-
|
|
358
|
+
dismissDiagnose();
|
|
484
359
|
} else {
|
|
485
360
|
setMaximized(false);
|
|
486
361
|
}
|
|
487
|
-
onOpenResource(ref);
|
|
488
362
|
}
|
|
489
363
|
|
|
490
364
|
export function DiagnoseSurface({
|
|
491
365
|
topInset = 0,
|
|
492
366
|
onOpenResource,
|
|
367
|
+
onOpenTimeline,
|
|
368
|
+
onOpenWorkspace,
|
|
369
|
+
onBrowseIssues,
|
|
493
370
|
}: {
|
|
494
371
|
topInset?: number;
|
|
495
372
|
/** Resolves an evidence subject into the embedding Radar surface. */
|
|
496
|
-
onOpenResource?: (ref: DiagnosisResourceRef) => void;
|
|
373
|
+
onOpenResource?: (ref: DiagnosisResourceRef, runID?: string | null) => void;
|
|
374
|
+
/** Opens the embedding Radar Timeline for one resource's change history. */
|
|
375
|
+
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
376
|
+
/** Lets a host whose drawer lives outside Radar's route tree cross into it. */
|
|
377
|
+
onOpenWorkspace?: () => void;
|
|
378
|
+
/** Opens the canonical Issues surface for a first focused investigation. */
|
|
379
|
+
onBrowseIssues?: () => void;
|
|
497
380
|
}) {
|
|
498
381
|
const d = useDiagnose();
|
|
499
382
|
const { data: contexts } = useContexts();
|
|
@@ -516,8 +399,9 @@ export function DiagnoseSurface({
|
|
|
516
399
|
}, []);
|
|
517
400
|
// Injected settings action: undefined = Radar's own Settings dialog;
|
|
518
401
|
// null = hide the gear + links.
|
|
519
|
-
const { consentCopy, onOpenSettings: hostOpenSettings } =
|
|
402
|
+
const { consentCopy, onOpenSettings: hostOpenSettings, renderRunActions } =
|
|
520
403
|
useDiagnoseCustomization();
|
|
404
|
+
const { embedded } = useNavCustomization();
|
|
521
405
|
const openSettings =
|
|
522
406
|
hostOpenSettings === undefined ? openDiagnoseSettings : hostOpenSettings;
|
|
523
407
|
const {
|
|
@@ -529,10 +413,16 @@ export function DiagnoseSurface({
|
|
|
529
413
|
panelBounds: { min: minW, max: maxW },
|
|
530
414
|
panelWidthKey: widthKey,
|
|
531
415
|
} = useDiagnoseLayout();
|
|
532
|
-
|
|
533
|
-
|
|
416
|
+
// Home has no detail pane competing for width, so its history is always part
|
|
417
|
+
// of the workspace. Only a focused run collapses history into a drawer when
|
|
418
|
+
// the two-pane layout would become cramped.
|
|
419
|
+
const persistentHistory = investigationHistoryIsPersistent({
|
|
420
|
+
maximized,
|
|
421
|
+
view: d.view,
|
|
422
|
+
surfaceWidth,
|
|
423
|
+
});
|
|
534
424
|
const historyOverlay =
|
|
535
|
-
!
|
|
425
|
+
!persistentHistory && historyOverlayOpen;
|
|
536
426
|
const { shouldRender: historyOverlayPresent, isOpen: historySlideOpen } =
|
|
537
427
|
useAnimatedUnmount(historyOverlay);
|
|
538
428
|
const dismissHistory = useCallback(() => {
|
|
@@ -541,7 +431,7 @@ export function DiagnoseSurface({
|
|
|
541
431
|
}, []);
|
|
542
432
|
useEffect(() => {
|
|
543
433
|
setHistoryOverlayOpen(false);
|
|
544
|
-
}, [
|
|
434
|
+
}, [persistentHistory, maximized, d.view]);
|
|
545
435
|
useEffect(() => {
|
|
546
436
|
if (historyOverlay) {
|
|
547
437
|
const target =
|
|
@@ -558,11 +448,30 @@ export function DiagnoseSurface({
|
|
|
558
448
|
ref,
|
|
559
449
|
onOpenResource,
|
|
560
450
|
setMaximized,
|
|
561
|
-
d.
|
|
451
|
+
d.dismissForNavigation,
|
|
452
|
+
narrow,
|
|
453
|
+
d.activeRunId ?? undefined,
|
|
454
|
+
);
|
|
455
|
+
},
|
|
456
|
+
[
|
|
457
|
+
d.activeRunId,
|
|
458
|
+
d.dismissForNavigation,
|
|
459
|
+
narrow,
|
|
460
|
+
onOpenResource,
|
|
461
|
+
setMaximized,
|
|
462
|
+
],
|
|
463
|
+
);
|
|
464
|
+
const openEvidenceTimeline = useCallback(
|
|
465
|
+
(scope: InvestigationTimelineScope) => {
|
|
466
|
+
if (!onOpenTimeline) return;
|
|
467
|
+
revealInvestigationDestination(
|
|
468
|
+
setMaximized,
|
|
469
|
+
d.dismissForNavigation,
|
|
562
470
|
narrow,
|
|
563
471
|
);
|
|
472
|
+
onOpenTimeline(scope);
|
|
564
473
|
},
|
|
565
|
-
[d.
|
|
474
|
+
[d.dismissForNavigation, narrow, onOpenTimeline, setMaximized],
|
|
566
475
|
);
|
|
567
476
|
|
|
568
477
|
const startResize = (e: React.MouseEvent) => {
|
|
@@ -660,6 +569,7 @@ export function DiagnoseSurface({
|
|
|
660
569
|
agentLabel={activeAgentLabel}
|
|
661
570
|
maximized={maximized}
|
|
662
571
|
onOpenResource={onOpenResource ? openEvidenceResource : undefined}
|
|
572
|
+
onOpenTimeline={onOpenTimeline ? openEvidenceTimeline : undefined}
|
|
663
573
|
/>
|
|
664
574
|
) : d.activeRunId && !d.runsLoaded ? (
|
|
665
575
|
// Deep-linked to a run before the list has ever loaded: show the load
|
|
@@ -676,9 +586,7 @@ export function DiagnoseSurface({
|
|
|
676
586
|
// investigation" placeholder would read as a broken link.
|
|
677
587
|
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
|
|
678
588
|
<p className="text-sm text-theme-text-secondary">
|
|
679
|
-
|
|
680
|
-
have changed, or its history may have been cleared. Check your account
|
|
681
|
-
and organization, or ask the creator for access.
|
|
589
|
+
{unavailableInvestigationMessage(embedded === true)}
|
|
682
590
|
</p>
|
|
683
591
|
<button
|
|
684
592
|
onClick={d.goHome}
|
|
@@ -706,25 +614,38 @@ export function DiagnoseSurface({
|
|
|
706
614
|
Select an investigation, or open a resource and click Investigate.
|
|
707
615
|
</div>
|
|
708
616
|
);
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
617
|
+
const home = d.needsConsent ? (
|
|
618
|
+
<div className="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6">
|
|
619
|
+
<ConsentCard
|
|
620
|
+
agentName={d.agentLabel}
|
|
621
|
+
agent={d.selectedAgent}
|
|
622
|
+
profile={d.profile}
|
|
623
|
+
copy={consentCopy}
|
|
624
|
+
onOpenSettings={openSettings ?? undefined}
|
|
625
|
+
error={d.consentError}
|
|
626
|
+
onApprove={d.approveConsent}
|
|
627
|
+
onCancel={d.cancelConsent}
|
|
628
|
+
/>
|
|
629
|
+
</div>
|
|
630
|
+
) : setupPending ? (
|
|
631
|
+
<div className="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6">
|
|
632
|
+
<AgentSetupNotice setupState={d.setupState} />
|
|
633
|
+
</div>
|
|
634
|
+
) : (
|
|
635
|
+
<InvestigationHome
|
|
636
|
+
agentLabel={d.agentLabel}
|
|
637
|
+
onBrowseIssues={
|
|
638
|
+
d.runsLoaded && d.runs.length === 0 ? onBrowseIssues : undefined
|
|
639
|
+
}
|
|
640
|
+
/>
|
|
722
641
|
);
|
|
723
642
|
|
|
724
643
|
const showHistory = !setupPending || d.runs.length > 0;
|
|
725
644
|
const historyVisible =
|
|
726
645
|
showHistory &&
|
|
727
|
-
(
|
|
646
|
+
(persistentHistory
|
|
647
|
+
? d.view === "home" || !historyCollapsed
|
|
648
|
+
: historyOverlay);
|
|
728
649
|
|
|
729
650
|
useLayoutEffect(() => {
|
|
730
651
|
if (
|
|
@@ -764,9 +685,9 @@ export function DiagnoseSurface({
|
|
|
764
685
|
)}
|
|
765
686
|
|
|
766
687
|
{/* Header */}
|
|
767
|
-
<div className="flex items-center justify-between border-b border-theme-border px-4 py-2.5">
|
|
688
|
+
<div className="relative z-30 flex items-center justify-between gap-3 border-b border-theme-border bg-theme-surface px-4 py-2.5">
|
|
768
689
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
769
|
-
{d.view !== "home"
|
|
690
|
+
{showHistory && (d.view !== "home" || !persistentHistory) ? (
|
|
770
691
|
<Tooltip
|
|
771
692
|
content={
|
|
772
693
|
historyVisible
|
|
@@ -783,7 +704,7 @@ export function DiagnoseSurface({
|
|
|
783
704
|
aria-expanded={historyVisible}
|
|
784
705
|
aria-controls="investigation-history"
|
|
785
706
|
onClick={() =>
|
|
786
|
-
|
|
707
|
+
persistentHistory
|
|
787
708
|
? setHistoryCollapsed((value) => !value)
|
|
788
709
|
: historyOverlay
|
|
789
710
|
? dismissHistory()
|
|
@@ -819,11 +740,11 @@ export function DiagnoseSurface({
|
|
|
819
740
|
)}
|
|
820
741
|
</div>
|
|
821
742
|
</div>
|
|
822
|
-
<div className="flex shrink-0 items-center gap-
|
|
743
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
823
744
|
{activeRun &&
|
|
824
|
-
|
|
745
|
+
canRerunInvestigation(d.view, activeRun, d.needsConsent) && (
|
|
825
746
|
<Tooltip
|
|
826
|
-
content="
|
|
747
|
+
content="Re-run this investigation from scratch"
|
|
827
748
|
position="bottom"
|
|
828
749
|
>
|
|
829
750
|
<button
|
|
@@ -838,9 +759,9 @@ export function DiagnoseSurface({
|
|
|
838
759
|
})
|
|
839
760
|
}
|
|
840
761
|
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
841
|
-
aria-label="
|
|
762
|
+
aria-label="Re-run this investigation from scratch"
|
|
842
763
|
>
|
|
843
|
-
<
|
|
764
|
+
<RotateCcw className="h-4 w-4" />
|
|
844
765
|
</button>
|
|
845
766
|
</Tooltip>
|
|
846
767
|
)}
|
|
@@ -848,33 +769,33 @@ export function DiagnoseSurface({
|
|
|
848
769
|
<div
|
|
849
770
|
className={`items-center gap-1 ${headerPresentation.runActionsClass || "flex"}`}
|
|
850
771
|
>
|
|
851
|
-
|
|
852
|
-
key={visibleRunDetail.id}
|
|
853
|
-
run={visibleRunDetail}
|
|
854
|
-
onChanged={d.updateRunSummary}
|
|
855
|
-
/>
|
|
856
|
-
{canCopyRunLink(visibleRunDetail) && (
|
|
857
|
-
<CopyRunLink
|
|
858
|
-
radarUrl={visibleRunDetail.radarUrl}
|
|
859
|
-
visibility={visibleRunDetail.visibility}
|
|
860
|
-
/>
|
|
861
|
-
)}
|
|
772
|
+
{renderRunActions?.({ run: visibleRunDetail, onRunUpdated: d.updateRunSummary })}
|
|
862
773
|
<InvestigationMenu run={visibleRunDetail} />
|
|
863
774
|
</div>
|
|
864
775
|
)}
|
|
865
|
-
|
|
866
|
-
<
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
aria-label={maximized ? "Restore" : "Expand"}
|
|
776
|
+
{(!maximized || d.canRestoreWorkspace) && (
|
|
777
|
+
<Tooltip
|
|
778
|
+
content={maximized ? "Restore" : "Expand"}
|
|
779
|
+
position="bottom"
|
|
870
780
|
>
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
781
|
+
<button
|
|
782
|
+
onClick={
|
|
783
|
+
maximized
|
|
784
|
+
? d.restoreWorkspace
|
|
785
|
+
: (onOpenWorkspace ??
|
|
786
|
+
(() => d.openWorkspace(d.activeRunId)))
|
|
787
|
+
}
|
|
788
|
+
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
|
|
789
|
+
aria-label={maximized ? "Restore" : "Expand"}
|
|
790
|
+
>
|
|
791
|
+
{maximized ? (
|
|
792
|
+
<Minimize2 className="h-4 w-4" />
|
|
793
|
+
) : (
|
|
794
|
+
<Maximize2 className="h-4 w-4" />
|
|
795
|
+
)}
|
|
796
|
+
</button>
|
|
797
|
+
</Tooltip>
|
|
798
|
+
)}
|
|
878
799
|
<Tooltip content="Close" position="bottom">
|
|
879
800
|
<button
|
|
880
801
|
onClick={d.close}
|
|
@@ -893,7 +814,7 @@ export function DiagnoseSurface({
|
|
|
893
814
|
only appears when expanded; keys keep the detail node identity-stable
|
|
894
815
|
as it comes and goes. */}
|
|
895
816
|
<div className="relative flex min-h-0 flex-1">
|
|
896
|
-
{!
|
|
817
|
+
{!persistentHistory && historyOverlayPresent && (
|
|
897
818
|
<div
|
|
898
819
|
aria-hidden="true"
|
|
899
820
|
onClick={dismissHistory}
|
|
@@ -909,7 +830,7 @@ export function DiagnoseSurface({
|
|
|
909
830
|
aria-hidden={!historyVisible}
|
|
910
831
|
inert={!historyVisible}
|
|
911
832
|
id="investigation-history"
|
|
912
|
-
className={`${historyVisible || (!
|
|
833
|
+
className={`${historyVisible || (!persistentHistory && historyOverlayPresent) ? "block" : "hidden"} ${persistentHistory ? "" : `absolute inset-y-0 left-0 z-20 max-w-[calc(100%-2rem)] ${TRANSITION_DRAWER} motion-reduce:transition-none ${historySlideOpen ? "translate-x-0 opacity-100" : "-translate-x-full opacity-0"}`} w-72 shrink-0 overflow-y-auto border-r border-theme-border bg-theme-surface px-3 py-3 outline-none`}
|
|
913
834
|
>
|
|
914
835
|
<RecentList
|
|
915
836
|
currentContext={currentContext}
|
|
@@ -925,22 +846,13 @@ export function DiagnoseSurface({
|
|
|
925
846
|
</aside>
|
|
926
847
|
)}
|
|
927
848
|
{d.view === "home" ? (
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
{maximized && (
|
|
936
|
-
<div
|
|
937
|
-
key="main"
|
|
938
|
-
className={`${MAXIMIZED_HOME_DETAIL_VISIBILITY_CLASS} min-h-0 min-w-0 flex-1 flex-col`}
|
|
939
|
-
>
|
|
940
|
-
{detail}
|
|
941
|
-
</div>
|
|
942
|
-
)}
|
|
943
|
-
</>
|
|
849
|
+
<div
|
|
850
|
+
key="home"
|
|
851
|
+
inert={historyOverlay}
|
|
852
|
+
className="flex-1 overflow-y-auto overflow-x-hidden"
|
|
853
|
+
>
|
|
854
|
+
{home}
|
|
855
|
+
</div>
|
|
944
856
|
) : (
|
|
945
857
|
<div
|
|
946
858
|
key="main"
|