-
Notifications
You must be signed in to change notification settings - Fork 44
Bug 1520857- add support in tc-web for displaying information from the lastFire table #413
base: master
Are you sure you want to change the base?
Changes from 3 commits
c57174f
7a33df8
5b5c97d
a154a50
f55678d
c5f5a16
87993e3
f98d385
4674382
0156365
e238f6f
8bb249f
2b28062
aa51ef9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import React, { Component, Fragment } from 'react'; | ||
import { Link } from 'react-router-dom'; | ||
import { string, bool, func, oneOfType, object } from 'prop-types'; | ||
import { string, bool, func, oneOfType, object, array } from 'prop-types'; | ||
import classNames from 'classnames'; | ||
import { equals, assocPath } from 'ramda'; | ||
import cloneDeep from 'lodash.clonedeep'; | ||
|
@@ -11,6 +11,10 @@ import List from '@material-ui/core/List'; | |
import ListItem from '@material-ui/core/ListItem'; | ||
import ListItemText from '@material-ui/core/ListItemText'; | ||
import ListSubheader from '@material-ui/core/ListSubheader'; | ||
import ExpansionPanel from '@material-ui/core/ExpansionPanel'; | ||
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'; | ||
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary'; | ||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; | ||
import TextField from '@material-ui/core/TextField'; | ||
import Switch from '@material-ui/core/Switch'; | ||
import FormGroup from '@material-ui/core/FormGroup'; | ||
|
@@ -21,15 +25,15 @@ import Typography from '@material-ui/core/Typography'; | |
import FlashIcon from 'mdi-react/FlashIcon'; | ||
import PlusIcon from 'mdi-react/PlusIcon'; | ||
import DeleteIcon from 'mdi-react/DeleteIcon'; | ||
import LinkIcon from 'mdi-react/LinkIcon'; | ||
import ContentSaveIcon from 'mdi-react/ContentSaveIcon'; | ||
import { docs } from 'taskcluster-lib-urls'; | ||
import Label from '@mozilla-frontend-infra/components/Label'; | ||
import ErrorPanel from '../ErrorPanel'; | ||
import Button from '../Button'; | ||
import SpeedDial from '../SpeedDial'; | ||
import SpeedDialAction from '../SpeedDialAction'; | ||
import DialogAction from '../DialogAction'; | ||
import DateDistance from '../DateDistance'; | ||
import { HOOKS_LAST_FIRE_TYPE } from '../../utils/constants'; | ||
import { hook } from '../../utils/prop-types'; | ||
import removeKeys from '../../utils/removeKeys'; | ||
|
||
|
@@ -114,12 +118,16 @@ const initialHook = { | |
subheader: { | ||
fontSize: theme.typography.pxToRem(16), | ||
}, | ||
hookLastFires: { | ||
width: '100%', | ||
}, | ||
})) | ||
/** A form to view/edit/create a hook */ | ||
export default class HookForm extends Component { | ||
static defaultProps = { | ||
isNewHook: false, | ||
hook: initialHook, | ||
hookLastFires: null, | ||
onTriggerHook: null, | ||
onCreateHook: null, | ||
onUpdateHook: null, | ||
|
@@ -131,6 +139,8 @@ export default class HookForm extends Component { | |
static propTypes = { | ||
/** A GraphQL hook response. Not needed when creating a new hook */ | ||
hook: hook.isRequired, | ||
/** Object */ | ||
hookLastFires: array, | ||
/** Set to `true` when creating a new hook. */ | ||
isNewHook: bool, | ||
/** Callback function fired when a hook is triggered. */ | ||
|
@@ -153,6 +163,8 @@ export default class HookForm extends Component { | |
|
||
state = { | ||
hook: null, | ||
hookLastFires: null, | ||
lastFirePanelExpanded: '', | ||
// eslint-disable-next-line react/no-unused-state | ||
previousHook: null, | ||
taskInput: '', | ||
|
@@ -165,14 +177,18 @@ export default class HookForm extends Component { | |
}; | ||
|
||
static getDerivedStateFromProps(props, state) { | ||
if (equals(props.hook, state.previousHook)) { | ||
if ( | ||
equals(props.hook, state.previousHook) && | ||
equals(props.hookLastFires, state.hookLastFires) | ||
) { | ||
return null; | ||
} | ||
|
||
const hook = props.isNewHook ? initialHook : props.hook; | ||
|
||
return { | ||
hook: props.hook, | ||
hookLastFires: props.hookLastFires, | ||
previousHook: props.hook, | ||
taskInput: JSON.stringify( | ||
removeKeys(cloneDeep(hook.task), ['__typename']), | ||
|
@@ -395,6 +411,12 @@ export default class HookForm extends Component { | |
), | ||
}); | ||
|
||
handleLastFirePanelChange = panel => (event, expanded) => { | ||
this.setState({ | ||
lastFirePanelExpanded: expanded ? panel : false, | ||
}); | ||
}; | ||
|
||
render() { | ||
const { | ||
actionLoading, | ||
|
@@ -412,10 +434,11 @@ export default class HookForm extends Component { | |
triggerSchemaInput, | ||
triggerContextInput, | ||
hook, | ||
hookLastFires, | ||
validation, | ||
lastFirePanelExpanded, | ||
} = this.state; | ||
/* eslint-disable-next-line no-underscore-dangle */ | ||
const lastFireTypeName = !isNewHook && hook.status.lastFire.__typename; | ||
|
||
return ( | ||
<Fragment> | ||
|
@@ -508,61 +531,85 @@ export default class HookForm extends Component { | |
/> | ||
</FormGroup> | ||
</ListItem> | ||
{!isNewHook && ( | ||
<Fragment> | ||
<ListItem> | ||
<ListItemText | ||
primary="Last Fired" | ||
secondary={ | ||
lastFireTypeName === HOOKS_LAST_FIRE_TYPE.NO_FIRE ? ( | ||
'n/a' | ||
) : ( | ||
<DateDistance from={hook.status.lastFire.time} /> | ||
) | ||
} | ||
/> | ||
</ListItem> | ||
{lastFireTypeName === HOOKS_LAST_FIRE_TYPE.SUCCESSFUL_FIRE ? ( | ||
<ListItem | ||
button | ||
component={Link} | ||
className={classes.listItemButton} | ||
to={`/tasks/${hook.status.lastFire.taskId}`}> | ||
<ListItemText | ||
primary="Last Fired Result" | ||
secondary={hook.status.lastFire.taskId} | ||
/> | ||
<LinkIcon /> | ||
</ListItem> | ||
) : ( | ||
<ListItem> | ||
<ListItemText | ||
primary="Last Fired Result" | ||
secondary={ | ||
lastFireTypeName === HOOKS_LAST_FIRE_TYPE.NO_FIRE ? ( | ||
'n/a' | ||
) : ( | ||
<pre> | ||
{JSON.stringify(hook.status.lastFire.error, null, 2)} | ||
</pre> | ||
) | ||
} | ||
/> | ||
</ListItem> | ||
)} | ||
<ListItem> | ||
<ListItem> | ||
{hookLastFires ? ( | ||
<List className={classes.hookLastFires}> | ||
<ListItemText | ||
primary="Next Scheduled Fire" | ||
secondary={ | ||
hook.status.nextScheduledDate ? ( | ||
<DateDistance from={hook.status.nextScheduledDate} /> | ||
) : ( | ||
'n/a' | ||
) | ||
primary={ | ||
<Typography variant="subtitle1"> | ||
Last Hook Fire Attempts | ||
</Typography> | ||
} | ||
/> | ||
</ListItem> | ||
</Fragment> | ||
{hookLastFires.map( | ||
({ taskId, taskCreateTime, firedBy, result, error }) => ( | ||
<ListItem key={taskId}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think a DataTable would be a good fit for the data. If result is |
||
<Grid container direction="column"> | ||
<Grid | ||
item | ||
container | ||
direction="row" | ||
justify="space-between"> | ||
<Grid item> | ||
<pre>{firedBy}</pre> | ||
</Grid> | ||
<Grid item> | ||
<pre> | ||
<DateDistance from={taskCreateTime} /> | ||
</pre> | ||
</Grid> | ||
</Grid> | ||
<Grid item> | ||
<ExpansionPanel | ||
expanded={ | ||
result === 'ERROR' && | ||
lastFirePanelExpanded === taskId | ||
} | ||
onChange={this.handleLastFirePanelChange(taskId)}> | ||
<ExpansionPanelSummary | ||
expandIcon={error && <ExpandMoreIcon />}> | ||
<Grid container justify="space-between"> | ||
<Grid item> | ||
{(result === 'SUCCESS' && ( | ||
<Link to={`/tasks/${taskId}`}> | ||
<Typography>{taskId}</Typography> | ||
</Link> | ||
)) || <Typography>{taskId}</Typography>} | ||
</Grid> | ||
<Grid item> | ||
<Label mini status={result.toLowerCase()}> | ||
{result} | ||
</Label> | ||
</Grid> | ||
</Grid> | ||
</ExpansionPanelSummary> | ||
<ExpansionPanelDetails> | ||
<ErrorPanel error={error} onClose={null} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could probably have errors be a separate column in the table and use simple text to display the error instead of having it in a error panel. We use error panels for when the server fails to return data or the user is trying to do something not allowed. |
||
</ExpansionPanelDetails> | ||
</ExpansionPanel> | ||
</Grid> | ||
</Grid> | ||
</ListItem> | ||
) | ||
)} | ||
</List> | ||
) : ( | ||
'' | ||
)} | ||
</ListItem> | ||
{!isNewHook && ( | ||
<ListItem> | ||
<ListItemText | ||
primary="Next Scheduled Fire" | ||
secondary={ | ||
hook.status.nextScheduledDate ? ( | ||
<DateDistance from={hook.status.nextScheduledDate} /> | ||
) : ( | ||
'n/a' | ||
) | ||
} | ||
/> | ||
</ListItem> | ||
)} | ||
<List> | ||
<ListItem> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
query HookLastFires($hookGroupId: ID!, $hookId: ID!) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Delete this file. See reason in other comment. |
||
hookLastFires(hookGroupId: $hookGroupId, hookId: $hookId) { | ||
taskId | ||
firedBy | ||
taskCreateTime | ||
result | ||
error | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import { hot } from 'react-hot-loader'; | ||
import React, { Component, Fragment } from 'react'; | ||
import { graphql, withApollo } from 'react-apollo'; | ||
import { graphql, withApollo, compose } from 'react-apollo'; | ||
import Spinner from '@mozilla-frontend-infra/components/Spinner'; | ||
import Dashboard from '../../../components/Dashboard'; | ||
import HookForm from '../../../components/HookForm'; | ||
|
@@ -10,18 +10,32 @@ import createHookQuery from './createHook.graphql'; | |
import deleteHookQuery from './deleteHook.graphql'; | ||
import updateHookQuery from './updateHook.graphql'; | ||
import triggerHookQuery from './triggerHook.graphql'; | ||
import hookLastFiresQuery from './hookLastFires.graphql'; | ||
|
||
@hot(module) | ||
@withApollo | ||
@graphql(hookQuery, { | ||
skip: ({ match: { params } }) => !params.hookId, | ||
options: ({ match: { params } }) => ({ | ||
variables: { | ||
hookGroupId: params.hookGroupId, | ||
hookId: decodeURIComponent(params.hookId), | ||
}, | ||
@compose( | ||
graphql(hookLastFiresQuery, { | ||
skip: ({ match: { params } }) => !params.hookId, | ||
options: ({ match: { params } }) => ({ | ||
variables: { | ||
hookGroupId: params.hookGroupId, | ||
hookId: decodeURIComponent(params.hookId), | ||
}, | ||
}), | ||
name: 'lastFiresData', | ||
}), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to create |
||
}) | ||
graphql(hookQuery, { | ||
skip: ({ match: { params } }) => !params.hookId, | ||
options: ({ match: { params } }) => ({ | ||
variables: { | ||
hookGroupId: params.hookGroupId, | ||
hookId: decodeURIComponent(params.hookId), | ||
}, | ||
}), | ||
name: 'hookData', | ||
}) | ||
) | ||
export default class ViewHook extends Component { | ||
state = { | ||
actionLoading: false, | ||
|
@@ -128,13 +142,13 @@ export default class ViewHook extends Component { | |
}; | ||
|
||
render() { | ||
const { isNewHook, data } = this.props; | ||
const { isNewHook, hookData, lastFiresData } = this.props; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One you merge the last fires data in |
||
const { error: err, dialogError, actionLoading, dialogOpen } = this.state; | ||
const error = (data && data.error) || err; | ||
const hookDataError = (hookData && hookData.error) || err; | ||
|
||
return ( | ||
<Dashboard title={isNewHook ? 'Create Hook' : 'Hook'}> | ||
<ErrorPanel error={error} /> | ||
<ErrorPanel error={hookDataError} /> | ||
{isNewHook ? ( | ||
<Fragment> | ||
<HookForm | ||
|
@@ -146,12 +160,16 @@ export default class ViewHook extends Component { | |
</Fragment> | ||
) : ( | ||
<Fragment> | ||
{!data.hook && data.loading && <Spinner loading />} | ||
{data.hook && ( | ||
{!hookData.hook && hookData.loading && <Spinner loading />} | ||
{hookData.hook && ( | ||
<HookForm | ||
dialogError={dialogError} | ||
actionLoading={actionLoading} | ||
hook={data.hook} | ||
hook={hookData.hook} | ||
hookLastFires={lastFiresData.hookLastFires.sort( | ||
(a, b) => | ||
new Date(b.taskCreateTime) - new Date(a.taskCreateTime) | ||
)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extract logic from jsx: const hookLastFires = // ...
// ...
hookLastFires={hookLastFires} |
||
dialogOpen={dialogOpen} | ||
onTriggerHook={this.handleTriggerHook} | ||
onUpdateHook={this.handleUpdateHook} | ||
|
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.
Let's use a different comment here to describe the property.