fix: WebUI files werden niet aan de commit toegevoegd

This commit is contained in:
2024-06-05 17:17:42 +02:00
parent 349cdd8424
commit 56570ee554
7 changed files with 12395 additions and 0 deletions

42
src/Web/package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "roommapper",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@babel/plugin-transform-private-property-in-object": "^7.24.7",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"antd": "^5.18.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"sass": "^1.77.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start src/index.jsx",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

12098
src/Web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { Button, Card } from 'antd';
import { API_ENDPOINT } from '../constants';
// Crude way of making an enum.
const tasks = Object.freeze({
START: 0,
STOP: 1,
MAP: 2,
});
export default class ControlField extends React.Component {
constructor(props) {
super(props);
// Save the state of the "message" field.
this.state = {
result: null
};
}
handleError(error) {
// Upload error messages to the "Output Message" field.
this.setState({ result: error.message });
};
handleClick(task) {
// Parse the pressed button and send the corresponding task to the server.
task = task === tasks.START ? 'start' : task === tasks.STOP ? 'stop' : 'map';
fetch(`${API_ENDPOINT}/roomba/control`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ task: task })
})
.then(response => response.json())
.then(data => this.setState({ result: JSON.stringify(data) }))
.catch(this.handleError);
}
render() {
return (
<div className='control'>
<div className='control-buttons'>
<Button type='primary' onClick={() => this.handleClick(tasks.START)}>Start</Button>
<Button type='primary' onClick={() => this.handleClick(tasks.MAP)}>Map</Button>
<Button type='primary' danger onClick={() => this.handleClick(tasks.STOP)}>Stop</Button>
</div>
<Card
className='output-field'
title="Output Message"
bordered={false}
>
<pre>{this.state.result}</pre>
</Card>
</div>
);
}
}

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { HeatMapOutlined } from '@ant-design/icons';
import { Layout, Menu, theme } from 'antd';
import Controlfield from './controlField';
import MapCanvas from './mapCanvas';
const { Content, Sider } = Layout;
// Sidebar menu items
const menuItems = [
{
key: 'map',
icon: React.createElement(HeatMapOutlined),
label: 'Map',
}
];
// I.v.m. het gebruik van de "antd" UI library moet er gebruik gemaakt worden
// van een "hook", deze react onderdelen kunnen niet gebruikt worden in een
// class en dus is dit het enigste niet-OOP gedeelte van de frontend.
const App = () => {
const { token: { colorBgContainer, borderRadiusLG } } = theme.useToken();
return (
<Layout hasSider>
<Sider
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
}}
theme='dark'
>
<Menu theme="dark" mode="inline" defaultSelectedKeys={['map']} items={menuItems} />
</Sider>
<Layout
theme='dark'
style={{ marginLeft: 200 }}
>
<Content
style={{ margin: '24px 16px 0', overflow: 'initial' }}
>
<div
style={{
padding: 24,
textAlign: 'left',
background: colorBgContainer,
borderRadius: borderRadiusLG,
}}
>
<div style={{ display: 'flex' }}>
<Controlfield />
<div style={{ 'marginLeft': '50px' }}>
<MapCanvas
width={600}
height={600}
onError={MapCanvas.handleError}
className='map-canvas'
/>
</div>
</div>
</div>
</Content>
</Layout>
</Layout>
);
};
export default App;

View File

@@ -0,0 +1,115 @@
import React from 'react';
import { Select, Input, Button, Form } from 'antd';
import { API_ENDPOINT } from '../constants';
export default class MapCanvas extends React.Component {
defaultWidth = 300;
defaultHeight = 300;
constructor(props) {
super(props);
// Due to `draw`'s asynchronous behavior, we need to bind `this` to the
// handler functionin order for it to be able to be used inside of `render`.
this.handleSubmit = this.handleSubmit.bind(this);
// Get a reference to the canvas element so we can draw on it
this.canvasRef = React.createRef();
this.state = {
inputValue: '8a97354d-d4ac-43c6-8b32-528cc24e3ede',
searchOption: 'id',
canvasWidth: props.width || this.defaultWidth,
canvasHeight: props.height || this.defaultHeight,
};
}
async handleSubmit() {
await this.draw();
}
// This is a lifecycle method that is called after the component has been
// rendered to the DOM. This is where we will draw the initial state
// on the canvas.
componentDidMount() {
this.draw();
}
async getLinePoints(search = 'id', field = '8a97354d-d4ac-43c6-8b32-528cc24e3ede') {
try {
const { searchOption, inputValue } = this.state;
const url = `${API_ENDPOINT}/database?${searchOption}=${inputValue}`;
const data = await (await fetch(url)).json();
// Combine all found sets into a singular array.
const map = [];
if (!Array.isArray(data) || data.length < 1) return map;
data.forEach((set) => set.Objects.forEach((coord) => map.push(coord)));
return map;
} catch (ex) {
this.props.onError(ex);
}
}
async draw() {
// Get the 2D context from the canvas element
const ctx = this.canvasRef.current.getContext('2d');
// Get the points to draw from the server
const points = await this.getLinePoints();
// Set the font for the text we will draw on the canvas
ctx.font = "24px Comic Sans MS";
// Clear the canvas before drawing
ctx.clearRect(0, 0, this.state.canvasWidth, this.state.canvasHeight);
// If there are no points, display a message and return
if (!points || points.length < 1) {
ctx.fillText("No valid entry found.", 10, this.state.canvasHeight / 2);
return;
}
// Loop trough all coordinates, draw a dot at each point to form a top-down
// view of the objects.
points.forEach(point => {
ctx.beginPath();
ctx.arc(point[1], point[0], 1, 0, 2 * Math.PI);
ctx.fill();
});
}
render() {
const { searchOption, inputValue } = this.state;
return (
<div>
<Form onFinish={this.handleSubmit} className='map-choice'>
<Select
className='map-selector'
value={searchOption}
onChange={(val) => this.setState({ searchOption: val })}
>
<Select value="id">ID</Select>
<Select value="version">Version</Select>
<Select value="date">Date</Select>
<Select value="all">All</Select>
</Select>
<Input
type="text"
value={inputValue}
onChange={(evt) => this.setState({ inputValue: evt.target.value })}
disabled={searchOption === 'all'}
/>
<Button type="default" htmlType="submit">Update Map</Button>
</Form>
<div>
<canvas
ref={this.canvasRef}
width={this.state.canvasWidth}
height={this.state.canvasHeight}
{...this.props}
/>
</div>
</div>
);
}
}

View File

@@ -0,0 +1 @@
export const API_ENDPOINT = 'http://127.0.0.1:5000/api/v1';

7
src/Web/src/index.jsx Normal file
View File

@@ -0,0 +1,7 @@
import ReactDOM from 'react-dom/client';
import Layout from './components/layout';
import './scss/index.scss';
ReactDOM
.createRoot(document.getElementById('root'))
.render(<Layout />);