Skip to content

Commit c122272

Browse files
authored
Dropdown and load spinner (#85)
* When a new log is loaded scroll to last ins Instead of scrolling and selected the last line which is usually junk, scroll to the last ins which is right above the error msg. * Make a dropdown list of example logs Load this from a global javascript object so we can easily change it from a server env. Also add a loading spinner when the user pastes a new log, loads a file, or loads an example log. Issue: - #80
1 parent 2b90157 commit c122272

File tree

6 files changed

+214
-27
lines changed

6 files changed

+214
-27
lines changed

index.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
content="Visualizing Linux Kernel BPF verifier log to help BPF programmers with debugging verification failures"
1010
/>
1111
<meta name="app-input" link=""/>
12-
<meta
13-
name="app-example-input"
14-
link="https://gist.githubusercontent.com/theihor/e0002c119414e6b40e2192bd7ced01b1/raw/866bcc155c2ce848dcd4bc7fd043a97f39a2d370/gistfile1.txt"
15-
/>
12+
<script>
13+
globalThis.exampleLinks = [
14+
["Simple Program", "https://gist.githubusercontent.com/theihor/e0002c119414e6b40e2192bd7ced01b1/raw/866bcc155c2ce848dcd4bc7fd043a97f39a2d370/gistfile1.txt"],
15+
["Subprogram Calls", "https://gist.githubusercontent.com/jordalgo/f59c216ba91e269632cc9fdc6fc67be3/raw/d058a99a48450131015608195cc5e33364312492/gistfile1.txt"],
16+
["bpf_for_loop", "https://gist.githubusercontent.com/jordalgo/061d908e411e3477848be5c36c99e596/raw/bb2a35f7d5288472801211f73c95ad6e71c6b808/gistfile1.txt"],
17+
["Long Program", "https://gist.githubusercontent.com/theihor/1bea72b50f6834c00b67a3087304260e/raw/9c0cb831a4924e5f0f63cc1e0d9620aec771d31f/pyperf600-v1.log"],
18+
];
19+
</script>
1620
<link rel="manifest" href="manifest.json" />
1721
<title>BPF Verifier Visualizer</title>
1822
</head>

src/App.tsx

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ import {
2020
import {
2121
VisualLogState,
2222
LogLineState,
23-
Example,
2423
HoveredLineHint,
2524
LoadStatus,
2625
MainContent,
2726
SelectedLineHint,
2827
ToolTip,
28+
Examples,
2929
} from "./components";
3030
import { ParsedLineType } from "./parser";
3131

@@ -39,6 +39,14 @@ function getEmptyVisualLogState(): VisualLogState {
3939
};
4040
}
4141

42+
function getEmptyLogLineState(): LogLineState {
43+
return {
44+
memSlotId: "",
45+
line: 0,
46+
cLine: "",
47+
};
48+
}
49+
4250
function getVisualLogState(
4351
verifierLogState: VerifierLogState,
4452
fullLogView: boolean,
@@ -118,18 +126,15 @@ function App() {
118126
const [visualLogState, setVisualLogState] = useState<VisualLogState>(
119127
getEmptyVisualLogState(),
120128
);
121-
const [hoveredState, setHoveredState] = useState<LogLineState>({
122-
memSlotId: "",
123-
line: -1,
124-
cLine: "", // unused
125-
});
126-
const [selectedState, setSelectedState] = useState<LogLineState>({
127-
memSlotId: "",
128-
line: 0,
129-
cLine: "",
130-
});
129+
const [hoveredState, setHoveredState] = useState<LogLineState>(
130+
getEmptyLogLineState(),
131+
);
132+
const [selectedState, setSelectedState] = useState<LogLineState>(
133+
getEmptyLogLineState(),
134+
);
131135
const [loadError, setLoadError] = useState<string | null>(null);
132136
const [fullLogView, setfullLogView] = useState<boolean>(false);
137+
const [isLoading, setIsLoading] = useState<boolean>(false);
133138

134139
const fileInputRef = useRef<HTMLInputElement>(null);
135140

@@ -345,16 +350,40 @@ function App() {
345350
(text: string) => {
346351
const newVerifierLogState = processRawLines(text.split("\n"));
347352
setVisualLogState(getVisualLogState(newVerifierLogState, fullLogView));
353+
setIsLoading(false);
348354
},
349355
[fullLogView],
350356
);
351357

358+
const prepareNewLog = useCallback(() => {
359+
setSelectedState(getEmptyLogLineState());
360+
setIsLoading(true);
361+
}, []);
362+
352363
const handlePaste = useCallback(
353364
(event: React.ClipboardEvent) => {
365+
prepareNewLog();
354366
const pastedText = event.clipboardData.getData("text");
355367
loadInputText(pastedText);
356368
},
357-
[loadInputText],
369+
[loadInputText, prepareNewLog],
370+
);
371+
372+
const handleLoadExample = useCallback(
373+
async (exampleLink: string) => {
374+
prepareNewLog();
375+
try {
376+
const response = await fetch(exampleLink);
377+
if (!response.ok) {
378+
throw new Error(`HTTP error! status: ${response.status}`);
379+
}
380+
const result = await response.text();
381+
loadInputText(result);
382+
} catch (error) {
383+
console.error("Error fetching data:", error);
384+
}
385+
},
386+
[loadInputText, prepareNewLog],
358387
);
359388

360389
function getServerInjectedInputLink(): string | null {
@@ -539,6 +568,7 @@ function App() {
539568

540569
const onFileInputChange = useCallback(
541570
async (e: React.ChangeEvent<HTMLInputElement>) => {
571+
prepareNewLog();
542572
const files = (e.target as HTMLInputElement).files;
543573
if (files?.[0]) {
544574
const fileBlob = files[0];
@@ -565,6 +595,7 @@ function App() {
565595
}
566596
const newVerifierLogState = processRawLines(rawLines);
567597
setVisualLogState(getVisualLogState(newVerifierLogState, fullLogView));
598+
setIsLoading(false);
568599
}
569600
},
570601
[],
@@ -606,7 +637,7 @@ function App() {
606637
ref={fileInputRef}
607638
/>
608639
</div>
609-
<Example />
640+
<Examples handleLoadExample={handleLoadExample} />
610641
<a
611642
href="https://github.com/theihor/bpfvv/blob/master/HOWTO.md"
612643
className="howto-link"
@@ -652,6 +683,13 @@ function App() {
652683
hoveredMemSlotId={hoveredState.memSlotId}
653684
/>
654685
)}
686+
{isLoading && (
687+
<div className="loader-container">
688+
<div className="loader-content">
689+
<div className="loader"></div>
690+
</div>
691+
</div>
692+
)}
655693
</div>
656694
);
657695
}

src/__snapshots__/App.test.tsx.snap

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ exports[`App renders the correct starting elements 1`] = `
5555
type="file"
5656
/>
5757
</div>
58+
<label
59+
class="line-nav-item"
60+
>
61+
Examples:
62+
</label>
63+
<select
64+
class="line-nav-item"
65+
id="log-example-dropdown"
66+
/>
67+
<button
68+
class="line-nav-item"
69+
id="load-example"
70+
>
71+
Load
72+
</button>
5873
<a
5974
class="howto-link"
6075
href="https://github.com/theihor/bpfvv/blob/master/HOWTO.md"
@@ -138,6 +153,21 @@ exports[`App renders the log visualizer when text is pasted 1`] = `
138153
type="file"
139154
/>
140155
</div>
156+
<label
157+
class="line-nav-item"
158+
>
159+
Examples:
160+
</label>
161+
<select
162+
class="line-nav-item"
163+
id="log-example-dropdown"
164+
/>
165+
<button
166+
class="line-nav-item"
167+
id="load-example"
168+
>
169+
Load
170+
</button>
141171
<a
142172
class="howto-link"
143173
href="https://github.com/theihor/bpfvv/blob/master/HOWTO.md"
@@ -449,7 +479,13 @@ exports[`App renders the log visualizer when text is pasted 1`] = `
449479
class="hint-line"
450480
id="hint-hovered-line"
451481
>
452-
<br />
482+
<span>
483+
[hovered raw line]
484+
1
485+
:
486+
</span>
487+
 
488+
314: (73) *(u8 *)(r7 +1303) = r1 ; frame1: R1_w=0 R7=map_value(off=0,ks=4,vs=2808,imm=0)
453489
</div>
454490
</div>
455491
</div>
@@ -512,6 +548,21 @@ exports[`App renders the log visualizer when text is pasted 2`] = `
512548
type="file"
513549
/>
514550
</div>
551+
<label
552+
class="line-nav-item"
553+
>
554+
Examples:
555+
</label>
556+
<select
557+
class="line-nav-item"
558+
id="log-example-dropdown"
559+
/>
560+
<button
561+
class="line-nav-item"
562+
id="load-example"
563+
>
564+
Load
565+
</button>
515566
<a
516567
class="howto-link"
517568
href="https://github.com/theihor/bpfvv/blob/master/HOWTO.md"

src/components.tsx

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactElement } from "react";
1+
import React, { ChangeEvent, ReactElement } from "react";
22
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
33
import {
44
BpfJmpKind,
@@ -156,15 +156,48 @@ function CallHtml({
156156
}
157157
}
158158

159-
export function Example() {
160-
const url = document
161-
.querySelector('meta[name="app-example-input"]')
162-
?.getAttribute("link");
163-
if (url) {
159+
declare global {
160+
var exampleLinks: [string, string][];
161+
}
162+
163+
export function Examples({
164+
handleLoadExample,
165+
}: {
166+
handleLoadExample: (exampleLink: string) => Promise<void>;
167+
}) {
168+
const exampleLinks: [string, string][] = globalThis.exampleLinks || [];
169+
170+
const [selectedOption, setSelectedOption] = useState(exampleLinks.length ? exampleLinks[0][1] : "");
171+
const handleChange = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
172+
setSelectedOption(event.target.value);
173+
}, []);
174+
175+
const onLoad = useCallback(() => {
176+
handleLoadExample(selectedOption);
177+
}, [selectedOption]);
178+
179+
if (exampleLinks) {
164180
return (
165-
<a id="example-link" href={`${window.location.pathname}?url=${url}`}>
166-
Load an example log
167-
</a>
181+
<>
182+
<label className="line-nav-item">Examples:</label>
183+
<select
184+
id="log-example-dropdown"
185+
className="line-nav-item"
186+
onChange={handleChange}
187+
value={selectedOption}
188+
>
189+
{exampleLinks.map((pair) => {
190+
return (
191+
<option key={pair[1]} value={pair[1]}>
192+
{pair[0]}
193+
</option>
194+
);
195+
})}
196+
</select>
197+
<button id="load-example" className="line-nav-item" onClick={onLoad}>
198+
Load
199+
</button>
200+
</>
168201
);
169202
} else {
170203
return <></>;

src/index.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,61 @@ h1 {
486486
.hint-line span {
487487
font-weight: bold;
488488
}
489+
490+
/* Loading Spinner */
491+
.loader-container {
492+
width: 100%;
493+
height: 100%;
494+
position: absolute;
495+
top: 0px;
496+
left: 0px;
497+
background-color: rgba(0, 0, 0, 0.2);
498+
}
499+
500+
.loader-content {
501+
width: 80px;
502+
top: 50%;
503+
left: 50%;
504+
transform: translate(-50%, -50%);
505+
position: absolute;
506+
}
507+
508+
.loader {
509+
aspect-ratio: 1;
510+
display: grid;
511+
-webkit-mask: conic-gradient(from 15deg, #0003, #000);
512+
mask: conic-gradient(from 15deg, #0003, #000);
513+
animation: load 1s steps(12) infinite;
514+
color: purple;
515+
width: 80px;
516+
}
517+
518+
.loader,
519+
.loader:before,
520+
.loader:after {
521+
background:
522+
radial-gradient(closest-side at 50% 12.5%, currentColor 90%, #0000 98%) 50%
523+
0/20% 80% repeat-y,
524+
radial-gradient(closest-side at 12.5% 50%, currentColor 90%, #0000 98%) 0
525+
50%/80% 20% repeat-x;
526+
}
527+
528+
.loader:before,
529+
.loader:after {
530+
content: "";
531+
grid-area: 1/1;
532+
transform: rotate(30deg);
533+
}
534+
535+
.loader:after {
536+
transform: rotate(60deg);
537+
}
538+
539+
@keyframes load {
540+
from {
541+
transform: rotate(0turn);
542+
}
543+
to {
544+
transform: rotate(1turn);
545+
}
546+
}

src/utils.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export function getVisibleLogLineRange(linesLen: number): {
4646
min: number;
4747
max: number;
4848
} {
49+
if (linesLen === 0) {
50+
return { min: 0, max: 0 };
51+
}
4952
const formattedLogLines = document.getElementById("formatted-log-lines");
5053
const logContainer = document.getElementById("log-container");
5154
if (!formattedLogLines || !logContainer) {

0 commit comments

Comments
 (0)