The main view shows all people who report to the selected user
and all Bugzilla components owned by any of them.
This commit is contained in:
Armen Zambrano G 2018-11-23 16:38:07 -05:00
Родитель d10ae5e2c1
Коммит ebfccb7d87
16 изменённых файлов: 1021 добавлений и 41 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -62,3 +62,6 @@ typings/
# Ignore Neutrino's build directory
build
# Ignore private static content
private

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

@ -12,6 +12,13 @@ module.exports = {
}
}
],
'@neutrinojs/jest'
'@neutrinojs/jest',
[
'@neutrinojs/copy', {
patterns: [
{ from: 'src/static/private/people.json', to: 'people.json' },
],
},
]
]
};

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

@ -23,13 +23,18 @@
]
},
"dependencies": {
"@material-ui/core": "^3.5.1",
"@mozilla-frontend-infra/components": "^2.0.0",
"prop-types": "^15",
"react": "^16",
"react-dom": "^16",
"react-hot-loader": "^4"
"react-hot-loader": "^4",
"react-loadable": "^5.5.0",
"react-router-dom": "^4.3.1"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.0.0-beta.1",
"@neutrinojs/copy": "^8.3.0",
"@neutrinojs/jest": "^9.0.0-beta.1",
"@neutrinojs/react": "^9.0.0-beta.1",
"eslint": "^5",

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

@ -1,3 +0,0 @@
.App {
padding: 20px;
}

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

@ -1,23 +0,0 @@
import { hot } from 'react-hot-loader';
import React, { Component } from 'react';
import './App.css';
class App extends Component {
state = {
name: 'bugzilla-dashboard',
};
render() {
const { name } = this.state;
return (
<div className="App">
<h1>
Welcome to
{name}
</h1>
</div>
);
}
}
export default hot(module)(App);

30
src/App/index.jsx Normal file
Просмотреть файл

@ -0,0 +1,30 @@
import { hot } from 'react-hot-loader';
import React, { Component } from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
import routes from './routes';
class App extends Component {
state = {
error: undefined,
};
render() {
const { error } = this.state;
return (
<div className="App">
{error && <ErrorPanel error={new Error(error)} />}
<BrowserRouter>
<Switch>
{routes.map(props => (
<Route key={props.path} {...props} />
))}
</Switch>
</BrowserRouter>
</div>
);
}
}
export default hot(module)(App);

9
src/App/routes.jsx Normal file
Просмотреть файл

@ -0,0 +1,9 @@
import loadable from '../utils/loadable';
export default [
{
component: loadable(() => import('../views/Main/index')),
path: '/',
exact: true,
},
];

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

@ -0,0 +1,28 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import CircularProgress from '@material-ui/core/CircularProgress';
const styles = ({
center: {
textAlign: 'center',
},
});
class Spinner extends PureComponent {
static propTypes = {
classes: PropTypes.shape({}).isRequired,
};
render() {
const { classes } = this.props;
return (
<div className={classes.center}>
<CircularProgress thickness={5} color="primary" />
</div>
);
}
}
export default withStyles(styles)(Spinner);

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

@ -0,0 +1,141 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
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;
};
class MainContainer extends Component {
state = {
ldapEmail: '',
reporteesComponents: undefined,
partialOrg: undefined,
};
static propTypes = {
ldapEmail: PropTypes.string,
};
static defaultProps = {
ldapEmail: '',
};
constructor(props) {
super(props);
const { ldapEmail } = this.props;
this.state = { ldapEmail };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
async componentDidMount() {
const { ldapEmail } = this.state;
if (ldapEmail !== '') {
this.retrieveData(ldapEmail);
}
}
async getReportees(ldapEmail) {
const partialOrg = await getAllReportees(ldapEmail);
this.setState({ partialOrg });
return partialOrg;
}
async reporteeComponents(bzOwners, partialOrg) {
// bzOwners uses the bugzilla email address as the key
// while partialOrg uses the LDAP email address
const reporteesComponents = Object.values(partialOrg)
.reduce((result, { bugzillaEmail, mail }) => {
const componentsOwned = bzOwners[bugzillaEmail] || bzOwners[mail];
if (componentsOwned) {
componentsOwned.forEach(({ product, component }) => {
result.push({
bugzillaEmail: bugzillaEmail || mail,
product,
component,
});
});
}
return result;
}, []);
this.setState({ reporteesComponents });
}
async retrieveData(ldapEmail) {
const [bzOwners, partialOrg] = await Promise.all([
getBugzillaOwners(),
this.getReportees(ldapEmail),
]);
this.reporteeComponents(bzOwners, partialOrg);
}
handleChange(event) {
this.setState({
ldapEmail: event.target.value,
reporteesComponents: undefined,
partialOrg: undefined,
});
}
async handleSubmit(event) {
event.preventDefault();
const { ldapEmail } = this.state;
this.retrieveData(ldapEmail);
}
render() {
const { ldapEmail, reporteesComponents, partialOrg } = this.state;
return (
<div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="ldapEmail">
LDAP email address:
<input id="ldapEmail" type="text" value={ldapEmail} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
{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 }) => (
<div key={component}>{`${product}::${component}`}</div>
))}
</div>
)}
</div>
</div>
)}
</div>
);
}
}
export default MainContainer;

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

@ -0,0 +1,21 @@
import getOrgChart from './getOrgChart';
const findReportees = (completeOrg, ldapEmail) => {
let allReportees = {};
allReportees[ldapEmail] = completeOrg[ldapEmail];
const { reportees } = completeOrg[ldapEmail];
if (reportees.length !== 0) {
reportees.forEach((reporteeEmail) => {
const partialOrg = findReportees(completeOrg, reporteeEmail);
allReportees = { ...allReportees, ...partialOrg };
});
}
return allReportees;
};
const getAllReportees = async (ldapEmail) => {
const completeOrg = await getOrgChart();
return findReportees(completeOrg, ldapEmail);
};
export default getAllReportees;

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

@ -0,0 +1,8 @@
const getBugzillaComponents = async () => {
const { products } = await (await fetch('https://bugzilla.mozilla.org/rest/product'
+ '?type=accessible&include_fields=name&include_fields=components'
+ '&exclude_fields=components.flag_types&exclude_fields=components.description')).json();
return products;
};
export default getBugzillaComponents;

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

@ -0,0 +1,22 @@
import getBugzillaComponents from './getBugzillaComponents';
const getBugzillaOwners = async () => {
const bzComponents = await getBugzillaComponents();
/* eslint-disable camelcase */
/* eslint-disable no-param-reassign */
const owners = bzComponents.reduce((result, product) => {
product.components.forEach(({ name, triage_owner }) => {
if (triage_owner && triage_owner !== '') {
if (!result[triage_owner]) {
result[triage_owner] = [];
}
result[triage_owner].push({ product: product.name, component: name });
}
});
return result;
}, {});
/* eslint-enable camelcase */
/* eslint-enable no-param-reassign */
return owners;
};
export default getBugzillaOwners;

34
src/utils/getOrgChart.js Normal file
Просмотреть файл

@ -0,0 +1,34 @@
const orgChart = (people) => {
const org = {};
people.forEach((person) => {
const { mail } = person;
if (!org[mail]) {
org[mail] = person;
org[mail].reportees = [];
} else {
org[mail] = {
...person,
reportees: org[mail].reportees,
};
}
const { manager } = person;
if (manager) {
const managerLDAPemail = manager.dn.split('mail=')[1].split(',o=')[0];
if (org[managerLDAPemail]) {
org[managerLDAPemail].reportees.push(mail);
} else {
org[managerLDAPemail] = {
reportees: [mail],
};
}
}
});
return org;
};
const getOrgChart = async () => {
const people = await (await fetch('people.json')).json();
return orgChart(people);
};
export default getOrgChart;

40
src/utils/loadable.jsx Normal file
Просмотреть файл

@ -0,0 +1,40 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Loadable from 'react-loadable';
import Spinner from '../components/Spinner';
class Loading extends PureComponent {
content() {
const { error, timedOut, pastDelay } = this.props;
if (error) {
throw error;
} else if (timedOut || pastDelay) {
return <Spinner />;
}
return null;
}
render() {
return <div>{this.content()}</div>;
}
}
Loading.propTypes = {
error: PropTypes.shape({}),
timedOut: PropTypes.bool,
pastDelay: PropTypes.bool,
};
Loading.defaultProps = {
error: '',
timedOut: false,
pastDelay: false,
};
export default loader => Loadable({
loader,
loading: Loading,
timeout: 10000,
});

11
src/views/Main/index.jsx Normal file
Просмотреть файл

@ -0,0 +1,11 @@
import React from 'react';
import MainContainer from '../../containers/MainContainer';
// eslint-disable-next-line react/prop-types
const Main = ({ location }) => (
<MainContainer
ldapEmail={new URLSearchParams(location.search).get('ldapEmail') || ''}
/>
);
export default Main;

673
yarn.lock

Разница между файлами не показана из-за своего большого размера Загрузить разницу