Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dashboard for ArangoLocalStorage operator #213

Merged
merged 14 commits into from
Jul 10, 2018
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ docs
pkg
tools
deps

dashboard
111 changes: 50 additions & 61 deletions dashboard/assets.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"devDependencies": {
"react-scripts": "1.1.4"
},
"proxy": "https://192.168.140.208:8528",
"proxy": "https://192.168.140.211:8528",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
Expand Down
28 changes: 0 additions & 28 deletions dashboard/src/App.css

This file was deleted.

35 changes: 24 additions & 11 deletions dashboard/src/App.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { Component } from 'react';
import ReactTimeout from 'react-timeout';
import DeploymentOperator from './deployment/DeploymentOperator.js';
import StorageOperator from './storage/StorageOperator.js';
import NoOperator from './NoOperator.js';
import Loading from './util/Loading.js';
import api from './api/api.js';
import api, { IsUnauthorized } from './api/api.js';
import { Container, Segment, Message } from 'semantic-ui-react';
import './App.css';
import { withAuth } from './auth/Auth.js';

const PodInfoView = ({pod, namespace}) => (
<Segment basic>
Expand All @@ -16,11 +17,14 @@ const PodInfoView = ({pod, namespace}) => (
</Segment>
);

const OperatorsView = ({error, deployment, pod, namespace}) => {
const OperatorsView = ({error, deployment, storage, pod, namespace}) => {
const podInfoView = (<PodInfoView pod={pod} namespace={namespace}/>);
if (deployment) {
return (<DeploymentOperator podInfoView={podInfoView} error={error}/>);
}
if (storage) {
return (<StorageOperator podInfoView={podInfoView} error={error}/>);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to avoid repeating yourself a bit, you could do something like this:

let Operator = NoOperator;
if (deployment) Operator = DeploymentOperator;
else if (storage) Operator = StorageOperator;
return (
  <Operator
    podInfoView={
      <PodInfoView pod={pod} namespace={namespace} />
    }
    error={error}
  />
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

return (<NoOperator podInfoView={podInfoView} error={error}/>);
}

Expand All @@ -43,24 +47,33 @@ class App extends Component {
reloadOperators = async() => {
try {
const operators = await api.get('/api/operators');
this.setState({operators, error: undefined});
this.setState({
operators,
error: undefined
});
} catch (e) {
this.setState({error: e.message});
this.setState({
error: e.message
});
if (IsUnauthorized(e)) {
this.props.doLogout();
}
}
this.props.setTimeout(this.reloadOperators, 10000);
}

render() {
if (this.state.operators) {
return <OperatorsView
return <OperatorsView
error={this.state.error}
deployment={this.state.operators.deployment}
pod={this.state.operators.pod}
namespace={this.state.operators.namespace}
/>;
deployment={this.state.operators.deployment}
storage={this.state.operators.storage}
pod={this.state.operators.pod}
namespace={this.state.operators.namespace}
/>;
}
return (<LoadingView/>);
}
}

export default ReactTimeout(App);
export default ReactTimeout(withAuth(App));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this is why HOCs have been losing some traction to render props recently: you're now injecting props from two places. This is probably fine for the time being but it can get a bit confusing as the complexity grows.

9 changes: 0 additions & 9 deletions dashboard/src/App.test.js

This file was deleted.

29 changes: 15 additions & 14 deletions dashboard/src/NoOperator.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import { Message } from 'semantic-ui-react';
import { Container, Message, Modal, Segment } from 'semantic-ui-react';

class NoOperator extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to Kube-ArangoDB</h1>
</header>
<p className="App-intro">
There are no operators available yet.
</p>
{this.props.podInfoView}
{(this.props.error) ? <Message error content={this.props.error}/> : null}
</div>
<Container>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This NoOperator component is exactly what I meant when I talked about presentational components btw. It doesn't have any state and it only contains display logic (i.e. "if there's an error message, put it here"). If you want to make it easier to swap out the styling later, you should try to extract all the actual presentation stuff into components like this one.

Minor nitpick: no point in using a class component here, you could just make it a function.

<Modal open>
<Modal.Header>Welcome to Kube-ArangoDB</Modal.Header>
<Modal.Content>
<Segment basic>
<Message color="orange">
There are no operators available yet.
</Message>
</Segment>
{this.props.podInfoView}
{(this.props.error) ? <Message error content={this.props.error}/> : null}
</Modal.Content>
</Modal>
</Container>
);
}
}
Expand Down
17 changes: 13 additions & 4 deletions dashboard/src/api/api.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
export function IsUnauthorized(e) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI in most JS styles only classes and components use leading uppercase. So this should be isUnauthorized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

return (e.status === 401);
}

export default {
token: '',

async decodeResults(result) {
const decoded = await result.json();
if (result.status === 401) {
throw Error(decoded.error || "Unauthorized")
}
if (result.status !== 200) {
throw Error(`Unexpected status ${result.status}`);
let message = decoded.error;
if (!message) {
if (result.status === 401) {
message = "Unauthorized";
} else {
message = `Unexpected status ${result.status}`;
}
}
throw Object.assign(new Error(message), { status: result.status });
}
return decoded;
},
Expand Down
12 changes: 12 additions & 0 deletions dashboard/src/auth/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ import { getSessionItem, setSessionItem } from "../util/Storage.js";

const tokenSessionKey = "auth-token";


// withAuth adds a doLogout property to the given component.
export function withAuth(WrappedComponent) {
return function AuthAwareComponent(props) {
return (
<LogoutContext.Consumer>
{doLogout => <WrappedComponent {...props} doLogout={doLogout} />}
</LogoutContext.Consumer>
);
}
}

class Auth extends Component {
state = {
authenticated: false,
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/auth/Login.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { Button, Container, Form, Icon, Message, Modal } from 'semantic-ui-react';
import { css } from 'react-emotion';

const LoginView = ({username, password, onUsernameChanged, onPasswordChanged, doLogin, error}) => (
<Container>
Expand All @@ -12,6 +13,7 @@ const LoginView = ({username, password, onUsernameChanged, onPasswordChanged, do
<label>Password</label>
<input type="password" value={password} onChange={(e) => onPasswordChanged(e.target.value)}/>
</Form.Field>
<Form.Button className={css`display:none`} type="submit" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason you have a hidden submit button here? Pressing enter in the input field should already trigger the form submit.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also curious why you're using css here but styled elsewhere as most uses of styled can also be replaced with css (and generally vice versa unless you want to apply the same class to different components, in which case you'll always need css).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hidden button was out of need. I expected it to work without, but it did not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like styled better, but that did not work in combination with Form.Button.

</Form>
{(error) ? <Message error content={error}/> : null}
</Container>
Expand Down
15 changes: 11 additions & 4 deletions dashboard/src/deployment/DeploymentDetails.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import ReactTimeout from 'react-timeout';
import React, { Component } from 'react';
import api from '../api/api.js';
import api, { IsUnauthorized } from '../api/api.js';
import Loading from '../util/Loading.js';
import MemberList from './MemberList.js';
import styled from 'react-emotion';
import { Loader } from 'semantic-ui-react';
import { withAuth } from '../auth/Auth.js';

const LoaderBox = styled('span')`
float: right;
Expand Down Expand Up @@ -39,7 +40,9 @@ class DeploymentDetails extends Component {

reloadDeployment = async() => {
try {
this.setState({loading:true});
this.setState({
loading: true
});
const result = await api.get(`/api/deployment/${this.props.name}`);
this.setState({
deployment: result,
Expand All @@ -51,6 +54,10 @@ class DeploymentDetails extends Component {
loading: false,
error: e.message
});
if (IsUnauthorized(e)) {
this.props.doLogout();
return;
}
}
this.props.setTimeout(this.reloadDeployment, 5000);
}
Expand All @@ -65,8 +72,8 @@ class DeploymentDetails extends Component {
<LoaderBox><Loader size="mini" active={this.state.loading} inline/></LoaderBox>
<MemberGroupsView memberGroups={d.member_groups} namespace={d.namespace}/>
</div>
);
);
}
}

export default ReactTimeout(DeploymentDetails);
export default ReactTimeout(withAuth(DeploymentDetails));
9 changes: 7 additions & 2 deletions dashboard/src/deployment/DeploymentList.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Icon, Loader, Popup, Table } from 'semantic-ui-react';
import { Link } from "react-router-dom";
import api from '../api/api.js';
import api, { IsUnauthorized } from '../api/api.js';
import CommandInstruction from '../util/CommandInstruction.js';
import Loading from '../util/Loading.js';
import React, { Component } from 'react';
import ReactTimeout from 'react-timeout';
import styled from 'react-emotion';
import { withAuth } from '../auth/Auth.js';

const LoaderBox = styled('span')`
float: right;
Expand Down Expand Up @@ -172,6 +173,10 @@ class DeploymentList extends Component {
});
} catch (e) {
this.setState({error: e.message, loading: false});
if (IsUnauthorized(e)) {
this.props.doLogout();
return;
}
}
this.props.setTimeout(this.reloadDeployments, 5000);
}
Expand All @@ -188,4 +193,4 @@ class DeploymentList extends Component {
}
}

export default ReactTimeout(DeploymentList);
export default ReactTimeout(withAuth(DeploymentList));
2 changes: 1 addition & 1 deletion dashboard/src/deployment/DeploymentOperator.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const StyledContentBox = styled('div')`
const ListView = () => (
<div>
<Header dividing>
ArangoDeployments
ArangoDeployment resources
</Header>
<DeploymentList/>
</div>
Expand Down
Loading