Step 5: Bring components together
Intro
The App.js
file contains the main React component
of the frontend application.
In this step we will fully replace it with new code in one go, and
then we will discuss the implementation details.
New App.js file
Locate the App.js
file and replace its contents with the following:
import { useEffect, useState } from "react";
import axios from "axios";
import { numberWithSpaces } from "./utils";
import ScenarioList from "./components/ScenarioList";
import SimulationControls from "./components/SimulationControls";
import SummaryStatistics from "./components/SummaryStatistics";
import MainPanel from "./components/MainPanel";
import Badge from "react-bootstrap/Badge";
function App() {
(1)
const MAIN_URL = process.env.REACT_APP_API_URL;
const INITIAL_TIME = "";
const [tasks, setTasks] = useState(new Map());
const [gantts, setGantts] = useState([]);
const [modelDateTime, setModelDateTime] = useState(INITIAL_TIME);
const [simulationProgressPct, setSimulationProgressPct] = useState(0);
const [stats, setStats] = useState({
serviceLevelPct: undefined,
expenses: undefined,
});
const [trucks, setTrucks] = useState([]);
const [nodes, setNodes] = useState(new Map());
const [arcs, setArcs] = useState([]);
const [stores, setStores] = useState([]);
const [warehouses, setWarehouses] = useState([]);
const [timeScale, setTimeScale] = useState(1);
const [blocking, setBlocking] = useState();
const [simulationState, setSimulationState] = useState("no_scenario") // "no_scenario", "ready_to_run", "running", "finished"
const [embeddedScenarioNames, setEmbeddedScenarioNames] = useState();
const [currentScenarioName, setCurrentScenarioName] = useState();
const [currentScenarioType, setCurrentScenarioType] = useState(); // "embedded", "custom"
const [errorMsg, setErrorMsg] = useState();
let taskEventStream;
let updateEventStream;
(2)
useEffect(() => {
let loading = true;
if (loading) {
loading = false;
const initScenario = () => {
axios({ url: MAIN_URL + "/scenarios", method: "GET" })
.then((response) => {
setEmbeddedScenarioNames(response.data);
let firstScenarioName = response.data[0];
onEmbeddedScenarioSelected(firstScenarioName);
})
.then(() => {});
};
initScenario();
}
}, []);
(3)
const processUpdateStreamMessage = (event) => {
const jsonData = JSON.parse(event.data);
const progressPct = jsonData.simulationProgressPct;
if (progressPct === 100) {
setSimulationState("finished");
}
setSimulationProgressPct(progressPct)
setModelDateTime(jsonData.modelDateTime);
setStats({
...stats,
serviceLevelPct: jsonData.stats.serviceLevelPct,
expenses: jsonData.stats.expenses,
});
const newTrucks = [];
jsonData.truckStats.forEach((element) => {
const truck = {
id: element.id,
name: element.name,
x: element.currentPositionX,
y: element.currentPositionY,
heading: element.currentHeading,
withCargo: element.withCargo,
expenses: element.expenses,
distanceTraveled: element.distanceTraveled,
};
newTrucks.push(truck);
});
setTrucks(newTrucks);
};
const format = (taskEvent) => {
const data = [
"Truck " + taskEvent.truck,
taskEvent.id,
new Date(parseDateTime(taskEvent.started)),
new Date(parseDateTime(taskEvent.completed)),
];
return data;
};
const parseDateTime = (str) => {
return Date.parse(str);
}
const processTaskStreamMessage = (event) => {
const taskData = JSON.parse(event.data);
setTasks((prevTasks) => new Map(prevTasks).set(taskData.id, taskData));
if (taskData.status.startsWith("COMPLETE")) {
setGantts((prevGantts) => [...prevGantts, format(taskData)])
}
};
(4)
const startSimulation = () => {
if (updateEventStream) {
updateEventStream.close();
}
updateEventStream = new EventSource(MAIN_URL + "/updatestream");
updateEventStream.onmessage = processUpdateStreamMessage;
updateEventStream.onerror = () => {
updateEventStream.close();
};
axios({ url: MAIN_URL + "/start", method: "POST" });
if (taskEventStream) {
taskEventStream.close();
}
taskEventStream = new EventSource(MAIN_URL + "/taskstream");
taskEventStream.onmessage = processTaskStreamMessage;
taskEventStream.onerror = (err) => {
console.error("simulation failed:", err);
taskEventStream.close();
};
setSimulationState("running");
}
const stopSimulation = () => {
axios({ url: MAIN_URL + "/stop", method: "POST" }).then(() => setSimulationState("ready_to_run"));
}
const resumeSimulation = () => {
axios({ url: MAIN_URL + "/resume", method: "POST" }).then(() => setSimulationState("running"));
}
const resetSimulation = () => {
axios({ url: MAIN_URL + "/reset", method: "POST" }).then(() => reset());
}
const simulateSlower = () => {
axios({ url: MAIN_URL + "/slower", method: "POST" }).then((response) => {
if (response.status === 200) {
setTimeScale(response.data.simulationSpeed)
}
});
}
const simulateFaster = () => {
axios({ url: MAIN_URL + "/faster", method: "POST" }).then((response) => {
if (response.status === 200) {
setTimeScale(response.data.simulationSpeed)
}
});
}
(5)
const onScenarioDtoReceivedFromServer = (scenario, scenarioType) => {
reset();
setModelDateTime(scenario.beginDate);
setArcs(scenario.arcs);
let nodesMap = new Map();
scenario.nodes.forEach((node) => nodesMap.set(node.id, node));
setNodes(nodesMap);
setStores(scenario.stores);
setWarehouses(scenario.warehouses);
setBlocking(false);
setErrorMsg();
setCurrentScenarioName(scenario.name);
setCurrentScenarioType(scenarioType);
}
(6)
const onEmbeddedScenarioSelected = (scenarioName) => {
if (scenarioName) {
axios({ url: MAIN_URL + "/init/embedded/" + scenarioName, method: "POST" })
.then((response) => onScenarioDtoReceivedFromServer(response.data, "embedded"))
.catch((response) => {
if (response.data) {
setErrorMsg(response.data.message);
if (response.data.scenarioName === scenarioName) {
setBlocking(true);
}
}
});
setSimulationState("ready_to_run");
}
}
const onCustomScenarioFileSelected = (scenarioFile) => {
let formData = new FormData();
formData.append("file", scenarioFile);
axios.post(MAIN_URL + "/init/custom", formData, {
headers: {
"Content-Type": "multipart/form-data",
}
})
.then((response) => onScenarioDtoReceivedFromServer(response.data, "custom"))
.catch((response) => {
if (response.data) {
setErrorMsg(response.data.message);
setBlocking(true);
}
});
}
(7)
const reset = () => {
setTasks(new Map());
setSimulationProgressPct(0);
setModelDateTime(INITIAL_TIME);
setTimeScale(1);
setStats({
...stats,
serviceLevelPct: undefined,
expenses: undefined,
});
setGantts([]);
setTrucks([]);
setSimulationState("ready_to_run");
};
(8)
return (
<>
<div className="box-scenarios">
<h4>Supply Chain Simulation</h4>
{errorMsg && (
<Badge className="error-message" bg="danger">
{errorMsg}
</Badge>
)}
<ScenarioList currentScenarioName={currentScenarioName}
currentScenarioType={currentScenarioType}
embeddedScenarioNames={embeddedScenarioNames}
onEmbeddedScenarioSelected={onEmbeddedScenarioSelected}
onCustomScenarioFileSelected={onCustomScenarioFileSelected} />
</div>
<div className="box-simulationcontrols">
<SimulationControls modelDateTime={modelDateTime}
simulationProgressPct={simulationProgressPct}
timeScale={timeScale}
simulationState={simulationState}
onStart={startSimulation}
onStop={stopSimulation}
onResume={resumeSimulation}
onReset={resetSimulation}
onSlower={simulateSlower}
onFaster={simulateFaster}
blocking={blocking} />
</div>
<div className="box-statistics">
<SummaryStatistics stats={stats}
numberWithSpaces={numberWithSpaces} />
</div>
<div className="box-mainpanel">
<MainPanel
arcs={arcs}
nodes={nodes}
stores={stores}
warehouses={warehouses}
trucks={trucks}
gantts={gantts}
tasks={[...tasks.values()]}
/>
</div>
</>
);
}
export default App;
1 | First, we declare some constants and state variables. When a state variable is updated, components that use it also get the updated value and can handle it (update a label, disable a button, update table contents, etc.). |
2 | This React hook is used to get embedded scenarios from the backend and show them in the UI once the frontend application gets loaded. |
3 | In the processUpdateStreamMessage method we handle the regular updates
that come from the backend about the ongoing simulation.
Several state variables get updated here, other components see these changes
and update their visualization (for instance, trucks are moving in the Animation component).
The processTaskStreamMessage method is
used to handle task-related events (when a task is created or its status is changed). |
4 | The startSimulation method calls the /start endpoint and also
subscribes by two kinds of updates: the /updatestream with regular updates
and the /taskstream with task-related updates.
EventSource used here provides subscribing to the event stream from the server.
Having subscribed once, we expect new data from the server.
As soon as the server has new data for the stream,
it will send them without any additional request.
That is, an asynchronous data stream will be built.
Other functions handle the respective user commands by sending server requests. |
5 | The onScenarioDtoReceivedFromServer function is called whenever
the server sends a scenario JSON in response to the /init/embedded or /init/custom
API call.
Here, the frontend application gets ready for a server-side simulation
by the received scenario. |
6 | The onEmbeddedScenarioSelected function is executed when the user
selects an embedded scenario in the UI. The /init/embedded endpoint get called here.
The onCustomScenarioFileSelected does the same for the custom scenario (/init/custom endpoint is called). |
7 | The reset function is used in the onScenarioDtoReceivedFromServer function
to clear state variables. |
8 | Here we combine our custom components with 3rd-party and embedded ones to create the final UI of our application UI |
Check the result
Make sure the backend application is started and is working (see Step 1). It should be available at localhost:8080.
Start the backend application.
If the frontend application is still running, go to its console window and stop it using the Ctrl+C
command.
Then, restart the frontend application:
npm start
Go to the application page in the browser (http://localhost:3000/). If the page has not been updated automatically, update it manually.
You should see the following:

Now we can choose one of the embedded scenarios and run a simulation on our server via a web page. Also, a custom scenario can be used (for example, the one from Part 3, see https://github.com/amalgama-llc/supply-chain-tutorial-part-3/tree/main/scenario):

Congratulations! You have created a web application for supply chain modeling.
Next step is to prepare the application for deploy.