-
Notifications
You must be signed in to change notification settings - Fork 74
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
Feature/dashboard #211
Conversation
dashboard/src/App.js
Outdated
} | ||
|
||
render() { | ||
setTimeout(this.reloadOperators.bind(this), 5000); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
dashboard/src/App.js
Outdated
} | ||
return ( | ||
<Container> | ||
{children} |
There was a problem hiding this comment.
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/>}
.
dashboard/src/App.js
Outdated
<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>. |
There was a problem hiding this comment.
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.
dashboard/src/App.js
Outdated
</Message> | ||
</Segment> | ||
</Container> | ||
); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
dashboard/src/api/api.js
Outdated
@@ -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> { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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}) => ( |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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.
dashboard/src/util/Loading.js
Outdated
import { Dimmer, Loader, Segment } from 'semantic-ui-react'; | ||
|
||
class Loading extends Component { | ||
render() { |
There was a problem hiding this comment.
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.
dashboard/src/App.js
Outdated
</Segment> | ||
</Container> | ||
<div> | ||
{deployment ? <DeploymentOperator pod-info={<PodInfoView pod={pod} namespace={namespace}/>}/> : <NoOperator />} |
There was a problem hiding this comment.
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}) => ( |
There was a problem hiding this comment.
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}/>)} |
There was a problem hiding this comment.
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}); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}`; |
There was a problem hiding this comment.
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 = {}; |
There was a problem hiding this comment.
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
.
dashboard/src/util/Loading.js
Outdated
</Dimmer> | ||
<div style={{"min-height":"3em"}}/> | ||
<div style={{minHeight:"3em"}}/> |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
dashboard/src/api/api.js
Outdated
|
||
var api = new Api(); | ||
|
||
export default api; |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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 | ||
}); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🤷♂️
There was a problem hiding this comment.
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}); |
There was a problem hiding this comment.
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
dashboard/src/auth/Login.js
Outdated
<Form onSubmit={doLogin}> | ||
<Form.Field> | ||
<label>Name</label> | ||
<input focus="true" value={username} onChange={(e) => usernameChanged(e.target.value)}/> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}`} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> | ||
); |
There was a problem hiding this comment.
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/
None of my comments are blocking. |
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.