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(
} />
+ } />
} />