diff --git a/web/frontend/src/components/CaseBuilder/CaseBuilder.tsx b/web/frontend/src/components/CaseBuilder/CaseBuilder.tsx index 124bd88..7b34c1c 100644 --- a/web/frontend/src/components/CaseBuilder/CaseBuilder.tsx +++ b/web/frontend/src/components/CaseBuilder/CaseBuilder.tsx @@ -12,7 +12,8 @@ import { BLANK_SCENARIO } from "../../core/Data/fixtures"; import "tabulator-tables/dist/css/tabulator.min.css"; import "../Common/Forms/Tables.css"; import { useState } from "react"; -import Footer from "./Footer"; +import { useNavigate } from "react-router"; +import Footer from "../Common/Footer"; import * as pako from "pako"; import { offerDownload } from "../Common/io"; import { preprocess } from "../../core/Operations/preprocessing"; @@ -31,6 +32,7 @@ export interface CaseBuilderSectionProps { } const CaseBuilder = () => { + const navigate = useNavigate(); const [scenario, setScenario] = useState(() => { const savedScenario = localStorage.getItem("scenario"); if (!savedScenario) return BLANK_SCENARIO; @@ -116,7 +118,7 @@ const CaseBuilder = () => { // Parse response const data = await response.json(); - console.log(data.job_id); + navigate(`/jobs/${data.job_id}`); }; return ( diff --git a/web/frontend/src/components/CaseBuilder/Header.tsx b/web/frontend/src/components/CaseBuilder/Header.tsx index 6839df7..d6996de 100644 --- a/web/frontend/src/components/CaseBuilder/Header.tsx +++ b/web/frontend/src/components/CaseBuilder/Header.tsx @@ -4,7 +4,7 @@ * Released under the modified BSD license. See COPYING.md for more details. */ -import styles from "./Header.module.css"; +import styles from "../Common/Header.module.css"; import SiteHeaderButton from "../Common/Buttons/SiteHeaderButton"; import { useRef } from "react"; import FileUploadElement from "../Common/Buttons/FileUploadElement"; diff --git a/web/frontend/src/components/CaseBuilder/Footer.module.css b/web/frontend/src/components/Common/Footer.module.css similarity index 100% rename from web/frontend/src/components/CaseBuilder/Footer.module.css rename to web/frontend/src/components/Common/Footer.module.css diff --git a/web/frontend/src/components/CaseBuilder/Footer.tsx b/web/frontend/src/components/Common/Footer.tsx similarity index 100% rename from web/frontend/src/components/CaseBuilder/Footer.tsx rename to web/frontend/src/components/Common/Footer.tsx diff --git a/web/frontend/src/components/CaseBuilder/Header.module.css b/web/frontend/src/components/Common/Header.module.css similarity index 100% rename from web/frontend/src/components/CaseBuilder/Header.module.css rename to web/frontend/src/components/Common/Header.module.css diff --git a/web/frontend/src/components/Jobs/Header.tsx b/web/frontend/src/components/Jobs/Header.tsx new file mode 100644 index 0000000..729e8aa --- /dev/null +++ b/web/frontend/src/components/Jobs/Header.tsx @@ -0,0 +1,20 @@ +/* + * UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment + * Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved. + * Released under the modified BSD license. See COPYING.md for more details. + */ + +import styles from "../Common/Header.module.css"; + +function Header() { + return ( +
+
+

UnitCommitment.jl

+

Solver

+
+
+ ); +} + +export default Header; diff --git a/web/frontend/src/components/Jobs/Jobs.module.css b/web/frontend/src/components/Jobs/Jobs.module.css new file mode 100644 index 0000000..fbf1ae4 --- /dev/null +++ b/web/frontend/src/components/Jobs/Jobs.module.css @@ -0,0 +1,19 @@ +/* + * UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment + * Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved. + * Released under the modified BSD license. See COPYING.md for more details. + */ + +.SolverLog { + white-space: preserve; + font-family: monospace; + padding: 12px; + background-color: var(--contrast-0); + color: var(--contrast-100); + border: var(--box-border); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + height: 40em; + overflow: auto; + scrollbar-width: none; +} \ No newline at end of file diff --git a/web/frontend/src/components/Jobs/Jobs.tsx b/web/frontend/src/components/Jobs/Jobs.tsx new file mode 100644 index 0000000..4d5b9d2 --- /dev/null +++ b/web/frontend/src/components/Jobs/Jobs.tsx @@ -0,0 +1,93 @@ +/* + * UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment + * Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved. + * Released under the modified BSD license. See COPYING.md for more details. + */ + +import { useParams } from "react-router"; +import { useEffect, useRef, useState } from "react"; +import Header from "./Header"; +import Footer from "../Common/Footer"; +import SectionHeader from "../Common/SectionHeader/SectionHeader"; +import styles from "./Jobs.module.css"; +import formStyles from "../Common/Forms/Form.module.css"; + +interface JobData { + log: string; + solution: any; +} + +const Jobs = () => { + const { jobId } = useParams(); + const [jobData, setJobData] = useState(null); + const logRef = useRef(null); + const previousLogRef = useRef(""); + const intervalRef = useRef(null); + + useEffect(() => { + const fetchJobData = async () => { + const backendUrl = process.env.REACT_APP_BACKEND_URL; + const response = await fetch(`${backendUrl}/jobs/${jobId}/view`); + if (!response.ok) { + console.error(response); + return; + } + const data = await response.json(); + + if (data.solution) { + // Stop polling if solution exists + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + // Parse solution + data.solution = JSON.parse(data.solution); + } + + // Update data + setJobData(data); + console.log(data); + }; + + // Fetch immediately + fetchJobData(); + + // Set up polling every second + intervalRef.current = setInterval(fetchJobData, 1000); + + // Cleanup interval on unmount + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [jobId]); + + // Auto-scroll to the bottom when log content changes + useEffect(() => { + if (jobData?.log && jobData.log !== previousLogRef.current) { + previousLogRef.current = jobData.log; + if (logRef.current) { + logRef.current.scrollTop = logRef.current.scrollHeight; + } + } + }, [jobData?.log]); + + return ( +
+
+
+ +
+
+ {jobData ? jobData.log : "Loading..."} +
+
+
+
+
+ ); +}; + +export default Jobs; diff --git a/web/frontend/src/index.tsx b/web/frontend/src/index.tsx index 66e22b4..632729e 100644 --- a/web/frontend/src/index.tsx +++ b/web/frontend/src/index.tsx @@ -9,6 +9,7 @@ import ReactDOM from "react-dom/client"; import reportWebVitals from "./reportWebVitals"; import CaseBuilder from "./components/CaseBuilder/CaseBuilder"; import { BrowserRouter, Navigate, Route, Routes } from "react-router"; +import Jobs from "./components/Jobs/Jobs"; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement, @@ -19,6 +20,7 @@ root.render( } /> + } /> } />