diff --git a/src/components/Button/index.jsx b/src/components/Button/index.jsx
index b5752d1c..9aaa60f7 100644
--- a/src/components/Button/index.jsx
+++ b/src/components/Button/index.jsx
@@ -50,7 +50,7 @@ export default class Button extends Component {
spanProps: object,
};
- handleButtonClick = () => {
+ handleButtonClick = e => {
const { onClick, track } = this.props;
if (track && process.env.GA_TRACKING_ID) {
@@ -60,7 +60,7 @@ export default class Button extends Component {
}
if (onClick) {
- onClick();
+ onClick(e);
}
};
diff --git a/src/components/DataTable/index.jsx b/src/components/DataTable/index.jsx
index bb7c9fb5..9f363b5a 100644
--- a/src/components/DataTable/index.jsx
+++ b/src/components/DataTable/index.jsx
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { Component, Fragment } from 'react';
import {
arrayOf,
func,
@@ -7,14 +7,28 @@ import {
oneOf,
oneOfType,
object,
+ bool,
} from 'prop-types';
+import { withStyles } from '@material-ui/core/styles';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableSortLabel from '@material-ui/core/TableSortLabel';
import TableRow from '@material-ui/core/TableRow';
+import TablePagination from '@material-ui/core/TablePagination';
+@withStyles(() => ({
+ noDisplay: {
+ visibility: 'hidden',
+ },
+ table: {
+ minWidth: 1020,
+ },
+ tableWrapper: {
+ overflowX: 'auto',
+ },
+}))
/**
* A table to display a set of data elements.
*/
@@ -26,6 +40,11 @@ export default class DataTable extends Component {
sortByHeader: null,
sortDirection: 'desc',
noItemsMessage: 'No items for this page.',
+ isPaginate: false,
+ };
+
+ state = {
+ page: 0,
};
static propTypes = {
@@ -66,6 +85,10 @@ export default class DataTable extends Component {
* A message to display when there is no items to display.
*/
noItemsMessage: string,
+ /**
+ * Whether to paginate the table
+ */
+ isPaginate: bool,
};
handleHeaderClick = ({ target }) => {
@@ -76,8 +99,13 @@ export default class DataTable extends Component {
}
};
+ handleChangePage = (event, page) => {
+ this.setState({ page });
+ };
+
render() {
const {
+ classes,
items,
columnsSize,
renderRow,
@@ -85,40 +113,69 @@ export default class DataTable extends Component {
sortByHeader,
sortDirection,
noItemsMessage,
+ isPaginate,
} = this.props;
const colSpan = columnsSize || (headers && headers.length) || 0;
+ const { page } = this.state;
+ const rowsPerPage = 5;
return (
-
- {headers && (
-
-
- {headers.map(header => (
-
-
-
- ))}
-
-
+
+
+
+ {headers && (
+
+
+ {headers.map(header => (
+
+
+
+ ))}
+
+
+ )}
+
+ {items.length === 0 ? (
+
+
+ {noItemsMessage}
+
+
+ ) : (
+ items
+ .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
+ .map(renderRow)
+ )}
+
+
+
+ {isPaginate && (
+
)}
-
- {items.length === 0 ? (
-
-
- {noItemsMessage}
-
-
- ) : (
- items.map(renderRow)
- )}
-
-
+
);
}
}
diff --git a/src/components/HookForm/index.jsx b/src/components/HookForm/index.jsx
index cc800b6c..d7a74a53 100644
--- a/src/components/HookForm/index.jsx
+++ b/src/components/HookForm/index.jsx
@@ -1,35 +1,44 @@
+/* eslint-disable no-plusplus */
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';
import CodeEditor from '@mozilla-frontend-infra/components/CodeEditor';
import Code from '@mozilla-frontend-infra/components/Code';
+import Drawer from '@material-ui/core/Drawer';
+import memoize from 'fast-memoize';
import { withStyles } from '@material-ui/core/styles';
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 TableRow from '@material-ui/core/TableRow';
+import TableCell from '@material-ui/core/TableCell';
import TextField from '@material-ui/core/TextField';
import Switch from '@material-ui/core/Switch';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Grid from '@material-ui/core/Grid';
import IconButton from '@material-ui/core/IconButton';
+import InformationVariantIcon from 'mdi-react/InformationVariantIcon';
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 LinkIcon from 'mdi-react/LinkIcon';
+import ErrorPanel from '../ErrorPanel';
+import DataTable from '../DataTable';
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 TableCellListItem from '../TableCellListItem';
import { hook } from '../../utils/prop-types';
import removeKeys from '../../utils/removeKeys';
@@ -114,12 +123,37 @@ const initialHook = {
subheader: {
fontSize: theme.typography.pxToRem(16),
},
+ displayBlock: {
+ display: 'block',
+ },
+ errorTableCell: {
+ whiteSpace: 'normal',
+ },
+ errorPanel: {
+ maxHeight: 300,
+ maxWidth: '75ch',
+ overflowY: 'scroll',
+ },
+ infoButton: {
+ marginLeft: -theme.spacing.double,
+ marginRight: theme.spacing.unit,
+ },
+ headline: {
+ paddingLeft: theme.spacing.triple,
+ paddingRight: theme.spacing.triple,
+ },
+ metadataContainer: {
+ paddingTop: theme.spacing.double,
+ paddingBottom: theme.spacing.double,
+ width: 400,
+ },
}))
/** 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,
@@ -129,8 +163,12 @@ export default class HookForm extends Component {
};
static propTypes = {
- /** A GraphQL hook response. Not needed when creating a new hook */
+ /** Part of a GraphQL hook response containing info about that hook.
+ Not needed when creating a new hook */
hook: hook.isRequired,
+ /** Part of the same Grahql hook response as above containing info
+ about some last hook fired attempts */
+ hookLastFires: array,
/** Set to `true` when creating a new hook. */
isNewHook: bool,
/** Callback function fired when a hook is triggered. */
@@ -153,6 +191,7 @@ export default class HookForm extends Component {
state = {
hook: null,
+ hookLastFires: null,
// eslint-disable-next-line react/no-unused-state
previousHook: null,
taskInput: '',
@@ -162,10 +201,15 @@ export default class HookForm extends Component {
taskValidJson: true,
triggerSchemaValidJson: true,
validation: {},
+ drawerOpen: false,
+ drawerData: null,
};
static getDerivedStateFromProps(props, state) {
- if (equals(props.hook, state.previousHook)) {
+ if (
+ equals(props.hook, state.previousHook) &&
+ equals(props.hookLastFires, state.hookLastFires)
+ ) {
return null;
}
@@ -173,6 +217,7 @@ export default class HookForm extends Component {
return {
hook: props.hook,
+ hookLastFires: props.hookLastFires,
previousHook: props.hook,
taskInput: JSON.stringify(
removeKeys(cloneDeep(hook.task), ['__typename']),
@@ -395,6 +440,39 @@ export default class HookForm extends Component {
),
});
+ handleDrawerClose = () => {
+ this.setState({
+ drawerOpen: false,
+ drawerData: null,
+ });
+ };
+
+ handleDrawerOpen = ({ target: { name } }) =>
+ memoize(
+ name => {
+ const { hookLastFires } = this.state;
+ let body;
+ const lastFiresLength = hookLastFires.length;
+
+ for (let i = 0; i < lastFiresLength; i++) {
+ const { taskId, error } = hookLastFires[i];
+
+ if (taskId === name) {
+ body = error;
+ break;
+ }
+ }
+
+ this.setState({
+ drawerOpen: true,
+ drawerData: { headline: name, body },
+ });
+ },
+ {
+ serializer: name => name,
+ }
+ )(name);
+
render() {
const {
actionLoading,
@@ -412,10 +490,12 @@ export default class HookForm extends Component {
triggerSchemaInput,
triggerContextInput,
hook,
+ hookLastFires,
validation,
+ drawerOpen,
+ drawerData,
} = this.state;
- /* eslint-disable-next-line no-underscore-dangle */
- const lastFireTypeName = !isNewHook && hook.status.lastFire.__typename;
+ const iconSize = 16;
return (
@@ -508,61 +588,79 @@ export default class HookForm extends Component {
/>
+
+ Last Fired Results
+ }
+ />
+ {hookLastFires && (
+ (
+
+
+ {(hookFire.result === 'SUCCESS' && (
+
+ {hookFire.taskId}}
+ />
+
+
+ )) || {hookFire.taskId}}
+
+
+ {hookFire.firedBy}
+
+
+
+
+
+
+
+
+ {(hookFire.result === 'ERROR' && (
+
+ )) || n/a}
+
+
+ )}
+ />
+ )}
+
{!isNewHook && (
-
-
-
- )
- }
- />
-
- {lastFireTypeName === HOOKS_LAST_FIRE_TYPE.SUCCESSFUL_FIRE ? (
-
-
-
-
- ) : (
-
-
- {JSON.stringify(hook.status.lastFire.error, null, 2)}
-
- )
- }
- />
-
- )}
-
-
- ) : (
- 'n/a'
- )
- }
- />
-
-
+
+
+ ) : (
+ 'n/a'
+ )
+ }
+ />
+
)}
@@ -764,6 +862,33 @@ export default class HookForm extends Component {
}
/>
)}
+
+
+
+ {drawerData && drawerData.headline}
+
+
+
+
+ )
+ }
+ />
+
+
+
+
);
}
diff --git a/src/views/Hooks/ViewHook/hook.graphql b/src/views/Hooks/ViewHook/hook.graphql
index 8db5474c..52d42ae2 100644
--- a/src/views/Hooks/ViewHook/hook.graphql
+++ b/src/views/Hooks/ViewHook/hook.graphql
@@ -45,5 +45,12 @@ query Hook($hookGroupId: ID!, $hookId: ID!) {
tags
extra
}
+ },
+ hookLastFires(hookGroupId: $hookGroupId, hookId: $hookId) {
+ taskId
+ firedBy
+ taskCreateTime
+ result
+ error
}
}
diff --git a/src/views/Hooks/ViewHook/index.jsx b/src/views/Hooks/ViewHook/index.jsx
index 6c22c99f..0d3cf9bd 100644
--- a/src/views/Hooks/ViewHook/index.jsx
+++ b/src/views/Hooks/ViewHook/index.jsx
@@ -131,6 +131,11 @@ export default class ViewHook extends Component {
const { isNewHook, data } = this.props;
const { error: err, dialogError, actionLoading, dialogOpen } = this.state;
const error = (data && data.error) || err;
+ const hookLastFires =
+ data.hookLastFires &&
+ data.hookLastFires.sort(
+ (a, b) => new Date(b.taskCreateTime) - new Date(a.taskCreateTime)
+ );
return (
@@ -152,6 +157,7 @@ export default class ViewHook extends Component {
dialogError={dialogError}
actionLoading={actionLoading}
hook={data.hook}
+ hookLastFires={hookLastFires}
dialogOpen={dialogOpen}
onTriggerHook={this.handleTriggerHook}
onUpdateHook={this.handleUpdateHook}