Initial prototype
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:
Родитель
d10ae5e2c1
Коммит
ebfccb7d87
|
@ -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;
|
||||
}
|
23
src/App.jsx
23
src/App.jsx
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||
});
|
|
@ -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
673
yarn.lock
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче