зеркало из 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 React from 'react';
|
||||||
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
|
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
|
|
||||||
import MainView from './MainView';
|
import MainView from './MainView';
|
||||||
import BugDetailsView from './BugDetailsView';
|
import BugDetailsView from './BugDetailsView';
|
||||||
|
|
||||||
function App({ store }) {
|
class App extends React.Component {
|
||||||
return (
|
constructor(props) {
|
||||||
<Provider store={store}>
|
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>
|
<HashRouter>
|
||||||
<main>
|
<main>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/main" component={MainView} />
|
(<Route
|
||||||
<Route path="/main?startday=:startday&endday=:endday&tree=:tree" component={MainView} />
|
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" component={BugDetailsView} />
|
||||||
<Route path="/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug" component={BugDetailsView} />
|
<Route path="/bugdetails?startday=:startday&endday=:endday&tree=:tree&bug=bug" component={BugDetailsView} />
|
||||||
<Redirect from="/" to="/main" />
|
<Redirect from="/" to="/main" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
App.propTypes = {
|
|
||||||
store: PropTypes.shape({}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -1,41 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { updateSelectedBugDetails, updateDateRange, updateTreeName } from './redux/actions';
|
|
||||||
import { getBugUrl } from '../helpers/url';
|
import { getBugUrl } from '../helpers/url';
|
||||||
|
|
||||||
class BugColumn extends React.Component {
|
// we're passing the mainview location object to bugdetails because using history.goBack()
|
||||||
constructor(props) {
|
// in bugdetailsview to navigate back to mainview displays this console warning:
|
||||||
super(props);
|
// "Hash history go(n) causes a full page reload in this browser"
|
||||||
this.updateStateData = this.updateStateData.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStateData() {
|
function BugColumn({ tree, startday, endday, data, location, graphData, updateAppState }) {
|
||||||
// bugdetailsview inherits data from the main view
|
const { id, summary } = data;
|
||||||
const { data, updateDates, updateTree, updateBugDetails, from, to, tree } = this.props;
|
return (
|
||||||
|
<div>
|
||||||
updateBugDetails(data.id, data.summary, 'BUG_DETAILS');
|
<a className="ml-1" target="_blank" rel="noopener noreferrer" href={getBugUrl(id)}>{id}</a>
|
||||||
updateTree(tree, 'BUG_DETAILS');
|
|
||||||
updateDates(from, to, 'BUG_DETAILS');
|
<span className="ml-1 small-text bug-details" onClick={() => updateAppState({ graphData })}>
|
||||||
}
|
<Link
|
||||||
|
to={{ pathname: '/bugdetails',
|
||||||
render() {
|
search: `?startday=${startday}&endday=${endday}&tree=${tree}&bug=${id}`,
|
||||||
const { tree, from, to } = this.props;
|
state: { startday, endday, tree, id, summary, location },
|
||||||
const { id } = this.props.data;
|
}}
|
||||||
return (
|
>
|
||||||
<div>
|
details
|
||||||
<a className="ml-1" target="_blank" rel="noopener noreferrer" href={getBugUrl(id)}>{id}</a>
|
</Link>
|
||||||
|
</span>
|
||||||
<span className="ml-1 small-text bug-details">
|
</div>
|
||||||
<Link onClick={this.updateStateData} to={{ pathname: '/bugdetails', search: `?startday=${from}&endday=${to}&tree=${tree}&bug=${id}` }}>
|
);
|
||||||
details
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BugColumn.propTypes = {
|
BugColumn.propTypes = {
|
||||||
|
@ -43,30 +33,22 @@ BugColumn.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
summary: PropTypes.string.isRequired,
|
summary: PropTypes.string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
updateDates: PropTypes.func,
|
startday: PropTypes.string.isRequired,
|
||||||
updateTree: PropTypes.func,
|
endday: PropTypes.string.isRequired,
|
||||||
updateBugDetails: PropTypes.func,
|
|
||||||
from: PropTypes.string.isRequired,
|
|
||||||
to: PropTypes.string.isRequired,
|
|
||||||
tree: 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 = {
|
BugColumn.defaultProps = {
|
||||||
updateTree: null,
|
location: null,
|
||||||
updateDates: null,
|
graphData: null,
|
||||||
updateBugDetails: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
export default BugColumn;
|
||||||
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);
|
|
||||||
|
|
|
@ -1,357 +1,130 @@
|
||||||
|
import 'react-table/react-table.css';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { Row, Col } from 'reactstrap';
|
||||||
import { Container, Row, Col } from 'reactstrap';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Icon from 'react-fontawesome';
|
import Icon from 'react-fontawesome';
|
||||||
|
|
||||||
import Navigation from './Navigation';
|
import { calculateMetrics, prettyDate } from './helpers';
|
||||||
import { fetchBugData, updateDateRange, updateTreeName, updateSelectedBugDetails } from './redux/actions';
|
import { bugDetailsEndpoint, getJobsUrl } from '../helpers/url';
|
||||||
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 BugLogColumn from './BugLogColumn';
|
import BugLogColumn from './BugLogColumn';
|
||||||
import ErrorMessages from './ErrorMessages';
|
import Layout from './Layout';
|
||||||
import { stateName } from './constants';
|
import GenericTable from './GenericTable';
|
||||||
import ErrorBoundary from './ErrorBoundary';
|
import withView from './View';
|
||||||
|
import DateOptions from './DateOptions';
|
||||||
|
|
||||||
class BugDetailsView extends React.Component {
|
const BugDetailsView = (props) => {
|
||||||
constructor(props) {
|
const { graphData, tableData, initialParamsSet, startday, endday, updateState, bug,
|
||||||
super(props);
|
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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
Header: 'Push Time',
|
Header: 'Push Time',
|
||||||
accessor: 'push_time',
|
accessor: 'push_time',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Tree',
|
Header: 'Tree',
|
||||||
accessor: 'tree',
|
accessor: 'tree',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Revision',
|
Header: 'Revision',
|
||||||
accessor: '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>,
|
Cell: _props =>
|
||||||
},
|
(<a
|
||||||
{
|
href={getJobsUrl({ repo: _props.original.tree, revision: _props.value, selectedJob: _props.original.job_id })}
|
||||||
Header: 'Platform',
|
target="_blank"
|
||||||
accessor: 'platform',
|
rel="noopener noreferrer"
|
||||||
},
|
>
|
||||||
{
|
{_props.value}
|
||||||
Header: 'Build Type',
|
</a>),
|
||||||
accessor: 'build_type',
|
},
|
||||||
},
|
{
|
||||||
{
|
Header: 'Platform',
|
||||||
Header: 'Test Suite',
|
accessor: 'platform',
|
||||||
accessor: 'test_suite',
|
},
|
||||||
minWidth: 200,
|
{
|
||||||
},
|
Header: 'Build Type',
|
||||||
{
|
accessor: 'build_type',
|
||||||
Header: 'Machine Name',
|
},
|
||||||
accessor: 'machine_name',
|
{
|
||||||
minWidth: 125,
|
Header: 'Test Suite',
|
||||||
},
|
accessor: 'test_suite',
|
||||||
{
|
minWidth: 200,
|
||||||
Header: 'Log',
|
},
|
||||||
accessor: 'job_id',
|
{
|
||||||
Cell: props => <BugLogColumn {...props} />,
|
Header: 'Machine Name',
|
||||||
minWidth: 110,
|
accessor: 'machine_name',
|
||||||
},
|
minWidth: 125,
|
||||||
];
|
},
|
||||||
const params = { startday: from, endday: to, tree, bug: bugId };
|
{
|
||||||
const { errorMessages, initialParamsSet } = this.state;
|
Header: 'Log',
|
||||||
|
accessor: 'job_id',
|
||||||
|
Cell: props => <BugLogColumn {...props} />,
|
||||||
|
minWidth: 110,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
let graphOneData = null;
|
let graphOneData = null;
|
||||||
let graphTwoData = null;
|
let graphTwoData = null;
|
||||||
|
|
||||||
if (graphs && graphs.length > 0) {
|
if (graphData.length > 0) {
|
||||||
({ graphOneData, graphTwoData } = calculateMetrics(graphs));
|
({ graphOneData, graphTwoData } = calculateMetrics(graphData));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container fluid style={{ marginBottom: '5rem', marginTop: '4.5rem', maxWidth: '1200px' }}>
|
<Layout
|
||||||
<Navigation
|
{...props}
|
||||||
params={params}
|
graphOneData={graphOneData}
|
||||||
tableApi={bugDetailsEndpoint}
|
graphTwoData={graphTwoData}
|
||||||
graphApi={graphsEndpoint}
|
dateOptions
|
||||||
bugId={bugId}
|
header={
|
||||||
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 &&
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs="12" className="mx-auto"><h1>Details for Bug {!bugId ? '' : bugId}</h1></Col>
|
<Col xs="12"><span className="pull-left"><Link to={(lastLocation || '/')}><Icon name="arrow-left" className="pr-1" />
|
||||||
</Row>
|
back</Link></span>
|
||||||
<Row>
|
|
||||||
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(from)} to ${prettyDate(to)} UTC`}</p>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{summary &&
|
{!errorMessages.length && !tableFailureStatus && !graphFailureStatus &&
|
||||||
<Row>
|
<React.Fragment>
|
||||||
<Col xs="4" className="mx-auto"><p className="text-secondary text-center">{summary}</p></Col>
|
<Row>
|
||||||
</Row>}
|
<Col xs="12" className="mx-auto"><h1>Details for Bug {!bug ? '' : bug}</h1></Col>
|
||||||
{bugDetails && bugDetails.count &&
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs="12" className="mx-auto"><p className="text-secondary">{bugDetails.count} total failures</p></Col>
|
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(startday)} to ${prettyDate(endday)} UTC`}</p>
|
||||||
</Row>}
|
</Col>
|
||||||
</React.Fragment>}
|
</Row>
|
||||||
|
{summary &&
|
||||||
<ErrorBoundary
|
<Row>
|
||||||
stateName={stateName.detailsViewGraphs}
|
<Col xs="4" className="mx-auto"><p className="text-secondary text-center">{summary}</p></Col>
|
||||||
>
|
</Row>}
|
||||||
{graphOneData && graphTwoData &&
|
{tableData && tableData.count &&
|
||||||
<GraphsContainer
|
<Row>
|
||||||
graphOneData={graphOneData}
|
<Col xs="12" className="mx-auto"><p className="text-secondary">{tableData.count} total failures</p></Col>
|
||||||
graphTwoData={graphTwoData}
|
</Row>}
|
||||||
name={stateName.detailsView}
|
</React.Fragment>}
|
||||||
tree={tree}
|
</React.Fragment>
|
||||||
graphName={stateName.detailsViewGraphs}
|
}
|
||||||
tableApi={bugDetailsEndpoint}
|
table={
|
||||||
params={params}
|
bug && initialParamsSet &&
|
||||||
graphApi={graphsEndpoint}
|
<GenericTable
|
||||||
bugId={bugId}
|
totalPages={tableData.total_pages}
|
||||||
dateOptions
|
columns={columns}
|
||||||
/>}
|
data={tableData.results}
|
||||||
</ErrorBoundary>
|
updateState={updateState}
|
||||||
|
/>
|
||||||
<ErrorBoundary
|
}
|
||||||
stateName={stateName.detailsView}
|
datePicker={
|
||||||
>
|
<DateOptions
|
||||||
{bugId && initialParamsSet &&
|
updateState={updateState}
|
||||||
<GenericTable
|
/>
|
||||||
bugs={bugDetails.results}
|
}
|
||||||
columns={columns}
|
/>
|
||||||
name={stateName.detailsView}
|
);
|
||||||
tableApi={bugDetailsEndpoint}
|
|
||||||
totalPages={bugDetails.total_pages}
|
|
||||||
params={params}
|
|
||||||
/>}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Container>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Container.propTypes = {
|
|
||||||
fluid: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BugDetailsView.propTypes = {
|
const defaultState = {
|
||||||
bugDetails: PropTypes.oneOfType([
|
route: '/bugdetails',
|
||||||
PropTypes.shape({}),
|
endpoint: bugDetailsEndpoint,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BugDetailsView.defaultProps = {
|
export default withView(defaultState)(BugDetailsView);
|
||||||
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);
|
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ButtonDropdown, DropdownToggle } from 'reactstrap';
|
import { ButtonDropdown, DropdownToggle } from 'reactstrap';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import DateRangePicker from './DateRangePicker';
|
import DateRangePicker from './DateRangePicker';
|
||||||
import { fetchBugData, updateDateRange, fetchBugsThenBugzilla } from './redux/actions';
|
import { ISODate } from './helpers';
|
||||||
import { setDateRange } from './helpers';
|
|
||||||
import { createApiUrl } from '../helpers/url';
|
|
||||||
import DropdownMenuItems from './DropdownMenuItems';
|
import DropdownMenuItems from './DropdownMenuItems';
|
||||||
|
|
||||||
class DateOptions extends React.Component {
|
export default class DateOptions extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -18,7 +15,6 @@ class DateOptions extends React.Component {
|
||||||
dateRange: '',
|
dateRange: '',
|
||||||
};
|
};
|
||||||
this.toggle = this.toggle.bind(this);
|
this.toggle = this.toggle.bind(this);
|
||||||
this.updateData = this.updateData.bind(this);
|
|
||||||
this.updateDateRange = this.updateDateRange.bind(this);
|
this.updateDateRange = this.updateDateRange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,26 +38,12 @@ class DateOptions extends React.Component {
|
||||||
// bug history is max 4 months
|
// bug history is max 4 months
|
||||||
from = 120;
|
from = 120;
|
||||||
}
|
}
|
||||||
this.updateData(from);
|
const startday = ISODate(moment().utc().subtract(from, 'days'));
|
||||||
}
|
this.props.updateState({ startday });
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { name, graphName, tree, tableApi, graphApi, bugId } = this.props;
|
const { updateState } = this.props;
|
||||||
const { dropdownOpen, dateRange } = this.state;
|
const { dropdownOpen, dateRange } = this.state;
|
||||||
const dateOptions = ['last 7 days', 'last 30 days', 'custom range', 'entire history'];
|
const dateOptions = ['last 7 days', 'last 30 days', 'custom range', 'entire history'];
|
||||||
|
|
||||||
|
@ -78,12 +60,7 @@ class DateOptions extends React.Component {
|
||||||
</ButtonDropdown>
|
</ButtonDropdown>
|
||||||
{dateRange === 'custom range' &&
|
{dateRange === 'custom range' &&
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
tree={tree}
|
updateState={updateState}
|
||||||
tableApi={tableApi}
|
|
||||||
graphApi={graphApi}
|
|
||||||
name={name}
|
|
||||||
graphName={graphName}
|
|
||||||
bugId={bugId}
|
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -91,31 +68,5 @@ class DateOptions extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
DateOptions.propTypes = {
|
DateOptions.propTypes = {
|
||||||
updateDates: PropTypes.func,
|
updateState: PropTypes.func.isRequired,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 from 'react';
|
||||||
import 'react-day-picker/lib/style.css';
|
import 'react-day-picker/lib/style.css';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import DayPickerInput from 'react-day-picker/DayPickerInput';
|
import DayPickerInput from 'react-day-picker/DayPickerInput';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { parseDate, formatDate } from 'react-day-picker/moment';
|
import { parseDate, formatDate } from 'react-day-picker/moment';
|
||||||
import { setTimeout } from 'timers';
|
import { setTimeout } from 'timers';
|
||||||
import { Button } from 'reactstrap';
|
import { Button } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { ISODate } from './helpers';
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -53,19 +50,12 @@ class DateRangePicker extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateData() {
|
updateData() {
|
||||||
const { graphName, fetchData, updateDates, fetchFullBugData, name, tree, bugId, tableApi, graphApi } = this.props;
|
const { from, to } = this.state;
|
||||||
const from = ISODate(moment(this.state.from));
|
|
||||||
const to = ISODate(moment(this.state.to));
|
|
||||||
const params = { startday: from, endday: to, tree };
|
|
||||||
|
|
||||||
if (bugId) {
|
const startday = ISODate(moment(from));
|
||||||
params.bug = bugId;
|
const endday = ISODate(moment(to));
|
||||||
fetchData(createApiUrl(tableApi, params), name);
|
|
||||||
} else {
|
this.props.updateState({ startday, endday });
|
||||||
fetchFullBugData(createApiUrl(tableApi, params), name);
|
|
||||||
}
|
|
||||||
fetchData(createApiUrl(graphApi, params), graphName);
|
|
||||||
updateDates(from, to, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -116,31 +106,9 @@ class DateRangePicker extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
DateRangePicker.propTypes = {
|
DateRangePicker.propTypes = {
|
||||||
updateDates: PropTypes.func,
|
updateState: PropTypes.func.isRequired,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
DateRangePicker.defaultProps = {
|
DateRangePicker.defaultProps = {
|
||||||
fetchData: null,
|
updateState: 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)(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';
|
import { processErrorMessage } from './helpers';
|
||||||
|
|
||||||
const ErrorMessages = ({ failureMessage, failureStatus, errorMessages }) => {
|
const ErrorMessages = ({ failureMessage, failureStatus, errorMessages }) => {
|
||||||
const messages = errorMessages.length > 0 ? errorMessages : processErrorMessage(failureMessage, failureStatus);
|
const messages = errorMessages.length ? errorMessages : processErrorMessage(failureMessage, failureStatus);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -16,7 +16,12 @@ const ErrorMessages = ({ failureMessage, failureStatus, errorMessages }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
ErrorMessages.propTypes = {
|
ErrorMessages.propTypes = {
|
||||||
failureMessage: PropTypes.object,
|
failureMessage: PropTypes.oneOfType([
|
||||||
|
PropTypes.object,
|
||||||
|
PropTypes.arrayOf(
|
||||||
|
PropTypes.string,
|
||||||
|
),
|
||||||
|
]),
|
||||||
failureStatus: PropTypes.number,
|
failureStatus: PropTypes.number,
|
||||||
errorMessages: PropTypes.array,
|
errorMessages: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,13 +3,10 @@ import { Table } from 'reactstrap';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
import 'react-table/react-table.css';
|
import 'react-table/react-table.css';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { fetchBugData, fetchBugsThenBugzilla } from './redux/actions';
|
import { sortData, tableRowStyling } from './helpers';
|
||||||
import { createApiUrl } from '../helpers/url';
|
|
||||||
import { sortData } from './helpers';
|
|
||||||
|
|
||||||
class GenericTable extends React.Component {
|
export default class GenericTable extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -21,65 +18,39 @@ class GenericTable extends React.Component {
|
||||||
this.updateTable = this.updateTable.bind(this);
|
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) {
|
updateTable(state) {
|
||||||
const { page, pageSize } = this.state;
|
let { page, pageSize } = this.state;
|
||||||
|
|
||||||
// table's page count starts at 0
|
// table's page count starts at 0
|
||||||
if (state.page + 1 !== page || state.pageSize !== pageSize) {
|
if (state.page + 1 !== page || state.pageSize !== pageSize) {
|
||||||
this.updateData(state.page + 1, state.pageSize);
|
page = state.page + 1;
|
||||||
this.setState({ page: state.page + 1, pageSize: state.pageSize });
|
pageSize = state.pageSize;
|
||||||
|
|
||||||
|
this.props.updateState({ page, pageSize }, true);
|
||||||
|
this.setState({ page, pageSize });
|
||||||
} else if (state.sorted.length > 0) {
|
} else if (state.sorted.length > 0) {
|
||||||
this.setState({ columnId: state.sorted[0].id, descending: state.sorted[0].desc });
|
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() {
|
render() {
|
||||||
const { bugs, columns, trStyling, totalPages } = this.props;
|
const { data, columns, totalPages } = this.props;
|
||||||
const { columnId, descending } = this.state;
|
const { columnId, descending } = this.state;
|
||||||
let sortedData = [];
|
let sortedData = [];
|
||||||
|
|
||||||
if (columnId) {
|
if (columnId) {
|
||||||
sortedData = sortData([...bugs], columnId, descending);
|
sortedData = sortData([...data], columnId, descending);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ReactTable
|
<ReactTable
|
||||||
manual
|
manual
|
||||||
data={sortedData.length > 0 ? sortedData : bugs}
|
data={sortedData.length > 0 ? sortedData : data}
|
||||||
onFetchData={this.updateTable}
|
onFetchData={this.updateTable}
|
||||||
pages={totalPages}
|
pages={totalPages}
|
||||||
showPageSizeOptions
|
showPageSizeOptions
|
||||||
columns={columns}
|
columns={columns}
|
||||||
className="-striped"
|
className="-striped"
|
||||||
getTrProps={trStyling ? this.bugRowStyling : () => ({})}
|
getTrProps={tableRowStyling}
|
||||||
showPaginationTop
|
showPaginationTop
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -94,36 +65,14 @@ Table.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
GenericTable.propTypes = {
|
GenericTable.propTypes = {
|
||||||
bugs: PropTypes.arrayOf(PropTypes.shape({})),
|
data: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||||
params: PropTypes.shape({
|
updateState: PropTypes.func,
|
||||||
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,
|
|
||||||
totalPages: PropTypes.number,
|
totalPages: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
GenericTable.defaultProps = {
|
GenericTable.defaultProps = {
|
||||||
trStyling: false,
|
|
||||||
fetchData: null,
|
|
||||||
fetchFullBugData: null,
|
|
||||||
totalPages: 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 PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Graph from './Graph';
|
import Graph from './Graph';
|
||||||
import DateOptions from './DateOptions';
|
|
||||||
import DateRangePicker from './DateRangePicker';
|
|
||||||
import { graphOneSpecs, graphTwoSpecs } from './constants';
|
import { graphOneSpecs, graphTwoSpecs } from './constants';
|
||||||
|
|
||||||
export default class GraphsContainer extends React.Component {
|
export default class GraphsContainer extends React.Component {
|
||||||
|
@ -21,34 +19,23 @@ export default class GraphsContainer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { graphOneData, graphTwoData, dateOptions, name, graphName, tree, bugId, tableApi, graphApi } = this.props;
|
const { graphOneData, graphTwoData, children } = this.props;
|
||||||
const { showGraphTwo } = this.state;
|
const { showGraphTwo } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Row className="pt-5">
|
<Row className="pt-5">
|
||||||
<Graph specs={graphOneSpecs} data={graphOneData} />
|
<Graph
|
||||||
|
specs={graphOneSpecs}
|
||||||
|
data={graphOneData}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs="12" className="mx-auto pb-5">
|
<Col xs="12" className="mx-auto pb-5">
|
||||||
<Button color="secondary" onClick={this.toggleGraph} className="d-inline-block mr-3">
|
<Button color="secondary" onClick={this.toggleGraph} className="d-inline-block mr-3">
|
||||||
{`${showGraphTwo ? 'less' : 'more'} graphs`}</Button>
|
{`${showGraphTwo ? 'less' : 'more'} graphs`}
|
||||||
{dateOptions ?
|
</Button>
|
||||||
<DateOptions
|
{children}
|
||||||
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}
|
|
||||||
/>}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{showGraphTwo &&
|
{showGraphTwo &&
|
||||||
|
@ -83,21 +70,10 @@ GraphsContainer.propTypes = {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dateOptions: PropTypes.bool,
|
children: PropTypes.object.isRequired,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
GraphsContainer.defaultProps = {
|
GraphsContainer.defaultProps = {
|
||||||
bugId: null,
|
|
||||||
graphOneData: null,
|
graphOneData: null,
|
||||||
graphTwoData: 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 React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { Row, Col } from 'reactstrap';
|
||||||
import { Container, Row, Col } from 'reactstrap';
|
|
||||||
import PropTypes from 'prop-types';
|
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 BugColumn from './BugColumn';
|
||||||
import { updateQueryParams, mergeData, calculateMetrics, prettyDate, validateQueryParams } from './helpers';
|
import { calculateMetrics, prettyDate, ISODate } from './helpers';
|
||||||
import GraphsContainer from './GraphsContainer';
|
import { bugsEndpoint } from '../helpers/url';
|
||||||
import { bugsEndpoint, graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl } from '../helpers/url';
|
import GenericTable from './GenericTable';
|
||||||
import ErrorMessages from './ErrorMessages';
|
import withView from './View';
|
||||||
import { stateName } from './constants';
|
import Layout from './Layout';
|
||||||
import ErrorBoundary from './ErrorBoundary';
|
import DateRangePicker from './DateRangePicker';
|
||||||
|
|
||||||
class MainView extends React.Component {
|
const MainView = (props) => {
|
||||||
constructor(props) {
|
const { graphData, tableData, initialParamsSet, startday, endday, updateState,
|
||||||
super(props);
|
tree, location, updateAppState } = props;
|
||||||
this.updateData = this.updateData.bind(this);
|
|
||||||
this.setQueryParams = this.setQueryParams.bind(this);
|
|
||||||
this.checkQueryValidation = this.checkQueryValidation.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
const columns = [
|
||||||
errorMessages: [],
|
{
|
||||||
initialParamsSet: null,
|
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() {
|
return (
|
||||||
this.setQueryParams();
|
<Layout
|
||||||
}
|
{...props}
|
||||||
|
graphOneData={graphOneData}
|
||||||
componentWillReceiveProps(nextProps) {
|
graphTwoData={graphTwoData}
|
||||||
const { from, to, tree, location, history } = nextProps;
|
header={
|
||||||
|
initialParamsSet &&
|
||||||
// update all data if the user edits dates or tree via the query params
|
<React.Fragment>
|
||||||
if (location.search !== this.props.location.search) {
|
<Row>
|
||||||
this.checkQueryValidation(parseQueryParams(location.search), this.state.initialParamsSet);
|
<Col xs="12" className="mx-auto pt-3"><h1>Intermittent Test Failures</h1></Col>
|
||||||
}
|
</Row>
|
||||||
// update query params if dates or tree are updated
|
<Row>
|
||||||
if (from !== this.props.from || to !== this.props.to || tree !== this.props.tree) {
|
<Col xs="12" className="mx-auto"><p className="subheader">{`${prettyDate(startday)} to ${prettyDate(endday)} UTC`}</p>
|
||||||
const queryString = createQueryParams({ startday: from, endday: to, tree });
|
</Col>
|
||||||
if (queryString !== location.search) {
|
</Row>
|
||||||
updateQueryParams('/main', queryString, history, this.props.location);
|
<Row>
|
||||||
|
<Col xs="12" className="mx-auto"><p className="text-secondary">{totalFailures} bugs in {totalRuns} pushes</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
}
|
table={
|
||||||
}
|
initialParamsSet &&
|
||||||
|
<GenericTable
|
||||||
setQueryParams() {
|
totalPages={tableData.total_pages}
|
||||||
const { from, to, tree, location, history, graphs, fetchData } = this.props;
|
columns={columns}
|
||||||
// if the query params are not specified, set params based on default props
|
data={tableData.results}
|
||||||
// otherwise update data based on the params
|
updateState={updateState}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
{(isFetchingGraphs || isFetchingBugs) &&
|
}
|
||||||
!(tableFailureStatus || graphFailureStatus || errorMessages.length > 0) &&
|
datePicker={
|
||||||
<div className="loading">
|
<DateRangePicker
|
||||||
<Icon
|
updateState={updateState}
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MainView.propTypes = {
|
MainView.propTypes = {
|
||||||
bugs: PropTypes.oneOfType([
|
location: PropTypes.shape({}).isRequired,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MainView.defaultProps = {
|
const defaultState = {
|
||||||
tableFailureMessage: null,
|
tree: 'trunk',
|
||||||
graphFailureMessage: null,
|
startday: ISODate(moment().utc().subtract(7, 'days')),
|
||||||
tableFailureStatus: null,
|
endday: ISODate(moment().utc()),
|
||||||
graphFailureStatus: null,
|
route: '/main',
|
||||||
fetchData: null,
|
endpoint: bugsEndpoint,
|
||||||
updateTree: null,
|
|
||||||
updateDates: null,
|
|
||||||
fetchFullBugData: null,
|
|
||||||
isFetchingBugs: null,
|
|
||||||
isFetchingGraphs: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
export default withView(defaultState)(MainView);
|
||||||
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);
|
|
||||||
|
|
|
@ -1,39 +1,22 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Collapse, Navbar, Nav, UncontrolledDropdown, DropdownToggle } from 'reactstrap';
|
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 DropdownMenuItems from './DropdownMenuItems';
|
||||||
import { treeOptions } from './constants';
|
import { treeOptions } from './constants';
|
||||||
|
|
||||||
class Navigation extends React.Component {
|
export default class Navigation extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { isOpen: false };
|
this.state = { isOpen: false };
|
||||||
|
|
||||||
this.toggle = this.toggle.bind(this);
|
this.toggle = this.toggle.bind(this);
|
||||||
this.updateData = this.updateData.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Navbar expand fixed="top" className="top-navbar">
|
<Navbar expand fixed="top" className="top-navbar">
|
||||||
|
@ -46,7 +29,7 @@ class Navigation extends React.Component {
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenuItems
|
<DropdownMenuItems
|
||||||
options={treeOptions}
|
options={treeOptions}
|
||||||
updateData={this.updateData}
|
updateData={tree => this.props.updateState({ tree })}
|
||||||
default={this.props.tree}
|
default={this.props.tree}
|
||||||
/>
|
/>
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
|
@ -61,40 +44,11 @@ Nav.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
Navigation.propTypes = {
|
Navigation.propTypes = {
|
||||||
params: PropTypes.shape({
|
tree: PropTypes.string,
|
||||||
startday: PropTypes.string.isRequired,
|
updateState: PropTypes.func,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Navigation.defaultProps = {
|
Navigation.defaultProps = {
|
||||||
bugId: null,
|
tree: null,
|
||||||
updateTree: null,
|
updateState: null,
|
||||||
fetchData: null,
|
|
||||||
fetchFullBugData: 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.',
|
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 errorMessageClass = 'text-danger py-4 d-block';
|
||||||
export const stateName = {
|
|
||||||
mainView: 'BUGS',
|
|
||||||
mainViewGraphs: 'BUGS_GRAPHS',
|
|
||||||
detailsView: 'BUG_DETAILS',
|
|
||||||
detailsViewGraphs: 'BUG_DETAILS_GRAPHS',
|
|
||||||
};
|
|
||||||
|
|
|
@ -10,12 +10,6 @@ export const prettyDate = function formatPrettyDate(date) {
|
||||||
return moment(date).format('ddd MMM D, YYYY');
|
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) {
|
export const formatBugs = function formatBugsForBugzilla(data) {
|
||||||
let bugs = '';
|
let bugs = '';
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
@ -123,3 +117,36 @@ export const validateQueryParams = function validateQueryParams(params, bugRequi
|
||||||
}
|
}
|
||||||
return messages;
|
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/treeherder-global.css';
|
||||||
import '../css/intermittent-failures.css';
|
import '../css/intermittent-failures.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import store from './redux/store';
|
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
render((
|
render((
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
<App store={store} />
|
<App />
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
), document.getElementById('root'));
|
), 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 = {
|
ErrorBoundary.propTypes = {
|
||||||
children: PropTypes.object.isRequired,
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.object,
|
||||||
|
PropTypes.bool,
|
||||||
|
]),
|
||||||
errorClasses: PropTypes.string,
|
errorClasses: PropTypes.string,
|
||||||
message: PropTypes.string,
|
message: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -37,4 +40,5 @@ ErrorBoundary.propTypes = {
|
||||||
ErrorBoundary.defaultProps = {
|
ErrorBoundary.defaultProps = {
|
||||||
errorClasses: '',
|
errorClasses: '',
|
||||||
message: '',
|
message: '',
|
||||||
|
children: null,
|
||||||
};
|
};
|
||||||
|
|
Загрузка…
Ссылка в новой задаче