@skyhook-io/radar-app 1.13.0 → 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/App.tsx +55 -9
- package/src/RadarApp.tsx +3 -3
- package/src/api/diagnose.test.ts +268 -0
- package/src/api/diagnose.ts +117 -14
- package/src/components/diagnose/AISettings.tsx +10 -5
- package/src/components/diagnose/AgentSetupNotice.tsx +5 -5
- package/src/components/diagnose/ApplyDialog.test.tsx +72 -0
- package/src/components/diagnose/DiagnoseContext.tsx +228 -65
- package/src/components/diagnose/DiagnoseSurface.test.tsx +317 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +621 -132
- package/src/components/diagnose/Home.test.tsx +293 -0
- package/src/components/diagnose/Home.tsx +307 -108
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +2170 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +2253 -0
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +257 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +214 -0
- package/src/components/diagnose/InvestigationView.test.ts +17 -0
- package/src/components/diagnose/InvestigationView.tsx +1911 -338
- package/src/components/diagnose/LocalDiagnoseAction.tsx +42 -25
- package/src/components/diagnose/agentCatalog.ts +1 -1
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +151 -0
- package/src/components/diagnose/investigationEvidence.test.ts +3109 -0
- package/src/components/diagnose/investigationEvidence.ts +3492 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +447 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +167 -0
- package/src/components/diagnose/investigationExplanation.test.ts +63 -0
- package/src/components/diagnose/investigationExplanation.ts +22 -0
- package/src/components/diagnose/investigationResourceEvidenceModel.ts +322 -0
- package/src/components/diagnose/investigationSourceFocus.test.ts +143 -0
- package/src/components/diagnose/investigationSourceFocus.ts +98 -0
- package/src/components/diagnose/investigationState.test.ts +695 -0
- package/src/components/diagnose/investigationState.ts +451 -0
- package/src/components/diagnose/parts.test.tsx +864 -3
- package/src/components/diagnose/parts.tsx +1340 -536
- package/src/components/diagnose/target.test.ts +39 -0
- package/src/components/diagnose/target.ts +36 -0
- package/src/components/diagnose/useDisclosureReveal.ts +117 -0
- package/src/components/execution/BatchExecutionView.render.test.tsx +35 -0
- package/src/components/execution/BatchExecutionView.tsx +2 -3
- package/src/components/execution/execution-definition.test.ts +19 -0
- package/src/components/execution/execution-definition.ts +2 -0
- package/src/components/gitops/GitOpsView.tsx +25 -3
- package/src/components/home/MCPSetupDialog.tsx +2 -2
- package/src/components/home/mcpToolCatalog.test.ts +22 -0
- package/src/components/home/mcpToolCatalog.ts +3 -2
- package/src/components/issues/IssuesPane.tsx +5 -1
- package/src/components/nav/navigation.test.ts +26 -0
- package/src/components/nav/navigation.ts +10 -0
- package/src/components/settings/SettingsDialog.tsx +11 -13
- package/src/components/workload/WorkloadView.tsx +1 -1
- package/src/context/DiagnoseCustomization.tsx +11 -8
- package/src/index.css +63 -79
- package/src/index.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The single controller for the AI assistant surface. One instance app-wide:
|
|
2
|
-
// the per-resource "
|
|
2
|
+
// the per-resource "Investigate" button and the global top-bar entry both dispatch
|
|
3
3
|
// here. Investigations are durable, server-side jobs (see internal/ai RunManager);
|
|
4
4
|
// this provider lists them, tracks which one is focused, and owns the push-content
|
|
5
5
|
// layout. The run lifetime is the server's, so closing/navigating never kills one.
|
|
@@ -17,19 +17,24 @@ import {
|
|
|
17
17
|
} from "react";
|
|
18
18
|
import {
|
|
19
19
|
fetchAgents,
|
|
20
|
+
getRun,
|
|
20
21
|
listRuns,
|
|
21
22
|
createRun,
|
|
22
23
|
recordConsent,
|
|
23
24
|
DiagnoseError,
|
|
24
25
|
} from "../../api/diagnose";
|
|
26
|
+
import { getApiBase } from "../../api/config";
|
|
25
27
|
import {
|
|
26
28
|
type RunSummary,
|
|
27
29
|
type AgentInfo,
|
|
28
30
|
type ExecutionProfile,
|
|
29
31
|
} from "../../api/diagnose";
|
|
32
|
+
import { runTargetKey } from "./target";
|
|
30
33
|
|
|
31
34
|
export interface Target {
|
|
32
35
|
kind: string;
|
|
36
|
+
/** Kubernetes API group; empty means core. */
|
|
37
|
+
group: string;
|
|
33
38
|
namespace: string;
|
|
34
39
|
name: string;
|
|
35
40
|
/** The issue this investigation is for, when it came from an issue. Hosts
|
|
@@ -43,7 +48,7 @@ export interface Target {
|
|
|
43
48
|
}
|
|
44
49
|
export type DiagnoseView = "home" | "investigation";
|
|
45
50
|
|
|
46
|
-
// Setup readiness of
|
|
51
|
+
// Setup readiness of local AI investigations, derived from the agents API:
|
|
47
52
|
// - "ready": an agent is installed and the engine is running (available)
|
|
48
53
|
// - "needs-install": the feature is supported here but no agent CLI is installed
|
|
49
54
|
// - "needs-restart": a supported agent is now on PATH but Radar booted before it
|
|
@@ -86,6 +91,7 @@ interface DiagnoseCtx {
|
|
|
86
91
|
approveConsent: () => void;
|
|
87
92
|
cancelConsent: () => void;
|
|
88
93
|
refreshRuns: () => void;
|
|
94
|
+
updateRunSummary: (run: RunSummary) => void;
|
|
89
95
|
dismissError: () => void;
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -95,6 +101,7 @@ interface DiagnoseCtx {
|
|
|
95
101
|
// churns the business context) doesn't re-render the whole shell.
|
|
96
102
|
interface DiagnoseLayoutCtx {
|
|
97
103
|
open: boolean;
|
|
104
|
+
close: () => void;
|
|
98
105
|
contentGutter: number; // px right-gutter for the content area when docked (0 = overlay/closed)
|
|
99
106
|
maximized: boolean;
|
|
100
107
|
setMaximized: Dispatch<SetStateAction<boolean>>;
|
|
@@ -106,16 +113,6 @@ interface DiagnoseLayoutCtx {
|
|
|
106
113
|
runningKeys: ReadonlySet<string>; // resources with a live investigation (see runTargetKey)
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
// Stable key for "is THIS resource being investigated right now" — built the same way
|
|
110
|
-
// from a run summary and from a button's target so the two always match.
|
|
111
|
-
export function runTargetKey(
|
|
112
|
-
kind: string,
|
|
113
|
-
namespace: string,
|
|
114
|
-
name: string,
|
|
115
|
-
): string {
|
|
116
|
-
return `${kind} ${namespace} ${name}`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
116
|
const Ctx = createContext<DiagnoseCtx | null>(null);
|
|
120
117
|
const LayoutCtx = createContext<DiagnoseLayoutCtx | null>(null);
|
|
121
118
|
|
|
@@ -164,7 +161,7 @@ export function agentLabelFor(name: string, fallbackLabel?: string): string {
|
|
|
164
161
|
}
|
|
165
162
|
|
|
166
163
|
// openDiagnoseSettings opens the Settings dialog (App.tsx listens for this DOM
|
|
167
|
-
// event) — the canonical home for AI
|
|
164
|
+
// event) — the canonical home for AI investigation config.
|
|
168
165
|
export function openDiagnoseSettings() {
|
|
169
166
|
window.dispatchEvent(
|
|
170
167
|
new CustomEvent("radar:open-settings", { detail: { section: "ai" } }),
|
|
@@ -186,7 +183,58 @@ function writeStored(key: string, value: string) {
|
|
|
186
183
|
}
|
|
187
184
|
}
|
|
188
185
|
|
|
189
|
-
|
|
186
|
+
function runIDFromLocation(browserURLState: boolean): string | null {
|
|
187
|
+
if (!diagnoseURLStateEnabled(browserURLState)) return null;
|
|
188
|
+
try {
|
|
189
|
+
return new URLSearchParams(window.location.search).get("ai-run");
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function writeRunIDToLocation(
|
|
196
|
+
id: string | null,
|
|
197
|
+
push: boolean,
|
|
198
|
+
browserURLState: boolean,
|
|
199
|
+
) {
|
|
200
|
+
if (!diagnoseURLStateEnabled(browserURLState)) return;
|
|
201
|
+
try {
|
|
202
|
+
const url = new URL(window.location.href);
|
|
203
|
+
if (id) url.searchParams.set("ai-run", id);
|
|
204
|
+
else url.searchParams.delete("ai-run");
|
|
205
|
+
window.history[push ? "pushState" : "replaceState"](
|
|
206
|
+
window.history.state,
|
|
207
|
+
"",
|
|
208
|
+
`${url.pathname}${url.search}${url.hash}`,
|
|
209
|
+
);
|
|
210
|
+
// History API writes do not notify BrowserRouter. Replaying the new state as
|
|
211
|
+
// popstate keeps every later navigate/setSearchParams call on the same live
|
|
212
|
+
// query string instead of letting a stale router snapshot erase ai-run.
|
|
213
|
+
window.dispatchEvent(
|
|
214
|
+
new PopStateEvent("popstate", { state: window.history.state }),
|
|
215
|
+
);
|
|
216
|
+
} catch {
|
|
217
|
+
/* URL APIs unavailable — panel state still works for this session */
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Fleet pages host the panel against a cluster-scoped API while remaining on a
|
|
222
|
+
// hub route such as /issues. Writing ?ai-run there would create a non-reloadable
|
|
223
|
+
// pseudo-link. The embedded /c/:id Radar tree and local Radar own their route and
|
|
224
|
+
// therefore keep the query as navigation state; Fleet copies run.radarUrl instead.
|
|
225
|
+
function diagnoseURLStateEnabled(browserURLState: boolean): boolean {
|
|
226
|
+
if (!browserURLState) return false;
|
|
227
|
+
const clusterScopedAPI = /\/c\/[^/]+\/api\/?$/.test(getApiBase());
|
|
228
|
+
return !clusterScopedAPI || /^\/c\/[^/]+/.test(window.location.pathname);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function DiagnoseProvider({
|
|
232
|
+
children,
|
|
233
|
+
browserURLState = true,
|
|
234
|
+
}: {
|
|
235
|
+
children: ReactNode;
|
|
236
|
+
browserURLState?: boolean;
|
|
237
|
+
}) {
|
|
190
238
|
const [available, setAvailable] = useState(false);
|
|
191
239
|
const [eligible, setEligible] = useState(false);
|
|
192
240
|
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
@@ -206,6 +254,26 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
206
254
|
const [open, setOpen] = useState(false);
|
|
207
255
|
const [view, setView] = useState<DiagnoseView>("home");
|
|
208
256
|
const [activeRunId, setActiveRunId] = useState<string | null>(null);
|
|
257
|
+
const activeRunIdRef = useRef(activeRunId);
|
|
258
|
+
activeRunIdRef.current = activeRunId;
|
|
259
|
+
// A missing/revoked durable link is terminal until the user explicitly
|
|
260
|
+
// navigates to it again. Keep polling the bounded history list (so a newly
|
|
261
|
+
// shared run can reappear), but do not hammer the exact 404 endpoint every
|
|
262
|
+
// four seconds while the unavailable state is already on screen.
|
|
263
|
+
const unavailableRunIDsRef = useRef(new Set<string>());
|
|
264
|
+
// Tracks whether the panel focus belongs to browser history. Fleet opens the
|
|
265
|
+
// same panel on /issues, where URL state is deliberately disabled; a generic
|
|
266
|
+
// panel launch must never be closed by the deep-link synchronization effect.
|
|
267
|
+
const urlRunIdRef = useRef(runIDFromLocation(browserURLState));
|
|
268
|
+
const writeFocusedRunID = useCallback(
|
|
269
|
+
(id: string | null, push: boolean) => {
|
|
270
|
+
// Set this before writeRunIDToLocation's synthetic popstate so our own
|
|
271
|
+
// listener can distinguish a programmatic close/home write from Back.
|
|
272
|
+
if (diagnoseURLStateEnabled(browserURLState)) urlRunIdRef.current = id;
|
|
273
|
+
writeRunIDToLocation(id, push, browserURLState);
|
|
274
|
+
},
|
|
275
|
+
[browserURLState],
|
|
276
|
+
);
|
|
209
277
|
const [runs, setRuns] = useState<RunSummary[]>([]);
|
|
210
278
|
const [runsLoaded, setRunsLoaded] = useState(false);
|
|
211
279
|
const [runsLoadFailed, setRunsLoadFailed] = useState(false);
|
|
@@ -332,33 +400,86 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
332
400
|
return () => window.removeEventListener("resize", onResize);
|
|
333
401
|
}, []);
|
|
334
402
|
|
|
335
|
-
const refreshRuns = useCallback(() => {
|
|
403
|
+
const refreshRuns = useCallback(async () => {
|
|
336
404
|
if (!available) return;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
405
|
+
try {
|
|
406
|
+
const r = await listRuns();
|
|
407
|
+
const focusedID = activeRunIdRef.current;
|
|
408
|
+
let focusedRun: RunSummary | null = null;
|
|
409
|
+
let retainFocusedSnapshot = false;
|
|
410
|
+
const listedFocusedRun = focusedID
|
|
411
|
+
? r.runs.find((run) => run.id === focusedID)
|
|
412
|
+
: undefined;
|
|
413
|
+
if (focusedID && listedFocusedRun) {
|
|
414
|
+
unavailableRunIDsRef.current.delete(focusedID);
|
|
415
|
+
} else if (focusedID && !unavailableRunIDsRef.current.has(focusedID)) {
|
|
416
|
+
try {
|
|
417
|
+
// A stable deep link can target a retained run older than the bounded
|
|
418
|
+
// history page. Refresh it directly too: its status and capabilities
|
|
419
|
+
// must not freeze at the first snapshot while the panel stays open.
|
|
420
|
+
focusedRun = await getRun(focusedID);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
// The bounded list is still authoritative for everything else. A
|
|
423
|
+
// missing/revoked focused run falls out and renders unavailable;
|
|
424
|
+
// transient direct-fetch failures keep the last useful snapshot.
|
|
425
|
+
const unavailable =
|
|
426
|
+
error instanceof DiagnoseError && error.status === 404;
|
|
427
|
+
if (unavailable) unavailableRunIDsRef.current.add(focusedID);
|
|
428
|
+
retainFocusedSnapshot = !unavailable;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
setRuns((prev) => {
|
|
432
|
+
let nextRuns = r.runs;
|
|
433
|
+
if (focusedRun) nextRuns = [focusedRun, ...nextRuns];
|
|
434
|
+
if (focusedID && retainFocusedSnapshot) {
|
|
435
|
+
const previous = prev.find((run) => run.id === focusedID);
|
|
436
|
+
if (previous) nextRuns = [previous, ...nextRuns];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// openRun/start can focus and insert a run while the direct fetch above
|
|
440
|
+
// is in flight. Preserve that newer focus instead of replacing it with
|
|
441
|
+
// the snapshot for the run that was active when this refresh started.
|
|
442
|
+
const currentFocusedID = activeRunIdRef.current;
|
|
443
|
+
if (
|
|
444
|
+
currentFocusedID &&
|
|
445
|
+
currentFocusedID !== focusedID &&
|
|
446
|
+
!nextRuns.some((run) => run.id === currentFocusedID)
|
|
447
|
+
) {
|
|
448
|
+
const current = prev.find((run) => run.id === currentFocusedID);
|
|
449
|
+
if (current) nextRuns = [current, ...nextRuns];
|
|
450
|
+
}
|
|
451
|
+
return nextRuns;
|
|
349
452
|
});
|
|
453
|
+
setRunsLoaded(true);
|
|
454
|
+
setRunsLoadFailed(false);
|
|
455
|
+
setHistoryDegraded(!!r.historyDegraded);
|
|
456
|
+
} catch {
|
|
457
|
+
// Leave runsLoaded false (a missing-run verdict needs a real list) but
|
|
458
|
+
// record the failure so the panel can say "retrying" instead of
|
|
459
|
+
// pretending nothing happened. The 4s poll keeps retrying while open.
|
|
460
|
+
setRunsLoadFailed(true);
|
|
461
|
+
}
|
|
350
462
|
}, [available]);
|
|
463
|
+
const updateRunSummary = useCallback((run: RunSummary) => {
|
|
464
|
+
setRuns((prev) =>
|
|
465
|
+
prev.some((item) => item.id === run.id)
|
|
466
|
+
? prev.map((item) => (item.id === run.id ? run : item))
|
|
467
|
+
: [run, ...prev],
|
|
468
|
+
);
|
|
469
|
+
}, []);
|
|
351
470
|
|
|
352
|
-
// A content-stable signature of the resources with a live
|
|
353
|
-
// so the per-resource
|
|
471
|
+
// A content-stable signature of the resources with a live investigation,
|
|
472
|
+
// so the per-resource Investigate buttons can show a "running" indicator even with the
|
|
354
473
|
// panel closed — and only re-render when the set actually changes, not every poll.
|
|
355
474
|
const runningSig = runs
|
|
356
|
-
.filter((r) => r.status === "running")
|
|
357
|
-
.map((r) => runTargetKey(r.kind, r.namespace, r.name))
|
|
475
|
+
.filter((r) => r.status === "running" || r.status === "stopping")
|
|
476
|
+
.map((r) => runTargetKey(r.kind, r.namespace, r.name, r.group))
|
|
358
477
|
.sort()
|
|
359
|
-
|
|
478
|
+
// resourceKey itself is pipe-delimited; newlines cannot occur in a
|
|
479
|
+
// Kubernetes group, Kind, namespace, or name.
|
|
480
|
+
.join("\n");
|
|
360
481
|
const runningKeys = useMemo(
|
|
361
|
-
() => new Set(runningSig ? runningSig.split("
|
|
482
|
+
() => new Set(runningSig ? runningSig.split("\n") : []),
|
|
362
483
|
[runningSig],
|
|
363
484
|
);
|
|
364
485
|
const hasRunning = runningSig.length > 0;
|
|
@@ -397,6 +518,7 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
397
518
|
if (seq !== startSeqRef.current) return;
|
|
398
519
|
setActiveRunId(run.id);
|
|
399
520
|
setView("investigation");
|
|
521
|
+
writeFocusedRunID(run.id, true);
|
|
400
522
|
})
|
|
401
523
|
.catch((e) => {
|
|
402
524
|
if (seq !== startSeqRef.current) return;
|
|
@@ -431,52 +553,90 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
431
553
|
},
|
|
432
554
|
[consentSurface, hosted],
|
|
433
555
|
);
|
|
434
|
-
const openRun = useCallback(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
556
|
+
const openRun = useCallback(
|
|
557
|
+
(id: string) => {
|
|
558
|
+
unavailableRunIDsRef.current.delete(id);
|
|
559
|
+
setStartError(null);
|
|
560
|
+
setActiveRunId(id);
|
|
561
|
+
setView("investigation");
|
|
562
|
+
setOpen(true);
|
|
563
|
+
writeFocusedRunID(id, true);
|
|
564
|
+
// A Fleet issue can point at a retained automatic run older than the
|
|
565
|
+
// bounded history page. Resolve the exact id instead of waiting for list.
|
|
566
|
+
getRun(id)
|
|
567
|
+
.then(updateRunSummary)
|
|
568
|
+
.catch((error) => {
|
|
569
|
+
if (error instanceof DiagnoseError && error.status === 404) {
|
|
570
|
+
unavailableRunIDsRef.current.add(id);
|
|
571
|
+
}
|
|
572
|
+
// The loaded-list state renders the durable unavailable message.
|
|
573
|
+
});
|
|
574
|
+
},
|
|
575
|
+
[updateRunSummary, writeFocusedRunID],
|
|
576
|
+
);
|
|
440
577
|
|
|
441
|
-
//
|
|
442
|
-
// the
|
|
443
|
-
//
|
|
578
|
+
// The run id is durable navigation state: direct loads and popstate focus the
|
|
579
|
+
// exact run, and the query remains in place so the address bar is copyable.
|
|
580
|
+
// Fetch by id rather than assuming the bounded recent list contains it.
|
|
444
581
|
useEffect(() => {
|
|
445
|
-
if (!available) return;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
null
|
|
455
|
-
""
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
582
|
+
if (!available || !diagnoseURLStateEnabled(browserURLState)) return;
|
|
583
|
+
const focusFromLocation = (fromPopState: boolean) => {
|
|
584
|
+
const id = runIDFromLocation(browserURLState);
|
|
585
|
+
if (!id) {
|
|
586
|
+
// A missing id on first mount is ordinary. On popstate it means "back
|
|
587
|
+
// out of this investigation" only when the focused run was itself
|
|
588
|
+
// installed by URL history; unrelated synthetic popstate events must
|
|
589
|
+
// not tear down a panel opened by an in-app action.
|
|
590
|
+
if (fromPopState && urlRunIdRef.current) {
|
|
591
|
+
setActiveRunId(null);
|
|
592
|
+
setView("home");
|
|
593
|
+
setOpen(false);
|
|
594
|
+
}
|
|
595
|
+
urlRunIdRef.current = null;
|
|
596
|
+
return;
|
|
460
597
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
598
|
+
urlRunIdRef.current = id;
|
|
599
|
+
unavailableRunIDsRef.current.delete(id);
|
|
600
|
+
setStartError(null);
|
|
601
|
+
setActiveRunId(id);
|
|
602
|
+
setView("investigation");
|
|
603
|
+
setOpen(true);
|
|
604
|
+
getRun(id)
|
|
605
|
+
.then(updateRunSummary)
|
|
606
|
+
.catch((error) => {
|
|
607
|
+
if (error instanceof DiagnoseError && error.status === 404) {
|
|
608
|
+
unavailableRunIDsRef.current.add(id);
|
|
609
|
+
}
|
|
610
|
+
// The list load owns the final missing/degraded state and keeps retrying.
|
|
611
|
+
});
|
|
612
|
+
};
|
|
613
|
+
focusFromLocation(false);
|
|
614
|
+
const onPopState = () => focusFromLocation(true);
|
|
615
|
+
window.addEventListener("popstate", onPopState);
|
|
616
|
+
return () => window.removeEventListener("popstate", onPopState);
|
|
617
|
+
}, [available, browserURLState, updateRunSummary]);
|
|
466
618
|
// Leaving the detail pane drops the failure that belonged to it. startError
|
|
467
619
|
// renders as the entire pane (maximized home still shows `detail`), where a
|
|
468
620
|
// message about a resource you just navigated away from has nothing to attach
|
|
469
621
|
// to and no way to be dismissed.
|
|
470
622
|
const openHome = useCallback(() => {
|
|
623
|
+
unavailableRunIDsRef.current.clear();
|
|
471
624
|
setView("home");
|
|
472
625
|
setStartError(null);
|
|
473
626
|
setOpen(true);
|
|
474
|
-
|
|
627
|
+
writeFocusedRunID(null, false);
|
|
628
|
+
}, [writeFocusedRunID]);
|
|
475
629
|
const goHome = useCallback(() => {
|
|
630
|
+
unavailableRunIDsRef.current.clear();
|
|
476
631
|
setView("home");
|
|
477
632
|
setStartError(null);
|
|
478
|
-
|
|
479
|
-
|
|
633
|
+
writeFocusedRunID(null, false);
|
|
634
|
+
}, [writeFocusedRunID]);
|
|
635
|
+
const close = useCallback(() => {
|
|
636
|
+
unavailableRunIDsRef.current.clear();
|
|
637
|
+
setOpen(false);
|
|
638
|
+
writeFocusedRunID(null, false);
|
|
639
|
+
}, [writeFocusedRunID]);
|
|
480
640
|
const consentBusyRef = useRef(false);
|
|
481
641
|
const approveConsent = useCallback(() => {
|
|
482
642
|
if (consentBusyRef.current) return;
|
|
@@ -557,6 +717,7 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
557
717
|
approveConsent,
|
|
558
718
|
cancelConsent,
|
|
559
719
|
refreshRuns,
|
|
720
|
+
updateRunSummary,
|
|
560
721
|
dismissError,
|
|
561
722
|
};
|
|
562
723
|
|
|
@@ -567,6 +728,7 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
567
728
|
const layout = useMemo<DiagnoseLayoutCtx>(
|
|
568
729
|
() => ({
|
|
569
730
|
open,
|
|
731
|
+
close,
|
|
570
732
|
contentGutter,
|
|
571
733
|
maximized,
|
|
572
734
|
setMaximized,
|
|
@@ -579,6 +741,7 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
|
|
|
579
741
|
}),
|
|
580
742
|
[
|
|
581
743
|
open,
|
|
744
|
+
close,
|
|
582
745
|
contentGutter,
|
|
583
746
|
maximized,
|
|
584
747
|
width,
|