diff options
author | Ernst Widerberg <ernstwi@kth.se> | 2021-10-06 16:11:06 +0200 |
---|---|---|
committer | Ernst Widerberg <ernstwi@kth.se> | 2021-10-06 16:11:06 +0200 |
commit | 46b9df3279f51479cfc607cbce8fb8b73bef69f7 (patch) | |
tree | ddca9489ce2779c5c7c23938cb5e666387ace775 /src |
Initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/components/App.js | 83 | ||||
-rw-r--r-- | src/components/Error.js | 33 | ||||
-rw-r--r-- | src/components/Header.js | 32 | ||||
-rw-r--r-- | src/components/List.js | 73 | ||||
-rw-r--r-- | src/components/Login.js | 96 | ||||
-rw-r--r-- | src/components/ObjectComponent.js | 69 | ||||
-rw-r--r-- | src/components/ObjectView.js | 46 | ||||
-rw-r--r-- | src/components/SearchForm.js | 72 | ||||
-rw-r--r-- | src/index.html | 17 | ||||
-rw-r--r-- | src/index.js | 6 | ||||
-rw-r--r-- | src/styles/main.css | 133 |
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; +} |