Add SSO authentication and fetch org data from TaskCluster secrets
This commit is contained in:
Родитель
aa318f9fa3
Коммит
9cea88f377
|
@ -16,7 +16,6 @@ module.exports = {
|
||||||
[
|
[
|
||||||
'@neutrinojs/copy', {
|
'@neutrinojs/copy', {
|
||||||
patterns: [
|
patterns: [
|
||||||
{ from: 'src/static/fakeOrg.json', to: 'people.json' },
|
|
||||||
{ from: 'src/static/triageOwners.json', to: 'triageOwners.json' },
|
{ from: 'src/static/triageOwners.json', to: 'triageOwners.json' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,9 @@
|
||||||
"@material-ui/core": "^3.9.2",
|
"@material-ui/core": "^3.9.2",
|
||||||
"@material-ui/icons": "^3.0.1",
|
"@material-ui/icons": "^3.0.1",
|
||||||
"@mozilla-frontend-infra/components": "^2.0.0",
|
"@mozilla-frontend-infra/components": "^2.0.0",
|
||||||
|
"auth0-js": "9.2.3",
|
||||||
"chart.js": "^2.7.3",
|
"chart.js": "^2.7.3",
|
||||||
|
"mitt": "^1.1.3",
|
||||||
"moment": "^2.23.0",
|
"moment": "^2.23.0",
|
||||||
"prop-types": "^15",
|
"prop-types": "^15",
|
||||||
"query-string": "^6.2.0",
|
"query-string": "^6.2.0",
|
||||||
|
@ -35,11 +37,13 @@
|
||||||
"react-dom": "^16",
|
"react-dom": "^16",
|
||||||
"react-hot-loader": "^4",
|
"react-hot-loader": "^4",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
|
"taskcluster-client-web": "9.0.0",
|
||||||
|
"taskcluster-lib-urls": "^12.0.0",
|
||||||
"typeface-roboto": "^0.0.54"
|
"typeface-roboto": "^0.0.54"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@neutrinojs/airbnb": "^9.0.0-beta.1",
|
"@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/jest": "^9.0.0-beta.1",
|
||||||
"@neutrinojs/react": "^9.0.0-beta.1",
|
"@neutrinojs/react": "^9.0.0-beta.1",
|
||||||
"eslint": "^5",
|
"eslint": "^5",
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { BrowserRouter, Switch, Route } from 'react-router-dom';
|
import { BrowserRouter, Switch, Route } from 'react-router-dom';
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
|
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
|
||||||
|
import Spinner from '@mozilla-frontend-infra/components/Spinner';
|
||||||
|
|
||||||
import Main from '../views/Main';
|
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 = () => ({
|
const styles = () => ({
|
||||||
'@global': {
|
'@global': {
|
||||||
|
@ -14,28 +22,80 @@ const styles = () => ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
class App extends Component {
|
|
||||||
|
class App extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
classes: PropTypes.shape({}).isRequired,
|
classes: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
authReady: false,
|
||||||
error: undefined,
|
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() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { authReady, error } = this.state;
|
||||||
const { error } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.container}>
|
<BrowserRouter>
|
||||||
{error && <ErrorPanel error={new Error(error)} />}
|
<div>
|
||||||
<BrowserRouter>
|
{error && <ErrorPanel error={new Error(error)} />}
|
||||||
<Switch>
|
{authReady ? (
|
||||||
<Route path="/" component={Main} />
|
<AuthContext.Provider value={this.authController}>
|
||||||
</Switch>
|
<Switch>
|
||||||
</BrowserRouter>
|
<PropsRoute path="/" exact component={Main} />
|
||||||
</div>
|
<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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import BugzillaComponents from '../BugzillaComponents';
|
||||||
import MainTabs from '../MainTabs';
|
import Reportees from '../Reportees';
|
||||||
|
|
||||||
const styles = ({
|
class MainView extends React.Component {
|
||||||
content: {
|
renderTabContents() {
|
||||||
display: 'flex',
|
const {
|
||||||
},
|
ldapEmail, partialOrg, onPersonDetails, teamComponents,
|
||||||
header: {
|
bugzillaComponents, onComponentDetails, selectedTabIndex,
|
||||||
margin: '0.5rem 0 0 0',
|
} = 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 = ({
|
render() {
|
||||||
ldapEmail, partialOrg, bugzillaComponents, teamComponents,
|
return this.renderTabContents();
|
||||||
onComponentDetails, onPersonDetails,
|
}
|
||||||
}) => (
|
}
|
||||||
<div>
|
|
||||||
<MainTabs
|
|
||||||
ldapEmail={ldapEmail}
|
|
||||||
partialOrg={partialOrg}
|
|
||||||
onPersonDetails={onPersonDetails}
|
|
||||||
teamComponents={teamComponents}
|
|
||||||
onComponentDetails={onComponentDetails}
|
|
||||||
bugzillaComponents={bugzillaComponents}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
MainView.propTypes = {
|
MainView.propTypes = {
|
||||||
ldapEmail: PropTypes.string.isRequired,
|
ldapEmail: PropTypes.string.isRequired,
|
||||||
|
@ -35,6 +53,7 @@ MainView.propTypes = {
|
||||||
teamComponents: PropTypes.arrayOf(PropTypes.shape({})),
|
teamComponents: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
onComponentDetails: PropTypes.func.isRequired,
|
onComponentDetails: PropTypes.func.isRequired,
|
||||||
onPersonDetails: PropTypes.func.isRequired,
|
onPersonDetails: PropTypes.func.isRequired,
|
||||||
|
selectedTabIndex: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
MainView.defaultProps = {
|
MainView.defaultProps = {
|
||||||
|
@ -42,4 +61,4 @@ MainView.defaultProps = {
|
||||||
teamComponents: [],
|
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();
|
||||||
|
}));
|
||||||
|
}
|
|
@ -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 = {};
|
let allReportees = {};
|
||||||
allReportees[ldapEmail] = completeOrg[ldapEmail];
|
allReportees[email] = completeOrg[email];
|
||||||
const { reportees } = completeOrg[ldapEmail];
|
const { reportees } = completeOrg[email];
|
||||||
if (reportees.length !== 0) {
|
if (reportees.length !== 0) {
|
||||||
reportees.forEach((reporteeEmail) => {
|
reportees.forEach((reporteeEmail) => {
|
||||||
const partialOrg = findReportees(completeOrg, reporteeEmail);
|
const partialOrg = findReportees(completeOrg, reporteeEmail);
|
||||||
|
@ -13,8 +49,8 @@ const findReportees = (completeOrg, ldapEmail) => {
|
||||||
return allReportees;
|
return allReportees;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllReportees = async (ldapEmail) => {
|
const getAllReportees = async (secretsClient, ldapEmail) => {
|
||||||
const completeOrg = await getOrgChart();
|
const completeOrg = await getOrgChart(secretsClient);
|
||||||
return findReportees(completeOrg, ldapEmail);
|
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 React, { Component } from 'react';
|
||||||
import MainContainer from '../../containers/MainContainer';
|
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 DEFAULT_STATE = {
|
||||||
const Main = ({ location }) => (
|
bugzillaComponents: {},
|
||||||
<MainContainer
|
partialOrg: undefined,
|
||||||
// XXX: Until we have SSO + real org access
|
teamComponents: {},
|
||||||
ldapEmail={new URLSearchParams(location.search).get('ldapEmail') || 'manager@mozilla.com'}
|
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={{}}
|
teams={{}}
|
||||||
onComponentDetails={() => null}
|
onComponentDetails={() => null}
|
||||||
onPersonDetails={() => null}
|
onPersonDetails={() => null}
|
||||||
|
selectedTabIndex={0}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.toJSON();
|
.toJSON();
|
||||||
|
@ -31,6 +32,7 @@ it('renders Manager who has reportees', () => {
|
||||||
teams={{}}
|
teams={{}}
|
||||||
onComponentDetails={() => null}
|
onComponentDetails={() => null}
|
||||||
onPersonDetails={() => null}
|
onPersonDetails={() => null}
|
||||||
|
selectedTabIndex={0}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.toJSON();
|
.toJSON();
|
||||||
|
@ -47,6 +49,7 @@ it('renders Manager who has reportees and teams', () => {
|
||||||
teams={Object.values(teamsConfig)}
|
teams={Object.values(teamsConfig)}
|
||||||
onComponentDetails={() => null}
|
onComponentDetails={() => null}
|
||||||
onPersonDetails={() => null}
|
onPersonDetails={() => null}
|
||||||
|
selectedTabIndex={0}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.toJSON();
|
.toJSON();
|
||||||
|
|
|
@ -1,745 +1,223 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`renders Manager who has reportees 1`] = `
|
exports[`renders Manager who has reportees 1`] = `
|
||||||
<div>
|
<div
|
||||||
|
className="Reportees-root-1"
|
||||||
|
>
|
||||||
<div
|
<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
|
<div
|
||||||
className="MuiTypography-root-73 MuiTypography-body1-82"
|
className="DrilldownIcon-svgWrapper-5"
|
||||||
style={
|
name="manager@mozilla.com"
|
||||||
Object {
|
onClick={[Function]}
|
||||||
"padding": 4,
|
onKeyPress={[Function]}
|
||||||
}
|
role="button"
|
||||||
}
|
tabIndex="0"
|
||||||
>
|
>
|
||||||
<div
|
<svg
|
||||||
className="Reportees-root-109"
|
aria-hidden="true"
|
||||||
|
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<div
|
<path
|
||||||
height="1rem"
|
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||||
>
|
/>
|
||||||
|
<path
|
||||||
</div>
|
d="M0 0h24v24H0z"
|
||||||
<div
|
fill="none"
|
||||||
className="Reportees-person-112"
|
/>
|
||||||
>
|
</svg>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders Manager who has reportees and teams 1`] = `
|
exports[`renders Manager who has reportees and teams 1`] = `
|
||||||
<div>
|
<div
|
||||||
|
className="Reportees-root-1"
|
||||||
|
>
|
||||||
<div
|
<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
|
<div
|
||||||
className="MuiTypography-root-73 MuiTypography-body1-82"
|
className="DrilldownIcon-svgWrapper-5"
|
||||||
style={
|
name="manager@mozilla.com"
|
||||||
Object {
|
onClick={[Function]}
|
||||||
"padding": 4,
|
onKeyPress={[Function]}
|
||||||
}
|
role="button"
|
||||||
}
|
tabIndex="0"
|
||||||
>
|
>
|
||||||
<div
|
<svg
|
||||||
className="Reportees-root-109"
|
aria-hidden="true"
|
||||||
|
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<div
|
<path
|
||||||
height="1rem"
|
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||||
>
|
/>
|
||||||
|
<path
|
||||||
</div>
|
d="M0 0h24v24H0z"
|
||||||
<div
|
fill="none"
|
||||||
className="Reportees-person-112"
|
/>
|
||||||
>
|
</svg>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`renders Someone with no reportees 1`] = `
|
exports[`renders Someone with no reportees 1`] = `
|
||||||
<div>
|
<div
|
||||||
|
className="Reportees-root-1"
|
||||||
|
>
|
||||||
<div
|
<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
|
<div
|
||||||
className="MuiTypography-root-73 MuiTypography-body1-82"
|
className="DrilldownIcon-svgWrapper-5"
|
||||||
style={
|
name="manager@mozilla.com"
|
||||||
Object {
|
onClick={[Function]}
|
||||||
"padding": 4,
|
onKeyPress={[Function]}
|
||||||
}
|
role="button"
|
||||||
}
|
tabIndex="0"
|
||||||
>
|
>
|
||||||
<div
|
<svg
|
||||||
className="Reportees-root-109"
|
aria-hidden="true"
|
||||||
|
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||||
|
focusable="false"
|
||||||
|
role="presentation"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<div
|
<path
|
||||||
height="1rem"
|
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||||
>
|
/>
|
||||||
|
<path
|
||||||
</div>
|
d="M0 0h24v24H0z"
|
||||||
<div
|
fill="none"
|
||||||
className="Reportees-person-112"
|
/>
|
||||||
>
|
</svg>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
216
yarn.lock
216
yarn.lock
|
@ -743,17 +743,6 @@
|
||||||
"@babel/runtime" "7.0.0"
|
"@babel/runtime" "7.0.0"
|
||||||
recompose "^0.29.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":
|
"@material-ui/system@^3.0.0-alpha.0":
|
||||||
version "3.0.0-alpha.2"
|
version "3.0.0-alpha.2"
|
||||||
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-3.0.0-alpha.2.tgz#096e80c8bb0f70aea435b9e38ea7749ee77b4e46"
|
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-loader "^8.0.4"
|
||||||
babel-merge "^2.0.1"
|
babel-merge "^2.0.1"
|
||||||
|
|
||||||
"@neutrinojs/copy@^8.3.0":
|
"@neutrinojs/copy@^9.0.0-beta.1":
|
||||||
version "8.3.0"
|
version "9.0.0-rc.0"
|
||||||
resolved "https://registry.yarnpkg.com/@neutrinojs/copy/-/copy-8.3.0.tgz#d41a7124b677103134063ef1a4f2a671da1b7758"
|
resolved "https://registry.yarnpkg.com/@neutrinojs/copy/-/copy-9.0.0-rc.0.tgz#9463e6557338a8433afff58fbaba206d38f2039b"
|
||||||
integrity sha1-1BpxJLZ3EDE0Bj7xpPKmcdobd1g=
|
integrity sha512-/Ucrl+jHi1lCbVtQ9tVPijLgP/vEjxWSORjTE9DF22BsyVv0HUPcWiWBj+Ahv3ZFS7B2gmMx6CK056OK+uqMmg==
|
||||||
dependencies:
|
dependencies:
|
||||||
copy-webpack-plugin "^4.5.1"
|
copy-webpack-plugin "^4.6.0"
|
||||||
deepmerge "^1.5.2"
|
|
||||||
|
|
||||||
"@neutrinojs/dev-server@9.0.0-beta.1":
|
"@neutrinojs/dev-server@9.0.0-beta.1":
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
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:
|
aws-sign2@~0.7.0:
|
||||||
version "0.7.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||||
|
@ -1447,6 +1447,13 @@ axobject-query@^2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ast-types-flow "0.0.7"
|
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:
|
babel-code-frame@^6.26.0:
|
||||||
version "6.26.0"
|
version "6.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
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"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||||
|
|
||||||
base64-js@^1.0.2:
|
base64-js@^1.0.2, base64-js@^1.2.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
|
||||||
integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==
|
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"
|
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
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:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.11"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
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"
|
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||||
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
|
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
|
||||||
|
|
||||||
component-emitter@^1.2.1:
|
component-emitter@^1.2.0, component-emitter@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
|
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
|
||||||
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
|
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
|
||||||
|
@ -2343,6 +2365,11 @@ cookie@0.3.1:
|
||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
|
||||||
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
|
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:
|
copy-concurrently@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
|
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"
|
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||||
|
|
||||||
copy-webpack-plugin@^4.5.1:
|
copy-webpack-plugin@^4.6.0:
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae"
|
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae"
|
||||||
integrity sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==
|
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"
|
shebang-command "^1.2.0"
|
||||||
which "^1.2.9"
|
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:
|
crypto-browserify@^3.11.0:
|
||||||
version "3.12.0"
|
version "3.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
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"
|
randombytes "^2.0.0"
|
||||||
randomfill "^1.0.3"
|
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:
|
css-loader@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
|
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"
|
assign-symbols "^1.0.0"
|
||||||
is-extendable "^1.0.1"
|
is-extendable "^1.0.1"
|
||||||
|
|
||||||
extend@~3.0.2:
|
extend@^3.0.0, extend@~3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
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"
|
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||||
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
||||||
|
|
||||||
form-data@~2.3.2:
|
form-data@^2.3.1, form-data@~2.3.2:
|
||||||
version "2.3.3"
|
version "2.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||||
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
|
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
|
||||||
|
@ -3775,6 +3814,11 @@ form-data@~2.3.2:
|
||||||
combined-stream "^1.0.6"
|
combined-stream "^1.0.6"
|
||||||
mime-types "^2.1.12"
|
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:
|
forwarded@~0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
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"
|
inherits "^2.0.3"
|
||||||
minimalistic-assert "^1.0.1"
|
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:
|
he@1.2.x:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
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-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
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:
|
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
|
||||||
version "2.5.5"
|
version "2.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
|
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:
|
dependencies:
|
||||||
postcss "^6.0.1"
|
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:
|
ieee754@^1.1.4:
|
||||||
version "1.1.12"
|
version "1.1.12"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
|
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"
|
argparse "^1.0.7"
|
||||||
esprima "^4.0.0"
|
esprima "^4.0.0"
|
||||||
|
|
||||||
jsbn@~0.1.0:
|
jsbn@^0.1.0, jsbn@~0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||||
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
||||||
|
@ -5442,11 +5513,6 @@ jsx-ast-utils@^2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes "^3.0.3"
|
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:
|
killable@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
|
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"
|
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
|
||||||
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
|
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
|
||||||
|
|
||||||
methods@~1.1.2:
|
methods@^1.1.1, methods@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||||
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
|
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
|
||||||
|
@ -5934,6 +6000,11 @@ mime@1.4.1:
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
|
||||||
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
|
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:
|
mime@^2.0.3, mime@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
|
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"
|
stream-each "^1.1.0"
|
||||||
through2 "^2.0.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:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
|
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"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
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:
|
query-string@^6.2.0:
|
||||||
version "6.2.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.2.0.tgz#468edeb542b7e0538f9f9b1aeb26f034f19c86e1"
|
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"
|
parse-json "^4.0.0"
|
||||||
pify "^3.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"
|
version "2.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||||
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
||||||
|
@ -7943,6 +8033,16 @@ snapdragon@^0.8.1:
|
||||||
source-map-resolve "^0.5.0"
|
source-map-resolve "^0.5.0"
|
||||||
use "^3.1.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:
|
sockjs-client@1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177"
|
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"
|
select-hose "^2.0.0"
|
||||||
spdy-transport "^2.0.18"
|
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:
|
split-string@^3.0.1, split-string@^3.0.2:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
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"
|
loader-utils "^1.1.0"
|
||||||
schema-utils "^1.0.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:
|
supports-color@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
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"
|
safe-buffer "^5.1.2"
|
||||||
yallist "^3.0.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:
|
terser-webpack-plugin@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz#cf7c25a1eee25bf121f4a587bb9e004e3f80e528"
|
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"
|
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
|
||||||
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
|
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:
|
url-loader@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8"
|
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8"
|
||||||
|
@ -9002,6 +9153,11 @@ wide-align@^1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width "^1.0.2 || 2"
|
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:
|
wordwrap@~0.0.2:
|
||||||
version "0.0.3"
|
version "0.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
||||||
|
|
Загрузка…
Ссылка в новой задаче