diff --git a/src/dashboard.rs b/src/dashboard.rs index 3465605..57d0da7 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -98,6 +98,8 @@ pub struct ExecuteResult { pub status: Status, pub start_time: String, pub elapsed: u64, + pub stdout: String, + pub stderr: String, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/main.rs b/src/main.rs index a701c55..0934900 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,11 +34,19 @@ pub enum RunMoonError { FromUtf8(#[from] std::string::FromUtf8Error), } +#[derive(Debug)] +struct CommandOutput { + duration: Duration, + stdout: String, + stderr: String, + success: bool, +} + fn run_moon( workdir: &Path, source: &MooncakeSource, args: &[&str], -) -> Result { +) -> Result { let start = Instant::now(); eprintln!( "{}", @@ -46,16 +54,18 @@ fn run_moon( .blue() .bold() ); - let mut cmd = std::process::Command::new("moon") + + let output = std::process::Command::new("moon") .current_dir(workdir) .args(args) - .spawn() + .output() .map_err(|e| RunMoonError::IOError(e))?; - let exit = cmd.wait().map_err(|e| RunMoonError::IOError(e))?; - if !exit.success() { - return Err(RunMoonError::ReturnNonZero(exit)); - } + + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + let elapsed = start.elapsed(); + eprintln!( "{}", format!( @@ -66,7 +76,13 @@ fn run_moon( .green() .bold() ); - Ok(elapsed) + + Ok(CommandOutput { + duration: elapsed, + stdout, + stderr, + success: output.status.success(), + }) } #[derive(Debug, thiserror::Error)] @@ -181,21 +197,31 @@ fn stat_mooncake( let _ = run_moon(workdir, source, &["clean"]); let r = run_moon(workdir, source, &cmd.args()).map_err(|e| StatMooncakeError::RunMoon(e)); - let status = if r.is_err() { - Status::Failure - } else { - Status::Success + let status = match r.as_ref() { + Ok(output) if output.success => Status::Success, + _ => Status::Failure, }; - let d = r.ok(); + let output = r.ok(); let start_time = Local::now() .with_timezone(&FixedOffset::east_opt(8 * 3600).unwrap()) .format("%Y-%m-%d %H:%M:%S.%3f") .to_string(); - let elapsed = d.map(|d| d.as_millis() as u64).unwrap_or(0); + let elapsed = output + .as_ref() + .map(|d| d.duration.as_millis() as u64) + .unwrap_or(0); let execute_result = ExecuteResult { status, start_time, elapsed, + stdout: output + .as_ref() + .map(|d| d.stdout.clone()) + .unwrap_or_default(), + stderr: output + .as_ref() + .map(|d| d.stderr.clone()) + .unwrap_or_default(), }; Ok(execute_result) } diff --git a/webapp/package.json b/webapp/package.json index cd83725..b106943 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -28,5 +28,6 @@ "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", "vite": "^5.4.1" - } + }, + "packageManager": "pnpm@9.5.0+sha256.dbdf5961c32909fb030595a9daa1dae720162e658609a8f92f2fa99835510ca5" } diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 540f9be..77274f2 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -29,6 +29,8 @@ interface ExecuteResult { status: Status; start_time: string; elapsed: number; + stdout: string; + stderr: string; } interface BackendState { @@ -56,9 +58,84 @@ async function get_data(): Promise { return parsedData[parsedData.length - 1]; } +interface ModalProps { + isOpen: boolean; + onClose: () => void; + data: ExecuteResult; + title: string; +} + +const DetailModal: React.FC = ({ isOpen, onClose, data, title }) => { + if (!isOpen) return null; + + useEffect(() => { + const handleEsc = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }; + window.addEventListener('keydown', handleEsc); + + return () => { + window.removeEventListener('keydown', handleEsc); + }; + }, [onClose]); + + + return ( +
+
+
+

{title}

+ +
+
+
+

Status: + + {data.status} + +

+

Start Time: {data.start_time}

+

Elapsed: {data.elapsed}ms

+
+ + {/* Stdout */} +
+
stdout
+
+
+ {data.stdout || "no stdout output"} +
+
+
+ + {/* Stderr */} +
+
stderr
+
+
+ {data.stderr || "no stderr output"} +
+
+
+
+
+
+ ); +}; + const App = () => { const [data, setData] = useState(null); const [error, setError] = useState(null); + const [selectedData, setSelectedData] = useState(null); + const [modalTitle, setModalTitle] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); const fetchData = async () => { try { @@ -77,29 +154,57 @@ const App = () => { fetchData(); }, []); - const getStatusStyle = (status: Status): string => { - return status === "Success" - ? "bg-green-200 text-green-800" - : "bg-red-200 text-red-800"; + const handleResultClick = (result: ExecuteResult, title: string) => { + setSelectedData(result); + setModalTitle(title); + setIsModalOpen(true); }; - const getStatusText = (status: Status, elapsed: number | null): string => { - return status === "Success" ? `${elapsed ?? '-'}` : "x"; - }; + const renderBackendState = ( + backendState: BackendState, + phase: string, + variant: 'stable' | 'bleeding', + stableCBT?: CBT | null + ) => { + const highlightDifference = ( + stable: ExecuteResult, + bleeding: ExecuteResult + ) => stable.status !== bleeding.status ? "bg-yellow-100" : ""; - const renderBackendState = (backendState: BackendState) => ( - <> - - {getStatusText(backendState.wasm.status, backendState.wasm.elapsed)} - - - {getStatusText(backendState.wasm_gc.status, backendState.wasm_gc.elapsed)} - - - {getStatusText(backendState.js.status, backendState.js.elapsed)} - - - ); + const getStatusStyle = (status: Status): string => { + return status === "Success" + ? "bg-green-200 text-green-800" + : "bg-red-200 text-red-800"; + }; + + const getStatusText = (status: Status, elapsed: number | null): string => { + return status === "Success" ? `${elapsed ?? '-'}` : "x"; + }; + + return ( + <> + {["wasm", "wasm_gc", "js"].map((key) => { + const result = backendState[key as keyof BackendState]; + const stableResult = stableCBT?.[phase.toLowerCase() as keyof CBT]?.[key as keyof BackendState]; + + return ( + handleResultClick( + result, + `${variant} - ${phase.toLowerCase()} - ${key}` + )} + > + {getStatusText(result.status, result.elapsed)} + + ); + })} + + ); + }; const renderTableRows = ( stableData: BuildState[], @@ -117,14 +222,6 @@ const App = () => { const stableCBT = stableEntry.cbts[versionIndex]; const bleedingCBT = bleedingEntry?.cbts[versionIndex]; - const highlightDifference = ( - stable: ExecuteResult, - bleeding: ExecuteResult - ) => - stable.status !== bleeding.status - ? "bg-yellow-100" // Highlight the difference - : ""; - return ( { {/* Stable Data */} {stableCBT ? ( <> - {renderBackendState(stableCBT.check)} - {renderBackendState(stableCBT.build)} - {renderBackendState(stableCBT.test)} + {renderBackendState(stableCBT.check, "Check", 'stable')} + {renderBackendState(stableCBT.build, "Build", 'stable')} + {renderBackendState(stableCBT.test, "Test", 'stable')} ) : ( @@ -189,69 +286,9 @@ const App = () => { {/* Bleeding Data */} {bleedingCBT ? ( <> - {/* Check */} - {["wasm", "wasm_gc", "js"].map((key) => ( - - {getStatusText( - bleedingCBT.check[key as keyof BackendState].status, - bleedingCBT.check[key as keyof BackendState].elapsed - )} - - ))} - {/* Build */} - {["wasm", "wasm_gc", "js"].map((key) => ( - - {getStatusText( - bleedingCBT.build[key as keyof BackendState].status, - bleedingCBT.build[key as keyof BackendState].elapsed - )} - - ))} - {/* Test */} - {["wasm", "wasm_gc", "js"].map((key) => ( - - {getStatusText( - bleedingCBT.test[key as keyof BackendState].status, - bleedingCBT.test[key as keyof BackendState].elapsed - )} - - ))} + {renderBackendState(bleedingCBT.check, "Check", 'bleeding', stableCBT)} + {renderBackendState(bleedingCBT.build, "Build", 'bleeding', stableCBT)} + {renderBackendState(bleedingCBT.test, "Test", 'bleeding', stableCBT)} ) : ( @@ -264,7 +301,6 @@ const App = () => { }); }; - return (
@@ -315,7 +351,7 @@ const App = () => { js wasm wasm gc - js + js wasm wasm gc js @@ -324,7 +360,7 @@ const App = () => { js wasm wasm gc - JS + js @@ -336,6 +372,14 @@ const App = () => {

Loading...

)}
+ {selectedData && ( + setIsModalOpen(false)} + data={selectedData} + title={modalTitle} + /> + )}
); };