Skip to content

Commit 04e7ae8

Browse files
lucasmerlinMichaelGrupp
authored andcommitted
Show error when rerun_js viewer panics or fails to load
- closes https://linear.app/rerun/issue/RR-3312/add-error-handling-and-loading-spinner-to-httpslatestcloudrerunio This handles both wasm load errors and panics and will show the error message and a clear caches and reload button. Also adds a basic loading spinner: https://github.com/user-attachments/assets/479839df-25ea-46cd-9669-1446e5926c49 Source-Ref: fac247abab92cb130472150c1ed18a6c5e5866d2
1 parent 57b0121 commit 04e7ae8

File tree

1 file changed

+82
-10
lines changed

1 file changed

+82
-10
lines changed

rerun_js/web-viewer/index.ts

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,23 @@ export class WebViewer {
362362
this.#canvas.style.height = options.height ?? "360px";
363363
parent.append(this.#canvas);
364364

365+
// Show loading spinner
366+
const loader = document.createElement("div");
367+
loader.id = "rerun-loader";
368+
loader.innerHTML = `
369+
<style>
370+
@keyframes rerun-spin { to { transform: rotate(360deg); } }
371+
</style>
372+
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; background-color: #1c1c1c; font-family: sans-serif; color: white;">
373+
<div style="width: 40px; height: 40px; border: 3px solid #444; border-top-color: white; border-radius: 50%; animation: rerun-spin 1s linear infinite;"></div>
374+
<div style="margin-top: 16px;">Loading Rerun…</div>
375+
</div>
376+
`;
377+
loader.style.position = "absolute";
378+
loader.style.inset = "0";
379+
parent.style.position = "relative";
380+
parent.append(loader);
381+
365382
// This yield appears to be necessary to ensure that the canvas is attached to the DOM
366383
// and visible. Without it we get occasionally get a panic about a failure to find a canvas
367384
// element with the given ID.
@@ -372,7 +389,14 @@ export class WebViewer {
372389
delete (options as any).base_url;
373390
}
374391

375-
let WebHandle_class = await load(base_url);
392+
let WebHandle_class: typeof wasm_bindgen.WebHandle;
393+
try {
394+
WebHandle_class = await load(base_url);
395+
} catch (e) {
396+
loader.remove();
397+
this.#fail("Failed to load rerun", String(e));
398+
throw e;
399+
}
376400
if (this.#state !== "starting") return;
377401

378402
const fullscreen = this.#allow_fullscreen
@@ -404,18 +428,33 @@ export class WebViewer {
404428
try {
405429
await this.#handle.start(this.#canvas);
406430
} catch (e) {
407-
this.stop();
431+
loader.remove();
432+
this.#fail("Failed to start", String(e));
408433
throw e;
409434
}
410435
if (this.#state !== "starting") return;
411436

437+
loader.remove();
412438
this.#state = "ready";
413439
this.#dispatch_event("ready");
414440

415441
if (rrd) {
416442
this.open(rrd);
417443
}
418444

445+
let self = this;
446+
447+
function check_for_panic() {
448+
if (self.#handle?.has_panicked()) {
449+
self.#fail("Rerun has crashed.", self.#handle?.panic_message());
450+
} else {
451+
let delay_ms = 1000;
452+
setTimeout(check_for_panic, delay_ms);
453+
}
454+
}
455+
456+
check_for_panic();
457+
419458
return;
420459
}
421460

@@ -566,7 +605,7 @@ export class WebViewer {
566605
try {
567606
this.#handle.add_receiver(url, options.follow_if_http);
568607
} catch (e) {
569-
this.stop();
608+
this.#fail("Failed to open recording", String(e));
570609
throw e;
571610
}
572611
}
@@ -589,7 +628,7 @@ export class WebViewer {
589628
try {
590629
this.#handle.remove_receiver(url);
591630
} catch (e) {
592-
this.stop();
631+
this.#fail("Failed to close recording", String(e));
593632
throw e;
594633
}
595634
}
@@ -624,6 +663,39 @@ export class WebViewer {
624663
this.#allow_fullscreen = false;
625664
}
626665

666+
#fail(message: string, error_message?: string) {
667+
console.error("WebViewer failure:", message, error_message);
668+
if (this.canvas?.parentElement) {
669+
const parent = this.canvas.parentElement;
670+
parent.innerHTML = `
671+
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: white; font-family: sans-serif; background-color: #1c1c1c;">
672+
<h1 id="fail-message"></h1>
673+
<pre id="fail-error" style="text-align: left;"></pre>
674+
<button id="fail-clear-cache">Clear caches and reload</button>
675+
</div>
676+
`;
677+
678+
document.getElementById("fail-message")!.textContent = message;
679+
680+
const errorEl = document.getElementById("fail-error")!;
681+
if (error_message) {
682+
errorEl.textContent = error_message;
683+
} else {
684+
errorEl.remove();
685+
}
686+
687+
document.getElementById("fail-clear-cache")!.addEventListener("click", async () => {
688+
if ("caches" in window) {
689+
const keys = await caches.keys();
690+
await Promise.all(keys.map((key) => caches.delete(key)));
691+
}
692+
window.location.reload();
693+
});
694+
}
695+
696+
this.stop();
697+
}
698+
627699
/**
628700
* Opens a new channel for sending log messages.
629701
*
@@ -643,7 +715,7 @@ export class WebViewer {
643715
try {
644716
this.#handle.open_channel(id, channel_name);
645717
} catch (e) {
646-
this.stop();
718+
this.#fail("Failed to open channel", String(e));
647719
throw e;
648720
}
649721

@@ -657,7 +729,7 @@ export class WebViewer {
657729
try {
658730
this.#handle.send_rrd_to_channel(id, data);
659731
} catch (e) {
660-
this.stop();
732+
this.#fail("Failed to send data", String(e));
661733
throw e;
662734
}
663735
};
@@ -672,7 +744,7 @@ export class WebViewer {
672744
try {
673745
this.#handle.send_table_to_channel(id, data);
674746
} catch (e) {
675-
this.stop();
747+
this.#fail("Failed to send table", String(e));
676748
throw e;
677749
}
678750
}
@@ -687,7 +759,7 @@ export class WebViewer {
687759
try {
688760
this.#handle.close_channel(id);
689761
} catch (e) {
690-
this.stop();
762+
this.#fail("Failed to close channel", String(e));
691763
throw e;
692764
}
693765
};
@@ -713,7 +785,7 @@ export class WebViewer {
713785
try {
714786
this.#handle.override_panel_state(panel, state);
715787
} catch (e) {
716-
this.stop();
788+
this.#fail("Failed to override panel state", String(e));
717789
throw e;
718790
}
719791
}
@@ -733,7 +805,7 @@ export class WebViewer {
733805
try {
734806
this.#handle.toggle_panel_overrides(value as boolean | undefined);
735807
} catch (e) {
736-
this.stop();
808+
this.#fail("Failed to toggle panel overrides", String(e));
737809
throw e;
738810
}
739811
}

0 commit comments

Comments
 (0)