mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 08:18:51 -06:00
web: display toast, maintain table stage, localStorage
This commit is contained in:
@@ -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>
|
||||
|
||||
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,
|
||||
];
|
||||
};
|
||||
|
||||
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