Bug 1485226 - remove redux from IFV (#4107)

This commit is contained in:
Sarah Clements 2018-10-09 12:38:14 -07:00 коммит произвёл GitHub
Родитель 68f6001822
Коммит 51d7e684df
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 688 добавлений и 1184 удалений

Просмотреть файл

@ -1,31 +1,56 @@
import React from 'react';
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import MainView from './MainView';
import BugDetailsView from './BugDetailsView';
function App({ store }) {
return (
<Provider store={store}>
class App extends React.Component {
constructor(props) {
super(props);
this.updateAppState = this.updateAppState.bind(this);
// keep track of the mainviews graph data so the API won't be
// called again when navigating back from bugdetailsview;
// table API will be called every time it mounts.
this.state = { graphData: null };
}
updateAppState(state) {
this.setState(state);
}
render() {
return (
<HashRouter>
<main>
<Switch>
<Route exact path="/main" component={MainView} />
<Route path="/main?startday=:startday&endday=:endday&tree=:tree" component={MainView} />
(<Route
exact
path="/main"
render={props =>
(<MainView
{...props}
mainGraphData={this.state.graphData}
updateAppState={this.updateAppState}
/>)}
/>)
(<Route
path="/main?startday=:startday&endday=:endday&tree=:tree"
render={props =>
(<MainView
{...props}
mainGraphData={this.state.graphData}
updateAppState={this.updateAppState}
/>)}
/>)
<Route path="/bugdetails" component={BugDetailsView} />
<Route path="/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug" component={BugDetailsView} />
<Redirect from="/" to="/main" />
</Switch>
</main>
</HashRouter>
</Provider>
);
);
}
}
App.propTypes = {
store: PropTypes.shape({}).isRequired,
};
export default App;

Просмотреть файл

@ -1,41 +1,31 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { updateSelectedBugDetails, updateDateRange, updateTreeName } from './redux/actions';
import { getBugUrl } from '../helpers/url';
class BugColumn extends React.Component {
constructor(props) {
super(props);
this.updateStateData = this.updateStateData.bind(this);
}
// we're passing the mainview location object to bugdetails because using history.goBack()
// in bugdetailsview to navigate back to mainview displays this console warning:
// "Hash history go(n) causes a full page reload in this browser"
updateStateData() {
// bugdetailsview inherits data from the main view
const { data, updateDates, updateTree, updateBugDetails, from, to, tree } = this.props;
updateBugDetails(data.id, data.summary, 'BUG_DETAILS');
updateTree(tree, 'BUG_DETAILS');
updateDates(from, to, 'BUG_DETAILS');
}
render() {
const { tree, from, to } = this.props;
const { id } = this.props.data;
return (
<div>
<a className="ml-1" target="_blank" rel="noopener noreferrer" href={getBugUrl(id)}>{id}</a>
&nbsp;
<span className="ml-1 small-text bug-details">
<Link onClick={this.updateStateData} to={{ pathname: '/bugdetails', search: `?startday=${from}&endday=${to}&tree=${tree}&bug=${id}` }}>
details
</Link>
</span>
</div>
);
}
function BugColumn({ tree, startday, endday, data, location, graphData, updateAppState }) {
const { id, summary } = data;
return (
<div>
<a className="ml-1" target="_blank" rel="noopener noreferrer" href={getBugUrl(id)}>{id}</a>
&nbsp;
<span className="ml-1 small-text bug-details" onClick={() => updateAppState({ graphData })}>
<Link
to={{ pathname: '/bugdetails',
search: `?startday=${startday}&endday=${endday}&tree=${tree}&bug=${id}`,
state: { startday, endday, tree, id, summary, location },
}}
>
details
</Link>
</span>
</div>
);
}
BugColumn.propTypes = {
@ -43,30 +33,22 @@ BugColumn.propTypes = {
id: PropTypes.number.isRequired,
summary: PropTypes.string.isRequired,
}).isRequired,
updateDates: PropTypes.func,
updateTree: PropTypes.func,
updateBugDetails: PropTypes.func,
from: PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
startday: PropTypes.string.isRequired,
endday: PropTypes.string.isRequired,
tree: PropTypes.string.isRequired,
location: PropTypes.shape({}),
graphData: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.shape({}),
),
PropTypes.shape({}),
]),
updateAppState: PropTypes.func.isRequired,
};
BugColumn.defaultProps = {
updateTree: null,
updateDates: null,
updateBugDetails: null,
location: null,
graphData: null,
};
const mapStateToProps = state => ({
from: state.dates.from,
to: state.dates.to,
tree: state.mainTree.tree,
});
const mapDispatchToProps = dispatch => ({
updateBugDetails: (bugId, summary, name) => dispatch(updateSelectedBugDetails(bugId, summary, name)),
updateDates: (from, to, name) => dispatch(updateDateRange(from, to, name)),
updateTree: (tree, name) => dispatch(updateTreeName(tree, name)),
});
export default connect(mapStateToProps, mapDispatchToProps)(BugColumn);
export default BugColumn;

Просмотреть файл

@ -1,357 +1,130 @@
import 'react-table/react-table.css';
import React from 'react';
import { connect } from 'react-redux';
import { Container, Row, Col } from 'reactstrap';
import PropTypes from 'prop-types';
import { Row, Col } from 'reactstrap';
import { Link } from 'react-router-dom';
import Icon from 'react-fontawesome';
import Navigation from './Navigation';
import { fetchBugData, updateDateRange, updateTreeName, updateSelectedBugDetails } from './redux/actions';
import GenericTable from './GenericTable';
import GraphsContainer from './GraphsContainer';
import { updateQueryParams, calculateMetrics, prettyDate, validateQueryParams } from './helpers';
import { bugDetailsEndpoint, graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl,
getJobsUrl, bugzillaBugsApi } from '../helpers/url';
import { calculateMetrics, prettyDate } from './helpers';
import { bugDetailsEndpoint, getJobsUrl } from '../helpers/url';
import BugLogColumn from './BugLogColumn';
import ErrorMessages from './ErrorMessages';
import { stateName } from './constants';
import ErrorBoundary from './ErrorBoundary';
import Layout from './Layout';
import GenericTable from './GenericTable';
import withView from './View';
import DateOptions from './DateOptions';
class BugDetailsView extends React.Component {
constructor(props) {
super(props);
const BugDetailsView = (props) => {
const { graphData, tableData, initialParamsSet, startday, endday, updateState, bug,
summary, errorMessages, lastLocation, tableFailureStatus, graphFailureStatus } = props;
this.updateData = this.updateData.bind(this);
this.setQueryParams = this.setQueryParams.bind(this);
this.checkQueryValidation = this.checkQueryValidation.bind(this);
this.state = {
errorMessages: [],
initialParamsSet: null,
};
}
componentDidMount() {
this.setQueryParams();
}
componentWillReceiveProps(nextProps) {
const { history, from, to, tree, location, summary, bugId, bugzillaData,
updateBugDetails } = nextProps;
if (location.search !== this.props.location.search) {
this.checkQueryValidation(parseQueryParams(location.search), this.state.initialParamsSet);
}
// update query params in the address bar if dates or tree are updated
if (from !== this.props.from || to !== this.props.to || tree !== this.props.tree) {
const queryString = createQueryParams({ startday: from, endday: to, tree, bug: bugId });
if (queryString !== location.search) {
updateQueryParams('/bugdetails', queryString, history, this.props.location);
}
}
if (bugzillaData.bugs && bugzillaData.bugs.length > 0 && bugzillaData.bugs[0].summary !== summary) {
updateBugDetails(bugzillaData.bugs[0].id, bugzillaData.bugs[0].summary, stateName.detailsView);
}
}
setQueryParams() {
const { from, to, tree, location, bugId, fetchData } = this.props;
// props for bug details is provided by MainView, so if they are missing
// (user pastes url into address bar) we need to check query strings
if (!from || !to || !tree || !bugId) {
this.checkQueryValidation(parseQueryParams(location.search));
} else {
this.setState({ initialParamsSet: true });
fetchData(createApiUrl(graphsEndpoint, { startday: from, endday: to, tree, bug: bugId }), stateName.detailsViewGraphs);
}
}
checkQueryValidation(params, urlChanged = false) {
const messages = validateQueryParams(params, true);
const { errorMessages, initialParamsSet } = this.state;
if (messages.length > 0) {
this.setState({ errorMessages: messages });
} else {
if (errorMessages.length > 0) {
this.setState({ errorMessages: [] });
}
if (!initialParamsSet) {
this.setState({ initialParamsSet: true });
}
this.updateData(params, urlChanged);
}
}
updateData(params, urlChanged = false) {
const { startday, endday, tree, bug } = params;
const { updateTree, updateDates, fetchData, bugId, updateBugDetails, summary } = this.props;
updateDates(startday, endday, stateName.detailsView);
updateTree(tree, stateName.detailsView);
if (bug) {
fetchData(createApiUrl(graphsEndpoint, params), stateName.detailsViewGraphs);
}
if (bug !== bugId) {
updateBugDetails(bug, summary, stateName.detailsView);
fetchData(bugzillaBugsApi('bug', { include_fields: 'summary,id', id: bug }), 'BUGZILLA_BUG_DETAILS');
}
// the table library fetches data directly when its component mounts and in response
// to a user selecting pagesize or page; this condition will prevent duplicate requests
// when this component mounts and when the table mounts.
if (urlChanged) {
fetchData(createApiUrl(bugDetailsEndpoint, params), stateName.detailsView);
}
}
render() {
const { graphs, tableFailureMessage, graphFailureMessage, from, to, bugDetails, tree, bugId, summary,
graphFailureStatus, tableFailureStatus, isFetchingGraphs, isFetchingBugs } = this.props;
const columns = [
{
Header: 'Push Time',
accessor: 'push_time',
},
{
Header: 'Tree',
accessor: 'tree',
},
{
Header: 'Revision',
accessor: 'revision',
Cell: props => <a href={getJobsUrl({ repo: props.original.tree, revision: props.value, selectedJob: props.original.job_id })} target="_blank" rel="noopener noreferrer">{props.value}</a>,
},
{
Header: 'Platform',
accessor: 'platform',
},
{
Header: 'Build Type',
accessor: 'build_type',
},
{
Header: 'Test Suite',
accessor: 'test_suite',
minWidth: 200,
},
{
Header: 'Machine Name',
accessor: 'machine_name',
minWidth: 125,
},
{
Header: 'Log',
accessor: 'job_id',
Cell: props => <BugLogColumn {...props} />,
minWidth: 110,
},
];
const params = { startday: from, endday: to, tree, bug: bugId };
const { errorMessages, initialParamsSet } = this.state;
{
Header: 'Push Time',
accessor: 'push_time',
},
{
Header: 'Tree',
accessor: 'tree',
},
{
Header: 'Revision',
accessor: 'revision',
Cell: _props =>
(<a
href={getJobsUrl({ repo: _props.original.tree, revision: _props.value, selectedJob: _props.original.job_id })}
target="_blank"
rel="noopener noreferrer"
>
{_props.value}
</a>),
},
{
Header: 'Platform',
accessor: 'platform',
},
{
Header: 'Build Type',
accessor: 'build_type',
},
{
Header: 'Test Suite',
accessor: 'test_suite',
minWidth: 200,
},
{
Header: 'Machine Name',
accessor: 'machine_name',
minWidth: 125,
},
{
Header: 'Log',
accessor: 'job_id',
Cell: props => <BugLogColumn {...props} />,
minWidth: 110,
},
];
let graphOneData = null;
let graphTwoData = null;
let graphOneData = null;
let graphTwoData = null;
if (graphs && graphs.length > 0) {
({ graphOneData, graphTwoData } = calculateMetrics(graphs));
}
if (graphData.length > 0) {
({ graphOneData, graphTwoData } = calculateMetrics(graphData));
}
return (
<Container fluid style={{ marginBottom: '5rem', marginTop: '4.5rem', maxWidth: '1200px' }}>
<Navigation
params={params}
tableApi={bugDetailsEndpoint}
graphApi={graphsEndpoint}
bugId={bugId}
name={stateName.detailsView}
graphName="BUG_DETAILS_GRAPHS"
tree={tree}
/>
{(isFetchingGraphs || isFetchingBugs) &&
!(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
<div className="loading">
<Icon
spin
name="cog"
size="4x"
/>
</div>}
{(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
<ErrorMessages
failureMessage={tableFailureStatus ? tableFailureMessage : graphFailureMessage}
failureStatus={tableFailureStatus || graphFailureStatus}
errorMessages={errorMessages}
/>}
<Row>
<Col xs="12"><span className="pull-left"><Link to="/"><Icon name="arrow-left" className="pr-1" />
back</Link></span>
</Col>
</Row>
{errorMessages.length === 0 &&
return (
<Layout
{...props}
graphOneData={graphOneData}
graphTwoData={graphTwoData}
dateOptions
header={
<React.Fragment>
<Row>
<Col xs="12" className="mx-auto"><h1>Details for Bug {!bugId ? '' : bugId}</h1></Col>
</Row>
<Row>
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(from)} to ${prettyDate(to)} UTC`}</p>
<Col xs="12"><span className="pull-left"><Link to={(lastLocation || '/')}><Icon name="arrow-left" className="pr-1" />
back</Link></span>
</Col>
</Row>
{summary &&
<Row>
<Col xs="4" className="mx-auto"><p className="text-secondary text-center">{summary}</p></Col>
</Row>}
{bugDetails && bugDetails.count &&
<Row>
<Col xs="12" className="mx-auto"><p className="text-secondary">{bugDetails.count} total failures</p></Col>
</Row>}
</React.Fragment>}
<ErrorBoundary
stateName={stateName.detailsViewGraphs}
>
{graphOneData && graphTwoData &&
<GraphsContainer
graphOneData={graphOneData}
graphTwoData={graphTwoData}
name={stateName.detailsView}
tree={tree}
graphName={stateName.detailsViewGraphs}
tableApi={bugDetailsEndpoint}
params={params}
graphApi={graphsEndpoint}
bugId={bugId}
dateOptions
/>}
</ErrorBoundary>
<ErrorBoundary
stateName={stateName.detailsView}
>
{bugId && initialParamsSet &&
<GenericTable
bugs={bugDetails.results}
columns={columns}
name={stateName.detailsView}
tableApi={bugDetailsEndpoint}
totalPages={bugDetails.total_pages}
params={params}
/>}
</ErrorBoundary>
</Container>);
}
}
Container.propTypes = {
fluid: PropTypes.bool,
{!errorMessages.length && !tableFailureStatus && !graphFailureStatus &&
<React.Fragment>
<Row>
<Col xs="12" className="mx-auto"><h1>Details for Bug {!bug ? '' : bug}</h1></Col>
</Row>
<Row>
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(startday)} to ${prettyDate(endday)} UTC`}</p>
</Col>
</Row>
{summary &&
<Row>
<Col xs="4" className="mx-auto"><p className="text-secondary text-center">{summary}</p></Col>
</Row>}
{tableData && tableData.count &&
<Row>
<Col xs="12" className="mx-auto"><p className="text-secondary">{tableData.count} total failures</p></Col>
</Row>}
</React.Fragment>}
</React.Fragment>
}
table={
bug && initialParamsSet &&
<GenericTable
totalPages={tableData.total_pages}
columns={columns}
data={tableData.results}
updateState={updateState}
/>
}
datePicker={
<DateOptions
updateState={updateState}
/>
}
/>
);
};
BugDetailsView.propTypes = {
bugDetails: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.shape({
count: PropTypes.number,
total_pages: PropTypes.number,
results: PropTypes.arrayOf(
PropTypes.shape({
push_time: PropTypes.string.isRequired,
platform: PropTypes.string.isRequired,
revision: PropTypes.string.isRequired,
test_suite: PropTypes.string.isRequired,
tree: PropTypes.string.isRequired,
build_type: PropTypes.string.isRequired,
job_id: PropTypes.number.isRequired,
bug_id: PropTypes.number.isRequired,
}),
),
}),
]),
graphs: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.arrayOf(
PropTypes.shape({
failure_count: PropTypes.number,
test_runs: PropTypes.number,
date: PropTypes.string,
}),
),
]).isRequired,
bugzillaData: PropTypes.shape({
bugs: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number,
summary: PropTypes.string,
}),
),
}).isRequired,
history: PropTypes.shape({}).isRequired,
location: PropTypes.shape({
search: PropTypes.string,
}).isRequired,
fetchData: PropTypes.func,
updateDates: PropTypes.func,
updateTree: PropTypes.func,
updateBugDetails: PropTypes.func,
from: PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
tree: PropTypes.string.isRequired,
bugId: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
summary: PropTypes.string,
tableFailureMessage: PropTypes.object,
graphFailureMessage: PropTypes.object,
tableFailureStatus: PropTypes.number,
graphFailureStatus: PropTypes.number,
isFetchingBugs: PropTypes.bool,
isFetchingGraphs: PropTypes.bool,
const defaultState = {
route: '/bugdetails',
endpoint: bugDetailsEndpoint,
};
BugDetailsView.defaultProps = {
tableFailureMessage: null,
graphFailureMessage: null,
tableFailureStatus: null,
graphFailureStatus: null,
fetchData: null,
updateTree: null,
updateDates: null,
updateBugDetails: null,
bugDetails: null,
bugId: null,
summary: null,
isFetchingBugs: null,
isFetchingGraphs: null,
};
const mapStateToProps = state => ({
bugDetails: state.bugDetailsData.data,
graphs: state.bugDetailsGraphData.data,
isFetchingBugs: state.bugDetailsData.isFetching,
isFetchingGraphs: state.bugDetailsGraphData.isFetching,
tableFailureMessage: state.bugDetailsData.message,
graphFailureMessage: state.bugDetailsGraphData.message,
tableFailureStatus: state.bugDetailsData.failureStatus,
graphFailureStatus: state.bugDetailsGraphData.failureStatus,
from: state.bugDetailsDates.from,
to: state.bugDetailsDates.to,
tree: state.bugDetailsTree.tree,
bugId: state.bugDetails.bugId,
summary: state.bugDetails.summary,
bugzillaData: state.bugzillaBugDetails.data,
});
const mapDispatchToProps = dispatch => ({
fetchData: (url, name) => dispatch(fetchBugData(url, name)),
updateDates: (from, to, name) => dispatch(updateDateRange(from, to, name)),
updateTree: (tree, name) => dispatch(updateTreeName(tree, name)),
updateBugDetails: (bugId, summary, name) => dispatch(updateSelectedBugDetails(bugId, summary, name)),
});
export default connect(mapStateToProps, mapDispatchToProps)(BugDetailsView);
export default withView(defaultState)(BugDetailsView);

Просмотреть файл

@ -1,16 +1,13 @@
import React from 'react';
import { ButtonDropdown, DropdownToggle } from 'reactstrap';
import { connect } from 'react-redux';
import moment from 'moment';
import PropTypes from 'prop-types';
import DateRangePicker from './DateRangePicker';
import { fetchBugData, updateDateRange, fetchBugsThenBugzilla } from './redux/actions';
import { setDateRange } from './helpers';
import { createApiUrl } from '../helpers/url';
import { ISODate } from './helpers';
import DropdownMenuItems from './DropdownMenuItems';
class DateOptions extends React.Component {
export default class DateOptions extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -18,7 +15,6 @@ class DateOptions extends React.Component {
dateRange: '',
};
this.toggle = this.toggle.bind(this);
this.updateData = this.updateData.bind(this);
this.updateDateRange = this.updateDateRange.bind(this);
}
@ -42,26 +38,12 @@ class DateOptions extends React.Component {
// bug history is max 4 months
from = 120;
}
this.updateData(from);
}
updateData(fromDate) {
const { fetchData, fetchFullBugData, updateDates, name, graphName, tree, tableApi, graphApi, bugId } = this.props;
const { from, to } = setDateRange(moment().utc(), fromDate);
const params = { startday: from, endday: to, tree };
if (bugId) {
params.bug = bugId;
fetchData(createApiUrl(tableApi, params), name);
} else {
fetchFullBugData(createApiUrl(tableApi, params), name);
}
fetchData(createApiUrl(graphApi, params), graphName);
updateDates(from, to, name);
const startday = ISODate(moment().utc().subtract(from, 'days'));
this.props.updateState({ startday });
}
render() {
const { name, graphName, tree, tableApi, graphApi, bugId } = this.props;
const { updateState } = this.props;
const { dropdownOpen, dateRange } = this.state;
const dateOptions = ['last 7 days', 'last 30 days', 'custom range', 'entire history'];
@ -78,12 +60,7 @@ class DateOptions extends React.Component {
</ButtonDropdown>
{dateRange === 'custom range' &&
<DateRangePicker
tree={tree}
tableApi={tableApi}
graphApi={graphApi}
name={name}
graphName={graphName}
bugId={bugId}
updateState={updateState}
/>}
</div>
);
@ -91,31 +68,5 @@ class DateOptions extends React.Component {
}
DateOptions.propTypes = {
updateDates: PropTypes.func,
fetchData: PropTypes.func,
fetchFullBugData: PropTypes.func,
tree: PropTypes.string.isRequired,
bugId: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
name: PropTypes.string.isRequired,
tableApi: PropTypes.string.isRequired,
graphApi: PropTypes.string.isRequired,
graphName: PropTypes.string.isRequired,
updateState: PropTypes.func.isRequired,
};
DateOptions.defaultProps = {
fetchData: null,
updateDates: null,
fetchFullBugData: null,
bugId: null,
};
const mapDispatchToProps = dispatch => ({
fetchData: (url, name) => dispatch(fetchBugData(url, name)),
fetchFullBugData: (url, name) => dispatch(fetchBugsThenBugzilla(url, name)),
updateDates: (from, to, name) => dispatch(updateDateRange(from, to, name)),
});
export default connect(null, mapDispatchToProps)(DateOptions);

Просмотреть файл

@ -1,18 +1,15 @@
import React from 'react';
import 'react-day-picker/lib/style.css';
import { connect } from 'react-redux';
import DayPickerInput from 'react-day-picker/DayPickerInput';
import moment from 'moment';
import PropTypes from 'prop-types';
import { parseDate, formatDate } from 'react-day-picker/moment';
import { setTimeout } from 'timers';
import { Button } from 'reactstrap';
import PropTypes from 'prop-types';
import { ISODate } from './helpers';
import { createApiUrl } from '../helpers/url';
import { fetchBugData, updateDateRange, fetchBugsThenBugzilla } from './redux/actions';
class DateRangePicker extends React.Component {
export default class DateRangePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -53,19 +50,12 @@ class DateRangePicker extends React.Component {
}
updateData() {
const { graphName, fetchData, updateDates, fetchFullBugData, name, tree, bugId, tableApi, graphApi } = this.props;
const from = ISODate(moment(this.state.from));
const to = ISODate(moment(this.state.to));
const params = { startday: from, endday: to, tree };
const { from, to } = this.state;
if (bugId) {
params.bug = bugId;
fetchData(createApiUrl(tableApi, params), name);
} else {
fetchFullBugData(createApiUrl(tableApi, params), name);
}
fetchData(createApiUrl(graphApi, params), graphName);
updateDates(from, to, name);
const startday = ISODate(moment(from));
const endday = ISODate(moment(to));
this.props.updateState({ startday, endday });
}
render() {
@ -116,31 +106,9 @@ class DateRangePicker extends React.Component {
}
DateRangePicker.propTypes = {
updateDates: PropTypes.func,
fetchData: PropTypes.func,
fetchFullBugData: PropTypes.func,
tree: PropTypes.string.isRequired,
bugId: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
name: PropTypes.string.isRequired,
tableApi: PropTypes.string.isRequired,
graphApi: PropTypes.string.isRequired,
graphName: PropTypes.string.isRequired,
updateState: PropTypes.func.isRequired,
};
DateRangePicker.defaultProps = {
fetchData: null,
updateDates: null,
fetchFullBugData: null,
bugId: null,
updateState: null,
};
const mapDispatchToProps = dispatch => ({
fetchData: (url, name) => dispatch(fetchBugData(url, name)),
fetchFullBugData: (url, name) => dispatch(fetchBugsThenBugzilla(url, name)),
updateDates: (from, to, name) => dispatch(updateDateRange(from, to, name)),
});
export default connect(null, mapDispatchToProps)(DateRangePicker);

Просмотреть файл

@ -1,48 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { hasError } from './redux/actions';
import { prettyErrorMessages } from './constants';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch() {
const { errorFound, stateName } = this.props;
// display fallback UI and reset isFetching to turn off the loading spinner
this.setState({ hasError: true }, () => errorFound(stateName));
// TODO: set up a logger to record error and { componentStack }
}
render() {
if (this.state.hasError) {
return <p className="text-danger py-2">{prettyErrorMessages.default}</p>;
}
return this.props.children;
}
}
ErrorBoundary.propTypes = {
stateName: PropTypes.string.isRequired,
children: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.bool,
]),
errorFound: PropTypes.func.isRequired,
};
ErrorBoundary.defaultProps = {
children: null,
};
const mapDispatchToProps = dispatch => ({
errorFound: name => dispatch(hasError(name)),
});
export default connect(null, mapDispatchToProps)(ErrorBoundary);

Просмотреть файл

@ -4,7 +4,7 @@ import { Alert } from 'reactstrap';
import { processErrorMessage } from './helpers';
const ErrorMessages = ({ failureMessage, failureStatus, errorMessages }) => {
const messages = errorMessages.length > 0 ? errorMessages : processErrorMessage(failureMessage, failureStatus);
const messages = errorMessages.length ? errorMessages : processErrorMessage(failureMessage, failureStatus);
return (
<div>
@ -16,7 +16,12 @@ const ErrorMessages = ({ failureMessage, failureStatus, errorMessages }) => {
};
ErrorMessages.propTypes = {
failureMessage: PropTypes.object,
failureMessage: PropTypes.oneOfType([
PropTypes.object,
PropTypes.arrayOf(
PropTypes.string,
),
]),
failureStatus: PropTypes.number,
errorMessages: PropTypes.array,
};

Просмотреть файл

@ -3,13 +3,10 @@ import { Table } from 'reactstrap';
import ReactTable from 'react-table';
import 'react-table/react-table.css';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { fetchBugData, fetchBugsThenBugzilla } from './redux/actions';
import { createApiUrl } from '../helpers/url';
import { sortData } from './helpers';
import { sortData, tableRowStyling } from './helpers';
class GenericTable extends React.Component {
export default class GenericTable extends React.Component {
constructor(props) {
super(props);
this.state = {
@ -21,65 +18,39 @@ class GenericTable extends React.Component {
this.updateTable = this.updateTable.bind(this);
}
updateData(page, pageSize) {
const { fetchData, fetchFullBugData, name, params, tableApi } = this.props;
params.page = page;
params.page_size = pageSize;
if (name === 'BUGS') {
fetchFullBugData(createApiUrl(tableApi, params), name);
} else {
fetchData(createApiUrl(tableApi, params), name);
}
}
updateTable(state) {
const { page, pageSize } = this.state;
let { page, pageSize } = this.state;
// table's page count starts at 0
if (state.page + 1 !== page || state.pageSize !== pageSize) {
this.updateData(state.page + 1, state.pageSize);
this.setState({ page: state.page + 1, pageSize: state.pageSize });
page = state.page + 1;
pageSize = state.pageSize;
this.props.updateState({ page, pageSize }, true);
this.setState({ page, pageSize });
} else if (state.sorted.length > 0) {
this.setState({ columnId: state.sorted[0].id, descending: state.sorted[0].desc });
}
}
bugRowStyling(state, bug) {
if (bug) {
const style = { color: '#aaa' };
if (bug.row.status === 'RESOLVED' || bug.row.status === 'VERIFIED') {
style.textDecoration = 'line-through';
return { style };
}
const disabledStrings = new RegExp('(disabled|annotated|marked)', 'i');
if (disabledStrings.test(bug.row.whiteboard)) {
return { style };
}
}
return {};
}
render() {
const { bugs, columns, trStyling, totalPages } = this.props;
const { data, columns, totalPages } = this.props;
const { columnId, descending } = this.state;
let sortedData = [];
if (columnId) {
sortedData = sortData([...bugs], columnId, descending);
sortedData = sortData([...data], columnId, descending);
}
return (
<ReactTable
manual
data={sortedData.length > 0 ? sortedData : bugs}
data={sortedData.length > 0 ? sortedData : data}
onFetchData={this.updateTable}
pages={totalPages}
showPageSizeOptions
columns={columns}
className="-striped"
getTrProps={trStyling ? this.bugRowStyling : () => ({})}
getTrProps={tableRowStyling}
showPaginationTop
/>
);
@ -94,36 +65,14 @@ Table.propTypes = {
};
GenericTable.propTypes = {
bugs: PropTypes.arrayOf(PropTypes.shape({})),
data: PropTypes.arrayOf(PropTypes.shape({})),
columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
params: PropTypes.shape({
startday: PropTypes.string.isRequired,
endday: PropTypes.string.isRequired,
tree: PropTypes.string.isRequired,
bug: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
}).isRequired,
fetchData: PropTypes.func,
fetchFullBugData: PropTypes.func,
name: PropTypes.string.isRequired,
tableApi: PropTypes.string.isRequired,
trStyling: PropTypes.bool,
updateState: PropTypes.func,
totalPages: PropTypes.number,
};
GenericTable.defaultProps = {
trStyling: false,
fetchData: null,
fetchFullBugData: null,
totalPages: null,
bugs: undefined,
data: undefined,
updateState: null,
};
const mapDispatchToProps = dispatch => ({
fetchData: (url, name) => dispatch(fetchBugData(url, name)),
fetchFullBugData: (url, name) => dispatch(fetchBugsThenBugzilla(url, name)),
});
export default connect(null, mapDispatchToProps)(GenericTable);

Просмотреть файл

@ -3,8 +3,6 @@ import { Row, Button, Col } from 'reactstrap';
import PropTypes from 'prop-types';
import Graph from './Graph';
import DateOptions from './DateOptions';
import DateRangePicker from './DateRangePicker';
import { graphOneSpecs, graphTwoSpecs } from './constants';
export default class GraphsContainer extends React.Component {
@ -21,34 +19,23 @@ export default class GraphsContainer extends React.Component {
}
render() {
const { graphOneData, graphTwoData, dateOptions, name, graphName, tree, bugId, tableApi, graphApi } = this.props;
const { graphOneData, graphTwoData, children } = this.props;
const { showGraphTwo } = this.state;
return (
<React.Fragment>
<Row className="pt-5">
<Graph specs={graphOneSpecs} data={graphOneData} />
<Graph
specs={graphOneSpecs}
data={graphOneData}
/>
</Row>
<Row>
<Col xs="12" className="mx-auto pb-5">
<Button color="secondary" onClick={this.toggleGraph} className="d-inline-block mr-3">
{`${showGraphTwo ? 'less' : 'more'} graphs`}</Button>
{dateOptions ?
<DateOptions
name={name}
graphName={graphName}
tree={tree}
bugId={bugId}
tableApi={tableApi}
graphApi={graphApi}
/> : <DateRangePicker
tree={tree}
tableApi={tableApi}
graphApi={graphApi}
name={name}
graphName={graphName}
bugId={bugId}
/>}
{`${showGraphTwo ? 'less' : 'more'} graphs`}
</Button>
{children}
</Col>
</Row>
{showGraphTwo &&
@ -83,21 +70,10 @@ GraphsContainer.propTypes = {
}),
),
),
dateOptions: PropTypes.bool,
tree: PropTypes.string.isRequired,
bugId: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
name: PropTypes.string.isRequired,
tableApi: PropTypes.string.isRequired,
graphApi: PropTypes.string.isRequired,
graphName: PropTypes.string.isRequired,
children: PropTypes.object.isRequired,
};
GraphsContainer.defaultProps = {
bugId: null,
graphOneData: null,
graphTwoData: null,
dateOptions: false,
};

Просмотреть файл

@ -0,0 +1,124 @@
import React from 'react';
import { Container } from 'reactstrap';
import PropTypes from 'prop-types';
import Icon from 'react-fontawesome';
import Navigation from './Navigation';
import GraphsContainer from './GraphsContainer';
import ErrorMessages from './ErrorMessages';
import { prettyErrorMessages, errorMessageClass } from './constants';
import ErrorBoundary from '../shared/ErrorBoundary';
const Layout = (props) => {
const { graphData, tableData, errorMessages, tree, isFetchingTable,
isFetchingGraphs, tableFailureStatus, graphFailureStatus, updateState,
graphOneData, graphTwoData, table, datePicker, header } = props;
let failureMessage = null;
if (tableFailureStatus) {
failureMessage = tableData;
} else if (graphFailureStatus) {
failureMessage = graphData;
}
return (
<Container fluid style={{ marginBottom: '5rem', marginTop: '5rem', maxWidth: '1200px' }}>
<Navigation
updateState={updateState}
tree={tree}
/>
{(isFetchingGraphs || isFetchingTable) &&
!(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
<div className="loading">
<Icon spin name="cog" size="4x" />
</div>}
{(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
<ErrorMessages
failureMessage={failureMessage}
failureStatus={tableFailureStatus || graphFailureStatus}
errorMessages={errorMessages}
/>}
{header}
<ErrorBoundary
errorClasses={errorMessageClass}
message={prettyErrorMessages.default}
>
{graphOneData && graphTwoData &&
<GraphsContainer
graphOneData={graphOneData}
graphTwoData={graphTwoData}
>
{datePicker}
</GraphsContainer>}
</ErrorBoundary>
<ErrorBoundary
errorClasses={errorMessageClass}
message={prettyErrorMessages.default}
>
{table}
</ErrorBoundary>
</Container>);
};
Container.propTypes = {
fluid: PropTypes.bool,
};
Layout.propTypes = {
history: PropTypes.shape({}).isRequired,
location: PropTypes.shape({
search: PropTypes.string,
}).isRequired,
datePicker: PropTypes.oneOfType([
PropTypes.shape({}), PropTypes.bool,
]),
header: PropTypes.oneOfType([
PropTypes.shape({}), PropTypes.bool,
]),
table: PropTypes.oneOfType([
PropTypes.shape({}), PropTypes.bool,
]),
graphOneData: PropTypes.arrayOf(
PropTypes.shape({}),
),
graphTwoData: PropTypes.arrayOf(
PropTypes.arrayOf(
PropTypes.shape({}),
), PropTypes.arrayOf(
PropTypes.shape({}),
),
),
tableData: PropTypes.shape({}),
graphData: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.shape({}),
),
PropTypes.shape({}),
]),
tree: PropTypes.string,
errorMessages: PropTypes.arrayOf(PropTypes.string).isRequired,
updateState: PropTypes.func.isRequired,
tableFailureStatus: PropTypes.number,
graphFailureStatus: PropTypes.number,
isFetchingTable: PropTypes.bool,
isFetchingGraphs: PropTypes.bool,
};
Layout.defaultProps = {
graphOneData: null,
graphTwoData: null,
dateOptions: null,
tableFailureStatus: null,
graphFailureStatus: null,
isFetchingTable: null,
isFetchingGraphs: null,
tableData: null,
graphData: null,
tree: null,
table: null,
header: null,
datePicker: null,
};
export default Layout;

Просмотреть файл

@ -1,316 +1,113 @@
import React from 'react';
import { connect } from 'react-redux';
import { Container, Row, Col } from 'reactstrap';
import { Row, Col } from 'reactstrap';
import PropTypes from 'prop-types';
import Icon from 'react-fontawesome';
import moment from 'moment';
import Navigation from './Navigation';
import GenericTable from './GenericTable';
import { fetchBugData, updateTreeName, updateDateRange, fetchBugsThenBugzilla } from './redux/actions';
import BugColumn from './BugColumn';
import { updateQueryParams, mergeData, calculateMetrics, prettyDate, validateQueryParams } from './helpers';
import GraphsContainer from './GraphsContainer';
import { bugsEndpoint, graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl } from '../helpers/url';
import ErrorMessages from './ErrorMessages';
import { stateName } from './constants';
import ErrorBoundary from './ErrorBoundary';
import { calculateMetrics, prettyDate, ISODate } from './helpers';
import { bugsEndpoint } from '../helpers/url';
import GenericTable from './GenericTable';
import withView from './View';
import Layout from './Layout';
import DateRangePicker from './DateRangePicker';
class MainView extends React.Component {
constructor(props) {
super(props);
this.updateData = this.updateData.bind(this);
this.setQueryParams = this.setQueryParams.bind(this);
this.checkQueryValidation = this.checkQueryValidation.bind(this);
const MainView = (props) => {
const { graphData, tableData, initialParamsSet, startday, endday, updateState,
tree, location, updateAppState } = props;
this.state = {
errorMessages: [],
initialParamsSet: null,
};
const columns = [
{
Header: 'Bug',
accessor: 'id',
headerClassName: 'bug-column-header',
className: 'bug-column',
maxWidth: 150,
Cell: _props =>
(<BugColumn
data={_props.original}
tree={tree}
startday={startday}
endday={endday}
location={location}
graphData={graphData}
updateAppState={updateAppState}
/>),
},
{
Header: 'Count',
accessor: 'count',
maxWidth: 100,
},
{
Header: 'Summary',
accessor: 'summary',
minWidth: 250,
},
{
Header: 'Whiteboard',
accessor: 'whiteboard',
minWidth: 150,
},
];
let graphOneData = null;
let graphTwoData = null;
let totalFailures = 0;
let totalRuns = 0;
if (graphData.length) {
({ graphOneData, graphTwoData, totalFailures, totalRuns } = calculateMetrics(graphData));
}
componentDidMount() {
this.setQueryParams();
}
componentWillReceiveProps(nextProps) {
const { from, to, tree, location, history } = nextProps;
// update all data if the user edits dates or tree via the query params
if (location.search !== this.props.location.search) {
this.checkQueryValidation(parseQueryParams(location.search), this.state.initialParamsSet);
}
// update query params if dates or tree are updated
if (from !== this.props.from || to !== this.props.to || tree !== this.props.tree) {
const queryString = createQueryParams({ startday: from, endday: to, tree });
if (queryString !== location.search) {
updateQueryParams('/main', queryString, history, this.props.location);
return (
<Layout
{...props}
graphOneData={graphOneData}
graphTwoData={graphTwoData}
header={
initialParamsSet &&
<React.Fragment>
<Row>
<Col xs="12" className="mx-auto pt-3"><h1>Intermittent Test Failures</h1></Col>
</Row>
<Row>
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(startday)} to ${prettyDate(endday)} UTC`}</p>
</Col>
</Row>
<Row>
<Col xs="12" className="mx-auto"><p className="text-secondary">{totalFailures} bugs in {totalRuns} pushes</p>
</Col>
</Row>
</React.Fragment>
}
}
}
setQueryParams() {
const { from, to, tree, location, history, graphs, fetchData } = this.props;
// if the query params are not specified, set params based on default props
// otherwise update data based on the params
if (location.search === '') {
const params = { startday: from, endday: to, tree };
const queryString = createQueryParams(params);
this.setState({ initialParamsSet: true });
updateQueryParams('/main', queryString, history, location);
if (Object.keys(graphs).length === 0) {
// only fetch graph data on initial page load; table component fetches
// data when being mounted
fetchData(createApiUrl(graphsEndpoint, params), stateName.mainViewGraphs);
}
} else {
// show an error message if query strings are missing when url is pasted into
// address bar, otherwise fetch data
this.checkQueryValidation(parseQueryParams(location.search));
}
}
checkQueryValidation(params, urlChanged = false) {
const messages = validateQueryParams(params);
const { errorMessages, initialParamsSet } = this.state;
if (messages.length > 0) {
this.setState({ errorMessages: messages });
} else {
if (errorMessages.length > 0) {
this.setState({ errorMessages: [] });
}
if (!initialParamsSet) {
this.setState({ initialParamsSet: true });
}
this.updateData(params, urlChanged);
}
}
updateData(params, urlChanged) {
const { startday, endday, tree } = params;
const { updateTree, updateDates, fetchData, fetchFullBugData } = this.props;
updateDates(startday, endday, stateName.mainView);
updateTree(tree, stateName.mainView);
fetchData(createApiUrl(graphsEndpoint, params), stateName.mainViewGraphs);
// the table library fetches data directly when its component mounts and in response
// to a user selecting pagesize or page; this condition will prevent duplicate requests
// when this component mounts and when the table mounts.
if (urlChanged) {
fetchFullBugData(createApiUrl(bugsEndpoint, params), stateName.mainView);
}
}
render() {
const { bugs, tableFailureMessage, graphFailureMessage, from, to, tree, bugzillaData, graphs,
tableFailureStatus, graphFailureStatus, isFetchingBugs, isFetchingGraphs } = this.props;
const columns = [
{
Header: 'Bug',
accessor: 'id',
headerClassName: 'bug-column-header',
className: 'bug-column',
maxWidth: 150,
Cell: props => <BugColumn data={props.original} />,
},
{
Header: 'Count',
accessor: 'count',
maxWidth: 100,
},
{
Header: 'Summary',
accessor: 'summary',
minWidth: 250,
},
{
Header: 'Whiteboard',
accessor: 'whiteboard',
minWidth: 150,
},
];
let bugsData = [];
let graphOneData = null;
let graphTwoData = null;
let totalFailures = 0;
let totalRuns = 0;
if (bugs.results && bugzillaData.bugs && bugzillaData.bugs.length > 0) {
bugsData = mergeData(bugs.results, bugzillaData.bugs);
}
if (graphs && graphs.length > 0) {
({ graphOneData, graphTwoData, totalFailures, totalRuns } = calculateMetrics(graphs));
}
const params = { startday: from, endday: to, tree };
const { errorMessages, initialParamsSet } = this.state;
return (
<Container fluid style={{ marginBottom: '5rem', marginTop: '5rem', maxWidth: '1200px' }}>
<Navigation
name={stateName.mainView}
graphName={stateName.mainViewGraphs}
tableApi={bugsEndpoint}
params={params}
graphApi={graphsEndpoint}
tree={tree}
table={
initialParamsSet &&
<GenericTable
totalPages={tableData.total_pages}
columns={columns}
data={tableData.results}
updateState={updateState}
/>
{(isFetchingGraphs || isFetchingBugs) &&
!(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
<div className="loading">
<Icon
spin
name="cog"
size="4x"
/>
</div>}
{(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
<ErrorMessages
failureMessage={tableFailureStatus ? tableFailureMessage : graphFailureMessage}
failureStatus={tableFailureStatus || graphFailureStatus}
errorMessages={errorMessages}
/>}
<Row>
<Col xs="12" className="mx-auto pt-3"><h1>Intermittent Test Failures</h1></Col>
</Row>
<Row>
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(from)} to ${prettyDate(to)} UTC`}</p>
</Col>
</Row>
<Row>
<Col xs="12" className="mx-auto"><p className="text-secondary">{totalFailures} bugs in {totalRuns} pushes</p>
</Col>
</Row>
<ErrorBoundary
stateName={stateName.mainViewGraphs}
>
{graphOneData && graphTwoData &&
<GraphsContainer
graphOneData={graphOneData}
graphTwoData={graphTwoData}
name={stateName.mainView}
params={params}
graphName={stateName.mainViewGraphs}
tableApi={bugsEndpoint}
graphApi={graphsEndpoint}
tree={tree}
/>}
</ErrorBoundary>
<ErrorBoundary
stateName={stateName.mainView}
>
{initialParamsSet &&
<GenericTable
bugs={bugsData}
columns={columns}
name={stateName.mainView}
tableApi={bugsEndpoint}
params={params}
totalPages={bugs.total_pages}
trStyling
/>}
</ErrorBoundary>
</Container>);
}
}
Container.propTypes = {
fluid: PropTypes.bool,
}
datePicker={
<DateRangePicker
updateState={updateState}
/>
}
/>
);
};
MainView.propTypes = {
bugs: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.shape({
count: PropTypes.number,
total_pages: PropTypes.number,
results: PropTypes.arrayOf(
PropTypes.shape({
bug_id: PropTypes.number,
bug_count: PropTypes.number,
}),
),
}),
]).isRequired,
graphs: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.arrayOf(
PropTypes.shape({
failure_count: PropTypes.number,
test_runs: PropTypes.number,
date: PropTypes.string,
}),
),
]).isRequired,
bugzillaData: PropTypes.shape({
bugs: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number,
failureStatus: PropTypes.string,
summary: PropTypes.string,
whiteboard: PropTypes.string,
}),
),
}).isRequired,
history: PropTypes.shape({}).isRequired,
location: PropTypes.shape({
search: PropTypes.string,
}).isRequired,
fetchData: PropTypes.func,
updateDates: PropTypes.func,
updateTree: PropTypes.func,
fetchFullBugData: PropTypes.func,
from: PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
tree: PropTypes.string.isRequired,
tableFailureMessage: PropTypes.object,
graphFailureMessage: PropTypes.object,
tableFailureStatus: PropTypes.number,
graphFailureStatus: PropTypes.number,
isFetchingBugs: PropTypes.bool,
isFetchingGraphs: PropTypes.bool,
location: PropTypes.shape({}).isRequired,
};
MainView.defaultProps = {
tableFailureMessage: null,
graphFailureMessage: null,
tableFailureStatus: null,
graphFailureStatus: null,
fetchData: null,
updateTree: null,
updateDates: null,
fetchFullBugData: null,
isFetchingBugs: null,
isFetchingGraphs: null,
const defaultState = {
tree: 'trunk',
startday: ISODate(moment().utc().subtract(7, 'days')),
endday: ISODate(moment().utc()),
route: '/main',
endpoint: bugsEndpoint,
};
const mapStateToProps = state => ({
bugs: state.bugsData.data,
graphs: state.bugsGraphData.data,
isFetchingBugs: state.bugsData.isFetching,
isFetchingGraphs: state.bugsGraphData.isFetching,
tableFailureMessage: state.bugsData.message,
tableFailureStatus: state.bugsData.failureStatus,
graphFailureMessage: state.bugsGraphData.message,
graphFailureStatus: state.bugsGraphData.failureStatus,
from: state.dates.from,
to: state.dates.to,
tree: state.mainTree.tree,
bugzillaData: state.bugzilla.data,
});
const mapDispatchToProps = dispatch => ({
fetchData: (url, name) => dispatch(fetchBugData(url, name)),
fetchFullBugData: (url, name) => dispatch(fetchBugsThenBugzilla(url, name)),
updateDates: (from, to, name) => dispatch(updateDateRange(from, to, name)),
updateTree: (tree, name) => dispatch(updateTreeName(tree, name)),
});
export default connect(mapStateToProps, mapDispatchToProps)(MainView);
export default withView(defaultState)(MainView);

Просмотреть файл

@ -1,39 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Collapse, Navbar, Nav, UncontrolledDropdown, DropdownToggle } from 'reactstrap';
import { updateTreeName, fetchBugData, fetchBugsThenBugzilla } from './redux/actions';
import { createApiUrl } from '../helpers/url';
import DropdownMenuItems from './DropdownMenuItems';
import { treeOptions } from './constants';
class Navigation extends React.Component {
export default class Navigation extends React.Component {
constructor(props) {
super(props);
this.state = { isOpen: false };
this.toggle = this.toggle.bind(this);
this.updateData = this.updateData.bind(this);
}
toggle() {
this.setState({ isOpen: !this.state.isOpen });
}
updateData(tree) {
const { updateTree, fetchData, fetchFullBugData, name, graphName, params, bugId, tableApi, graphApi } = this.props;
params.tree = tree;
if (bugId) {
fetchData(createApiUrl(tableApi, params), name);
} else {
fetchFullBugData(createApiUrl(tableApi, params), name);
}
fetchData(createApiUrl(graphApi, params), graphName);
updateTree(tree, name);
}
render() {
return (
<Navbar expand fixed="top" className="top-navbar">
@ -46,7 +29,7 @@ class Navigation extends React.Component {
</DropdownToggle>
<DropdownMenuItems
options={treeOptions}
updateData={this.updateData}
updateData={tree => this.props.updateState({ tree })}
default={this.props.tree}
/>
</UncontrolledDropdown>
@ -61,40 +44,11 @@ Nav.propTypes = {
};
Navigation.propTypes = {
params: PropTypes.shape({
startday: PropTypes.string.isRequired,
endday: PropTypes.string.isRequired,
tree: PropTypes.string.isRequired,
bug: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
}).isRequired,
updateTree: PropTypes.func,
fetchData: PropTypes.func,
fetchFullBugData: PropTypes.func,
name: PropTypes.string.isRequired,
tree: PropTypes.string.isRequired,
bugId: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
tableApi: PropTypes.string.isRequired,
graphApi: PropTypes.string.isRequired,
graphName: PropTypes.string.isRequired,
tree: PropTypes.string,
updateState: PropTypes.func,
};
Navigation.defaultProps = {
bugId: null,
updateTree: null,
fetchData: null,
fetchFullBugData: null,
tree: null,
updateState: null,
};
const mapDispatchToProps = dispatch => ({
updateTree: (tree, name) => dispatch(updateTreeName(tree, name)),
fetchData: (url, name) => dispatch(fetchBugData(url, name)),
fetchFullBugData: (url, name) => dispatch(fetchBugsThenBugzilla(url, name)),
});
export default connect(null, mapDispatchToProps)(Navigation);

Просмотреть файл

@ -0,0 +1,190 @@
import React from 'react';
import PropTypes from 'prop-types';
import { updateQueryParams, validateQueryParams, getData, mergeData, formatBugs } from './helpers';
import { graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl,
bugzillaBugsApi } from '../helpers/url';
const withView = defaultState => WrappedComponent =>
class View extends React.Component {
constructor(props) {
super(props);
this.updateData = this.updateData.bind(this);
this.setQueryParams = this.setQueryParams.bind(this);
this.checkQueryValidation = this.checkQueryValidation.bind(this);
this.getTableData = this.getTableData.bind(this);
this.getGraphData = this.getGraphData.bind(this);
this.updateState = this.updateState.bind(this);
this.getBugDetails = this.getBugDetails.bind(this);
this.default = (this.props.location.state || defaultState);
this.state = {
errorMessages: [],
initialParamsSet: false,
tree: (this.default.tree || null),
startday: (this.default.startday || null),
endday: (this.default.endday || null),
bug: (this.default.id || null),
summary: (this.default.summary || null),
tableData: {},
tableFailureStatus: null,
isFetchingTable: false,
graphData: [],
graphFailureStatus: null,
isFetchingGraphs: false,
page: 0,
pageSize: 20,
lastLocation: (this.default.location || null),
};
}
componentDidMount() {
this.setQueryParams();
}
componentWillReceiveProps(nextProps) {
const { location } = nextProps;
// update all data if the user edits dates, tree or bug via the query params
if (location.search !== this.props.location.search) {
this.checkQueryValidation(parseQueryParams(location.search), this.state.initialParamsSet);
}
}
setQueryParams() {
const { location, history } = this.props;
const { startday, endday, tree, bug } = this.state;
const params = { startday, endday, tree };
if (bug) {
params.bug = bug;
}
if (location.search !== '' && !location.state) {
// update data based on the params or show error if params are missing
this.checkQueryValidation(parseQueryParams(location.search));
} else {
// if the query params are not specified for mainview, set params based on default state
if (location.search === '') {
const queryString = createQueryParams(params);
updateQueryParams(defaultState.route, queryString, history, location);
}
this.setState({ initialParamsSet: true });
this.getGraphData(createApiUrl(graphsEndpoint, params));
}
}
async getBugDetails(url) {
const { data, failureStatus } = await getData(url);
if (!failureStatus && data.bugs.length === 1) {
this.setState({ summary: data.bugs[0].summary });
}
}
async getTableData(url) {
this.setState({ tableFailureStatus: null, isFetchingTable: true });
const { data, failureStatus } = await getData(url);
if (defaultState.route === '/main' && !failureStatus) {
const bugs_list = formatBugs(data.results);
const bugzillaUrl = bugzillaBugsApi('bug', {
include_fields: 'id,status,summary,whiteboard',
id: bugs_list,
});
const bugzillaData = await getData(bugzillaUrl);
const results = mergeData(data.results, bugzillaData.data.bugs);
data.results = results;
}
this.setState({ tableData: data, tableFailureStatus: failureStatus, isFetchingTable: false });
}
async getGraphData(url) {
this.setState({ graphFailureStatus: null, isFetchingGraphs: true });
const { data, failureStatus } = await getData(url);
this.setState({ graphData: data, graphFailureStatus: failureStatus, isFetchingGraphs: false });
}
updateState(updatedObj, updateTable = false) {
this.setState(updatedObj, () => {
const { startday, endday, tree, page, pageSize, bug } = this.state;
const params = { startday, endday, tree, page, pageSize };
if (bug) {
params.bug = bug;
}
if (!updateTable) {
this.getGraphData(createApiUrl(graphsEndpoint, params));
}
this.getTableData(createApiUrl(defaultState.endpoint, params));
// update query params if dates or tree are updated
const queryString = createQueryParams(params);
updateQueryParams(defaultState.route, queryString, this.props.history, this.props.location);
});
}
updateData(params, urlChanged = false) {
const { mainGraphData } = this.props;
if (mainGraphData && !urlChanged) {
this.setState({ graphData: mainGraphData });
} else {
this.getGraphData(createApiUrl(graphsEndpoint, params));
}
// the table library fetches data directly when its component mounts and in response
// to a user selecting pagesize or page; this condition will prevent duplicate requests
// when this component mounts and when the table mounts.
if (urlChanged) {
this.getTableData(createApiUrl(defaultState.endpoint, params));
}
if (params.bug && Object.keys(this.state.tableData).length) {
this.getBugDetails(bugzillaBugsApi('bug', { include_fields: 'summary', id: params.bug }));
}
}
checkQueryValidation(params, urlChanged = false) {
const { errorMessages, initialParamsSet, summary } = this.state;
const messages = validateQueryParams(params, defaultState.route === '/bugdetails');
const updates = {};
if (messages.length > 0) {
this.setState({ errorMessages: messages });
} else {
if (errorMessages.length) {
updates.errorMessages = [];
}
if (!initialParamsSet) {
updates.initialParamsSet = true;
}
if (summary) {
// reset summary
updates.summary = null;
}
this.setState({ ...updates, ...params });
this.updateData(params, urlChanged);
}
}
render() {
const updateState = { updateState: this.updateState };
const newProps = { ...this.props, ...this.state, ...updateState };
return (
<WrappedComponent {...newProps} />
);
}
};
withView.propTypes = {
history: PropTypes.shape({}).isRequired,
location: PropTypes.shape({
search: PropTypes.string,
}).isRequired,
};
export default withView;

Просмотреть файл

@ -56,10 +56,4 @@ export const prettyErrorMessages = {
status503: 'There was a problem retrieving the data. Please try again in a minute.',
};
// names of the specific redux state slices to update when dispatching actions
export const stateName = {
mainView: 'BUGS',
mainViewGraphs: 'BUGS_GRAPHS',
detailsView: 'BUG_DETAILS',
detailsViewGraphs: 'BUG_DETAILS_GRAPHS',
};
export const errorMessageClass = 'text-danger py-4 d-block';

Просмотреть файл

@ -10,12 +10,6 @@ export const prettyDate = function formatPrettyDate(date) {
return moment(date).format('ddd MMM D, YYYY');
};
export const setDateRange = function setISODateRange(day, numDays) {
const to = ISODate(day);
const from = ISODate(day.subtract(numDays, 'days'));
return { from, to };
};
export const formatBugs = function formatBugsForBugzilla(data) {
let bugs = '';
for (let i = 0; i < data.length; i++) {
@ -123,3 +117,36 @@ export const validateQueryParams = function validateQueryParams(params, bugRequi
}
return messages;
};
export const getData = async function getData(url) {
let failureStatus = null;
const response = await fetch(url);
if (!response.ok) {
failureStatus = response.status;
}
if (response.headers.get('content-type') === 'text/html' && failureStatus) {
return { data: { [failureStatus]: response.statusText }, failureStatus };
}
const data = await response.json();
return { data, failureStatus };
};
export const tableRowStyling = function tableRowStyling(state, bug) {
if (bug) {
const style = { color: '#aaa' };
if (bug.row.status === 'RESOLVED' || bug.row.status === 'VERIFIED') {
style.textDecoration = 'line-through';
return { style };
}
const disabledStrings = new RegExp('(disabled|annotated|marked)', 'i');
if (disabledStrings.test(bug.row.whiteboard)) {
return { style };
}
}
return {};
};

Просмотреть файл

@ -7,12 +7,11 @@ import 'font-awesome/css/font-awesome.css';
import '../css/treeherder-global.css';
import '../css/intermittent-failures.css';
import App from './App';
import store from './redux/store';
function load() {
render((
<AppContainer>
<App store={store} />
<App />
</AppContainer>
), document.getElementById('root'));
}

Просмотреть файл

@ -1,68 +0,0 @@
import { formatBugs } from '../helpers';
import { bugzillaBugsApi } from '../../helpers/url';
const fetchBugDataFailure = (name, error, failureStatus) => ({
type: `FETCH_${name}_FAILURE`,
message: error,
failureStatus,
});
export const updateSelectedBugDetails = (bugId, summary, name) => ({
type: `UPDATE_SELECTED_${name}`,
bugId,
summary,
});
export const processingRequest = name => ({
type: `REQUESTING_${name}_DATA`,
});
export const hasError = name => ({
type: `${name}_ERROR`,
});
export const fetchBugData = (url, name) => (dispatch) => {
// reset when fetching data after a previous failure
let status = null;
dispatch(fetchBugDataFailure(name, {}, null));
dispatch(processingRequest(name));
return fetch(url)
.then((response) => {
status = response.status;
return response.json();
})
.then((data) => {
if (status === 200) {
return dispatch({
type: `FETCH_${name}_SUCCESS`,
data,
});
}
return dispatch(fetchBugDataFailure(name, data, status));
});
};
export const fetchBugsThenBugzilla = (url, name) => (dispatch, getState) => (
dispatch(fetchBugData(url, name))
.then(() => {
if (!getState().bugsData.failureStatus) {
const { results } = getState().bugsData.data;
const bugs_list = formatBugs(results);
return dispatch(fetchBugData(bugzillaBugsApi('bug', {
include_fields: 'id,status,summary,whiteboard',
id: bugs_list,
}), `BUGZILLA_${name}`));
}
})
);
export const updateDateRange = (from, to, name) => ({
type: `UPDATE_${name}_DATE_RANGE`,
from,
to,
});
export const updateTreeName = (tree, name) => ({
type: `UPDATE_${name}_VIEW_TREE`,
tree,
});

Просмотреть файл

@ -1,71 +0,0 @@
import moment from 'moment';
import { setDateRange } from '../helpers';
export const fetchData = (name = '') => (state = { data: {}, message: {}, failureStatus: null, isFetching: true }, action) => {
switch (action.type) {
case `REQUESTING_${name}_DATA`:
return {
...state,
isFetching: true,
};
case `FETCH_${name}_SUCCESS`:
return {
...state,
data: action.data,
isFetching: false,
};
case `FETCH_${name}_FAILURE`:
return {
...state,
message: action.message,
failureStatus: action.failureStatus,
isFetching: false,
};
case `${name}_ERROR`:
return {
...state,
isFetching: false,
};
default:
return state;
}
};
export const updateDates = (name = '') => (state = setDateRange(moment().utc(), 7), action) => {
switch (action.type) {
case `UPDATE_${name}_DATE_RANGE`:
return {
...state,
from: action.from,
to: action.to,
};
default:
return state;
}
};
export const updateTree = (name = '') => (state = { tree: 'trunk' }, action) => {
switch (action.type) {
case `UPDATE_${name}_VIEW_TREE`:
return {
...state,
tree: action.tree,
};
default:
return state;
}
};
export const updateBugDetails = (name = '') => (state = { bugId: null, summary: null }, action) => {
switch (action.type) {
case `UPDATE_SELECTED_${name}`:
return {
...state,
bugId: action.bugId,
summary: action.summary,
};
default:
return state;
}
};

Просмотреть файл

@ -1,19 +0,0 @@
import { combineReducers } from 'redux';
import { fetchData, updateDates, updateTree, updateBugDetails } from './reducers';
const rootReducer = combineReducers({
bugsData: fetchData('BUGS'),
bugDetailsData: fetchData('BUG_DETAILS'),
bugsGraphData: fetchData('BUGS_GRAPHS'),
bugDetailsGraphData: fetchData('BUG_DETAILS_GRAPHS'),
dates: updateDates('BUGS'),
bugDetailsDates: updateDates('BUG_DETAILS'),
mainTree: updateTree('BUGS'),
bugDetailsTree: updateTree('BUG_DETAILS'),
bugDetails: updateBugDetails('BUG_DETAILS'),
bugzilla: fetchData('BUGZILLA_BUGS'),
bugzillaBugDetails: fetchData('BUGZILLA_BUG_DETAILS'),
});
export default rootReducer;

Просмотреть файл

@ -1,8 +0,0 @@
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './root_reducer';
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;

Просмотреть файл

@ -29,7 +29,10 @@ export default class ErrorBoundary extends React.Component {
}
ErrorBoundary.propTypes = {
children: PropTypes.object.isRequired,
children: PropTypes.oneOfType([
PropTypes.object,
PropTypes.bool,
]),
errorClasses: PropTypes.string,
message: PropTypes.string,
};
@ -37,4 +40,5 @@ ErrorBoundary.propTypes = {
ErrorBoundary.defaultProps = {
errorClasses: '',
message: '',
children: null,
};