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

Feature/dashboard #211

Merged
merged 23 commits into from
Jul 9, 2018
Merged

Feature/dashboard #211

merged 23 commits into from
Jul 9, 2018

Conversation

ewoutp
Copy link
Contributor

@ewoutp ewoutp commented Jul 2, 2018

This PR lays the groundwork for a dashboard for all operators in kube-arangodb.

This PR implements some of the functionality of the ArangoDeployment operator dashboard.
More functionality will be added in later PRs.
Other operators (replication & storage) will also be handled in other PRs.

@ghost ghost assigned ewoutp Jul 2, 2018
@ghost ghost added the 2 - Working label Jul 2, 2018
}

render() {
setTimeout(this.reloadOperators.bind(this), 5000);
Copy link

Choose a reason for hiding this comment

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

This should happen in componentDidMount. Having side-effects in a render function is generally a bad idea.

Copy link

Choose a reason for hiding this comment

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

Also I think you want to use setInterval as setTimeout is for one-off delayed code.

Copy link

Choose a reason for hiding this comment

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

Also keep in mind setTimeout/setInterval will still keep firing if the component is unmounted. So you need to clean those up in componentWillUnmount.

}
return (
<Container>
{children}
Copy link

Choose a reason for hiding this comment

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

This could be inlined as {this.state.operators.deployment ? <DeploymentOperator/> : <NoOperator/>}.

<Message>
<Message.Header>Kube-ArangoDB</Message.Header>
<p>
Running in Pod <b>{this.state.operators.pod}</b> in namespace <b>{this.state.operators.namespace}</b>.
Copy link

Choose a reason for hiding this comment

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

These will be undefined until the request in componentDidMount resolves. React will render those variables as empty strings in this case but that'll look a bit ugly.

</Message>
</Segment>
</Container>
);
Copy link

Choose a reason for hiding this comment

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

TBH, I'd probably extract most of the render function into a separate component that's entirely presentational, e.g.

const OperatorsView = ({deployment, pod, namespace}) => (
  <Container>
    {deployment ? <DeploymentOperator /> : <NoOperator />}
    <Segment basic>
      <Message>
        <Message.Header>Kube-ArangoDB</Message.Header>
        <p>Running in Pod <b>{pod}</b> in namespace <b>{namespace}</b>.</p>
      </Message>
    </Segment>
  </Container>
));
class App extends Component {
  state = {}
  componentDidMount () {
    this.reloadOperators();
    this._interval = setInterval(this.reloadOperators, 5000);
  }
  componentWillUnmount () {
    clearInterval(this._interval);
  }
  reloadOperators = async () => {
    const operators = await apiGet('...');
    this.setState({ operators });
  }
  render () {
    if (this.state.operators) {
      return <OperatorsView {...this.state.operators} />;
  }
  return <div>Loading...</div>;
}

Keep in mind this still doesn't account for timeouts or errors in the async stuff.

Copy link

Choose a reason for hiding this comment

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

FYI the {...this.state.operators} part is just me being lazy because I'm coding in the browser. I'd unpack the props explicitly and pass them individually. The reloadOperators = async () => {...} stuff binds the function to the instance so you don't need to bind it explicitly when creating the timeout/interval. This is generally a good practice when dealing with things like event handlers but not entirely necessary here (just easier to avoid mistakes by doing it here too).

@@ -0,0 +1,8 @@
// apiGet performs a GET request on the API with given local URL.
// The result is decoded from JSON and returned.
export async function apiGet(localURL: string): Promise<Any> {
Copy link

Choose a reason for hiding this comment

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

Here you're using TypeScript syntax but the files are JS files.

If you're going to use Create React App you probably want to use it with the TypeScript react-scripts:
https://github.com/Microsoft/TypeScript-React-Starter#create-our-new-project

Copy link

Choose a reason for hiding this comment

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

BTW if you do use this, make sure to simplify the extends list in the tslint.json it generates:

"extends": ["tslint-config-prettier"],

The defaults are overly pedantic and tend to get in the way.

}

render() {
setTimeout(this.reloadDeployments.bind(this), 5000);
Copy link

Choose a reason for hiding this comment

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

Again see above about side-effects.

return (<div>No deployments</div>);
}
return (
<Table striped celled>
Copy link

Choose a reason for hiding this comment

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

Again, I'd like to keep the presentational part in a presentational component to make it easier to swap out later.

return (
<Table.Row>
<Table.Cell><Icon name="bell" color="red"/></Table.Cell>
<Table.Cell>{this.props.info.name}</Table.Cell>
Copy link

Choose a reason for hiding this comment

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

No reason to use a class based component unless you have some kind of state. Also I'd unpack the info properties into props directly.

</Table.Header>
);

const RowView = ({info}) => (
Copy link

Choose a reason for hiding this comment

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

I'd pass in the item's properties individually to make the props more explicit here.

<HeaderView/>
<Table.Body>
{
(items) ? items.map((item) => <RowView key={item.name} info={item}/>) : <p>No items</p>
Copy link

Choose a reason for hiding this comment

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

i.e. here I'd do <RowView key={item.name} name={item.name} mode={item.mode} readyCount={item.ready_pod_count} totalCount={item.pod_count} /> so it's clearer what this takes.

import { Dimmer, Loader, Segment } from 'semantic-ui-react';

class Loading extends Component {
render() {
Copy link

Choose a reason for hiding this comment

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

Again, if this doesn't need state or anything special, just use a function component here. Also helps increasing the visibility of what props it takes.

</Segment>
</Container>
<div>
{deployment ? <DeploymentOperator pod-info={<PodInfoView pod={pod} namespace={namespace}/>}/> : <NoOperator />}
Copy link

Choose a reason for hiding this comment

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

BTW React components normally use camelCase for prop names instead of kebab-case as HTML does.

import Loading from '../util/Loading.js';
//import CommandInstruction from '../util/CommandInstruction.js';

const MemberGroupsView = ({member_groups}) => (
Copy link

Choose a reason for hiding this comment

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

Here too camelCase would be more idiomatic.

<Segment>
<Header>{group}</Header>
<List divided>
{members.map((item) => <MemberView memberInfo={item} active={item.id === activeMemberID} onClick={onClick}/>)}
Copy link

Choose a reason for hiding this comment

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

May want to add key={item.id} to help React diffing renders when the members prop changes.

state = {};

onClick = (id) => {
this.setState({activeMemberID:(this.state.activeMemberID === id) ? null : id});
Copy link

Choose a reason for hiding this comment

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

When using this.state to set state, you should use the functional form of setState:

this.setState(({activeMemberID}) => ({
  activeMemberID: activeMemberID === id ? null : id
}));

Using this.state to derive new state is error-prone as setState is potentially async.

Copy link

Choose a reason for hiding this comment

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

BTW, I'd call the method "handleClick" so you pass onClick={this.handleClick} but I honestly have no idea where that convention comes from.

}
</Table.Body>
</Table>
);

const EmptyView = () => (<div>No deployments</div>);

function createDeleteCommand(name, namespace) {
return `kubectl delete ArangoDeployment -n ${namespace} ${name}`;
Copy link

Choose a reason for hiding this comment

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

If you're not using this in multiple places I'd just inline it.

import {CopyToClipboard} from 'react-copy-to-clipboard';

class CommandInstruction extends Component {
state = {};
Copy link

Choose a reason for hiding this comment

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

In this case I'd initialise state = {open: false} explicitly because having it undefined doesn't really add any meaning distinct from false.

</Dimmer>
<div style={{"min-height":"3em"}}/>
<div style={{minHeight:"3em"}}/>
Copy link

Choose a reason for hiding this comment

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

For consistency you might want to use styled for this one. Remember emotion also provides a css template handler which simply generates a classname if you don't want to keep track of another component just for the CSS class.

import Loading from './util/Loading.js';
import api from './api/api.js';
import { Container, Segment, Message } from 'semantic-ui-react';
import './App.css';
Copy link

Choose a reason for hiding this comment

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

If you're no longer using these styles here, I'd recommend removing this import.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

they are (already) being removed in the next PR

@@ -0,0 +1,24 @@
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
Copy link

Choose a reason for hiding this comment

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

You may want to replace the external stylesheet with a few styled components to get rid of the CSS file. Right now you have three ways to define styling (semantic-ui, plain CSS and emotion) which seems excessive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

plain css will be gone


var api = new Api();

export default api;
Copy link

Choose a reason for hiding this comment

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

Any reason you're using a "singleton class" instead of simply an object? You can use this in plain object methods too.

import LogoutContext from './LogoutContext.js';
import { getSessionItem, setSessionItem } from "../util/Storage.js";

const tokenSessionKey = "auth-token";
Copy link

Choose a reason for hiding this comment

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

Traditionally this would be called TOKEN_SESSION_KEY but that's not a hard rule.

authenticated: true,
showLoading: false,
token: api.token
});
Copy link

Choose a reason for hiding this comment

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

If I understand this right, you're setting the global api instance's token property to the one from state, then doing a call and discarding the result, then setting the state's token to that of the api.

Does the token change during the request? Does the request have side-effects? I'm guessing you only use the request to determine whether you're authenticated and authorized.

Copy link

Choose a reason for hiding this comment

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

I'm not really happy with the Auth component and the LogoutContext but I can't think of an easy way to improve on this either, so lgtm 🤷‍♂️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

correct.

if (this.token) {
headers['Authorization'] = `bearer ${this.token}`;
}
const result = await fetch(localURL, {headers});
Copy link

Choose a reason for hiding this comment

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

Just an FYI because fetch can be a bit too low-level sometimes and have cross-browser surprises: https://yarnpkg.com/en/package/axios

<Form onSubmit={doLogin}>
<Form.Field>
<label>Name</label>
<input focus="true" value={username} onChange={(e) => usernameChanged(e.target.value)}/>
Copy link

Choose a reason for hiding this comment

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

Conventionally this would be called "onUsernameChange" I think. This also helps indicate it's an event handler rather than e.g. a flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

const MemberGroupsView = ({memberGroups, namespace}) => (
<div>
{memberGroups.map((item) => <MemberList
key={`server-group-${item.group}`}
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're prefixing the keys? They're local to their parent so this doesn't seem to do much.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed


class DeploymentList extends Component {
state = {
items: undefined,
Copy link

Choose a reason for hiding this comment

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

If you need an explicit "this is a thing but it's not set yet" value, null is generally preferable to undefined as it's difficult to distinguish values that exist but are undefined form values that are undefined because they don't exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

</Dimmer>
<div style={{minHeight:"3em"}}/>
</Segment>
);
Copy link

Choose a reason for hiding this comment

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

BTW the indenting is a bit messy. Have you heard of our lord and saviour, Prettier? https://prettier.io/

@pluma
Copy link

pluma commented Jul 9, 2018

None of my comments are blocking. :shipit: if @neunhoef approves

@ewoutp ewoutp merged commit 279c736 into master Jul 9, 2018
@ghost ghost removed the 4 - Review label Jul 9, 2018
@ewoutp ewoutp deleted the feature/dashboard branch July 9, 2018 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants