summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorErnst Widerberg <ernstwi@kth.se>2021-10-06 16:11:06 +0200
committerErnst Widerberg <ernstwi@kth.se>2021-10-06 16:11:06 +0200
commit46b9df3279f51479cfc607cbce8fb8b73bef69f7 (patch)
treeddca9489ce2779c5c7c23938cb5e666387ace775 /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/components/App.js83
-rw-r--r--src/components/Error.js33
-rw-r--r--src/components/Header.js32
-rw-r--r--src/components/List.js73
-rw-r--r--src/components/Login.js96
-rw-r--r--src/components/ObjectComponent.js69
-rw-r--r--src/components/ObjectView.js46
-rw-r--r--src/components/SearchForm.js72
-rw-r--r--src/index.html17
-rw-r--r--src/index.js6
-rw-r--r--src/styles/main.css133
11 files changed, 660 insertions, 0 deletions
diff --git a/src/components/App.js b/src/components/App.js
new file mode 100644
index 0000000..3a1d65d
--- /dev/null
+++ b/src/components/App.js
@@ -0,0 +1,83 @@
+import React from "react";
+import {
+ BrowserRouter as Router,
+ Switch,
+ Route,
+ Link,
+ useParams
+} from "react-router-dom";
+import { Button } from "semantic-ui-react";
+
+import Error from "./Error";
+import Header from "./Header";
+import List from "./List";
+import Login from "./Login";
+import ObjectView from "./ObjectView";
+
+import "../styles/main.css";
+
+class App extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ token: localStorage.getItem("token"),
+ error: null
+ };
+
+ this.clearError = this.clearError.bind(this);
+ this.clearToken = this.clearToken.bind(this);
+ this.setError = this.setError.bind(this);
+ this.setToken = this.setToken.bind(this);
+ }
+
+ setToken(token) {
+ this.setState({ token: token });
+ localStorage.setItem("token", token);
+ }
+
+ clearToken() {
+ this.setState({ token: null });
+ localStorage.removeItem("token");
+ }
+
+ setError(msg) {
+ this.setState({ error: msg });
+ }
+
+ clearError() {
+ this.setState({ error: null });
+ }
+
+ render() {
+ if (this.state.error !== null)
+ return (
+ <Error
+ error={this.state.error}
+ clearError={this.clearError}
+ clearToken={this.clearToken}
+ />
+ );
+ // if (this.state.token === null)
+ // return <Login setToken={this.setToken} setError={this.setError} />;
+ return (
+ <Router>
+ <Header clearToken={this.clearToken} />
+ <Switch>
+ <Route path="/:id">
+ <MakeObjectView />
+ </Route>
+ <Route path="/">
+ <List setError={this.setError} />
+ </Route>
+ </Switch>
+ </Router>
+ );
+ }
+}
+
+function MakeObjectView() {
+ let { id } = useParams();
+ return <ObjectView id={id} setError={this.setError} />;
+}
+
+export default App;
diff --git a/src/components/Error.js b/src/components/Error.js
new file mode 100644
index 0000000..56cc09c
--- /dev/null
+++ b/src/components/Error.js
@@ -0,0 +1,33 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Button, Message } from "semantic-ui-react";
+
+class Error extends React.Component {
+ static propTypes = {
+ clearError: PropTypes.func.isRequired,
+ clearToken: PropTypes.func.isRequired,
+ error: PropTypes.string.isRequired
+ };
+
+ render() {
+ return (
+ <div id="error-container">
+ <Message negative>
+ <Message.Header>Internal server error</Message.Header>
+ <p>{this.props.error}</p>
+ <Button
+ color="red"
+ onClick={() => {
+ this.props.clearToken();
+ this.props.clearError();
+ }}
+ >
+ Sign out
+ </Button>
+ </Message>
+ </div>
+ );
+ }
+}
+
+export default Error;
diff --git a/src/components/Header.js b/src/components/Header.js
new file mode 100644
index 0000000..72234b6
--- /dev/null
+++ b/src/components/Header.js
@@ -0,0 +1,32 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Button } from "semantic-ui-react";
+import { Link } from "react-router-dom";
+
+class Header extends React.Component {
+ static propTypes = {
+ clearToken: PropTypes.func.isRequired
+ };
+
+ render() {
+ return (
+ <div id="header">
+ <ul>
+ <li>
+ <Link to="/">Home</Link>
+ </li>
+ </ul>
+ <hr />
+ <ul>
+ <li>
+ <Link to="/" onClick={this.props.clearToken}>
+ Sign out
+ </Link>
+ </li>
+ </ul>
+ </div>
+ );
+ }
+}
+
+export default Header;
diff --git a/src/components/List.js b/src/components/List.js
new file mode 100644
index 0000000..5f12aa0
--- /dev/null
+++ b/src/components/List.js
@@ -0,0 +1,73 @@
+import React from "react";
+import { Button } from "semantic-ui-react";
+
+import ObjectComponent from "./ObjectComponent";
+import SearchForm from "./SearchForm";
+
+class List extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ objects: [],
+ filter: {
+ field: "default-field",
+ value: ""
+ }
+ };
+
+ this.getData = this.getData.bind(this);
+ }
+
+ componentDidMount() {
+ this.getData();
+ }
+
+ // Fetch data from external source, update state
+ getData() {
+ fetch("http://localhost:8000/sc/v0/get", {
+ headers: {
+ Authorization: "Basic " + btoa("user1:pw1")
+ }
+ })
+ .then(resp => resp.json())
+ .then(data => this.setState({ objects: data }));
+ }
+
+ filter(field, value) {
+ this.setState(
+ {
+ filter: {
+ field: field,
+ value: value
+ }
+ },
+ this.getData
+ );
+ }
+
+ render() {
+ return (
+ <div id="list-container">
+ <div id="controls">
+ <div id="action">
+ <Button.Group>
+ <Button>Action 1</Button>
+ <Button>Action 2</Button>
+ </Button.Group>
+ </div>
+ <div id="search">
+ <SearchForm filter={this.filter} />
+ </div>
+ </div>
+ <div id="main">
+ {this.state.objects.map(data => {
+ console.log(data);
+ return <ObjectComponent {...data} key={data._id} />;
+ })}
+ </div>
+ </div>
+ );
+ }
+}
+
+export default List;
diff --git a/src/components/Login.js b/src/components/Login.js
new file mode 100644
index 0000000..7555dcf
--- /dev/null
+++ b/src/components/Login.js
@@ -0,0 +1,96 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Button, Input } from "semantic-ui-react";
+
+class Login extends React.Component {
+ static propTypes = {
+ setToken: PropTypes.func.isRequired,
+ setError: PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ email: "",
+ password: "",
+ error: false
+ };
+
+ this.handleInput = this.handleInput.bind(this);
+ this.login = this.login.bind(this);
+ this.logout = this.logout.bind(this);
+ }
+
+ // NOTE: btoa() limits email, password to ASCII
+ login(email, password) {
+ const url = process.env.JWT_URL + "/api/v1.0/auth";
+ fetch(url, {
+ method: "POST",
+ headers: { Authorization: "Basic " + btoa(email + ":" + password) }
+ })
+ .then(resp => {
+ if (resp.status !== 200) throw resp;
+ return resp;
+ })
+ .then(resp => resp.json())
+ .then(data => {
+ this.props.setToken(data.access_token);
+ })
+ .catch(resp => {
+ if (resp.status === 401) this.setState({ error: true });
+ else
+ this.props.setError(
+ `Unexpected response status: ${resp.status} ${resp.statusText}`
+ );
+ });
+ }
+
+ logout() {
+ localStorage.removeItem("token");
+ }
+
+ handleInput(e) {
+ this.setState({
+ [e.target.name]: e.target.value
+ });
+ }
+
+ render() {
+ return (
+ <div id="login-container">
+ <form
+ id="login"
+ onSubmit={e => {
+ e.preventDefault();
+ this.login(this.state.email, this.state.password);
+ }}
+ >
+ <h1></h1>
+ <Input
+ type="text"
+ name="email"
+ placeholder="Username..."
+ onChange={this.handleInput}
+ required
+ />
+ <Input
+ type="password"
+ placeholder="Password..."
+ name="password"
+ onChange={this.handleInput}
+ required
+ />
+ <Button color="green" className="submit" type="submit">
+ Sign in
+ </Button>
+ {this.state.error && (
+ <p className="error">Wrong username or password</p>
+ )}
+ </form>
+ </div>
+ );
+ }
+}
+
+export default Login;
diff --git a/src/components/ObjectComponent.js b/src/components/ObjectComponent.js
new file mode 100644
index 0000000..56bdc7c
--- /dev/null
+++ b/src/components/ObjectComponent.js
@@ -0,0 +1,69 @@
+import React from "react";
+
+class ObjectComponent extends React.Component {
+ render() {
+ console.log(this.props);
+ let { user_presentation, ...rest } = this.props;
+ return (
+ <div className="object">
+ <h1>
+ <a href={`/${this.props._id}`}>Scan {this.props._id}</a>
+ </h1>
+ <GenericTable data={rest} />
+ <UserPresentation
+ description={user_presentation.description}
+ data={user_presentation.data}
+ />
+ </div>
+ );
+ }
+}
+
+function GenericTable(props) {
+ return (
+ <table>
+ <tbody>
+ {Object.entries(props.data).map(([key, value]) => {
+ return (
+ <tr key={key}>
+ <td>{key}</td>
+ <td>{value}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ );
+}
+
+function UserPresentation(props) {
+ return (
+ <div className="user-presentation">
+ <div className="header">Scanner-unique data</div>
+ {props.description && (
+ <div className="description">{props.description}</div>
+ )}
+ <table>
+ <tbody>
+ {Object.entries(props.data).map(
+ ([key, { data, display_name, description }]) => {
+ return (
+ <tr key={key}>
+ <td>{display_name}</td>
+ {description && (
+ <td className="description">
+ {description}
+ </td>
+ )}
+ <td>{data.toString()}</td>
+ </tr>
+ );
+ }
+ )}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+export default ObjectComponent;
diff --git a/src/components/ObjectView.js b/src/components/ObjectView.js
new file mode 100644
index 0000000..4a04c93
--- /dev/null
+++ b/src/components/ObjectView.js
@@ -0,0 +1,46 @@
+import React from "react";
+
+import ObjectComponent from "./ObjectComponent";
+
+class ObjectView extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ object: null
+ };
+
+ this.getData = this.getData.bind(this);
+ }
+
+ componentDidMount() {
+ this.getData();
+ }
+
+ getData() {
+ fetch("http://localhost:8000/sc/v0/get", {
+ headers: {
+ Authorization: "Basic " + btoa("user1:pw1")
+ }
+ })
+ .then(resp => resp.json())
+ // TODO: Proper API call to get single object
+ .then(data => data.filter(x => x._id == this.props.id)[0])
+ // .then(data => {
+ // console.log(data);
+ // return data;
+ // })
+ .then(object => this.setState({ object: object }));
+ }
+
+ render() {
+ return (
+ <div id="object-view">
+ {this.state.object === null ? null : (
+ <ObjectComponent {...this.state.object} />
+ )}
+ </div>
+ );
+ }
+}
+
+export default ObjectView;
diff --git a/src/components/SearchForm.js b/src/components/SearchForm.js
new file mode 100644
index 0000000..0dc288c
--- /dev/null
+++ b/src/components/SearchForm.js
@@ -0,0 +1,72 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Button, Select, Input, Icon } from "semantic-ui-react";
+
+class SearchForm extends React.Component {
+ static propTypes = {
+ filter: PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ field: "default-field",
+ value: ""
+ };
+
+ this.clearSearch = this.clearSearch.bind(this);
+ this.handleInput = this.handleInput.bind(this);
+ this.submitSearch = this.submitSearch.bind(this);
+ }
+
+ handleInput(e) {
+ this.setState({
+ [e.target.name]: e.target.value
+ });
+ }
+
+ clearSearch(_) {
+ this.setState({ value: "" });
+ this.props.filter(null, null);
+ }
+
+ submitSearch(e) {
+ e.preventDefault();
+ this.props.filter(this.state.field, this.state.value);
+ }
+
+ render() {
+ const searchOptions = [
+ {
+ key: "default-field",
+ value: "default-field",
+ text: "Default field"
+ }
+ ];
+ return (
+ <form onSubmit={this.submitSearch}>
+ <Input
+ action
+ type="text"
+ name="value"
+ placeholder="Search..."
+ iconPosition="left"
+ onChange={this.handleInput}
+ value={this.state.value}
+ >
+ <input />
+ <Icon name="delete" link onClick={this.clearSearch} />
+ <Select
+ name="field"
+ options={searchOptions}
+ defaultValue="default-field"
+ onChange={this.handleInput}
+ />
+ <Button type="submit">Search</Button>
+ </Input>
+ </form>
+ );
+ }
+}
+
+export default SearchForm;
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..fd42814
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <link
+ rel="stylesheet"
+ href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css"
+ />
+ <script src="http://localhost:8097"></script>
+ <title></title>
+ </head>
+ <body>
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root"></div>
+ <script type="module" src="./index.js"></script>
+ </body>
+</html>
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..83ae8e3
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,6 @@
+import React from "react";
+import { render } from "react-dom";
+
+import App from "./components/App";
+
+render(<App />, document.getElementById("root"));
diff --git a/src/styles/main.css b/src/styles/main.css
new file mode 100644
index 0000000..4e78591
--- /dev/null
+++ b/src/styles/main.css
@@ -0,0 +1,133 @@
+body {
+ background: #fff;
+ font-size: 14px;
+ color: #003049;
+ line-height: 1.2;
+}
+
+#root {
+ display: flex;
+}
+
+#list-container,
+#object-view {
+ width: 100%;
+}
+
+/* Header */
+
+#header {
+ margin: 0;
+ border-right: 1px solid black;
+ width: 10em;
+}
+
+#header li {
+ list-style-type: none;
+}
+
+#header ul {
+ padding-left: 2em;
+}
+
+#header hr {
+ width: 70%;
+}
+
+/* Object */
+.object {
+ border: 1px solid black;
+ border-radius: 25px;
+ padding: 2em;
+ margin: 1em;
+ width: 40em;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.object.good {
+ background-color: green;
+}
+
+.object.bad {
+ background-color: red;
+}
+
+.object td {
+ padding-right: 2em;
+}
+
+.object .user-presentation {
+ margin-top: 2em;
+}
+
+.object .user-presentation .header {
+ font-weight: bold;
+ font-size: 1.5em;
+ margin-bottom: 0.5em;
+}
+
+.object .user-presentation .description {
+ font-style: italic;
+ margin-bottom: 1em;
+}
+
+/* List */
+
+#list-container > #controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px;
+}
+
+#list-container > #pagination {
+ display: flex;
+ justify-content: center;
+}
+
+/* Login */
+
+#login-container {
+ display: flex;
+ justify-content: center;
+}
+
+#login {
+ margin-top: 20vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: center;
+}
+
+#login > h1 {
+ font-size: 20px;
+ margin-bottom: 32px;
+}
+
+#login > .ui.input {
+ display: block;
+ margin-bottom: 1em;
+}
+
+#login > .ui.button {
+ margin-bottom: 1em;
+}
+
+.error {
+ color: red;
+ font-style: italic;
+ font-size: 0.9em;
+}
+
+/* Error */
+
+#error-container {
+ display: flex;
+ justify-content: center;
+}
+
+#error-container > .ui.message {
+ margin-top: 20vh;
+}