ui/perfherder/alerts/AlertTable.jsx (513 lines of code) (raw):
import React from 'react';
import PropTypes from 'prop-types';
import { Container, Form, FormGroup, Table, Row, Col } from 'reactstrap';
import orderBy from 'lodash/orderBy';
import {
alertStatusMap,
maximumVisibleAlertSummaryRows,
browsertimeId,
} from '../perf-helpers/constants';
import {
genericErrorMessage,
errorMessageClass,
} from '../../helpers/constants';
import RepositoryModel from '../../models/repository';
import {
getInitializedAlerts,
containsText,
updateAlertSummary,
} from '../perf-helpers/helpers';
import TruncatedText from '../../shared/TruncatedText';
import ErrorBoundary from '../../shared/ErrorBoundary';
import TableColumnHeader from '../shared/TableColumnHeader';
import SortButtonDisabled from '../shared/SortButtonDisabled';
import { tableSort, getNextSort, sort, sortTables } from '../perf-helpers/sort';
import AlertTableRow from './AlertTableRow';
import AlertHeader from './AlertHeader';
import StatusDropdown from './StatusDropdown';
import DownstreamSummary from './DownstreamSummary';
import AlertActionPanel from './AlertActionPanel';
import SelectAlertsDropdown from './SelectAlertsDropdown';
import CollapsableRows from './CollapsableRows';
export default class AlertTable extends React.Component {
constructor(props) {
super(props);
this.state = {
alertSummary: null,
downstreamIds: [],
filteredAlerts: [],
filteredAndSortedAlerts: [],
allSelected: false,
selectedAlerts: [],
tableConfig: {
Test: {
name: 'Test',
sortValue: 'title',
currentSort: tableSort.default,
},
Platform: {
name: 'Platform',
sortValue: 'machine_platform',
currentSort: tableSort.default,
},
TagsOptions: {
name: 'Tags & Options',
sortValue: 'tags',
currentSort: tableSort.default,
},
Magnitude: {
name: 'Magnitude of Change',
sortValue: 'amount_abs',
currentSort: tableSort.default,
},
Confidence: {
name: 'Confidence',
sortValue: 't_value',
currentSort: tableSort.default,
},
DebuggingInformation: {
name: 'Debug Tools',
sortValue: '',
currentSort: tableSort.default,
},
NoiseProfile: {
name: 'Information',
sortValue: 'noise_profile',
currentSort: tableSort.default,
},
},
};
}
componentDidMount() {
this.processAlerts();
}
componentDidUpdate(prevProps) {
const { filters, alertSummary } = this.props;
if (prevProps.filters !== filters) {
this.updateFilteredAlerts();
}
if (prevProps.alertSummary !== alertSummary) {
this.processAlerts();
}
}
processAlerts = () => {
const { alertSummary, optionCollectionMap } = this.props;
const alerts = getInitializedAlerts(alertSummary, optionCollectionMap);
alertSummary.alerts = orderBy(
alerts,
['starred', 'title'],
['desc', 'desc'],
);
this.setState({ alertSummary }, () => {
this.getDownstreamList();
this.updateFilteredAlerts();
});
};
getDownstreamList = () => {
const { alertSummary } = this.state;
const downstreamIds = [
...new Set(
alertSummary.alerts
.map((alert) => {
if (
alert.status === alertStatusMap.downstream &&
alert.summary_id !== alertSummary.id
) {
return [alert.summary_id];
}
return [];
})
.reduce((a, b) => [...a, ...b], []),
),
];
this.setState({ downstreamIds });
};
filterAlert = (alert) => {
const {
hideDownstream,
hideAssignedToOthers,
filterText,
} = this.props.filters;
const { username } = this.props.user;
const { alertSummary } = this.state;
const notRelatedDownstream =
alert.summary_id === alertSummary.id ||
alert.status !== alertStatusMap.downstream;
const concealableReassigned =
hideDownstream &&
alert.status === alertStatusMap.reassigned &&
alert.related_summary_id !== alertSummary.id;
const concealableDownstream =
hideDownstream && alert.status === alertStatusMap.downstream;
const concealableInvalid =
hideDownstream && alert.status === alertStatusMap.invalid;
const concealableAssignedToOthers =
hideAssignedToOthers && alertSummary.assignee_username !== username;
const matchesFilters =
notRelatedDownstream &&
!concealableReassigned &&
!concealableDownstream &&
!concealableInvalid &&
!concealableAssignedToOthers;
if (!filterText) return matchesFilters;
const textToTest = `${alert.title} ${
alertSummary.bug_number && alertSummary.bug_number.toString()
} ${alertSummary.revision.toString()}`;
// searching with filter input and one or more metricFilter buttons on
// will produce different results compared to when all filters are off
return containsText(textToTest, filterText) && matchesFilters;
};
getAlertsSortedByDefault = (filteredAlerts) => {
const fields = [
'starred',
'backfill_record',
'is_regression',
't_value',
'amount_abs',
'title',
];
const sortOrders = ['desc', 'asc', 'desc', 'desc', 'desc', 'asc'];
return orderBy(filteredAlerts, fields, sortOrders);
};
updateFilteredAlerts = () => {
const { alertSummary, tableConfig } = this.state;
Object.keys(tableConfig).forEach((key) => {
tableConfig[key].currentSort = tableSort.default;
});
const filteredAlerts = alertSummary.alerts.filter((alert) =>
this.filterAlert(alert),
);
const filteredAndSortedAlerts = this.getAlertsSortedByDefault(
filteredAlerts,
);
this.setState({
tableConfig,
filteredAlerts,
filteredAndSortedAlerts,
allSelected: false,
selectedAlerts: [],
});
};
updateAssignee = async (newAssigneeUsername) => {
const {
updateAlertSummary,
updateViewState,
fetchAlertSummaries,
} = this.props;
const { alertSummary } = this.state;
const { data, failureStatus } = await updateAlertSummary(alertSummary.id, {
assignee_username: newAssigneeUsername,
});
if (!failureStatus) {
// now refresh UI, by syncing with backend
fetchAlertSummaries(alertSummary.id);
} else {
updateViewState({
errorMessages: [
`Failed to set new assignee "${newAssigneeUsername}". (${data})`,
],
});
}
return { failureStatus };
};
changeRevision = async (newRevisionTo, newRevisionFrom) => {
const {
updateAlertSummary,
updateViewState,
fetchAlertSummaries,
} = this.props;
const { alertSummary } = this.state;
const { data, failureStatus } = await updateAlertSummary(alertSummary.id, {
revision: newRevisionTo,
prev_push_revision: newRevisionFrom,
});
if (!failureStatus) {
// now refresh UI, by syncing with backend
fetchAlertSummaries(alertSummary.id);
} else {
updateViewState({
errorMessages: [`Failed to set revisions. (${data})`],
});
}
return { failureStatus };
};
setSelectedAlerts = ({ selectedAlerts, allSelected }) =>
this.setState({
selectedAlerts,
allSelected,
});
onChangeSort = (currentColumn) => {
const { tableConfig } = this.state;
const { filteredAlerts } = this.state;
const { default: defaultSort } = tableSort;
const { currentSort, sortValue } = currentColumn;
const nextSort = getNextSort(currentSort);
Object.keys(tableConfig).forEach((key) => {
tableConfig[key].currentSort = defaultSort;
});
currentColumn.currentSort = nextSort;
let filteredAndSortedAlerts = this.getAlertsSortedByDefault(filteredAlerts);
if (nextSort !== defaultSort) {
filteredAndSortedAlerts = sort(
sortValue,
nextSort,
filteredAlerts,
sortTables.alert,
);
}
this.setState({ filteredAndSortedAlerts, tableConfig });
};
render() {
const {
user,
projects,
frameworks,
alertSummaries,
issueTrackers,
fetchAlertSummaries,
updateViewState,
bugTemplate,
modifyAlert,
performanceTags,
} = this.props;
const {
alertSummary,
downstreamIds,
filteredAlerts,
allSelected,
selectedAlerts,
tableConfig,
filteredAndSortedAlerts,
} = this.state;
const downstreamIdsLength = downstreamIds.length;
const repo = alertSummary
? projects.find((repo) => repo.name === alertSummary.repository)
: null;
const repoModel = new RepositoryModel(repo);
return (
<Container fluid className="px-0 max-width-default">
<ErrorBoundary
errorClasses={errorMessageClass}
message={genericErrorMessage}
>
{filteredAlerts.length > 0 && alertSummary && (
<Form>
<Container fluid className="bg-lightgray border">
<Row className="px-0 max-width-default">
<Col
xs={10}
className="text-left alert-summary-header-element"
>
<FormGroup check className="d-inline-flex">
<SelectAlertsDropdown
setSelectedAlerts={this.setSelectedAlerts}
user={user}
filteredAlerts={filteredAlerts}
allSelected={allSelected}
alertSummaryId={alertSummary.id.toString()}
/>
<AlertHeader
frameworks={frameworks}
alertSummary={alertSummary}
repoModel={repoModel}
issueTrackers={issueTrackers}
user={user}
updateAssignee={this.updateAssignee}
changeRevision={this.changeRevision}
updateViewState={updateViewState}
/>
</FormGroup>
</Col>
<Col className="d-flex justify-content-end p-2">
<StatusDropdown
alertSummary={alertSummary}
updateState={(state) => this.setState(state)}
repoModel={repoModel}
updateViewState={updateViewState}
issueTrackers={issueTrackers}
bugTemplate={bugTemplate}
user={user}
filteredAlerts={filteredAlerts}
frameworks={frameworks}
performanceTags={performanceTags}
/>
</Col>
</Row>
</Container>
<Table className="compare-table mb-0">
<tbody>
<tr className="border subtest-header">
<th aria-label="Select alerts"> </th>
<th aria-label="Star alert or open graph"> </th>
<th className="align-bottom">
<TableColumnHeader
column={tableConfig.Test}
data-testid={`${alertSummary.id} ${tableConfig.Test}`}
onChangeSort={this.onChangeSort}
/>
</th>
<th className="align-bottom">
<TableColumnHeader
column={tableConfig.Platform}
onChangeSort={this.onChangeSort}
/>
</th>
{alertSummary.framework === browsertimeId && (
<th className="align-bottom text-nowrap">
<span
data-testid={`${alertSummary.id} ${tableConfig.DebuggingInformation.name}`}
>
{tableConfig.DebuggingInformation.name}
</span>
<SortButtonDisabled
column={tableConfig.DebuggingInformation}
/>
</th>
)}
<th className="align-bottom">
<TableColumnHeader
column={tableConfig.NoiseProfile}
onChangeSort={this.onChangeSort}
/>
</th>
<th className="align-bottom text-nowrap">
<span>{tableConfig.TagsOptions.name}</span>
<SortButtonDisabled column={tableConfig.TagsOptions} />
</th>
<th className="align-bottom">
<TableColumnHeader
column={tableConfig.Magnitude}
onChangeSort={this.onChangeSort}
/>
</th>
<th className="align-bottom">
<TableColumnHeader
column={tableConfig.Confidence}
onChangeSort={this.onChangeSort}
/>
</th>
</tr>
{filteredAndSortedAlerts.length <=
maximumVisibleAlertSummaryRows &&
filteredAndSortedAlerts.map((alert) => (
<AlertTableRow
key={alert.id}
alertSummary={alertSummary}
alert={alert}
frameworks={frameworks}
user={user}
updateSelectedAlerts={(alerts) => this.setState(alerts)}
selectedAlerts={selectedAlerts}
updateViewState={updateViewState}
modifyAlert={modifyAlert}
fetchAlertSummaries={fetchAlertSummaries}
/>
))}
{filteredAndSortedAlerts.length >
maximumVisibleAlertSummaryRows && (
<CollapsableRows
filteredAndSortedAlerts={filteredAndSortedAlerts}
alertSummary={alertSummary}
frameworks={frameworks}
user={user}
updateSelectedAlerts={(alerts) => this.setState(alerts)}
selectedAlerts={selectedAlerts}
updateViewState={updateViewState}
modifyAlert={modifyAlert}
fetchAlertSummaries={fetchAlertSummaries}
/>
)}
{downstreamIdsLength > 0 && (
<tr
className={`${
alertSummary.notes
? 'border-top border-left border-right'
: 'border'
}`}
>
<td
colSpan="9"
className="text-left text-muted pl-3 py-4"
>
<span className="font-weight-bold">
Downstream alert summaries:{' '}
</span>
{downstreamIds.map((id, index) => (
<DownstreamSummary
key={id}
id={id}
alertSummaries={alertSummaries}
position={downstreamIdsLength - 1 - index}
updateViewState={updateViewState}
/>
))}
</td>
</tr>
)}
</tbody>
</Table>
{alertSummary.notes ||
allSelected ||
selectedAlerts.length > 0 ? (
<div className="border mb-4 sticky-footer max-width-default text-left text-muted p-0">
{alertSummary.notes && (
<div className="bg-white px-3 py-4">
<TruncatedText
color="darker-info"
title="Notes: "
maxLength={167}
text={alertSummary.notes}
/>
</div>
)}
{selectedAlerts.length > 0 && (
<AlertActionPanel
selectedAlerts={selectedAlerts}
allSelected={allSelected}
alertSummaries={alertSummaries}
alertSummary={alertSummary}
fetchAlertSummaries={fetchAlertSummaries}
updateState={(state) => this.setState(state)}
updateViewState={updateViewState}
modifyAlert={modifyAlert}
/>
)}
</div>
) : (
<br />
)}
</Form>
)}
</ErrorBoundary>
</Container>
);
}
}
AlertTable.propTypes = {
alertSummary: PropTypes.shape({}),
user: PropTypes.shape({}).isRequired,
alertSummaries: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
issueTrackers: PropTypes.arrayOf(PropTypes.shape({})),
optionCollectionMap: PropTypes.shape({}).isRequired,
filters: PropTypes.shape({
filterText: PropTypes.string,
hideDownstream: PropTypes.bool,
hideAssignedToOthers: PropTypes.bool,
}).isRequired,
fetchAlertSummaries: PropTypes.func.isRequired,
updateViewState: PropTypes.func.isRequired,
bugTemplate: PropTypes.shape({}),
modifyAlert: PropTypes.func,
updateAlertSummary: PropTypes.func,
projects: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
performanceTags: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
};
AlertTable.defaultProps = {
alertSummary: null,
issueTrackers: [],
bugTemplate: null,
modifyAlert: undefined,
// leverage dependency injection
// to improve code testability
updateAlertSummary,
};