Skip to content
This repository has been archived by the owner on Mar 23, 2019. It is now read-only.

Provisioners, Worker Types, Workers, Worker #65

Merged
merged 31 commits into from
May 2, 2018

Conversation

helfi92
Copy link
Contributor

@helfi92 helfi92 commented Apr 19, 2018

Resolves #15
Resolves #16
Resolves #18
Resolves #17

Implemented so that we have consistent dropdowns across the site
A similar implementation to the ConnectionDataTable except that it takes raw data and no pagination
`QuarantineButton` was created mainly because it has
somewhat a complex dialog where a user can modify the
quarantineUntil date. To isolate the state management
as well as possible code duplication, this component
was created.
This component integrates gracefully in a TableCell. It
aligns list items with column headers along with other styling
which otherwise needs to be duplicated in every component that
would use a `ListItem` inside a `TableCell`.
Done in order to be consistent with the
worker types view
@helfi92 helfi92 self-assigned this Apr 19, 2018
@helfi92
Copy link
Contributor Author

helfi92 commented Apr 19, 2018

View Provisioners

viewprovisioners

View Worker Types

viewworkertypes

View Workers

viewworkers

View Worker

viewworker
viewworkerinfobutton

Copy link
Contributor

@eliperelman eliperelman left a comment

Choose a reason for hiding this comment

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

So far this is looking pretty hot. In the first round here, I'd also like to request some UI changes:

  • In the provisioners card list, use the same style I have for the hovering list item link icons as I do in the task view, so they are faded normally, but are bright on hover. Feel free to extract into a mixin in theme.js if it makes sense.
  • On the Worker Types view, remove the paper/card wrapper from the select box. Experiment with positioning the select on the right.
  • On the Workers view, remove the paper/card wrapper around the select box. Experiment with position the select on the right. If there are any actions, do not render them here at the top, but try using the SpeedDial component.
  • On the Worker view, remove the paper/card wrapper surrounding that metadata. We can probably remove the words "Worker Details". Move the actions to a Speed Dial.
  • For the slide-up task metadata, can you make that come in from the right instead of the bottom?

export default class ProvisionerDetailsCard extends Component {
static propTypes = {
/** A `react-router-dom` history object */
history: object,
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of passing history as a prop from the parent, let's use the withRouter decorator.

import { withRouter } from 'react-router-dom';

@withRouter
export default class ProvisionerDetailsCard extends Component {

/** A `react-router-dom` history object */
history: object,
/** A GraphQL provisioner response. */
provisioner,
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be required.

<ListItemText primary="Description" />
{showDescription ? <ChevronUpIcon /> : <ChevronDownIcon />}
</ListItem>
<Collapse in={showDescription} timeout="auto" unmountOnExit>
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like it's possible to not have a description. So let's remove the chevron icons when the description is not there and render the n/a, otherwise render the collapse.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. This will, however, require setting a min-height on the card in order to have height consistency with the other cards which should be ok IMO.

title="Quarantine?"
body={
<Fragment>
<Markdown>
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this from being Markdown and just use the name of the property as "Quarantine Until".

<Markdown>
Quarantining a worker allows the machine to remain alive but not
accept jobs. Note that a quarantine can be lifted by setting
`quarantineUntil` to the present time (or somewhere in the past).
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove parentheses from text.

data: { fetchMore },
} = this.props;

return fetchMore({
Copy link
Contributor

Choose a reason for hiding this comment

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

In the future it looks like we can make this more generic in a util.


return (
<Dashboard
title="View Workers"
Copy link
Contributor

Choose a reason for hiding this comment

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

Make this just "Workers".

</CardContent>
</Card>
<br />
{loading ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the loading check here? The connection table already has a loading indicator.

} = this.props;

return (
<Switch>
Copy link
Contributor

Choose a reason for hiding this comment

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

😻

);

@hot(module)
export default class Task extends Component {
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to Provisioners or something.

* A button that displays a drawer when clicked.
* The drawer opens above all other content until a selection is selected.
*/
export default class ButtonDrawer extends PureComponent {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure the value of this component. Since it's completely controlled by the parent, and doesn't have any additional functionality or styles, I think it's OK to inline these into their parent component.

@@ -11,7 +12,7 @@ const IS_TOUCH = 'ontouchstart' in document.documentElement;
@withStyles(theme => ({
speedDial: {
position: 'absolute',
bottom: theme.spacing.double,
top: theme.spacing.double,
Copy link
Contributor

Choose a reason for hiding this comment

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

I want these to stay at the bottom. The log view doesn't use this component and it isn't a speed dial, so I want to be consistent with material.

/**
* A SpeedDialAction that displays a dialog when clicked.
*/
export default class SpeedDialogAction extends PureComponent {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it makes sense to combine these components. The button can be inlined into its parent, and the "async action" dialog can be its own component.

onClick={this.handleOpen}
{...props}
/>
<Dialog open={open} onClose={this.handleClose}>
Copy link
Contributor

Choose a reason for hiding this comment

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

page={page}
onChangePage={this.handlePageChange}
/>
<div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why multiple divs instead of a Fragment? Why are we rendering 2 tables when loading/why are we changing the existing loading layout?

Copy link
Contributor Author

@helfi92 helfi92 Apr 24, 2018

Choose a reason for hiding this comment

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

@eliperelman The outermost div can definitely be a Fragment but the second div is used to keep the table footer from scrolling with the table. To have TablePagination not scroll when the table is small as well as the arrows not positioned at the end of the scroll, it needs to be outside the Table component (see example in material docs). Next, to display the spinner, a Table with a TableFooter is created in order to have the spinner positioned at the same spot as the arrows in TablePagination.

To stop rendering 2 tables, the second Table can be replaced with:

<div className={classes.spinner}>
  <Spinner size={24} />
</div>
spinner: {
  height: 56,
  minHeight: 56,
  paddingRight: 2,
  display: 'flex',
  alignItems: 'center',
  flexDirection: 'row-reverse',
},

I think ^ makes more sense to use.

Copy link
Contributor

@eliperelman eliperelman left a comment

Choose a reason for hiding this comment

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

Round 2. For UI changes:

  • Upon seeing the right drawer, I think it's overkill. We are already linking to the task from the Worker table, and that has the information about the task, so the drawer is redundant. I think we should kill this and just link to the task.
  • I mentioned it in the comments, but let's use the Speed Dial component as intended by material-ui, and place it in the lower right, with sub-actions appearing on hover.
  • Can you make the icons that appear in the table to be a little smaller? The default of 24 seems a bit big, so maybe 16-20?

<TableSortLabel
active={header === sortByHeader}
direction={sortDirection || 'desc'}
onClick={() => this.handleHeaderClick(header)}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this passed by reference, and use the target/currentTarget ID to drive the passed value?

handleHeaderClick = e => {
  this.props.onHeaderClick(e.target.id);
};

id={header}
onClick={this.props.onHeaderClick && this.handleHeaderClick}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, but I think we should (when we can) try to use IDs for anchors only. We could rely on e.target.textContent instead, if that's ok with you.

Copy link
Contributor

Choose a reason for hiding this comment

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

try to use IDs for anchors only

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, by anchors I mean things like toolbar, header, footer, etc. I would typically use ids as fragment identifier to jump to anchors. There's also the specificity war that can happen with CSS. I don't think it's the end of the world if we use an id here but using something like textContent will avoid using an id and is altogether less code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let me address the points:

fragment identifier to jump to anchors

We don't use this feature since we aren't really document-based but app and JS driven. It's useful in docs, though, but in there we won't be doing a bunch of event bindings.

There's also the specificity war that can happen with CSS

We aren't using CSS, nor do we do any CSS bindings based on IDs.

using something like textContent

While this would work, this relies on the item's representation in the DOM vs. our own identifier, which may not always align. Best to be explicit.


@withStyles({
tableWrapper: {
overflowX: 'auto',
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this be resolved by mui/material-ui#11062?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Weird, it seems to be working now with our current 1.0.0-beta.41. I'll just remove it.

<TableSortLabel
active={header === sortByHeader}
direction={sortDirection || 'desc'}
onClick={() => this.handleHeaderClick(header)}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Same concern about passing by reference.

/**
* A button that displays a dialog when clicked.
*/
export default class DialogAction extends PureComponent {
Copy link
Contributor

Choose a reason for hiding this comment

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

Split this component up. The button should live in the parent component, and the "async action dialog" can be its own component.

Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be a Component since the props can't be shallowly compared.

} = this.props;

return (
<FormControl className={classes.root} {...props}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this preferred over <TextField select />?

awsProvisionerWorkerTypeSummaries,
} = this.props;
const sortByProperty = camelCase(sortBy);
// Normalize worker types for aws-provisioner-v1
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think this normalization should happen on the server, or in a util function?

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 think a util function makes more sense here. We have 14 provisioners and this normalization is only for aws-provisioner.

static propTypes = {
/** Workers GraphQL PageConnection instance. */
workersConnection: shape({
edges: array,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be typed stronger?


createSortedWorkersConnection = memoizeWith(
() =>
`${toString(this.props.workersConnection.edges)}-${this.state.sortBy}-${
Copy link
Contributor

Choose a reason for hiding this comment

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

See other comment about calculating this.

provisionerId={provisionerId}
onPageChange={this.handlePageChange}
awsProvisionerWorkerTypeSummaries={
awsProvisionerWorkerTypeSummaries
Copy link
Contributor

Choose a reason for hiding this comment

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

lol prettier

return (
<Switch>
<RouteWithProps
path={`${path}/:provisionerId/worker-types/:workerType/workers/:workerGroup?/:workerId?`}
Copy link
Contributor

Choose a reason for hiding this comment

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

I see workerGroup and workerId are optional, but I couldn't find how their presence affected a view. Could you point me to the right place?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This needs to be removed. We don't have dropdowns in the worker view anymore since most users come to this page via the workers view. I will revise.

@helfi92
Copy link
Contributor Author

helfi92 commented Apr 26, 2018

Provisioners

screen shot 2018-04-26 at 3 09 01 pm


Worker Types

screen shot 2018-04-26 at 3 09 31 pm


Workers

screen shot 2018-04-26 at 3 09 51 pm


Worker

screen shot 2018-04-26 at 3 10 22 pm

const fromNow = distanceInWordsToNow(from, { addSuffix: true });
const offsetNow = offset && distanceInWords(offset, from);
const fromNow = formatDistanceStrict(from, new Date(), { addSuffix: true });
const offsetNow = offset && formatDistance(from, offset);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do these new methods return the same output as before? I noticed the version bump, but not sure what the difference is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, they were renamed and the arguments order was switched. The change is well explained in the CHANGELOG.

/**
* A button that displays a dialog when clicked.
*/
export default class DialogAction extends PureComponent {
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be a Component since the props can't be shallowly compared.

/**
* A dropdown menu.
*/
export default class Dropdown extends Component {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still not sure the point of this component.

getTableData = memoizeWith(
({ sortBy, sortDirection }) => `${sortBy}-${sortDirection}`,
() => {
const { worker } = this.props;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think these should still be arguments to this function. Pass the props and state you need to the memoizer so it stays pure.

);

handleHeaderClick = sortByHeader => {
const sortBy = sortByHeader;
Copy link
Contributor

Choose a reason for hiding this comment

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

This variable doesn't do anything, so you can rename the argument to this name.

<Button
className={classes.infoButton}
size="small"
onClick={() => this.handleDrawerOpen(workerType)}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use the generated ID approach here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@eliperelman workerType is an object here, not a string. How would you gain reference to the object without having to loop over the sorted worker types connection?

Copy link
Contributor

Choose a reason for hiding this comment

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

You would need to loop over the sorted connection to look up the worker type by the ID.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't this make it O(n)? The current implementation is O(1).

Copy link
Contributor

Choose a reason for hiding this comment

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

It also creates n event handlers instead of 1.


createSortedWorkersConnection = memoizeWith(
(workersConnections, sortBy, sortDirection) => {
const sorted = pipe(
Copy link
Contributor

Choose a reason for hiding this comment

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

Move definition outside class so it's not regenerated every call.

return `${ids.join('-')}-${sortBy}-${sortDirection}`;
},
() => {
const { workersConnection } = this.props;
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the props and state from arguments to keep this pure.

</TableCell>
<TableCell>
{quarantineUntil
? formatDistanceStrict(Date(), quarantineUntil, { unit: 'd' })
Copy link
Contributor

Choose a reason for hiding this comment

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

new Date()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The parameter can be of type Date | String | Number https://date-fns.org/v2.0.0-alpha.7/docs/formatDistanceStrict#arguments. I'll be sure to be consistent everywhere.

workerType={params.workerType}
provisionerId={params.provisionerId}
/>
<SpeedDial
Copy link
Contributor

Choose a reason for hiding this comment

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

We can remove the speed dial if there are no actions.

@@ -26,7 +26,7 @@ export default class DateDistance extends Component {

render() {
const { from, offset } = this.props;
const fromNow = formatDistanceStrict(from, new Date(), { addSuffix: true });
const fromNow = formatDistanceStrict(from, Date(), { addSuffix: true });
Copy link
Contributor

Choose a reason for hiding this comment

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

Change Date to new Date().

tooltipTitle={
<div>
<div>{action.title}</div>
<div>{action.description}</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove description. The description will be in the dialog that gets opened, and putting it here will be potentially too large and redundant to show every time.

<div>{action.description}</div>
</div>
}
onClick={() => this.handleActionClick(action)}
Copy link
Contributor

Choose a reason for hiding this comment

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

We can leave this for now if it keeps it simple. Maybe add a TODO to come back later.

@helfi92 helfi92 requested a review from eliperelman May 1, 2018 13:25
This was referenced May 1, 2018
@helfi92 helfi92 merged commit 70f0e27 into taskcluster:master May 2, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants