Refactor: Move data fetch into MainContainer & generic metrics

Also make fetching metrics for a component generic in order to add a
full summary of various metrics in a future detailed view for a specific Bugzilla component.
This commit is contained in:
Armen Zambrano G 2018-12-14 10:23:26 -05:00 коммит произвёл Armen Zambrano
Родитель 626af8c429
Коммит 76662b7794
14 изменённых файлов: 504 добавлений и 149 удалений

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

@ -45,6 +45,7 @@
"lint-staged": "^8.1.0",
"neutrino": "^9.0.0-beta.1",
"node-fetch": "^2.3.0",
"react-test-renderer": "^16.6.3",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-dev-server": "^3"

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

@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
const linkToQuery = (key, metrics, alt) => (
<a href={metrics[key].link} alt={alt}>{metrics[key].count}</a>
);
const BugzillaComponent = ({ product, component, metrics }) => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ margin: '0 1rem 0 0' }}>{`${product}::${component}`}</span>
{metrics.untriaged
&& linkToQuery('untriaged', metrics, 'Number of untriaged bugs')
}
</div>
);
BugzillaComponent.propTypes = {
product: PropTypes.string.isRequired,
component: PropTypes.string.isRequired,
metrics: PropTypes.shape({}),
};
BugzillaComponent.defaultProps = {
metrics: {},
};
export default BugzillaComponent;

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

@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import BugzillaComponent from '../BugzillaComponent';
import Reportees from '../Reportees';
const sortByComponentName = (a, b) => {
let result = (a.product <= b.product);
if (a.product === b.product) {
result = a.component <= b.component;
}
return result ? -1 : 1;
};
const MainView = ({ ldapEmail, partialOrg, bugzillaComponents }) => (
<div key={ldapEmail}>
<h3>{partialOrg[ldapEmail].cn}</h3>
<div style={{ display: 'flex' }}>
<Reportees ldapEmail={ldapEmail} partialOrg={partialOrg} />
{Object.values(bugzillaComponents).length > 0 && (
<div>
<h4>Components</h4>
{Object.values(bugzillaComponents)
.sort(sortByComponentName)
.map(({ component, product, metrics }) => (
<BugzillaComponent
key={`${product}::${component}`}
product={product}
component={component}
metrics={metrics}
/>
))}
</div>
)}
</div>
</div>
);
MainView.propTypes = {
ldapEmail: PropTypes.string.isRequired,
partialOrg: PropTypes.shape({}).isRequired,
bugzillaComponents: PropTypes.shape({}),
};
MainView.defaultProps = {
bugzillaComponents: {},
};
export default MainView;

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

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
const sortByPersonName = (a, b) => (a.cn <= b.cn ? -1 : 1);
const Reportees = ({ ldapEmail, partialOrg }) => (
<div style={{ margin: '0 1rem 0 0' }}>
<h4>Reportees</h4>
{Object.values(partialOrg)
.filter(({ cn }) => cn !== ldapEmail)
.sort(sortByPersonName)
.map(({ cn, mail }) => (
<div key={mail}>
<span>{`${cn} `}</span>
</div>
))}
</div>
);
Reportees.propTypes = {
ldapEmail: PropTypes.string.isRequired,
partialOrg: PropTypes.shape({}).isRequired,
};
export default Reportees;

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

@ -1,59 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import OpenInNew from '@material-ui/icons/OpenInNew';
import { withStyles } from '@material-ui/core/styles';
import getLinkToComponent from '../../utils/bugzilla/getLinkToComponent';
import getUntriagedBugsCount from '../../utils/bugzilla/getUntriagedBugsCount';
const styles = theme => ({
root: {
color: theme.palette.text.primary,
},
icon: {
margin: 0,
fontSize: '1rem',
verticalAlign: 'text-top',
},
});
class BugzillaComponentSummary extends React.Component {
state = {
untriaged: undefined,
};
static propTypes = {
classes: PropTypes.shape({}).isRequired,
product: PropTypes.string.isRequired,
component: PropTypes.string.isRequired,
};
async componentDidMount() {
this.fetchData(this.props);
}
async fetchData({ product, component }) {
const untriaged = await getUntriagedBugsCount(product, component);
this.setState({ untriaged });
}
render() {
const { classes, product, component } = this.props;
const { untriaged } = this.state;
return (
<div className={classes.root}>
<span>{`${product}::${component}`}</span>
<a
href={getLinkToComponent(product, component)}
target="_blank"
rel="noopener noreferrer"
title="Link to component's untriaged bugs"
>
<OpenInNew className={classes.icon} />
</a>
{!!untriaged && <span title="Number of untriaged bugs">{untriaged}</span>}
</div>
);
}
}
export default withStyles(styles)(BugzillaComponentSummary);

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

@ -1,23 +1,14 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import BugzillaComponentSummary from '../BugzillaComponentSummary';
import MainView from '../../components/MainView';
import getAllReportees from '../../utils/getAllReportees';
import getBugzillaOwners from '../../utils/getBugzillaOwners';
const sortByPersonName = (a, b) => (a.cn <= b.cn ? -1 : 1);
const sortByComponentName = (a, b) => {
let result = (a.product <= b.product);
if (a.product === b.product) {
result = a.component <= b.component;
}
return result ? -1 : 1;
};
import getBugsCountAndLink from '../../utils/bugzilla/getBugsCountAndLink';
class MainContainer extends Component {
state = {
ldapEmail: '',
reporteesComponents: undefined,
bugzillaComponents: undefined,
partialOrg: undefined,
};
@ -50,24 +41,45 @@ class MainContainer extends Component {
return partialOrg;
}
async reporteeComponents(bzOwners, partialOrg) {
async bugzillaComponents(bzOwners, partialOrg) {
// bzOwners uses the bugzilla email address as the key
// while partialOrg uses the LDAP email address
const reporteesComponents = Object.values(partialOrg)
/* eslint-disable no-param-reassign */
const bugzillaComponents = Object.values(partialOrg)
.reduce((result, { bugzillaEmail, mail }) => {
const componentsOwned = bzOwners[bugzillaEmail] || bzOwners[mail];
if (componentsOwned) {
componentsOwned.forEach(({ product, component }) => {
result.push({
if (!result[`${product}::${component}`]) {
result[`${product}::${component}`] = {};
}
result[`${product}::${component}`] = {
bugzillaEmail: bugzillaEmail || mail,
product,
component,
});
metrics: {},
};
});
}
return result;
}, []);
this.setState({ reporteesComponents });
}, {});
/* eslint-enable no-param-reassign */
// This will list the components but will not show metrics
this.setState({ bugzillaComponents });
// Let's fetch the metrics for each component
Object.values(bugzillaComponents)
.map(async ({ product, component }) => {
const metric = 'untriaged';
const { count, link } = await getBugsCountAndLink(product, component, metric);
bugzillaComponents[`${product}::${component}`].metrics = {
[metric]: {
count,
link,
},
};
this.setState({ bugzillaComponents });
});
}
async retrieveData(ldapEmail) {
@ -75,13 +87,13 @@ class MainContainer extends Component {
getBugzillaOwners(),
this.getReportees(ldapEmail),
]);
this.reporteeComponents(bzOwners, partialOrg);
this.bugzillaComponents(bzOwners, partialOrg);
}
handleChange(event) {
this.setState({
ldapEmail: event.target.value,
reporteesComponents: undefined,
bugzillaComponents: undefined,
partialOrg: undefined,
});
}
@ -93,43 +105,18 @@ class MainContainer extends Component {
}
render() {
const { ldapEmail, reporteesComponents, partialOrg } = this.state;
const {
ldapEmail, bugzillaComponents, partialOrg,
} = this.state;
return (
<div>
{partialOrg && (
<div key={ldapEmail}>
<h3>{partialOrg[ldapEmail].cn}</h3>
<div style={{ display: 'flex' }}>
{partialOrg && (
<div style={{ margin: '0 1rem 0 0' }}>
<h4>Reportees</h4>
{Object.values(partialOrg)
.filter(({ cn }) => cn !== ldapEmail)
.sort(sortByPersonName)
.map(({ cn, mail }) => (
<div key={mail}>
<span>{`${cn} `}</span>
</div>
))}
</div>
)}
{reporteesComponents && reporteesComponents.length > 0 && (
<div>
<h4>Components</h4>
{reporteesComponents
.sort(sortByComponentName)
.map(({ product, component }) => (
<BugzillaComponentSummary
key={`${product}::${component}`}
product={product}
component={component}
/>
))}
</div>
)}
</div>
</div>
<MainView
ldapEmail={ldapEmail}
partialOrg={partialOrg}
bugzillaComponents={bugzillaComponents}
/>
)}
</div>
);

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

@ -0,0 +1,26 @@
import { stringify } from 'query-string';
import fetchJson from '../fetchJson';
import settings from './settings';
import METRICS from './metrics';
const queryBugzilla = async queryParameters => (
fetchJson(`${settings.BZ_HOST}/rest/bug?${queryParameters}`));
const getBugzillaComponentLink = queryParameters => (
`${settings.BZ_HOST}/buglist.cgi?${stringify(queryParameters)}`);
/* eslint-disable camelcase */
const getBugsCountAndLink = async (product, component, metric) => {
const baseParams = {
product,
component,
...METRICS[metric],
};
const link = getBugzillaComponentLink(baseParams);
const { bug_count = 0 } = await queryBugzilla(
stringify({ ...baseParams, count_only: 1 }),
);
return { count: bug_count, link };
};
export default getBugsCountAndLink;

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

@ -1,8 +0,0 @@
import { stringify } from 'query-string';
import settings from './settings';
import untriagedParameters from './untriagedParameters';
const getBugzillaComponentLink = (product, component) => (
`${settings.BZ_HOST}/buglist.cgi?${stringify({ product, component, ...untriagedParameters() })}`);
export default getBugzillaComponentLink;

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

@ -1,23 +0,0 @@
import { stringify } from 'query-string';
import untriagedParameters from './untriagedParameters';
import fetchJson from '../fetchJson';
import settings from './settings';
const queryBugzilla = async queryParameters => (
fetchJson(`${settings.BZ_HOST}/rest/bug?${queryParameters}`));
const getUntriagedBugsCount = async (product, component) => {
// eslint-disable-next-line camelcase
const { bug_count = 0 } = await queryBugzilla(
stringify({
product,
component,
...untriagedParameters(),
count_only: 1, // This only returns the bug count
}),
);
// eslint-disable-next-line camelcase
return bug_count;
};
export default getUntriagedBugsCount;

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

@ -0,0 +1,13 @@
/* eslint-disable indent */
/* eslint-disable object-property-newline */
/* eslint-disable no-multi-spaces */
const METRICS = {
untriaged: {
f1: 'bug_severity', o1: 'notequals', v1: 'enhancement',
f2: 'keywords', o2: 'notsubstring', v2: 'meta',
f3: 'resolution', o3: 'isempty',
limit: 0,
},
};
export default METRICS;

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

@ -0,0 +1,72 @@
import React from 'react';
import renderer from 'react-test-renderer';
import MainView from '../../src/components/MainView';
const partialOrg = {
'someone@mozilla.com': {
cn: 'Someone',
mail: 'someone@mozilla.com',
manager: {
dn: 'mail=manager@mozilla.com,o=com,dc=mozilla',
},
},
'manager@mozilla.com': {
cn: 'Manager',
mail: 'manager@mozilla.com',
manager: null,
},
};
const bugzillaComponents = {
'Core::DOM: IndexedDB': {
bugzillaEmail: 'someone@mozilla.com',
component: 'DOM: IndexedDB',
product: 'Core',
},
'Core::JavaScript Engine': {
bugzillaEmail: 'someone@mozilla.com',
component: 'JavaScript Engine',
product: 'Core',
},
'Core::DOM: Core & HTML': {
bugzillaEmail: 'someone@mozilla.com',
component: 'DOM: Core & HTML',
product: 'Core',
metrics: {
untriaged: {
count: 944,
link: 'https://bugzilla.mozilla.org/buglist.cgi?component=DOM%3A%20Core%20%26%20HTML&f1=bug_severity&f2=keywords&f3=resolution&limit=0&o1=notequals&o2=notsubstring&o3=isempty&product=Core&v1=enhancement&v2=meta',
},
},
},
'Toolkit::Async Tooling': {
bugzillaEmail: 'manager@mozilla.com',
component: 'Async Tooling',
product: 'Toolkit',
},
};
it('renders Someone with no reportees', () => {
const tree = renderer
.create((
<MainView
ldapEmail="someone@mozilla.com"
partialOrg={partialOrg}
bugzillaComponents={bugzillaComponents}
/>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders Manager who has reportees', () => {
const tree = renderer
.create((
<MainView
ldapEmail="manager@mozilla.com"
partialOrg={partialOrg}
bugzillaComponents={bugzillaComponents}
/>
))
.toJSON();
expect(tree).toMatchSnapshot();
});

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

@ -0,0 +1,241 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders Manager who has reportees 1`] = `
<div>
<h3>
Manager
</h3>
<div
style={
Object {
"display": "flex",
}
}
>
<div
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
<h4>
Reportees
</h4>
<div>
<span>
Manager
</span>
</div>
<div>
<span>
Someone
</span>
</div>
</div>
<div>
<h4>
Components
</h4>
<div
style={
Object {
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
Core::DOM: Core & HTML
</span>
<a
alt="Number of untriaged bugs"
href="https://bugzilla.mozilla.org/buglist.cgi?component=DOM%3A%20Core%20%26%20HTML&f1=bug_severity&f2=keywords&f3=resolution&limit=0&o1=notequals&o2=notsubstring&o3=isempty&product=Core&v1=enhancement&v2=meta"
>
944
</a>
</div>
<div
style={
Object {
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
Core::DOM: IndexedDB
</span>
</div>
<div
style={
Object {
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
Core::JavaScript Engine
</span>
</div>
<div
style={
Object {
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
Toolkit::Async Tooling
</span>
</div>
</div>
</div>
</div>
`;
exports[`renders Someone with no reportees 1`] = `
<div>
<h3>
Someone
</h3>
<div
style={
Object {
"display": "flex",
}
}
>
<div
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
<h4>
Reportees
</h4>
<div>
<span>
Manager
</span>
</div>
<div>
<span>
Someone
</span>
</div>
</div>
<div>
<h4>
Components
</h4>
<div
style={
Object {
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
Core::DOM: Core & HTML
</span>
<a
alt="Number of untriaged bugs"
href="https://bugzilla.mozilla.org/buglist.cgi?component=DOM%3A%20Core%20%26%20HTML&f1=bug_severity&f2=keywords&f3=resolution&limit=0&o1=notequals&o2=notsubstring&o3=isempty&product=Core&v1=enhancement&v2=meta"
>
944
</a>
</div>
<div
style={
Object {
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
Core::DOM: IndexedDB
</span>
</div>
<div
style={
Object {
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
Core::JavaScript Engine
</span>
</div>
<div
style={
Object {
"display": "flex",
"justifyContent": "space-between",
}
}
>
<span
style={
Object {
"margin": "0 1rem 0 0",
}
}
>
Toolkit::Async Tooling
</span>
</div>
</div>
</div>
</div>
`;

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

@ -1,5 +0,0 @@
describe('simple', () => {
it('should be sane', () => {
expect(false).not.toBe(true);
});
});

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

@ -7055,7 +7055,7 @@ react-hot-loader@^4:
react-lifecycles-compat "^3.0.4"
shallowequal "^1.0.2"
react-is@^16.3.2:
react-is@^16.3.2, react-is@^16.6.3:
version "16.6.3"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0"
integrity sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA==
@ -7090,6 +7090,16 @@ react-router@^4.3.1:
prop-types "^15.6.1"
warning "^4.0.1"
react-test-renderer@^16.6.3:
version "16.6.3"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.6.3.tgz#5f3a1a7d5c3379d46f7052b848b4b72e47c89f38"
integrity sha512-B5bCer+qymrQz/wN03lT0LppbZUDRq6AMfzMKrovzkGzfO81a9T+PWQW6MzkWknbwODQH/qpJno/yFQLX5IWrQ==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.2"
react-is "^16.6.3"
scheduler "^0.11.2"
react-transition-group@^2.2.1:
version "2.5.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.0.tgz#70bca0e3546102c4dc5cf3f5f57f73447cce6874"