web: display toast, maintain table stage, localStorage

web
Alinson S. Xavier 4 months ago
parent 0cf93e7aa0
commit ee7a948a78

@ -6,7 +6,7 @@
import Papa from "papaparse";
import { Buses, UnitCommitmentScenario } from "../../core/fixtures";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import {
CellComponent,
ColumnDefinition,
@ -154,21 +154,21 @@ interface BusesTableProps {
function computeBusesTableHeight(scenario: UnitCommitmentScenario): string {
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`;
}
function BusesTable(props: BusesTableProps) {
const tableContainerRef = useRef<HTMLDivElement | null>(null);
const tableRef = useRef<Tabulator | null>(null);
const [isTableBuilt, setTableBuilt] = useState<Boolean>(false);
useEffect(() => {
const scenario = props.scenario;
const onCellEdited = (cell: CellComponent) => {
let newValue = cell.getValue();
let oldValue = cell.getOldValue();
// eslint-disable-next-line eqeqeq
if (newValue == oldValue) return;
if (cell.getField() === "Name") {
if (newValue === "") {
props.onBusDeleted(oldValue);
@ -188,18 +188,54 @@ function BusesTable(props: BusesTableProps) {
}
}
};
if (tableContainerRef.current === null) return;
const table = new Tabulator(tableContainerRef.current, {
layout: "fitColumns",
data: generateBusesTableData(scenario),
columns: generateBusesTableColumns(scenario),
height: computeBusesTableHeight(scenario),
});
table.on("cellEdited", (cell) => {
onCellEdited(cell);
});
}, [props]);
if (tableRef.current === null) {
tableRef.current = new Tabulator(tableContainerRef.current, {
layout: "fitColumns",
data: generateBusesTableData(props.scenario),
columns: generateBusesTableColumns(props.scenario),
height: computeBusesTableHeight(props.scenario),
});
tableRef.current.on("tableBuilt", () => {
setTableBuilt(true);
});
}
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} />;
}

@ -26,16 +26,27 @@ import {
renameBus,
} from "../../core/Operations/busOperations";
import {
changeParameter,
changeTimeHorizon,
changeTimeStep,
} from "../../core/Operations/parameterOperations";
import { preprocess } from "../../core/Operations/preprocessing";
import Toast from "../Common/Forms/Toast";
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 = () => {
setScenario(BLANK_SCENARIO);
setAndSaveScenario(BLANK_SCENARIO);
};
const onSave = () => {
@ -48,7 +59,7 @@ const CaseBuilder = () => {
const onBusCreated = () => {
const newScenario = createBus(scenario);
setScenario(newScenario);
setAndSaveScenario(newScenario);
};
const onBusDataChanged = (
@ -58,16 +69,16 @@ const CaseBuilder = () => {
): ValidationError | null => {
const [newScenario, err] = changeBusData(bus, field, newValue, scenario);
if (err) {
console.log(err);
setToastMessage(err.message);
return err;
}
setScenario(newScenario);
setAndSaveScenario(newScenario);
return null;
};
const onBusDeleted = (bus: string) => {
const newScenario = deleteBus(bus, scenario);
setScenario(newScenario);
setAndSaveScenario(newScenario);
};
const onBusRenamed = (
@ -76,15 +87,15 @@ const CaseBuilder = () => {
): ValidationError | null => {
const [newScenario, err] = renameBus(oldName, newName, scenario);
if (err) {
console.log(err);
setToastMessage(err.message);
return err;
}
setScenario(newScenario);
setAndSaveScenario(newScenario);
return null;
};
const onDataChanged = (newScenario: UnitCommitmentScenario) => {
setScenario(newScenario);
setAndSaveScenario(newScenario);
};
const onLoad = (scenario: UnitCommitmentScenario) => {
@ -94,32 +105,29 @@ const CaseBuilder = () => {
// Validate and assign default values
if (!validate(preprocessed)) {
setToastMessage("Error loading JSON file");
console.error(validate.errors);
return;
}
setScenario(preprocessed);
setAndSaveScenario(preprocessed);
setToastMessage("Data loaded successfully");
};
const onParameterChanged = (key: string, value: string) => {
let newScenario, err;
if (key === "Time horizon (h)") {
const [newScenario, err] = changeTimeHorizon(scenario, value);
if (err) {
return err;
}
setScenario(newScenario);
return null;
[newScenario, err] = changeTimeHorizon(scenario, value);
} else if (key === "Time step (min)") {
[newScenario, err] = changeTimeStep(scenario, value);
} else {
[newScenario, err] = changeParameter(scenario, key, value);
}
if (key === "Time step (min)") {
const [newScenario, err] = changeTimeStep(scenario, value);
if (err) {
return err;
}
setScenario(newScenario);
return null;
if (err) {
setToastMessage(err.message);
return err;
}
console.log("onParameterChanged", key, value);
setAndSaveScenario(newScenario);
return null;
};
@ -139,6 +147,7 @@ const CaseBuilder = () => {
onBusDeleted={onBusDeleted}
onDataChanged={onDataChanged}
/>
<Toast message={toastMessage} />
</div>
<Footer />
</div>

@ -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;
}

@ -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,
];
};
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,
];
};

Loading…
Cancel
Save