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/components | |
Initial commit
Diffstat (limited to 'src/components')
| -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 |
8 files changed, 504 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; |
