mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 00:08:52 -06:00
web: display toast, maintain table stage, localStorage
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import Papa from "papaparse";
|
import Papa from "papaparse";
|
||||||
import { Buses, UnitCommitmentScenario } from "../../core/fixtures";
|
import { Buses, UnitCommitmentScenario } from "../../core/fixtures";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
CellComponent,
|
CellComponent,
|
||||||
ColumnDefinition,
|
ColumnDefinition,
|
||||||
@@ -154,21 +154,21 @@ interface BusesTableProps {
|
|||||||
|
|
||||||
function computeBusesTableHeight(scenario: UnitCommitmentScenario): string {
|
function computeBusesTableHeight(scenario: UnitCommitmentScenario): string {
|
||||||
const numBuses = Object.keys(scenario.Buses).length;
|
const numBuses = Object.keys(scenario.Buses).length;
|
||||||
const height = 65 + Math.min(numBuses, 15) * 28;
|
const height = 70 + Math.min(numBuses, 15) * 28;
|
||||||
return `${height}px`;
|
return `${height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BusesTable(props: BusesTableProps) {
|
function BusesTable(props: BusesTableProps) {
|
||||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const tableRef = useRef<Tabulator | null>(null);
|
||||||
|
const [isTableBuilt, setTableBuilt] = useState<Boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scenario = props.scenario;
|
|
||||||
const onCellEdited = (cell: CellComponent) => {
|
const onCellEdited = (cell: CellComponent) => {
|
||||||
let newValue = cell.getValue();
|
let newValue = cell.getValue();
|
||||||
let oldValue = cell.getOldValue();
|
let oldValue = cell.getOldValue();
|
||||||
// eslint-disable-next-line eqeqeq
|
// eslint-disable-next-line eqeqeq
|
||||||
if (newValue == oldValue) return;
|
if (newValue == oldValue) return;
|
||||||
|
|
||||||
if (cell.getField() === "Name") {
|
if (cell.getField() === "Name") {
|
||||||
if (newValue === "") {
|
if (newValue === "") {
|
||||||
props.onBusDeleted(oldValue);
|
props.onBusDeleted(oldValue);
|
||||||
@@ -188,18 +188,54 @@ function BusesTable(props: BusesTableProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tableContainerRef.current === null) return;
|
if (tableContainerRef.current === null) return;
|
||||||
const table = new Tabulator(tableContainerRef.current, {
|
if (tableRef.current === null) {
|
||||||
layout: "fitColumns",
|
tableRef.current = new Tabulator(tableContainerRef.current, {
|
||||||
data: generateBusesTableData(scenario),
|
layout: "fitColumns",
|
||||||
columns: generateBusesTableColumns(scenario),
|
data: generateBusesTableData(props.scenario),
|
||||||
height: computeBusesTableHeight(scenario),
|
columns: generateBusesTableColumns(props.scenario),
|
||||||
});
|
height: computeBusesTableHeight(props.scenario),
|
||||||
table.on("cellEdited", (cell) => {
|
});
|
||||||
onCellEdited(cell);
|
tableRef.current.on("tableBuilt", () => {
|
||||||
});
|
setTableBuilt(true);
|
||||||
}, [props]);
|
});
|
||||||
|
}
|
||||||
|
if (isTableBuilt) {
|
||||||
|
const newHeight = computeBusesTableHeight(props.scenario);
|
||||||
|
const newColumns = generateBusesTableColumns(props.scenario);
|
||||||
|
const newData = generateBusesTableData(props.scenario);
|
||||||
|
const oldRows = tableRef.current.getRows();
|
||||||
|
|
||||||
|
// Update data
|
||||||
|
tableRef.current.replaceData(newData).then(() => {});
|
||||||
|
|
||||||
|
// Update columns
|
||||||
|
if (newColumns.length !== tableRef.current.getColumns().length) {
|
||||||
|
tableRef.current.setColumns(newColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update height
|
||||||
|
if (tableRef.current.options.height !== newHeight) {
|
||||||
|
tableRef.current.setHeight(newHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
if (tableRef.current.getRows().length === oldRows.length + 1) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const rows = tableRef.current!.getRows()!;
|
||||||
|
const lastRow = rows[rows.length - 1]!;
|
||||||
|
lastRow.scrollTo().then((r) => {});
|
||||||
|
lastRow.getCell("Name").edit();
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update callbacks
|
||||||
|
tableRef.current.off("cellEdited");
|
||||||
|
tableRef.current.on("cellEdited", (cell) => {
|
||||||
|
onCellEdited(cell);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [props, isTableBuilt]);
|
||||||
|
|
||||||
return <div className="tableContainer" ref={tableContainerRef} />;
|
return <div className="tableContainer" ref={tableContainerRef} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,27 @@ import {
|
|||||||
renameBus,
|
renameBus,
|
||||||
} from "../../core/Operations/busOperations";
|
} from "../../core/Operations/busOperations";
|
||||||
import {
|
import {
|
||||||
|
changeParameter,
|
||||||
changeTimeHorizon,
|
changeTimeHorizon,
|
||||||
changeTimeStep,
|
changeTimeStep,
|
||||||
} from "../../core/Operations/parameterOperations";
|
} from "../../core/Operations/parameterOperations";
|
||||||
import { preprocess } from "../../core/Operations/preprocessing";
|
import { preprocess } from "../../core/Operations/preprocessing";
|
||||||
|
import Toast from "../Common/Forms/Toast";
|
||||||
|
|
||||||
const CaseBuilder = () => {
|
const CaseBuilder = () => {
|
||||||
const [scenario, setScenario] = useState(TEST_SCENARIO);
|
const [scenario, setScenario] = useState(() => {
|
||||||
|
const savedScenario = localStorage.getItem("scenario");
|
||||||
|
return savedScenario ? JSON.parse(savedScenario) : TEST_SCENARIO;
|
||||||
|
});
|
||||||
|
const [toastMessage, setToastMessage] = useState<string>("");
|
||||||
|
|
||||||
|
const setAndSaveScenario = (scenario: UnitCommitmentScenario) => {
|
||||||
|
setScenario(scenario);
|
||||||
|
localStorage.setItem("scenario", JSON.stringify(scenario));
|
||||||
|
};
|
||||||
|
|
||||||
const onClear = () => {
|
const onClear = () => {
|
||||||
setScenario(BLANK_SCENARIO);
|
setAndSaveScenario(BLANK_SCENARIO);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSave = () => {
|
const onSave = () => {
|
||||||
@@ -48,7 +59,7 @@ const CaseBuilder = () => {
|
|||||||
|
|
||||||
const onBusCreated = () => {
|
const onBusCreated = () => {
|
||||||
const newScenario = createBus(scenario);
|
const newScenario = createBus(scenario);
|
||||||
setScenario(newScenario);
|
setAndSaveScenario(newScenario);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBusDataChanged = (
|
const onBusDataChanged = (
|
||||||
@@ -58,16 +69,16 @@ const CaseBuilder = () => {
|
|||||||
): ValidationError | null => {
|
): ValidationError | null => {
|
||||||
const [newScenario, err] = changeBusData(bus, field, newValue, scenario);
|
const [newScenario, err] = changeBusData(bus, field, newValue, scenario);
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err);
|
setToastMessage(err.message);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
setScenario(newScenario);
|
setAndSaveScenario(newScenario);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBusDeleted = (bus: string) => {
|
const onBusDeleted = (bus: string) => {
|
||||||
const newScenario = deleteBus(bus, scenario);
|
const newScenario = deleteBus(bus, scenario);
|
||||||
setScenario(newScenario);
|
setAndSaveScenario(newScenario);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBusRenamed = (
|
const onBusRenamed = (
|
||||||
@@ -76,15 +87,15 @@ const CaseBuilder = () => {
|
|||||||
): ValidationError | null => {
|
): ValidationError | null => {
|
||||||
const [newScenario, err] = renameBus(oldName, newName, scenario);
|
const [newScenario, err] = renameBus(oldName, newName, scenario);
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err);
|
setToastMessage(err.message);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
setScenario(newScenario);
|
setAndSaveScenario(newScenario);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDataChanged = (newScenario: UnitCommitmentScenario) => {
|
const onDataChanged = (newScenario: UnitCommitmentScenario) => {
|
||||||
setScenario(newScenario);
|
setAndSaveScenario(newScenario);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLoad = (scenario: UnitCommitmentScenario) => {
|
const onLoad = (scenario: UnitCommitmentScenario) => {
|
||||||
@@ -94,32 +105,29 @@ const CaseBuilder = () => {
|
|||||||
|
|
||||||
// Validate and assign default values
|
// Validate and assign default values
|
||||||
if (!validate(preprocessed)) {
|
if (!validate(preprocessed)) {
|
||||||
|
setToastMessage("Error loading JSON file");
|
||||||
console.error(validate.errors);
|
console.error(validate.errors);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setScenario(preprocessed);
|
|
||||||
|
setAndSaveScenario(preprocessed);
|
||||||
|
setToastMessage("Data loaded successfully");
|
||||||
};
|
};
|
||||||
|
|
||||||
const onParameterChanged = (key: string, value: string) => {
|
const onParameterChanged = (key: string, value: string) => {
|
||||||
|
let newScenario, err;
|
||||||
if (key === "Time horizon (h)") {
|
if (key === "Time horizon (h)") {
|
||||||
const [newScenario, err] = changeTimeHorizon(scenario, value);
|
[newScenario, err] = changeTimeHorizon(scenario, value);
|
||||||
if (err) {
|
} else if (key === "Time step (min)") {
|
||||||
return err;
|
[newScenario, err] = changeTimeStep(scenario, value);
|
||||||
}
|
} else {
|
||||||
setScenario(newScenario);
|
[newScenario, err] = changeParameter(scenario, key, value);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
if (err) {
|
||||||
if (key === "Time step (min)") {
|
setToastMessage(err.message);
|
||||||
const [newScenario, err] = changeTimeStep(scenario, value);
|
return err;
|
||||||
if (err) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
setScenario(newScenario);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
setAndSaveScenario(newScenario);
|
||||||
console.log("onParameterChanged", key, value);
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,6 +147,7 @@ const CaseBuilder = () => {
|
|||||||
onBusDeleted={onBusDeleted}
|
onBusDeleted={onBusDeleted}
|
||||||
onDataChanged={onDataChanged}
|
onDataChanged={onDataChanged}
|
||||||
/>
|
/>
|
||||||
|
<Toast message={toastMessage} />
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
23
web/src/components/Common/Forms/Toast.module.css
Normal file
23
web/src/components/Common/Forms/Toast.module.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.Toast {
|
||||||
|
width: 600px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 4px 4px 16px -2px rgba(0, 0, 0, 0.5);
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #424242;
|
||||||
|
color: white;
|
||||||
|
padding: 0 16px;
|
||||||
|
position: fixed;
|
||||||
|
top: 48px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 48px;
|
||||||
|
}
|
||||||
35
web/src/components/Common/Forms/Toast.tsx
Normal file
35
web/src/components/Common/Forms/Toast.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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 "./Toast.module.css";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface ToastProps {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Toast = (props: ToastProps) => {
|
||||||
|
const [isVisible, setVisible] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.message.length === 0) return;
|
||||||
|
setVisible(true);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
}, 5000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [props.message]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.Toast} style={{ opacity: isVisible ? 1 : 0 }}>
|
||||||
|
{props.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toast;
|
||||||
@@ -121,3 +121,24 @@ export const changeTimeStep = (
|
|||||||
null,
|
null,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const changeParameter = (
|
||||||
|
scenario: UnitCommitmentScenario,
|
||||||
|
key: string,
|
||||||
|
valueStr: string,
|
||||||
|
): [UnitCommitmentScenario, ValidationError | null] => {
|
||||||
|
const value = parseFloat(valueStr);
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return [scenario, { message: `Invalid value: ${valueStr}` }];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...scenario,
|
||||||
|
Parameters: {
|
||||||
|
...scenario.Parameters,
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user