зеркало из https://github.com/mozilla/treeherder.git
Bug 1485226 - remove redux from IFV (#4107)
This commit is contained in:
Родитель
68f6001822
Коммит
51d7e684df
|
@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<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,
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче