Add SSO authentication and fetch org data from TaskCluster secrets

This commit is contained in:
Rail Aliiev 2018-12-18 22:21:09 -05:00 коммит произвёл Armen Zambrano G
Родитель aa318f9fa3
Коммит 9cea88f377
22 изменённых файлов: 1239 добавлений и 1170 удалений

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

@ -16,7 +16,6 @@ module.exports = {
[
'@neutrinojs/copy', {
patterns: [
{ from: 'src/static/fakeOrg.json', to: 'people.json' },
{ from: 'src/static/triageOwners.json', to: 'triageOwners.json' },
],
},

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

@ -26,7 +26,9 @@
"@material-ui/core": "^3.9.2",
"@material-ui/icons": "^3.0.1",
"@mozilla-frontend-infra/components": "^2.0.0",
"auth0-js": "9.2.3",
"chart.js": "^2.7.3",
"mitt": "^1.1.3",
"moment": "^2.23.0",
"prop-types": "^15",
"query-string": "^6.2.0",
@ -35,11 +37,13 @@
"react-dom": "^16",
"react-hot-loader": "^4",
"react-router-dom": "^4.3.1",
"taskcluster-client-web": "9.0.0",
"taskcluster-lib-urls": "^12.0.0",
"typeface-roboto": "^0.0.54"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.0.0-beta.1",
"@neutrinojs/copy": "^8.3.0",
"@neutrinojs/copy": "^9.0.0-beta.1",
"@neutrinojs/jest": "^9.0.0-beta.1",
"@neutrinojs/react": "^9.0.0-beta.1",
"eslint": "^5",

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

@ -1,10 +1,18 @@
import { hot } from 'react-hot-loader';
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import { withStyles } from '@material-ui/core/styles';
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
import Spinner from '@mozilla-frontend-infra/components/Spinner';
import Main from '../views/Main';
import PropsRoute from '../components/PropsRoute';
import AuthContext from '../components/auth/AuthContext';
import AuthController from '../components/auth/AuthController';
import NotFound from '../components/NotFound';
import Auth0Login from '../views/Auth0Login';
import config from '../config';
const styles = () => ({
'@global': {
@ -14,28 +22,80 @@ const styles = () => ({
},
},
});
class App extends Component {
class App extends React.Component {
static propTypes = {
classes: PropTypes.shape({}).isRequired,
};
state = {
authReady: false,
error: undefined,
};
authController = new AuthController();
componentWillUnmount() {
this.authController.removeListener(
'user-session-changed',
this.handleUserSessionChanged,
);
}
handleUserSessionChanged = (userSession) => {
// Consider auth "ready" when we have no userSession, a userSession with no
// renewAfter, or a renewAfter that is not in the past. Once auth is
// ready, it never becomes non-ready again.
const { authReady } = this.state;
if (!authReady) {
const newState = !userSession
|| !userSession.renewAfter
|| new Date(userSession.renewAfter) > new Date();
this.setState({ authReady: newState });
}
};
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this.authController.on(
'user-session-changed',
this.handleUserSessionChanged,
);
// we do not want to automatically load a user session on the login views; this is
// a hack until they get an entry point of their own with no UI.
if (!window.location.pathname.startsWith(config.redirectRoute)) {
this.authController.loadUserSession();
} else {
this.setState({ authReady: true });
}
}
render() {
const { classes } = this.props;
const { error } = this.state;
const { authReady, error } = this.state;
return (
<div className={classes.container}>
{error && <ErrorPanel error={new Error(error)} />}
<BrowserRouter>
<Switch>
<Route path="/" component={Main} />
</Switch>
</BrowserRouter>
</div>
<BrowserRouter>
<div>
{error && <ErrorPanel error={new Error(error)} />}
{authReady ? (
<AuthContext.Provider value={this.authController}>
<Switch>
<PropsRoute path="/" exact component={Main} />
<PropsRoute
path={config.redirectRoute}
component={Auth0Login}
setUserSession={this.authController.setUserSession}
/>
<Route component={NotFound} />
</Switch>
</AuthContext.Provider>
) : (
<Spinner loading />
)}
</div>
</BrowserRouter>
);
}
}

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

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import CredentialsMenu from '../../views/CredentialsMenu';
const styles = theme => ({
styledToolbar: {
display: 'flex',
justifyContent: 'space-between',
'min-height': theme.spacing.unit * 1,
},
});
const Header = ({ classes, selectedTabIndex, handleTabChange }) => (
<AppBar position="static">
<Toolbar className={classes.styledToolbar}>
<Tabs value={selectedTabIndex} onChange={handleTabChange}>
<Tab label="Reportees" />
<Tab label="Teams" />
<Tab label="Components" />
</Tabs>
<CredentialsMenu />
</Toolbar>
</AppBar>
);
Header.propTypes = {
classes: PropTypes.shape({}).isRequired,
selectedTabIndex: PropTypes.number.isRequired,
handleTabChange: PropTypes.func.isRequired,
};
export default withStyles(styles)(Header);

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

@ -1,126 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
import BugzillaComponents from '../BugzillaComponents';
import Reportees from '../Reportees';
const TabContainer = (props) => {
const { children } = props;
return (
<Typography component="div" style={{ padding: 4 }}>
{children}
</Typography>
);
};
TabContainer.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper,
},
grow: {
flexGrow: 1,
},
styledToolbar: {
'min-height': 48,
},
});
class MainTabs extends React.Component {
state = {
selectedTabIndex: 0,
};
handleChange = (event, selectedTabIndex) => {
this.setState({ selectedTabIndex });
};
renderTabContents(tabIndex) {
const {
ldapEmail, partialOrg, onPersonDetails, teamComponents,
bugzillaComponents, onComponentDetails,
} = this.props;
switch (tabIndex) {
case 0:
return (
<Reportees
ldapEmail={ldapEmail}
partialOrg={partialOrg}
onPersonDetails={onPersonDetails}
/>
);
case 1:
return (
<BugzillaComponents
bugzillaComponents={teamComponents}
onComponentDetails={onComponentDetails}
/>
);
case 2:
return (
<BugzillaComponents
bugzillaComponents={bugzillaComponents}
onComponentDetails={onComponentDetails}
/>
);
default:
return null;
}
}
render() {
const { classes, partialOrg, ldapEmail } = this.props;
const { selectedTabIndex } = this.state;
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar className={classes.styledToolbar}>
<Tabs value={selectedTabIndex} onChange={this.handleChange}>
<Tab label="Reportees" />
<Tab label="Teams" />
<Tab label="Components" />
</Tabs>
<div className={classes.grow} />
<Typography variant="subtitle1" color="inherit">
{partialOrg[ldapEmail].cn}
</Typography>
</Toolbar>
</AppBar>
<TabContainer>
{this.renderTabContents(selectedTabIndex)}
</TabContainer>
</div>
);
}
}
MainTabs.propTypes = {
classes: PropTypes.shape({}).isRequired,
ldapEmail: PropTypes.string.isRequired,
partialOrg: PropTypes.shape({}).isRequired,
bugzillaComponents: PropTypes.arrayOf(PropTypes.shape({})),
teamComponents: PropTypes.arrayOf(PropTypes.shape({})),
onComponentDetails: PropTypes.func.isRequired,
onPersonDetails: PropTypes.func.isRequired,
};
MainTabs.defaultProps = {
bugzillaComponents: [],
teamComponents: [],
};
export default withStyles(styles)(MainTabs);

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

@ -1,32 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import MainTabs from '../MainTabs';
import BugzillaComponents from '../BugzillaComponents';
import Reportees from '../Reportees';
const styles = ({
content: {
display: 'flex',
},
header: {
margin: '0.5rem 0 0 0',
},
});
class MainView extends React.Component {
renderTabContents() {
const {
ldapEmail, partialOrg, onPersonDetails, teamComponents,
bugzillaComponents, onComponentDetails, selectedTabIndex,
} = this.props;
switch (selectedTabIndex) {
case 0: {
return (
<Reportees
ldapEmail={ldapEmail}
partialOrg={partialOrg}
onPersonDetails={onPersonDetails}
/>
);
}
case 1: {
return (
<BugzillaComponents
bugzillaComponents={teamComponents}
onComponentDetails={onComponentDetails}
/>
);
}
case 2: {
return (
<BugzillaComponents
bugzillaComponents={bugzillaComponents}
onComponentDetails={onComponentDetails}
/>
);
}
default: {
return null;
}
}
}
const MainView = ({
ldapEmail, partialOrg, bugzillaComponents, teamComponents,
onComponentDetails, onPersonDetails,
}) => (
<div>
<MainTabs
ldapEmail={ldapEmail}
partialOrg={partialOrg}
onPersonDetails={onPersonDetails}
teamComponents={teamComponents}
onComponentDetails={onComponentDetails}
bugzillaComponents={bugzillaComponents}
/>
</div>
);
render() {
return this.renderTabContents();
}
}
MainView.propTypes = {
ldapEmail: PropTypes.string.isRequired,
@ -35,6 +53,7 @@ MainView.propTypes = {
teamComponents: PropTypes.arrayOf(PropTypes.shape({})),
onComponentDetails: PropTypes.func.isRequired,
onPersonDetails: PropTypes.func.isRequired,
selectedTabIndex: PropTypes.number.isRequired,
};
MainView.defaultProps = {
@ -42,4 +61,4 @@ MainView.defaultProps = {
teamComponents: [],
};
export default withStyles(styles)(MainView);
export default MainView;

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

@ -0,0 +1,4 @@
import React from 'react';
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
export default () => <ErrorPanel error="The requested route was not found." />;

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

@ -0,0 +1,11 @@
import React from 'react';
import { Route } from 'react-router-dom';
const PropsRoute = ({ component, ...props }) => (
<Route
{...props}
render={routeProps => React.createElement(component, Object.assign({}, routeProps, props))}
/>
);
export default PropsRoute;

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

@ -0,0 +1,5 @@
import React from 'react';
const AuthContext = React.createContext();
export default AuthContext;

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

@ -0,0 +1,109 @@
import mitt from 'mitt';
import UserSession from './UserSession';
import { renew as auth0Renew } from './auth0';
/**
* Controller for authentication-related pieces of the site.
*
* This encompasses knowledge of which authentication mechanisms are enabled, including
* credentials menu items, ongoing expiration monitoring, and any additional required UI.
* It also handles synchronizing sign-in status across tabs.
*/
export default class AuthController {
constructor() {
const events = mitt();
this.on = events.on;
this.off = events.off;
this.emit = events.emit;
this.renewalTimer = null;
window.addEventListener('storage', ({ storageArea, key }) => {
if (storageArea === localStorage && key === 'userSession') {
this.loadUserSession();
}
});
}
/**
* Reset the renewal timer based on the given user session.
*/
resetRenewalTimer(userSession) {
if (this.renewalTimer) {
window.clearTimeout(this.renewalTimer);
this.renewalTimer = null;
}
if (userSession && userSession.renewAfter) {
let timeout = Math.max(0, new Date(userSession.renewAfter) - new Date());
// if the timeout is in the future, apply up to a few minutes to it
// randomly. This avoids multiple tabs all trying to renew at the
// same time.
if (timeout > 0) {
timeout += Math.random() * 5 * 60 * 1000;
}
this.renewalTimer = window.setTimeout(() => {
this.renewalTimer = null;
this.renew({ userSession });
}, timeout);
}
}
/**
* Load the current user session (from localStorage).
*
* This will emit the user-session-changed event, but does not
* return the user session.
*/
loadUserSession() {
const storedUserSession = localStorage.getItem('userSession');
const userSession = storedUserSession
? UserSession.deserialize(storedUserSession)
: null;
this.userSession = userSession;
this.resetRenewalTimer(userSession);
this.emit('user-session-changed', userSession);
}
/**
* Get the current userSession instance
*/
getUserSession() {
return this.userSession;
}
/**
* Set the current user session, or (if null) delete the current user session.
*
* This will change the user session in all open windows/tabs, eventually triggering
* a call to any onSessionChanged callbacks.
*/
setUserSession = (userSession) => {
if (!userSession) {
localStorage.removeItem('userSession');
} else {
localStorage.setItem('userSession', userSession.serialize());
}
// localStorage updates do not trigger event listeners on the current window/tab,
// so invoke it directly
this.loadUserSession();
};
/**
* Renew the user session. This is not possible for all auth methods, and will trivially succeed
* for methods that do not support it. If it fails, the user will be logged out.
*/
async renew({ userSession }) {
try {
await auth0Renew({ userSession, authController: this });
} catch (err) {
this.setUserSession(null);
}
}
}

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

@ -0,0 +1,102 @@
import { OIDCCredentialAgent, Secrets } from 'taskcluster-client-web';
import { withRootUrl } from 'taskcluster-lib-urls';
const urls = withRootUrl('https://taskcluster.net');
/**
* An object representing a user session. Tools supports a variety of login methods,
* so this combines them all in a single representation.
*
* UserSessions are immutable -- when anything about the session changes, a new instance
* replaces the old. The `userChanged` method is useful to distinguish changes to the
* user identity from mere token renewals.
*
* Common properties are:
*
* - type - 'oidc' or 'credentials'
* - name - user name
* - clientArgs - arguments to pass to taskcluster-client-web Client constructors
* - renewAfter - date (Date or string) after which this session should be renewed,
* if applicable
*
* When type is 'oidc':
*
* - oidcProvider -- the provider (see taskcluster-login)
* - accessToken -- the accessToken to pass to taskcluster-login
* - fullName -- user's full name
* - picture -- URL of an image of the user
* - oidcSubject -- the 'sub' field of the id_token (useful for debugging user issues)
*
* When the type is 'credentials':
*
* - credentials -- the Taskcluster credentials (with or without a certificate)
*
* To fetch Taskcluster credentials for the user regardless of type, use the getCredentials
* method.
*/
export default class UserSession {
constructor(options) {
Object.assign(this, options);
if (this.accessToken) {
this.credentialAgent = new OIDCCredentialAgent({
accessToken: this.accessToken,
url: urls.api('login', 'v1', `/oidc-credentials/${this.oidcProvider}`),
});
}
}
static fromCredentials(credentials) {
return new UserSession({ type: 'credentials', credentials });
}
static fromOIDC(options) {
return new UserSession({ type: 'oidc', ...options });
}
// determine whether the user changed from old to new; this is used by other components
// to determine when to update in response to a sign-in/sign-out event
static userChanged(oldUser, newUser) {
if (!oldUser && !newUser) {
return false;
}
if (!oldUser || !newUser) {
return true;
}
return oldUser.type !== newUser.type || oldUser.name !== newUser.name;
}
// get the user's name
get name() {
return (
this.fullName
|| (this.credentials && this.credentials.clientId)
|| 'unknown'
);
}
// get the args used to create a new client object
get clientArgs() {
return this.credentialAgent
? { credentialAgent: this.credentialAgent }
: { credentials: this.credentials };
}
// load Taskcluster credentials for this user
getCredentials() {
return this.credentials
? Promise.resolve(this.credentials)
: this.credentialAgent.getCredentials({});
}
static deserialize(value) {
return new UserSession(JSON.parse(value));
}
serialize() {
return JSON.stringify({ ...this, credentialAgent: undefined });
}
getTaskClusterSecretsClient = () => new Secrets({ ...this.clientArgs, rootUrl: 'https://taskcluster.net' });
}

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

@ -0,0 +1,40 @@
import { fromNow } from 'taskcluster-client-web';
import { WebAuth } from 'auth0-js';
import UserSession from './UserSession';
import config from '../../config';
export const webAuth = new WebAuth(config.auth0Options);
export function userSessionFromAuthResult(authResult) {
return UserSession.fromOIDC({
oidcProvider: 'mozilla-auth0',
accessToken: authResult.accessToken,
fullName: authResult.idTokenPayload.name,
email: authResult.idTokenPayload.email,
picture: authResult.idTokenPayload.picture,
oidcSubject: authResult.idTokenPayload.sub,
// per https://wiki.mozilla.org/Security/Guidelines/OpenID_connect#Session_handling
renewAfter: fromNow('15 minutes'),
});
}
/* eslint-disable consistent-return */
export async function renew({ userSession, authController }) {
if (
!userSession
|| userSession.type !== 'oidc'
|| userSession.oidcProvider !== 'mozilla-auth0'
) {
return;
}
return new Promise((accept, reject) => webAuth.renewAuth({}, (err, authResult) => {
if (err) {
return reject(err);
} if (!authResult) {
return reject(new Error('no authResult'));
}
authController.setUserSession(userSessionFromAuthResult(authResult));
accept();
}));
}

18
src/config.js Normal file
Просмотреть файл

@ -0,0 +1,18 @@
const loginCallbackRoute = '/callback';
const config = {
redirectRoute: loginCallbackRoute,
taskclusterSecrets: {
orgData: 'project/bugzilla-management-dashboard/realOrg',
},
auth0Options: {
domain: 'auth.mozilla.auth0.com',
clientID: 'DGloMN2BXb0AC7lF5eRyOe1GXweqBAiI',
redirectUri: new URL(loginCallbackRoute, window.location).href,
scope: 'taskcluster-credentials full-user-credentials openid profile email',
audience: 'login.taskcluster.net',
responseType: 'token id_token',
},
};
export default config;

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

@ -1,210 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import MainView from '../../components/MainView';
import BugzillaComponentDetails from '../../components/BugzillaComponentDetails';
import PersonDetails from '../../components/PersonDetails';
import getAllReportees from '../../utils/getAllReportees';
import getBugzillaOwners from '../../utils/getBugzillaOwners';
import getBugsCountAndLink from '../../utils/bugzilla/getBugsCountAndLink';
import METRICS from '../../utils/bugzilla/metrics';
import TEAMS_CONFIG from '../../teamsConfig';
class MainContainer extends Component {
state = {
ldapEmail: '',
bugzillaComponents: {},
partialOrg: undefined,
teamComponents: {},
showComponent: undefined,
showPerson: undefined,
};
static propTypes = {
ldapEmail: PropTypes.string,
};
static defaultProps = {
ldapEmail: '',
};
constructor(props) {
super(props);
const { ldapEmail } = this.props;
this.state.ldapEmail = ldapEmail;
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleShowComponentDetails = this.handleShowComponentDetails.bind(this);
this.handleShowPersonDetails = this.handleShowPersonDetails.bind(this);
this.handleComponentBackToMenu = this.handleComponentBackToMenu.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 bugzillaComponents(bzOwners, partialOrg) {
// bzOwners uses the bugzilla email address as the key
// while partialOrg uses the LDAP email address
/* 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 }) => {
if (!result[`${product}::${component}`]) {
result[`${product}::${component}`] = {};
}
result[`${product}::${component}`] = {
label: `${product}::${component}`,
bugzillaEmail: bugzillaEmail || mail,
product,
component,
metrics: {},
};
});
}
return result;
}, {});
/* 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 { metrics } = bugzillaComponents[`${product}::${component}`];
await Promise.all(Object.keys(METRICS).map(async (metric) => {
metrics[metric] = await getBugsCountAndLink(product, component, metric);
metrics[metric].label = METRICS[metric].label;
}));
this.setState({ bugzillaComponents });
});
}
async retrieveData(ldapEmail) {
const [bzOwners, partialOrg] = await Promise.all([
getBugzillaOwners(),
this.getReportees(ldapEmail),
]);
this.teamsData();
this.bugzillaComponents(bzOwners, partialOrg);
}
async teamsData() {
const teamComponents = Object.assign({}, TEAMS_CONFIG);
// This will cause the teams to be displayed before having any metrics
this.setState({ teamComponents });
Object.entries(teamComponents).map(async ([teamKey, teamInfo]) => {
const team = {
teamKey,
...teamInfo,
metrics: {},
};
const { product, component } = teamInfo;
await Promise.all(Object.keys(METRICS).map(async (metric) => {
team.metrics[metric] = await getBugsCountAndLink(product, component, metric);
}));
teamComponents[teamKey] = team;
this.setState({ teamComponents });
});
}
handleChange(event) {
this.setState({
ldapEmail: event.target.selectedTabIndex,
bugzillaComponents: undefined,
partialOrg: undefined,
});
}
async handleSubmit(event) {
event.preventDefault();
const { ldapEmail } = this.state;
this.retrieveData(ldapEmail);
}
handleShowComponentDetails(event, properties) {
event.preventDefault();
const { componentKey, teamKey } = properties;
// IDEA: In the future we could unify bugzilla components and teams into
// the same data structure and make this logic simpler. We could use a
// property 'team' to distinguish a component from a set of components
if (teamKey) {
this.setState(prevState => ({
showComponent: {
title: prevState.teamComponents[teamKey].label,
...prevState.teamComponents[teamKey],
},
}));
} else {
this.setState(prevState => ({
showComponent: {
title: componentKey,
...prevState.bugzillaComponents[componentKey],
},
}));
}
}
handleShowPersonDetails(event, properties) {
event.preventDefault();
const { partialOrg } = this.state;
this.setState({
showPerson: partialOrg[properties.ldapEmail],
});
}
handleComponentBackToMenu(event) {
event.preventDefault();
this.setState({
showComponent: undefined,
showPerson: undefined,
});
}
render() {
const {
ldapEmail, showComponent, showPerson, bugzillaComponents, partialOrg, teamComponents,
} = this.state;
return (
<div>
{showComponent && (
<BugzillaComponentDetails
{...showComponent}
title={showComponent.title}
onGoBack={this.handleComponentBackToMenu}
/>
)}
{showPerson && (
<PersonDetails
person={showPerson}
bugzillaComponents={Object.values(bugzillaComponents)}
onGoBack={this.handleComponentBackToMenu}
/>
)}
{!showComponent && !showPerson && partialOrg && (
<MainView
ldapEmail={ldapEmail}
partialOrg={partialOrg}
bugzillaComponents={Object.values(bugzillaComponents)}
teamComponents={Object.values(teamComponents)}
onComponentDetails={this.handleShowComponentDetails}
onPersonDetails={this.handleShowPersonDetails}
/>
)}
</div>
);
}
}
export default MainContainer;

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

@ -1,9 +1,45 @@
import getOrgChart from './getOrgChart';
import config from '../config';
const findReportees = (completeOrg, ldapEmail) => {
const buildOrgChartData = (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],
};
}
}
if (!org[mail].bugzillaEmail) {
org[mail].bugzillaEmail = mail;
}
});
return org;
};
const getOrgChart = async (secretsClient) => {
const { secret } = await await secretsClient.get(config.taskclusterSecrets.orgData);
return buildOrgChartData(secret.employees);
};
const findReportees = (completeOrg, email) => {
let allReportees = {};
allReportees[ldapEmail] = completeOrg[ldapEmail];
const { reportees } = completeOrg[ldapEmail];
allReportees[email] = completeOrg[email];
const { reportees } = completeOrg[email];
if (reportees.length !== 0) {
reportees.forEach((reporteeEmail) => {
const partialOrg = findReportees(completeOrg, reporteeEmail);
@ -13,8 +49,8 @@ const findReportees = (completeOrg, ldapEmail) => {
return allReportees;
};
const getAllReportees = async (ldapEmail) => {
const completeOrg = await getOrgChart();
const getAllReportees = async (secretsClient, ldapEmail) => {
const completeOrg = await getOrgChart(secretsClient);
return findReportees(completeOrg, ldapEmail);
};

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

@ -1,37 +0,0 @@
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],
};
}
}
if (!org[mail].bugzillaEmail) {
org[mail].bugzillaEmail = mail;
}
});
return org;
};
const getOrgChart = async () => {
const people = await (await fetch('people.json')).json();
return orgChart(people);
};
export default getOrgChart;

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

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
import { webAuth, userSessionFromAuthResult } from '../../components/auth/auth0';
export default class Auth0Login extends React.PureComponent {
static propTypes = {
history: PropTypes.shape({}).isRequired,
setUserSession: PropTypes.func.isRequired,
};
state = {};
componentDidMount() {
const { history, setUserSession } = this.props;
if (!window.location.hash) {
webAuth.authorize();
} else if (window !== window.top) {
// for silent renewal, auth0-js opens this page in an iframe, and expects
// a postMessage back, and that's it.
window.parent.postMessage(window.location.hash, window.origin);
} else {
webAuth.parseHash(window.location.hash, (loginError, authResult) => {
if (loginError) {
this.setState({ loginError });
} else {
setUserSession(userSessionFromAuthResult(authResult));
if (window.opener) {
window.close();
} else {
history.push('/');
}
}
});
}
}
render() {
const { loginError } = this.state;
if (loginError) {
return <ErrorPanel error={loginError} />;
}
if (window.location.hash) {
return <p>Logging in..</p>;
}
return <p>Redirecting..</p>;
}
}

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

@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import AuthContext from '../../components/auth/AuthContext';
import config from '../../config';
const styles = theme => ({
button: {
margin: theme.spacing.unit,
},
});
class CredentialsMenu extends React.PureComponent {
static contextType = AuthContext;
static propTypes = {
classes: PropTypes.shape({}).isRequired,
};
static handleLoginRequest() {
const loginView = new URL(config.redirectRoute, window.location);
window.open(loginView, '_blank');
}
componentDidMount() {
const { context } = this;
if (context) {
context.on(
'user-session-changed',
this.handleUserSessionChanged,
);
}
}
componentWillUnmount() {
const { context } = this.context;
if (context) {
context.off(
'user-session-changed',
this.handleUserSessionChanged,
);
}
}
handleUserSessionChanged = () => {
this.forceUpdate();
};
render() {
// note: an update to the userSession will cause a forceUpdate
const { context } = this;
const { classes } = this.props;
const userSession = context && context.getUserSession();
return (
userSession ? (
<Button
size="small"
variant="contained"
color="secondary"
className={classes.button}
onClick={() => context.setUserSession(null)}
>
Sign out
</Button>
) : (
<Button
size="small"
variant="contained"
color="secondary"
className={classes.button}
onClick={CredentialsMenu.handleLoginRequest}
>
Sign in
</Button>
)
);
}
}
export default withStyles(styles)(CredentialsMenu);

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

@ -1,12 +1,239 @@
import React from 'react';
import MainContainer from '../../containers/MainContainer';
import React, { Component } from 'react';
import AuthContext from '../../components/auth/AuthContext';
import Header from '../../components/Header';
import MainView from '../../components/MainView';
import BugzillaComponentDetails from '../../components/BugzillaComponentDetails';
import PersonDetails from '../../components/PersonDetails';
import getAllReportees from '../../utils/getAllReportees';
import getBugzillaOwners from '../../utils/getBugzillaOwners';
import getBugsCountAndLink from '../../utils/bugzilla/getBugsCountAndLink';
import METRICS from '../../utils/bugzilla/metrics';
import TEAMS_CONFIG from '../../teamsConfig';
// eslint-disable-next-line react/prop-types
const Main = ({ location }) => (
<MainContainer
// XXX: Until we have SSO + real org access
ldapEmail={new URLSearchParams(location.search).get('ldapEmail') || 'manager@mozilla.com'}
/>
);
const DEFAULT_STATE = {
bugzillaComponents: {},
partialOrg: undefined,
teamComponents: {},
selectedTabIndex: 0,
showComponent: undefined,
showPerson: undefined,
};
export default Main;
class MainContainer extends Component {
static contextType = AuthContext;
state = DEFAULT_STATE;
constructor(props) {
super(props);
this.handleShowComponentDetails = this.handleShowComponentDetails.bind(this);
this.handleShowPersonDetails = this.handleShowPersonDetails.bind(this);
this.handleComponentBackToMenu = this.handleComponentBackToMenu.bind(this);
}
componentDidMount() {
const { context } = this;
if (context) {
context.on(
'user-session-changed',
this.handleUserSessionChanged,
);
this.fetchData();
}
}
componentWillUnmount() {
const { context } = this.context;
if (context) {
context.off(
'user-session-changed',
this.handleUserSessionChanged,
);
}
}
async getReportees(userSession, ldapEmail) {
const secretsClient = userSession.getTaskClusterSecretsClient();
const partialOrg = await getAllReportees(secretsClient, ldapEmail);
this.setState({ partialOrg });
return partialOrg;
}
handleChangeSelectedTab = (event, selectedTabIndex) => {
this.setState({ selectedTabIndex });
};
handleUserSessionChanged = () => {
this.fetchData();
};
fetchData() {
const { context } = this;
const userSession = context && context.getUserSession();
if (userSession) {
const { location } = this.props;
const ldapEmail = new URLSearchParams(location.search).get('ldapEmail') || (userSession && userSession.email);
this.setState({ ldapEmail });
this.retrieveData(userSession, ldapEmail);
} else {
this.setState(DEFAULT_STATE);
}
}
async bugzillaComponents(bzOwners, partialOrg) {
// bzOwners uses the bugzilla email address as the key
// while partialOrg uses the LDAP email address
/* 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 }) => {
if (!result[`${product}::${component}`]) {
result[`${product}::${component}`] = {};
}
result[`${product}::${component}`] = {
label: `${product}::${component}`,
bugzillaEmail: bugzillaEmail || mail,
product,
component,
metrics: {},
};
});
}
return result;
}, {});
/* 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 { metrics } = bugzillaComponents[`${product}::${component}`];
await Promise.all(Object.keys(METRICS).map(async (metric) => {
metrics[metric] = await getBugsCountAndLink(product, component, metric);
metrics[metric].label = METRICS[metric].label;
}));
this.setState({ bugzillaComponents });
});
}
async retrieveData(userSession, ldapEmail) {
const [bzOwners, partialOrg] = await Promise.all([
getBugzillaOwners(),
this.getReportees(userSession, ldapEmail),
]);
this.teamsData();
this.bugzillaComponents(bzOwners, partialOrg);
}
async teamsData() {
const teamComponents = Object.assign({}, TEAMS_CONFIG);
// This will cause the teams to be displayed before having any metrics
this.setState({ teamComponents });
Object.entries(teamComponents).map(async ([teamKey, teamInfo]) => {
const team = {
teamKey,
...teamInfo,
metrics: {},
};
const { product, component } = teamInfo;
await Promise.all(Object.keys(METRICS).map(async (metric) => {
team.metrics[metric] = await getBugsCountAndLink(product, component, metric);
}));
teamComponents[teamKey] = team;
this.setState({ teamComponents });
});
}
handleShowComponentDetails(event, properties) {
event.preventDefault();
const { componentKey, teamKey } = properties;
// IDEA: In the future we could unify bugzilla components and teams into
// the same data structure and make this logic simpler. We could use a
// property 'team' to distinguish a component from a set of components
if (teamKey) {
this.setState(prevState => ({
showComponent: {
title: prevState.teamComponents[teamKey].label,
...prevState.teamComponents[teamKey],
},
}));
} else {
this.setState(prevState => ({
showComponent: {
title: componentKey,
...prevState.bugzillaComponents[componentKey],
},
}));
}
}
handleShowPersonDetails(event, properties) {
event.preventDefault();
const { partialOrg } = this.state;
this.setState({
showPerson: partialOrg[properties.ldapEmail],
});
}
handleComponentBackToMenu(event) {
event.preventDefault();
this.setState({
showComponent: undefined,
showPerson: undefined,
});
}
render() {
const {
showComponent,
showPerson,
bugzillaComponents,
ldapEmail,
partialOrg,
teamComponents,
selectedTabIndex,
} = this.state;
const { context } = this;
const userSession = context.getUserSession();
return (
<div>
<Header
selectedTabIndex={selectedTabIndex}
handleTabChange={this.handleChangeSelectedTab}
/>
{!userSession && <h3>Please sign in</h3>}
{showComponent && (
<BugzillaComponentDetails
{...showComponent}
title={showComponent.title}
onGoBack={this.handleComponentBackToMenu}
/>
)}
{showPerson && (
<PersonDetails
person={showPerson}
bugzillaComponents={Object.values(bugzillaComponents)}
onGoBack={this.handleComponentBackToMenu}
/>
)}
{!showComponent && !showPerson && partialOrg && userSession && (
<MainView
ldapEmail={ldapEmail}
partialOrg={partialOrg}
bugzillaComponents={Object.values(bugzillaComponents)}
teamComponents={Object.values(teamComponents)}
onComponentDetails={this.handleShowComponentDetails}
onPersonDetails={this.handleShowPersonDetails}
selectedTabIndex={selectedTabIndex}
/>
)}
</div>
);
}
}
export default MainContainer;

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

@ -15,6 +15,7 @@ it('renders Someone with no reportees', () => {
teams={{}}
onComponentDetails={() => null}
onPersonDetails={() => null}
selectedTabIndex={0}
/>
))
.toJSON();
@ -31,6 +32,7 @@ it('renders Manager who has reportees', () => {
teams={{}}
onComponentDetails={() => null}
onPersonDetails={() => null}
selectedTabIndex={0}
/>
))
.toJSON();
@ -47,6 +49,7 @@ it('renders Manager who has reportees and teams', () => {
teams={Object.values(teamsConfig)}
onComponentDetails={() => null}
onPersonDetails={() => null}
selectedTabIndex={0}
/>
))
.toJSON();

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

@ -1,745 +1,223 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders Manager who has reportees 1`] = `
<div>
<div
className="Reportees-root-1"
>
<div
className="MainTabs-root-3"
height="1rem"
>
 
</div>
<div
className="Reportees-person-4"
>
<header
className="MuiPaper-root-15 MuiPaper-elevation4-21 MuiAppBar-root-6 MuiAppBar-positionStatic-10 MuiAppBar-colorPrimary-13"
>
<div
className="MuiToolbar-root-42 MuiToolbar-regular-44 MuiToolbar-gutters-43 MainTabs-styledToolbar-5"
>
<div
className="MuiTabs-root-46"
>
<div
className="MuiTabs-flexContainer-47"
>
<div
className="MuiTabs-scroller-49 MuiTabs-fixed-50"
onScroll={[Function]}
role="tablist"
style={
Object {
"marginBottom": 0,
}
}
>
<div
className="MuiTabs-flexContainer-47"
>
<button
aria-selected={true}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57 MuiTab-selected-60"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Reportees
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
<button
aria-selected={false}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Teams
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
<button
aria-selected={false}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Components
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
</div>
<span
className="MuiPrivateTabIndicator-root-70 MuiPrivateTabIndicator-colorSecondary-72 MuiTabs-indicator-54"
style={
Object {
"left": 0,
"width": 0,
}
}
/>
</div>
</div>
</div>
<div
className="MainTabs-grow-4"
/>
<h6
className="MuiTypography-root-73 MuiTypography-subtitle1-91 MuiTypography-colorInherit-102"
>
Manager
</h6>
</div>
</header>
<div
className="MuiTypography-root-73 MuiTypography-body1-82"
style={
Object {
"padding": 4,
}
}
className="DrilldownIcon-svgWrapper-5"
name="manager@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<div
className="Reportees-root-109"
<svg
aria-hidden="true"
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<div
height="1rem"
>
 
</div>
<div
className="Reportees-person-112"
>
<div
className="DrilldownIcon-svgWrapper-113"
name="manager@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Manager
</span>
</div>
<div
className="Reportees-person-112"
>
<div
className="DrilldownIcon-svgWrapper-113"
name="someone@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Someone
</span>
</div>
</div>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Manager
</span>
</div>
<div
className="Reportees-person-4"
>
<div
className="DrilldownIcon-svgWrapper-5"
name="someone@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Someone
</span>
</div>
</div>
`;
exports[`renders Manager who has reportees and teams 1`] = `
<div>
<div
className="Reportees-root-1"
>
<div
className="MainTabs-root-3"
height="1rem"
>
 
</div>
<div
className="Reportees-person-4"
>
<header
className="MuiPaper-root-15 MuiPaper-elevation4-21 MuiAppBar-root-6 MuiAppBar-positionStatic-10 MuiAppBar-colorPrimary-13"
>
<div
className="MuiToolbar-root-42 MuiToolbar-regular-44 MuiToolbar-gutters-43 MainTabs-styledToolbar-5"
>
<div
className="MuiTabs-root-46"
>
<div
className="MuiTabs-flexContainer-47"
>
<div
className="MuiTabs-scroller-49 MuiTabs-fixed-50"
onScroll={[Function]}
role="tablist"
style={
Object {
"marginBottom": 0,
}
}
>
<div
className="MuiTabs-flexContainer-47"
>
<button
aria-selected={true}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57 MuiTab-selected-60"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Reportees
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
<button
aria-selected={false}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Teams
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
<button
aria-selected={false}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Components
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
</div>
<span
className="MuiPrivateTabIndicator-root-70 MuiPrivateTabIndicator-colorSecondary-72 MuiTabs-indicator-54"
style={
Object {
"left": 0,
"width": 0,
}
}
/>
</div>
</div>
</div>
<div
className="MainTabs-grow-4"
/>
<h6
className="MuiTypography-root-73 MuiTypography-subtitle1-91 MuiTypography-colorInherit-102"
>
Manager
</h6>
</div>
</header>
<div
className="MuiTypography-root-73 MuiTypography-body1-82"
style={
Object {
"padding": 4,
}
}
className="DrilldownIcon-svgWrapper-5"
name="manager@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<div
className="Reportees-root-109"
<svg
aria-hidden="true"
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<div
height="1rem"
>
 
</div>
<div
className="Reportees-person-112"
>
<div
className="DrilldownIcon-svgWrapper-113"
name="manager@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Manager
</span>
</div>
<div
className="Reportees-person-112"
>
<div
className="DrilldownIcon-svgWrapper-113"
name="someone@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Someone
</span>
</div>
</div>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Manager
</span>
</div>
<div
className="Reportees-person-4"
>
<div
className="DrilldownIcon-svgWrapper-5"
name="someone@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Someone
</span>
</div>
</div>
`;
exports[`renders Someone with no reportees 1`] = `
<div>
<div
className="Reportees-root-1"
>
<div
className="MainTabs-root-3"
height="1rem"
>
 
</div>
<div
className="Reportees-person-4"
>
<header
className="MuiPaper-root-15 MuiPaper-elevation4-21 MuiAppBar-root-6 MuiAppBar-positionStatic-10 MuiAppBar-colorPrimary-13"
>
<div
className="MuiToolbar-root-42 MuiToolbar-regular-44 MuiToolbar-gutters-43 MainTabs-styledToolbar-5"
>
<div
className="MuiTabs-root-46"
>
<div
className="MuiTabs-flexContainer-47"
>
<div
className="MuiTabs-scroller-49 MuiTabs-fixed-50"
onScroll={[Function]}
role="tablist"
style={
Object {
"marginBottom": 0,
}
}
>
<div
className="MuiTabs-flexContainer-47"
>
<button
aria-selected={true}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57 MuiTab-selected-60"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Reportees
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
<button
aria-selected={false}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Teams
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
<button
aria-selected={false}
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onContextMenu={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
role="tab"
tabIndex="0"
type="button"
>
<span
className="MuiTab-wrapper-63"
>
<span
className="MuiTab-labelContainer-64"
>
<span
className="MuiTab-label-65"
>
Components
</span>
</span>
</span>
<span
className="MuiTouchRipple-root-124"
/>
</button>
</div>
<span
className="MuiPrivateTabIndicator-root-70 MuiPrivateTabIndicator-colorSecondary-72 MuiTabs-indicator-54"
style={
Object {
"left": 0,
"width": 0,
}
}
/>
</div>
</div>
</div>
<div
className="MainTabs-grow-4"
/>
<h6
className="MuiTypography-root-73 MuiTypography-subtitle1-91 MuiTypography-colorInherit-102"
>
Someone
</h6>
</div>
</header>
<div
className="MuiTypography-root-73 MuiTypography-body1-82"
style={
Object {
"padding": 4,
}
}
className="DrilldownIcon-svgWrapper-5"
name="manager@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<div
className="Reportees-root-109"
<svg
aria-hidden="true"
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<div
height="1rem"
>
 
</div>
<div
className="Reportees-person-112"
>
<div
className="DrilldownIcon-svgWrapper-113"
name="manager@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Manager
</span>
</div>
<div
className="Reportees-person-112"
>
<div
className="DrilldownIcon-svgWrapper-113"
name="someone@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Someone
</span>
</div>
</div>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Manager
</span>
</div>
<div
className="Reportees-person-4"
>
<div
className="DrilldownIcon-svgWrapper-5"
name="someone@mozilla.com"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex="0"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
/>
<path
d="M0 0h24v24H0z"
fill="none"
/>
</svg>
</div>
<span>
Someone
</span>
</div>
</div>
`;

216
yarn.lock
Просмотреть файл

@ -743,17 +743,6 @@
"@babel/runtime" "7.0.0"
recompose "^0.29.0"
"@material-ui/lab@^3.0.0-alpha.30":
version "3.0.0-alpha.30"
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-3.0.0-alpha.30.tgz#c6c64d0ff2b28410a09e4009f3677499461f3df8"
integrity sha512-d8IXbkQO92Ln7f/Tzy8Q5cLi/sMWH/Uz1xrOO5NKUgg42whwyCuoT9ErddDPFNQmPi9d1C7A5AG8ONjEAbAIyQ==
dependencies:
"@babel/runtime" "^7.2.0"
"@material-ui/utils" "^3.0.0-alpha.2"
classnames "^2.2.5"
keycode "^2.1.9"
prop-types "^15.6.0"
"@material-ui/system@^3.0.0-alpha.0":
version "3.0.0-alpha.2"
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-3.0.0-alpha.2.tgz#096e80c8bb0f70aea435b9e38ea7749ee77b4e46"
@ -822,13 +811,12 @@
babel-loader "^8.0.4"
babel-merge "^2.0.1"
"@neutrinojs/copy@^8.3.0":
version "8.3.0"
resolved "https://registry.yarnpkg.com/@neutrinojs/copy/-/copy-8.3.0.tgz#d41a7124b677103134063ef1a4f2a671da1b7758"
integrity sha1-1BpxJLZ3EDE0Bj7xpPKmcdobd1g=
"@neutrinojs/copy@^9.0.0-beta.1":
version "9.0.0-rc.0"
resolved "https://registry.yarnpkg.com/@neutrinojs/copy/-/copy-9.0.0-rc.0.tgz#9463e6557338a8433afff58fbaba206d38f2039b"
integrity sha512-/Ucrl+jHi1lCbVtQ9tVPijLgP/vEjxWSORjTE9DF22BsyVv0HUPcWiWBj+Ahv3ZFS7B2gmMx6CK056OK+uqMmg==
dependencies:
copy-webpack-plugin "^4.5.1"
deepmerge "^1.5.2"
copy-webpack-plugin "^4.6.0"
"@neutrinojs/dev-server@9.0.0-beta.1":
version "9.0.0-beta.1"
@ -1430,6 +1418,18 @@ atob@^2.1.1:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
auth0-js@9.2.3:
version "9.2.3"
resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-9.2.3.tgz#f98650a742c83567e887703c5972d0c7275e403d"
integrity sha1-+YZQp0LINWfoh3A8WXLQxydeQD0=
dependencies:
base64-js "^1.2.0"
idtoken-verifier "^1.1.1"
qs "^6.4.0"
superagent "^3.8.2"
url-join "^1.1.0"
winchan "^0.2.0"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -1447,6 +1447,13 @@ axobject-query@^2.0.1:
dependencies:
ast-types-flow "0.0.7"
b64@4.x.x:
version "4.1.2"
resolved "https://registry.yarnpkg.com/b64/-/b64-4.1.2.tgz#7015372ba8101f7fb18da070717a93c28c8580d8"
integrity sha512-+GUspBxlH3CJaxMUGUE1EBoWM6RKgWiYwUDal0qdf8m3ArnXNN1KzKVo5HOnE/FSq4HHyWf3TlHLsZI8PKQgrQ==
dependencies:
hoek "6.x.x"
babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@ -1649,7 +1656,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base64-js@^1.0.2:
base64-js@^1.0.2, base64-js@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==
@ -1732,6 +1739,21 @@ boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
boom@7.x.x:
version "7.3.0"
resolved "https://registry.yarnpkg.com/boom/-/boom-7.3.0.tgz#733a6d956d33b0b1999da3fe6c12996950d017b9"
integrity sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==
dependencies:
hoek "6.x.x"
bounce@1.x.x:
version "1.2.3"
resolved "https://registry.yarnpkg.com/bounce/-/bounce-1.2.3.tgz#2b286d36eb21d5f08fe672dd8cd37a109baad121"
integrity sha512-3G7B8CyBnip5EahCZJjnvQ1HLyArC6P5e+xcolo13BVI9ogFaDOsNMAE7FIWliHtIkYI8/nTRCvCY9tZa3Mu4g==
dependencies:
boom "7.x.x"
hoek "6.x.x"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -2249,7 +2271,7 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
component-emitter@^1.2.1:
component-emitter@^1.2.0, component-emitter@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
@ -2343,6 +2365,11 @@ cookie@0.3.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
cookiejar@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
@ -2360,7 +2387,7 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-webpack-plugin@^4.5.1:
copy-webpack-plugin@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae"
integrity sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==
@ -2459,6 +2486,13 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
cryptiles@4.x.x:
version "4.1.3"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-4.1.3.tgz#2461d3390ea0b82c643a6ba79f0ed491b0934c25"
integrity sha512-gT9nyTMSUC1JnziQpPbxKGBbUg8VL7Zn2NB4E1cJYvuXdElHrwxrV9bmltZGDzet45zSDGyYceueke1TjynGzw==
dependencies:
boom "7.x.x"
crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@ -2476,6 +2510,11 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
crypto-js@^3.1.9-1:
version "3.1.9-1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8"
integrity sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=
css-loader@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
@ -3473,7 +3512,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
extend@~3.0.2:
extend@^3.0.0, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@ -3766,7 +3805,7 @@ forever-agent@~0.6.1:
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
form-data@~2.3.2:
form-data@^2.3.1, form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
@ -3775,6 +3814,11 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formidable@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659"
integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -4127,6 +4171,17 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
hawk@^7.0.7:
version "7.0.10"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-7.0.10.tgz#960f72edac9c6b9114c8387886d7278fba9119eb"
integrity sha512-3RWF4SXN9CdZ1VDAe6Pn3Rd0tC3Lw+GV+esX5oKCrXoScZK3Ri6dl5Wt986M/hlzU+GuapTGiB0rBhGeRIBQsw==
dependencies:
b64 "4.x.x"
boom "7.x.x"
cryptiles "4.x.x"
hoek "6.x.x"
sntp "3.x.x"
he@1.2.x:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@ -4157,6 +4212,11 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoek@6.x.x:
version "6.1.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c"
integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
@ -4357,6 +4417,17 @@ icss-utils@^2.1.0:
dependencies:
postcss "^6.0.1"
idtoken-verifier@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-1.2.0.tgz#4654f1f07ab7a803fc9b1b8b36057e2a87ad8b09"
integrity sha512-8jmmFHwdPz8L73zGNAXHHOV9yXNC+Z0TUBN5rafpoaFaLFltlIFr1JkQa3FYAETP23eSsulVw0sBiwrE8jqbUg==
dependencies:
base64-js "^1.2.0"
crypto-js "^3.1.9-1"
jsbn "^0.1.0"
superagent "^3.8.2"
url-join "^1.1.0"
ieee754@^1.1.4:
version "1.1.12"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
@ -5279,7 +5350,7 @@ js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0:
argparse "^1.0.7"
esprima "^4.0.0"
jsbn@~0.1.0:
jsbn@^0.1.0, jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
@ -5442,11 +5513,6 @@ jsx-ast-utils@^2.0.1:
dependencies:
array-includes "^3.0.3"
keycode@^2.1.9:
version "2.2.0"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
integrity sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=
killable@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@ -5866,7 +5932,7 @@ merge@^1.2.0:
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
methods@~1.1.2:
methods@^1.1.1, methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
@ -5934,6 +6000,11 @@ mime@1.4.1:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
mime@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.0.3, mime@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
@ -6039,6 +6110,11 @@ mississippi@^3.0.0:
stream-each "^1.1.0"
through2 "^2.0.0"
mitt@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.1.3.tgz#528c506238a05dce11cd914a741ea2cc332da9b8"
integrity sha512-mUDCnVNsAi+eD6qA0HkRkwYczbLHJ49z17BGe2PYRhZL4wpZUFZGJHU7/5tmvohoma+Hdn0Vh/oJTiPEmgSruA==
mixin-deep@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
@ -7054,6 +7130,20 @@ qs@6.5.2, qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
qs@^6.4.0, qs@^6.5.1:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
query-string@^6.1.0:
version "6.4.2"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.4.2.tgz#8be1dbd105306aebf86022144f575a29d516b713"
integrity sha512-DfJqAen17LfLA3rQ+H5S4uXphrF+ANU1lT2ijds4V/Tj4gZxA3gx5/tg1bz7kYCmwna7LyJNCYqO7jNRzo3aLw==
dependencies:
decode-uri-component "^0.2.0"
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"
query-string@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.2.0.tgz#468edeb542b7e0538f9f9b1aeb26f034f19c86e1"
@ -7291,7 +7381,7 @@ read-pkg@^4.0.1:
parse-json "^4.0.0"
pify "^3.0.0"
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
@ -7943,6 +8033,16 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
sntp@3.x.x:
version "3.0.2"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-3.0.2.tgz#3f0b5de6115681dce82a9478691f0e5c552de5a3"
integrity sha512-MCAPpBPFjNp1fwDVCLSRuWuH9gONtb2R+lS1esC6Mp8lP6jy60FVUtP/Qr0jBvcWAVbhzx06y1b6ptXiy32dug==
dependencies:
boom "7.x.x"
bounce "1.x.x"
hoek "6.x.x"
teamwork "3.x.x"
sockjs-client@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177"
@ -8072,6 +8172,11 @@ spdy@^3.4.1:
select-hose "^2.0.0"
spdy-transport "^2.0.18"
split-on-first@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.0.0.tgz#648af4ce9a28fbcaadd43274455f298b55025fc6"
integrity sha512-mjA57TQtdWztVZ9THAjGNpgbuIrNfsNrGa5IyK94NoPaT4N14M+GI4jD7t4arLjFkYRQWdETC5RxFzLWouoB3A==
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@ -8288,6 +8393,22 @@ style-loader@^0.23.1:
loader-utils "^1.1.0"
schema-utils "^1.0.0"
superagent@^3.8.2:
version "3.8.3"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"
integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==
dependencies:
component-emitter "^1.2.0"
cookiejar "^2.1.0"
debug "^3.1.0"
extend "^3.0.0"
form-data "^2.3.1"
formidable "^1.2.0"
methods "^1.1.1"
mime "^1.4.1"
qs "^6.5.1"
readable-stream "^2.3.5"
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@ -8350,6 +8471,31 @@ tar@^4:
safe-buffer "^5.1.2"
yallist "^3.0.2"
taskcluster-client-web@9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/taskcluster-client-web/-/taskcluster-client-web-9.0.0.tgz#f160f264852683e36cc4f7bcaa57895ad91b3f6e"
integrity sha512-cuAk5JLtX9SSngsfUiIARWWYj0uGKt2vB6yqT3w2gSR3mXReLb9AlQHsgpL51VgdpbXw2YMopUV8U8auCmSBmQ==
dependencies:
crypto-js "^3.1.9-1"
hawk "^7.0.7"
query-string "^6.1.0"
taskcluster-lib-urls "^10.0.0"
taskcluster-lib-urls@^10.0.0:
version "10.1.1"
resolved "https://registry.yarnpkg.com/taskcluster-lib-urls/-/taskcluster-lib-urls-10.1.1.tgz#67d5b9449b947e5234eafdd15c46267dde29bf74"
integrity sha512-tdrK++rCX73FMXk/cXwS6RLTjA3pX8hJlxg1ECLs3L3llCOPMNhQ4wi6lb6yMgHc/s5on/Edj6AlAH7gkxzgPg==
taskcluster-lib-urls@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/taskcluster-lib-urls/-/taskcluster-lib-urls-12.0.0.tgz#f56190eec9e9597d37a42ad0e7f461e8e0e6732b"
integrity sha512-OrEFE0m3p/+mGsmIwjttLhSKg3io6MpJLhYtPNjVSZA9Ix8Y5tprN3vM6a3MjWt5asPF6AKZsfT43cgpGwJB0g==
teamwork@3.x.x:
version "3.2.0"
resolved "https://registry.yarnpkg.com/teamwork/-/teamwork-3.2.0.tgz#27916edab815459c1a4686252eb18fb5925f49fa"
integrity sha512-xAmJ8PIVjRZMXAHgUuOP8ITsv0SedyWAit2UWiNImXgg/F+BxrsG46ZegElNBM0Dwp+iMfbigg/Ll/M2oDRYww==
terser-webpack-plugin@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz#cf7c25a1eee25bf121f4a587bb9e004e3f80e528"
@ -8643,6 +8789,11 @@ urix@^0.1.0:
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-join@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78"
integrity sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=
url-loader@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8"
@ -9002,6 +9153,11 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
winchan@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.1.tgz#19b334e49f7c07c0849f921f405fad87dfc8a1da"
integrity sha512-QrG9q+ObfmZBxScv0HSCqFm/owcgyR5Sgpiy1NlCZPpFXhbsmNHhTiLWoogItdBUi0fnU7Io/5ABEqRta5/6Dw==
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"