Step 4: Add React components

Intro

A React application is composed of components, each representing some piece of functionality. React components can contain other components. In fact, the whole application is also represented by a high-level component, which in our case is defined in the App.js file (this file will be updated in the next step).

Now let’s create components that will display more detailed data about the simulation including animation.

The source code for all components created in this step is stored in *.js files inside the components folder.

Sketching the UI

Before we move on, let’s think about how our web application should present its data to the user, what parts there will be in its UI.

If we take the simulation mode in the desktop application as the role model, here is the sketch of our future web UI:

A sketch of the web UI

So, here we have some kind of a control panel on the left, and animation and real-time simulation information on the right.

Control panel (left side)

There are three parts that the control panel can be divided into: scenario list, simulation controls, and summary statistics. We will now create a React component for each part.

ScenarioList component

Create a components folder in the src folder. This is the folder where all custom React component files will be placed.

Inside this new folder, create a new file ScenarioList.js:

ScenarioList.js
import React from "react";
import Form from "react-bootstrap/Form";
import Dropdown from 'react-bootstrap/Dropdown';
import DropdownButton from 'react-bootstrap/DropdownButton';
import Badge from 'react-bootstrap/Badge';

function ScenarioList({currentScenarioName, currentScenarioType, embeddedScenarioNames, onEmbeddedScenarioSelected, onCustomScenarioFileSelected}) {

  return (
    <>
    To set up a simulation scenario:<br></br>
    <DropdownButton id="scenario-list" title="Select an embedded scenario" className="ms-2">
      {embeddedScenarioNames &&
            embeddedScenarioNames.map((scenarioName) => (
              <Dropdown.Item key={scenarioName} onClick={(e) => onEmbeddedScenarioSelected(scenarioName)}>{scenarioName}</Dropdown.Item>
            ))}
    </DropdownButton>

    <Form.Group controlId="formFile" className="mb-2 mt-2 ms-2">
      <Form.Label>or upload a <i>custom</i> scenario:</Form.Label>
      <Form.Control type="file" 
                    onChange={(event) => {
                        if (event.target.files) {
                          let file = event.target.files[0];
                          onCustomScenarioFileSelected(file);
                        }
                      }
                    }/>

    </Form.Group>
    <b className="mt-3">Scenario:</b>
    <br></br>
    <label className="mt-1 ms-1 me-2">{currentScenarioName ? currentScenarioName : "not selected"}</label>
    <Badge bg="secondary">{currentScenarioType || ""}</Badge>
    </>
  );
}

export default ScenarioList;

ScenarioList displays a list of embedded scenario names and fires a message when the user selects a scenario.

Scenario list

SimulationControls component

Create a new file SimulationControls.js:

SimulationControls.js
import React from "react";
import Button from 'react-bootstrap/Button';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import ProgressBar from 'react-bootstrap/ProgressBar';
import { formatDateTime } from "../utils";

const formatTimeScale = (val) => {
  if (val >= 1) {
    return val + " x";
  } else {
    return "1/" + 1 / val + " x";
  }
}

function SimulationControls({modelDateTime, simulationProgressPct, timeScale, simulationState, blocking,
  onStart, onStop, onResume, onReset, onSlower, onFaster}) {

    const stateMapping = {"no_scenario": "Please set a scenario", 
                          "ready_to_run" : "Ready to run",
                          "running" : "Running",
                          "finished": "Finished"};
    return (
    <div>
        <h5>Simulation</h5>

        <ProgressBar animated={simulationProgressPct < 100} now={simulationProgressPct} label={`${simulationProgressPct}%`}></ProgressBar>

        <div className="mb-2 mt-2">
          <label style={{width: "6em"}}>Model time:</label>
          {modelDateTime ? formatDateTime(modelDateTime) : " <press Start>"}
        </div>
        <div className="mb-2 mt-2">
          <label style={{width: "6em"}}>Status:</label>
          {stateMapping[simulationState]}
        </div>
        <div className="mb-2 mt-2">
          <label style={{width: "6em"}}>Speed:</label>
          <label style={{width: "4em", textAlign: "left"}}>{formatTimeScale(timeScale)}</label>
          <ButtonGroup>
            <Button variant="outline-primary" size="sm" onClick={(e) => onSlower()}>
              Slower
            </Button>          
            <Button variant="outline-primary" size="sm" onClick={(e) => onFaster()}>
              Faster
            </Button>
          </ButtonGroup>
        </div>

        <button
          className="large-button deep-blue"
          disabled={simulationProgressPct > 0 || simulationState !== "ready_to_run" || blocking}
          onClick={(e) => onStart()}
        >
          START
        </button>
        <button
          className="large-button deep-blue"
          disabled={simulationProgressPct === 0 || simulationState !== "ready_to_run" || blocking}
          onClick={(e) => onResume()}
        >
          RESUME
        </button>
        <button
          className="large-button deep-blue"
          disabled={simulationState !== "running" || blocking}
          onClick={(e) => onStop()}
        >
          STOP
        </button>
        <button
          className="large-button yellow"
          disabled={simulationState === "no_scenario" || blocking}
          onClick={(e) => onReset()}
        >
          RESET
        </button>
        <br></br>
    </div>
    );
}

export default SimulationControls;

SimulationControls shows current model time and the buttons to control the simulation (start, stop, etc.).

Simulation controls

SummaryStatistics component

Create a new file SummaryStatistics.js:

SummaryStatistics.js
import React from "react";

function SummaryStatistics(props) {
    return (
      <>
        <h5 className="mt-1">Summary statistics</h5>
        <table>
          <thead>
            <tr>
              <th>Indicator name</th>
              <th>Value</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Service level</td>
              <td>{props.stats.serviceLevelPct ? props.stats.serviceLevelPct : 0} %</td>
            </tr>
            <tr>
              <td>Expenses</td>
              <td>
                $ {props.numberWithSpaces((props.stats.expenses||0).toFixed(2))}
              </td>
            </tr>
          </tbody>
        </table>
      </>
    );
}

export default SummaryStatistics;

SummaryStatistics displays the overall simulation statistics, as in the Summary statistics part in the desktop application.

Summary statistics

These three new components will work together like this:

Left panel of the UI

Main panel (on the right)

The right part contains animation and three tab-controlled tables/charts.

Animation component

Create a new Animation.js file:

Animation.js
import React, { Suspense, Fragment } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Plane, Line, Html } from "@react-three/drei";


const Truck = ({truck}) => {
    return (
        <group position={[truck.x, -0.5, truck.y]} rotation-y={truck.heading + Math.PI}>
            <mesh key={"truckhead-" + truck.id} position={[0, 0, 0]}>
                <boxGeometry args={[0.8, 0.7, 0.4]} />
                <meshStandardMaterial color={"green"} />
            </mesh>
            <mesh key={"truckbody-" + truck.id} position={[0, 0, 1.7]}>
                <boxGeometry args={[1, 1, 3]} />
                <meshStandardMaterial color={truck.withCargo ? "royalblue" : "gray"} />
            </mesh>
            <Html center position={[2, 0, 0]}>
              <div style={{ background: truck.withCargo ? "rgba(180,200,250,0.5)" : "rgba(220,220,220,0.5)" , whiteSpace: "nowrap"}}>{truck.name}</div>
            </Html>
        </group>
    )
}


const Store = ({store, nodes}) => {
    return (
        <group key={"store-" + store.name} position={[nodes.get(store.node).x, -0.75, nodes.get(store.node).y]}>
            <mesh   scale={[2, 1, 2]}>
                    <boxGeometry args={[2, 2, 2]} />
                    <meshStandardMaterial color={"red"} />
            </mesh>
            <Html center position={[0, 0, 2.7]}>
              <div style={{ background: "rgba(240,200,200,0.5)", whiteSpace: "nowrap"}}>{store.name}</div>
            </Html>
        </group>
    )
}

const Warehouse = ({warehouse, nodes}) => {
    return (
        <group key={"warehouse-" + warehouse.name} position={[nodes.get(warehouse.node).x, -0.75, nodes.get(warehouse.node).y]}>
            <mesh   scale={[2, 1, 2]}>
                    <boxGeometry args={[2, 2, 2]} />
                    <meshStandardMaterial color={"orange"} />
            </mesh>
            <Html center position={[0, 0, 2.7]}>
              <div style={{ background: "rgba(240,240,190,0.5)", whiteSpace: "nowrap"}}>{warehouse.name}</div>
            </Html>
        </group>
    )
}

const MainScene = ({arcs, nodes, stores, warehouses, trucks}) => {
    return (
        <>
            {arcs.map((arc) => (
                <Line
                    key={"arc-" + arc.source + "-" + arc.dest}
                    points={[
                    [nodes.get(arc.source).x, -0.6, nodes.get(arc.source).y],
                    ...arc.points.map((point) => [point.x, -0.6, point.y]),
                    [nodes.get(arc.dest).x, -0.6, nodes.get(arc.dest).y],
                    ]} />
            ))}
            {stores.map((store) => (
                <Store store={store} nodes={nodes}/>
            ))}
            {warehouses.map((warehouse) => ( 
                <Warehouse warehouse={warehouse} nodes={nodes} />
            ))}
            {trucks.map((truck) => (
                <Truck truck={truck} />
            ))}

            <Plane
                    scale={[2000, 2000, 0]}
                    position={[0, -0.75, 0]}
                    rotation-x={Math.PI * -0.5}
                />

            {/* scene setup */}
            <ambientLight intensity={0.1} />
            <directionalLight intensity={1} color="white" />
            <OrbitControls makeDefault />
        </>
    )
}


function Animation({arcs, nodes, stores, warehouses, trucks}) {
    return (
        <Suspense fallback={null}>
            <Canvas camera={{
                position: [0, 400, 0],
                fov: 45,
                far: 2000,
            }}>
                <MainScene arcs={arcs} nodes={nodes} stores={stores} warehouses={warehouses} trucks={trucks} />
            </Canvas>
        </Suspense>
    );
}

export default Animation;

The Animation component shows road graph, assets, and trucks in real time.

Animation

TruckStatistics component

Create a new TruckStatistics.js file:

TruckStatistics.js
import React from "react";
import { numberWithSpaces } from "../utils";

function TruckStatistics({trucks}) {
    return (
        <table>
            <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Expenses</th>
                <th>Distance traveled, km</th>
            </tr>
            </thead>
            {trucks && (
            <tbody>
                {trucks.map((truck) => (
                <tr key={truck.id}>
                    <td>{truck.id}</td>
                    <td>{truck.name}</td>
                    <td>$ {numberWithSpaces(truck.expenses.toFixed(2))}</td>
                    <td>{truck.distanceTraveled.toFixed(2)}</td>
                </tr>
                ))}
            </tbody>
            )}
        </table>
    );
}

export default TruckStatistics;

TruckStatistics shows a table with one line per each truck that looks similar to the 'Trucks' table in the desktop application.

Trucks table

GanttChart component

Create a new GanttChart.js file:

GanttChart.js
import React from "react";
import { Chart } from "react-google-charts";

function GanttChart({gantts}) {

  const columns = [
      { type: "string", id: "Truck" },
      { type: "string", id: "Name" },
      { type: "datetime", id: "Start" },
      { type: "datetime", id: "End" },
  ];
    
  const data = [columns, ...gantts];
  const options = {
    enableInteractivity: false,
    tooltip: { trigger: "none" },
    width: "100%",
    height: 400,
    groupByRowLabel: false,
    timeline: {
      colorByRowLabel: true,
      singleColor: "0065ad",
    },
  };

  return (
        <>
        {gantts.length === 0 
          ? "Will be displayed when some transportation task is finished." 
          : (<Chart chartType="Timeline" data={data} options={options} />)}
        </>
  );
}

export default GanttChart;

GanttChart displays a chart for each completed task (like the 'Gantt Chart' part in the desktop application).

Gantt chart

TaskStatistics component

Create a new TaskStatistics.js file:

TaskStatistics.js
import React from "react";
import { formatDateTime } from "../utils";

function TaskStatistics({tasks}) {
    return (
        <table>
            <thead>
            <tr>
                <th>Id</th>
                <th>Truck</th>
                <th>Source</th>
                <th>Destination</th>
                <th>Created</th>
                <th>Started</th>
                <th>Deadline</th>
                <th>Completed</th>
                <th>Status</th>
            </tr>
            </thead>
            <tbody>
            {tasks &&
                tasks.map((task) => (
                <tr key={task.id}>
                    <td>{task.id}</td>
                    <td>{task.truck}</td>
                    <td>{task.source}</td>
                    <td>{task.destination}</td>
                    <td>{formatDateTime(task.created)}</td>
                    <td>{formatDateTime(task.started)}</td>
                    <td>{formatDateTime(task.deadline)}</td>
                    <td>{formatDateTime(task.completed)}</td>
                    <td>{task.status.replaceAll('_', ' ')}</td>
                </tr>
                ))}
            </tbody>
        </table>
    );
}

export default TaskStatistics;

TaskStatistics lists the tasks, both completed and not. Its contents resembles that of the 'Tasks' table in the desktop application.

List of tasks

MainPanel component

The MainPanel component will group the four previous components, also adding a split between the animation and tables/chart and grouping the tasks, trucks, and gantt chart into a tab container.

Create a new MainPanel.js file:

MainPanel.js
import React from "react";
import { Tab, Row, Col, Nav } from "react-bootstrap";
import Split from 'react-split'
import TaskStatistics from "./TaskStatistics";
import TruckStatistics from "./TruckStatistics";
import Animation from "./Animation";
import GanttChart from "./GanttChart";

function MainPanel({arcs, nodes, warehouses, stores, trucks, tasks, gantts}) {
  return (
    <Split 
      direction="vertical"
      style={{ height: "96vh"}}>
        <div className="animation-panel">
            <Animation  arcs={arcs}
                        nodes={nodes}
                        warehouses={warehouses}
                        stores={stores}
                        trucks={trucks}/>
        </div>
        <div>
        <Tab.Container id="top-tabs" defaultActiveKey="trucks">
          <Row className="gx-0" style={{height: "100%"}}>
            <Col style={{height: "100%"}}>
              <Nav variant="underline" className="flex-row">
                <Nav.Item>
                  <Nav.Link eventKey="trucks">Trucks</Nav.Link>
                </Nav.Item>
                <Nav.Item>
                  <Nav.Link eventKey="gantt">Gantt chart</Nav.Link>
                </Nav.Item>
                <Nav.Item>
                  <Nav.Link eventKey="tasks">Tasks</Nav.Link>
                </Nav.Item>
              </Nav>
              <Tab.Content className="m-1" style={{ overflowY: "scroll", height: `calc(100% - 2rem)`}}>
                <Tab.Pane eventKey="trucks">
                  <TruckStatistics trucks={trucks} /> 
                </Tab.Pane>
                <Tab.Pane eventKey="gantt">
                  <GanttChart gantts={gantts} /> 
                </Tab.Pane>
                <Tab.Pane eventKey="tasks">
                  <TaskStatistics tasks={tasks} />
                </Tab.Pane>
              </Tab.Content>
            </Col>
          </Row>
        </Tab.Container>
        </div>
    </Split>
  );
}

export default MainPanel;

Check the result

We have added several components, but they are not used in the App.js yet.

So, just make sure the application can be started (the npm start command in the console) and it shows the previous UI in the browser without error.