From 3e2d41ca853c5f72b7402aade9c79d0290b0ef17 Mon Sep 17 00:00:00 2001 From: Jordan Rome Date: Mon, 4 Aug 2025 10:24:46 -0400 Subject: [PATCH] Add C Source Line View This adds a collapsible C source line view on the right, which contains all C files from the log and their C lines. This view is in sync with the main log lines view; meaning if a user clicks on a line in one, we'll hightlight the associated lines in the other. This also removes C lines from the main log view by default but a checkbox was added so users can add them back in as desired. Additional changes: - remove line numbers from the main log view - remove Go To Line input box - Add C Line to State Panel --- eslint.config.js | 2 +- package.json | 2 +- src/App.test.tsx | 224 +++++++----- src/App.tsx | 376 ++++++++++++++------ src/__snapshots__/App.test.tsx.snap | 508 ++++++++++++++++++++++++++++ src/analyzer.ts | 52 ++- src/components.tsx | 264 ++++++++++++--- src/index.css | 74 +++- src/parser.ts | 6 +- src/test-data.ts | 133 ++++++++ src/utils.tsx | 44 ++- 11 files changed, 1413 insertions(+), 272 deletions(-) create mode 100644 src/__snapshots__/App.test.tsx.snap create mode 100644 src/test-data.ts diff --git a/eslint.config.js b/eslint.config.js index c5d39b9..6dffb62 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,6 +47,6 @@ export default [ }, }, { - ignores: ['node_modules/**', 'dist/**', 'coverage/**'], + ignores: ['node_modules/**', 'dist/**', 'coverage/**', 'src/__snapshots__/**'], }, ]; diff --git a/package.json b/package.json index 52249cb..1e4d8e9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "serve": "vite preview", "test": "jest", "format": "prettier src/* --write", - "lint": "eslint src/*", + "lint": "eslint", "typecheck": "tsc --noEmit" }, "eslintConfig": { diff --git a/src/App.test.tsx b/src/App.test.tsx index 2f45efd..a34e177 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -4,50 +4,22 @@ import "@testing-library/jest-dom"; import { render, createEvent, fireEvent } from "@testing-library/react"; import App from "./App"; +import { SAMPLE_LOG_DATA_1, SAMPLE_LOG_DATA_2 } from "./test-data"; -describe("App", () => { - it("does not throw", () => { - render(); - - const inputEl = document.getElementById("input-text"); - expect(inputEl).toBeTruthy(); - expect(inputEl).toBeVisible(); - }); +const DOM_EL_FAIL = "DOM Element missing"; +describe("App", () => { it("renders the correct starting elements", () => { - render(); - - const exampleLinkEl = document.getElementById("example-link"); - expect(exampleLinkEl?.innerHTML).toBe("Load an example log"); - - const inputEl = document.getElementById("input-text"); - expect(inputEl?.getAttribute("placeholder")).toBe( - "Paste a verifier log here or choose a file", - ); - expect(inputEl?.tagName).toBe("TEXTAREA"); - - const fileInputEl = document.getElementById("file-input"); - expect(fileInputEl?.tagName).toBe("INPUT"); - - const gotoLineEl = document.getElementById("goto-line-input"); - expect(gotoLineEl?.tagName).toBe("INPUT"); - - const gotoStartEl = document.getElementById("goto-start"); - expect(gotoStartEl?.tagName).toBe("BUTTON"); - - const gotoEndEl = document.getElementById("goto-end"); - expect(gotoEndEl?.tagName).toBe("BUTTON"); - - const clearEl = document.getElementById("clear"); - expect(clearEl?.tagName).toBe("BUTTON"); + const { container } = render(); + expect(container).toMatchSnapshot(); }); it("renders the log visualizer when text is pasted", async () => { - render(); + const { container, rerender } = render(); const inputEl = document.getElementById("input-text"); if (!inputEl) { - fail(); + throw new Error(DOM_EL_FAIL); } fireEvent( @@ -60,51 +32,26 @@ describe("App", () => { }), ); - const logContainerEl = document.getElementById("log-container"); - expect(logContainerEl).toBeTruthy(); - expect(logContainerEl).toBeVisible(); - - const logLinesEl = document.getElementById("line-numbers-idx"); - expect(logLinesEl?.innerHTML).toBe( - '
1
', - ); - - const logLinesPcEl = document.getElementById("line-numbers-pc"); - expect(logLinesPcEl?.innerHTML).toBe( - '
314:
', - ); + rerender(); + expect(container).toMatchSnapshot(); + expect(document.getElementById("c-source-container")).toBeTruthy(); expect(document.getElementById("formatted-log-lines")).toBeTruthy(); - - const firstLine = document.getElementById("line-0"); - expect(firstLine?.innerHTML).toBe( - '*(u8 *)(r7 +1303) = r1', - ); - - const hintSelectedLineEl = document.getElementById("hint-selected-line"); - expect(hintSelectedLineEl?.innerHTML).toBe( - "[selected raw line] 1: 314: (73) *(u8 *)(r7 +1303) = r1 ; frame1: R1_w=0 R7=map_value(off=0,ks=4,vs=2808,imm=0)", - ); - - expect(document.getElementById("state-panel-shown")).toBeTruthy(); - - const statePanelHeader = document.getElementById("state-panel-header"); - expect(statePanelHeader?.innerHTML).toBe( - "
Line: 1
PC: 314
Frame: 0
", - ); - - //TODO: add tests for state panel content + expect(document.getElementById("state-panel")).toBeTruthy(); // Hit the clear button and make sure we go back to the intial state const clearEl = document.getElementById("clear"); if (!clearEl) { - fail(); + throw new Error(DOM_EL_FAIL); } fireEvent(clearEl, createEvent.click(clearEl)); - expect(document.getElementById("log-container")).toBeFalsy(); - expect(document.getElementById("state-panel-shown")).toBeFalsy(); - expect(document.getElementById("input-text")).toBeTruthy(); - expect(document.getElementById("input-text")).toBeVisible(); + + rerender(); + expect(container).toMatchSnapshot(); + + expect(document.getElementById("c-source-container")).toBeFalsy(); + expect(document.getElementById("formatted-log-lines")).toBeFalsy(); + expect(document.getElementById("state-panel")).toBeFalsy(); }); it("jumps to the next/prev instruction on key up/down", async () => { @@ -112,38 +59,33 @@ describe("App", () => { const inputEl = document.getElementById("input-text"); if (!inputEl) { - fail(); + throw new Error(DOM_EL_FAIL); } fireEvent( inputEl, createEvent.paste(inputEl, { clipboardData: { - getData: () => - ` - 0: (18) r1 = 0x11 ; R1_w=17 -2: (b7) r2 = 0 ; R2_w=0 -3: (85) call bpf_obj_new_impl#54651 ; R0_w=ptr_or_null_node_data(id=2,ref_obj_id=2) refs=2 -4: (bf) r6 = r0 ; R0_w=ptr_or_null_node_data(id=2,ref_obj_id=2) R6_w=ptr_or_null_node_data(id=2,ref_obj_id=2) refs=2 -5: (b7) r7 = 1 ; R7_w=1 refs=2 -; if (!n) @ rbtree.c:199 -6: (15) if r6 == 0x0 goto pc+104 ; R6_w=ptr_node_data(ref_obj_id=2) refs=2 -7: (b7) r1 = 4 ; R1_w=4 ref - `, + getData: () => SAMPLE_LOG_DATA_1, }, }), ); + // Need to show all log lines + const checkboxEl = document.getElementById("show-full-log"); + if (!checkboxEl) { + throw new Error(DOM_EL_FAIL); + } + fireEvent(checkboxEl, createEvent.click(checkboxEl)); + const logContainerEl = document.getElementById("log-container"); - expect(logContainerEl).toBeTruthy(); - expect(logContainerEl).toBeVisible(); const line5 = document.getElementById("line-5"); const line6 = document.getElementById("line-6"); const line7 = document.getElementById("line-7"); - if (!line5 || !line6 || !line7 || !logContainerEl) { - fail(); + if (!line5 || !line7 || !line6 || !logContainerEl) { + throw new Error(DOM_EL_FAIL); } expect(line5.innerHTML).toBe( @@ -174,4 +116,110 @@ describe("App", () => { expect(line6.classList.contains("selected-line")).toBeFalsy(); expect(line7.classList.contains("selected-line")).toBeFalsy(); }); + + it("c lines and state panel containers are collapsible", async () => { + render(); + + const inputEl = document.getElementById("input-text"); + if (!inputEl) { + throw new Error("Input text is missing"); + } + + fireEvent( + inputEl, + createEvent.paste(inputEl, { + clipboardData: { + getData: () => SAMPLE_LOG_DATA_1, + }, + }), + ); + + const cSourceEl = document.getElementById("c-source-container"); + const cSourceFile = cSourceEl?.querySelector(".c-source-file"); + + expect(cSourceFile).toBeVisible(); + + const cSourceHideShow = cSourceEl?.querySelector(".hide-show-button"); + + if (!cSourceHideShow) { + throw new Error(DOM_EL_FAIL); + } + + fireEvent(cSourceHideShow, createEvent.click(cSourceHideShow)); + expect(cSourceFile).not.toBeVisible(); + + const statePanelEl = document.getElementById("state-panel"); + const statePanelHeader = document.getElementById("state-panel-header"); + + expect(statePanelHeader).toBeVisible(); + + const statePanelHideShow = statePanelEl?.querySelector(".hide-show-button"); + + if (!statePanelHideShow) { + throw new Error(DOM_EL_FAIL); + } + + fireEvent(statePanelHideShow, createEvent.click(statePanelHideShow)); + expect(statePanelHeader).not.toBeVisible(); + }); + + it("highlights the associated c source or log line(s) when the other is clicked ", async () => { + render(); + + const inputEl = document.getElementById("input-text"); + if (!inputEl) { + throw new Error(DOM_EL_FAIL); + } + + fireEvent( + inputEl, + createEvent.paste(inputEl, { + clipboardData: { + getData: () => SAMPLE_LOG_DATA_2, + }, + }), + ); + + const line4El = document.getElementById("line-4"); + const cLineEl = document.getElementById("line-rbtree.c:198"); + if (!line4El || !cLineEl) { + throw new Error(DOM_EL_FAIL); + } + + expect(line4El.classList).not.toContain("selected-line"); + expect(cLineEl.classList).not.toContain("selected-line"); + + // Click on the first instruction log line + fireEvent(line4El, createEvent.click(line4El)); + + expect(line4El.classList).toContain("selected-line"); + expect(cLineEl.classList).toContain("selected-line"); + + // Click on another log line + const line10El = document.getElementById("line-10"); + if (!line10El) { + throw new Error(DOM_EL_FAIL); + } + + fireEvent(line10El, createEvent.click(line10El)); + + expect(line4El.classList).not.toContain("selected-line"); + expect(cLineEl.classList).not.toContain("selected-line"); + + // Click on the first c source line + fireEvent(cLineEl, createEvent.click(cLineEl)); + + expect(line4El.classList).toContain("selected-line"); + expect(cLineEl.classList).toContain("selected-line"); + + // The other instructions for this source line should also be selected + const followingIns = ["line-5", "line-6", "line-7", "line-8"]; + followingIns.forEach((lineId) => { + const el = document.getElementById(lineId); + if (!el) { + throw new Error(DOM_EL_FAIL); + } + expect(el.classList).toContain("selected-line"); + }); + }); }); diff --git a/src/App.tsx b/src/App.tsx index f96888b..3df5e61 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,18 @@ import React from "react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { CSourceMap, VerifierLogState, processRawLines } from "./analyzer"; +import { + VerifierLogState, + processRawLines, + getEmptyVerifierState, +} from "./analyzer"; import { fetchLogFromUrl, getVisibleIdxRange, - normalIdx, - scrollToLine, + scrollToLogLine, + scrollToCLine, + siblingInsLine, } from "./utils"; import { @@ -19,37 +24,46 @@ import { SelectedLineHint, ToolTip, } from "./components"; -import { ParsedLineType } from "./parser"; +import { ParsedLine, ParsedLineType } from "./parser"; const ContentRaw = ({ loadError, verifierLogState, + logLines, selectedLine, selectedMemSlotId, + selectedCLine, handlePaste, handleMainContentClick, + handleCLinesClick, handleLogLinesClick, handleLogLinesOver, handleLogLinesOut, }: { loadError: string | null; verifierLogState: VerifierLogState; + logLines: ParsedLine[]; selectedLine: number; selectedMemSlotId: string; + selectedCLine: number; handlePaste: (event: React.ClipboardEvent) => void; handleMainContentClick: (event: React.MouseEvent) => void; + handleCLinesClick: (event: React.MouseEvent) => void; handleLogLinesClick: (event: React.MouseEvent) => void; handleLogLinesOver: (event: React.MouseEvent) => void; handleLogLinesOut: (event: React.MouseEvent) => void; }) => { if (loadError) { return
{loadError}
; - } else if (verifierLogState.lines.length > 0) { + } else if (logLines.length > 0) { return ( ({ - lines: [], - bpfStates: [], - cSourceMap: new CSourceMap(), - }); + const [verifierLogState, setVerifierLogState] = useState( + getEmptyVerifierState(), + ); const [hoveredState, setHoveredState] = useState({ memSlotId: "", line: -1, + cLine: "", // unused }); const [selectedState, setSelectedState] = useState({ memSlotId: "", line: 0, + cLine: "", }); const [loadError, setLoadError] = useState(null); - const { line: selectedLine, memSlotId: selectedMemSlotId } = selectedState; + const [fullLogView, setfullLogView] = useState(false); + const fileInputRef = useRef(null); - const setSelectedLineScroll = useCallback( - (nextSelected: number) => { - const lines = verifierLogState.lines; - setSelectedState((prevSelected) => { - let { min, max } = getVisibleIdxRange(lines.length); - if (nextSelected < min + 8 || nextSelected > max - 8) { - scrollToLine(nextSelected, lines.length); - } - if (nextSelected < 0 || nextSelected >= lines.length) { - return prevSelected; - } - return { line: nextSelected, memSlotId: "" }; - }); + const { cLines, cLineIdtoIdx } = verifierLogState; + + const [logLines, logLineIdToIdx] = useMemo(() => { + const logLines: ParsedLine[] = []; + const logLineIdToIdx: Map = new Map(); + + let idx = 0; + verifierLogState.lines.forEach((line) => { + if (line.type !== ParsedLineType.C_SOURCE || fullLogView) { + logLines.push(line); + logLineIdToIdx.set(line.idx, idx++); + } + }); + + return [logLines, logLineIdToIdx]; + }, [verifierLogState, fullLogView]); + + const { line: selectedLine, memSlotId: selectedMemSlotId } = selectedState; + const selectedLineIdx = logLineIdToIdx.get(selectedLine) || 0; + const hoveredLineIdx = logLineIdToIdx.get(hoveredState.line) || 0; + const selectedCLine = useMemo(() => { + let clineId = ""; + if (selectedState.cLine) { + clineId = selectedState.cLine; + } else { + const parsedLine = verifierLogState.lines[selectedState.line]; + if (!parsedLine) { + return 0; + } + if (parsedLine.type === ParsedLineType.C_SOURCE) { + clineId = parsedLine.id; + } else { + clineId = + verifierLogState.cSourceMap.logLineToCLine.get(selectedState.line) || + ""; + } + } + return verifierLogState.cSourceMap.cSourceLines.get(clineId)?.lineNum || 0; + }, [verifierLogState, selectedState]); + + const setSelectedAndScroll = useCallback( + ( + nextInsLineId: number, + nextCLineId: string, + nextInsLineIdx: number, + nextCLineIdx: number, + memSlotId: string = "", + ) => { + const logRange = getVisibleIdxRange(logLines.length); + if ( + (nextInsLineIdx < logRange.min + 8 || + nextInsLineIdx > logRange.max - 8) && + !(nextInsLineIdx < 0 || nextInsLineIdx >= logLines.length) + ) { + scrollToLogLine(nextInsLineIdx, logLines.length); + } + const cLinesRange = getVisibleIdxRange(cLines.length); + if ( + (nextCLineIdx < cLinesRange.min + 8 || + nextCLineIdx > cLinesRange.max - 8) && + !(nextCLineIdx < 0 || nextCLineIdx >= cLines.length) + ) { + scrollToCLine(nextCLineIdx, cLines.length); + } + setSelectedState({ line: nextInsLineId, memSlotId, cLine: nextCLineId }); }, - [verifierLogState], + [logLines, cLines], ); const onGotoStart = useCallback(() => { - setSelectedLineScroll(0); - }, [setSelectedLineScroll]); + if (logLines.length === 0) { + return; + } + const lineId = logLines[0].idx; + const clineId = + verifierLogState.cSourceMap.logLineToCLine.get(lineId) || ""; + setSelectedAndScroll(lineId, "", 0, cLineIdtoIdx.get(clineId) || 0); + }, [logLines, verifierLogState]); - const onGotoEnd = useCallback(() => { - setSelectedLineScroll(verifierLogState.lines.length - 1); - }, [setSelectedLineScroll, verifierLogState]); + function onGotoEnd() { + if (logLines.length === 0) { + return; + } + const lineId = logLines[logLines.length - 1].idx; + const clineId = + verifierLogState.cSourceMap.logLineToCLine.get(lineId) || ""; + setSelectedAndScroll( + lineId, + "", + logLines.length - 1, + cLineIdtoIdx.get(clineId) || 0, + ); + } const onClear = useCallback(() => { - setVerifierLogState({ - lines: [], - bpfStates: [], - cSourceMap: new CSourceMap(), - }); - setSelectedState({ line: 0, memSlotId: "" }); + setVerifierLogState(getEmptyVerifierState()); + setSelectedState({ line: 0, memSlotId: "", cLine: "" }); const fiCurrent = fileInputRef.current; if (fiCurrent) { fiCurrent.value = ""; } }, []); - function siblingInstructionLine(idx: number, delta: number = 1): number { - // if delta is 1 we are looking for the next instruction - // if delta is -1 we are looking for the previous instruction - const n = verifierLogState.lines.length; - for (let i = normalIdx(idx + delta, n); 0 <= i && i < n; i += delta) { - const line = verifierLogState.lines[i]; - if (line.type === ParsedLineType.INSTRUCTION) { - return i; - } - } - return normalIdx(idx, n); - } - - function nextInstructionLine(idx: number): number { - return siblingInstructionLine(idx, 1); - } - - function prevInstructionLine(idx: number): number { - return siblingInstructionLine(idx, -1); - } + const onLogToggle = useCallback(() => { + setfullLogView((prev) => !prev); + }, []); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { let delta = 0; - let { min, max } = getVisibleIdxRange(verifierLogState.lines.length); + let areCLinesInFocus = selectedState.cLine !== ""; + let { min, max } = getVisibleIdxRange( + areCLinesInFocus ? cLines.length : logLines.length, + ); let page = max - min + 1; switch (e.key) { case "ArrowDown": case "j": - delta = nextInstructionLine(selectedLine) - selectedLine; + delta = 1; break; case "ArrowUp": case "k": - delta = prevInstructionLine(selectedLine) - selectedLine; + delta = -1; break; case "PageDown": delta = page; @@ -174,15 +239,44 @@ function App() { onGotoEnd(); return; case "Escape": - setSelectedState((prevSelected) => { - return { ...prevSelected, memSlotId: "" }; - }); + setSelectedState({ line: 0, memSlotId: "", cLine: "" }); break; default: return; } e.preventDefault(); - setSelectedLineScroll(selectedLine + delta); + if (areCLinesInFocus) { + const currentIdx = cLineIdtoIdx.get(selectedState.cLine) || 0; + let nextIdx = currentIdx + delta; + if (cLines[nextIdx] === "") { + nextIdx += delta; + } + const logLines = verifierLogState.cSourceMap.cLineToLogLines.get( + selectedState.cLine, + ); + let logLineId = 0; + if (logLines && logLines.size > 0) { + [logLineId] = logLines; + } + setSelectedAndScroll( + logLineId, + cLines[nextIdx], + logLineIdToIdx.get(logLineId) || -1, + nextIdx, + ); + } else { + const currInsIdx = logLineIdToIdx.get(selectedState.line) || 0; + let nextInsIdx = siblingInsLine(logLines, currInsIdx, delta); + const logLineId = logLines[nextInsIdx].idx; + const cLineId = + verifierLogState.cSourceMap.logLineToCLine.get(logLineId) || ""; + setSelectedAndScroll( + logLineId, + "", + nextInsIdx, + cLineIdtoIdx.get(cLineId) || -1, + ); + } }; document.addEventListener("keydown", handleKeyDown); @@ -191,16 +285,19 @@ function App() { document.removeEventListener("keydown", handleKeyDown); }; }, [ + logLines, + cLines, + cLineIdtoIdx, + selectedState, verifierLogState, - selectedLine, - setSelectedLineScroll, + logLineIdToIdx, onGotoStart, onGotoEnd, ]); useEffect(() => { onGotoEnd(); - }, [verifierLogState, onGotoEnd]); + }, [verifierLogState]); const loadInputText = useCallback((text: string) => { setVerifierLogState(processRawLines(text.split("\n"))); @@ -228,12 +325,80 @@ function App() { } }, [loadInputText]); + useEffect(() => { + let logLines: Set = new Set(); + let cLine: string; + let cLineEl: HTMLElement | null; + if (selectedState.cLine) { + cLineEl = document.getElementById(`line-${selectedState.cLine}`); + if (cLineEl) { + cLineEl.classList.add("selected-line"); + } + logLines = + verifierLogState.cSourceMap.cLineToLogLines.get(selectedState.cLine) || + new Set(); + for (let logLine of logLines) { + const logLineEl = document.getElementById(`line-${logLine}`); + if (logLineEl) { + logLineEl.classList.add("selected-line"); + } + } + } else if (selectedState.line) { + const parsedLine = verifierLogState.lines[selectedState.line]; + cLine = + parsedLine.type === ParsedLineType.C_SOURCE + ? parsedLine.id + : verifierLogState.cSourceMap.logLineToCLine.get( + selectedState.line, + ) || ""; + cLineEl = document.getElementById(`line-${cLine}`); + if (cLineEl) { + cLineEl.classList.add("selected-line"); + } + } + return () => { + if (cLineEl) { + cLineEl.classList.remove("selected-line"); + } + for (let logLine of logLines) { + const logLineEl = document.getElementById(`line-${logLine}`); + if (logLineEl) { + logLineEl.classList.remove("selected-line"); + } + } + }; + }, [verifierLogState, selectedState]); + const handleMainContentClick = useCallback(() => { setSelectedState((prevSelected) => { return { ...prevSelected, memSlotId: "" }; }); }, []); + const handleCLinesClick = useCallback( + (e: React.MouseEvent) => { + const target = e.target as HTMLElement; + const cline = target.closest(".c-source-line"); + let clineId = ""; + if (cline) { + clineId = cline.getAttribute("data-id") || ""; + } + const logLines = verifierLogState.cSourceMap.cLineToLogLines.get(clineId); + if (logLines && logLines.size > 0) { + const [firstItem] = logLines; + setSelectedAndScroll( + firstItem, + clineId, + logLineIdToIdx.get(firstItem) || 0, + -1, + ); + } else { + setSelectedState({ line: 0, memSlotId: "", cLine: clineId }); + } + }, + [verifierLogState, logLineIdToIdx, cLineIdtoIdx], + ); + const handleLogLinesClick = useCallback( (e: React.MouseEvent) => { const target = e.target as HTMLElement; @@ -247,17 +412,25 @@ function App() { const clickedLine = target.closest(".log-line"); if (clickedLine) { - const lineIndex = parseInt( + const lineId = parseInt( clickedLine.getAttribute("line-index") || "0", 10, ); - setSelectedState({ - line: normalIdx(lineIndex, verifierLogState.lines.length), + const parsedLine = verifierLogState.lines[lineId]; + const clineId = + parsedLine.type == ParsedLineType.C_SOURCE + ? parsedLine.id + : verifierLogState.cSourceMap.logLineToCLine.get(lineId) || ""; + setSelectedAndScroll( + lineId, + "", + -1, + cLineIdtoIdx.get(clineId) || -1, memSlotId, - }); + ); } }, - [verifierLogState], + [logLines, verifierLogState, cLineIdtoIdx], ); const handleLogLinesOver = useCallback( @@ -271,36 +444,22 @@ function App() { } const memSlot = hoveredElement.closest(".mem-slot") as HTMLElement; if (memSlot) { - setHoveredState({ memSlotId: memSlot.id, line: hoveredLine }); + setHoveredState({ + memSlotId: memSlot.id, + line: hoveredLine, + cLine: "", + }); } else { - setHoveredState({ memSlotId: "", line: hoveredLine }); + setHoveredState({ memSlotId: "", line: hoveredLine, cLine: "" }); } }, [], ); const handleLogLinesOut = useCallback(() => { - setHoveredState({ memSlotId: "", line: -1 }); + setHoveredState({ memSlotId: "", line: -1, cLine: "" }); }, []); - const onLineInputChange = useCallback( - (e: React.ChangeEvent) => { - const lines = verifierLogState.lines; - const newValue = parseInt(e.target.value, 10); - if (Number.isNaN(newValue)) { - return; - } - if (newValue <= 0) { - setSelectedLineScroll(0); - } else if (newValue > lines.length) { - setSelectedLineScroll(lines.length - 1); - } else { - setSelectedLineScroll(newValue - 1); - } - }, - [verifierLogState, setSelectedLineScroll], - ); - const onFileInputChange = useCallback( async (e: React.ChangeEvent) => { const files = (e.target as HTMLInputElement).files; @@ -338,19 +497,7 @@ function App() {

BPF Verifier Visualizer

- - + - +
diff --git a/src/__snapshots__/App.test.tsx.snap b/src/__snapshots__/App.test.tsx.snap new file mode 100644 index 0000000..55297c2 --- /dev/null +++ b/src/__snapshots__/App.test.tsx.snap @@ -0,0 +1,508 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`App renders the correct starting elements 1`] = ` +
+
+
+ +