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', {
|
||||
patterns: [
|
||||
{ from: 'src/static/fakeOrg.json', to: 'people.json' },
|
||||
{ from: 'src/static/triageOwners.json', to: 'triageOwners.json' },
|
||||
],
|
||||
},
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
"@material-ui/core": "^3.9.2",
|
||||
"@material-ui/icons": "^3.0.1",
|
||||
"@mozilla-frontend-infra/components": "^2.0.0",
|
||||
"auth0-js": "9.2.3",
|
||||
"chart.js": "^2.7.3",
|
||||
"mitt": "^1.1.3",
|
||||
"moment": "^2.23.0",
|
||||
"prop-types": "^15",
|
||||
"query-string": "^6.2.0",
|
||||
|
@ -35,11 +37,13 @@
|
|||
"react-dom": "^16",
|
||||
"react-hot-loader": "^4",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"taskcluster-client-web": "9.0.0",
|
||||
"taskcluster-lib-urls": "^12.0.0",
|
||||
"typeface-roboto": "^0.0.54"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@neutrinojs/airbnb": "^9.0.0-beta.1",
|
||||
"@neutrinojs/copy": "^8.3.0",
|
||||
"@neutrinojs/copy": "^9.0.0-beta.1",
|
||||
"@neutrinojs/jest": "^9.0.0-beta.1",
|
||||
"@neutrinojs/react": "^9.0.0-beta.1",
|
||||
"eslint": "^5",
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import { hot } from 'react-hot-loader';
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BrowserRouter, Switch, Route } from 'react-router-dom';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
|
||||
import Spinner from '@mozilla-frontend-infra/components/Spinner';
|
||||
|
||||
import Main from '../views/Main';
|
||||
import PropsRoute from '../components/PropsRoute';
|
||||
import AuthContext from '../components/auth/AuthContext';
|
||||
import AuthController from '../components/auth/AuthController';
|
||||
import NotFound from '../components/NotFound';
|
||||
import Auth0Login from '../views/Auth0Login';
|
||||
import config from '../config';
|
||||
|
||||
const styles = () => ({
|
||||
'@global': {
|
||||
|
@ -14,28 +22,80 @@ const styles = () => ({
|
|||
},
|
||||
},
|
||||
});
|
||||
class App extends Component {
|
||||
|
||||
class App extends React.Component {
|
||||
static propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
authReady: false,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
authController = new AuthController();
|
||||
|
||||
componentWillUnmount() {
|
||||
this.authController.removeListener(
|
||||
'user-session-changed',
|
||||
this.handleUserSessionChanged,
|
||||
);
|
||||
}
|
||||
|
||||
handleUserSessionChanged = (userSession) => {
|
||||
// Consider auth "ready" when we have no userSession, a userSession with no
|
||||
// renewAfter, or a renewAfter that is not in the past. Once auth is
|
||||
// ready, it never becomes non-ready again.
|
||||
const { authReady } = this.state;
|
||||
if (!authReady) {
|
||||
const newState = !userSession
|
||||
|| !userSession.renewAfter
|
||||
|| new Date(userSession.renewAfter) > new Date();
|
||||
this.setState({ authReady: newState });
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this.authController.on(
|
||||
'user-session-changed',
|
||||
this.handleUserSessionChanged,
|
||||
);
|
||||
|
||||
// we do not want to automatically load a user session on the login views; this is
|
||||
// a hack until they get an entry point of their own with no UI.
|
||||
if (!window.location.pathname.startsWith(config.redirectRoute)) {
|
||||
this.authController.loadUserSession();
|
||||
} else {
|
||||
this.setState({ authReady: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const { error } = this.state;
|
||||
const { authReady, error } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
{error && <ErrorPanel error={new Error(error)} />}
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" component={Main} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
{error && <ErrorPanel error={new Error(error)} />}
|
||||
{authReady ? (
|
||||
<AuthContext.Provider value={this.authController}>
|
||||
<Switch>
|
||||
<PropsRoute path="/" exact component={Main} />
|
||||
<PropsRoute
|
||||
path={config.redirectRoute}
|
||||
component={Auth0Login}
|
||||
setUserSession={this.authController.setUserSession}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</AuthContext.Provider>
|
||||
) : (
|
||||
<Spinner loading />
|
||||
)}
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
import CredentialsMenu from '../../views/CredentialsMenu';
|
||||
|
||||
const styles = theme => ({
|
||||
styledToolbar: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
'min-height': theme.spacing.unit * 1,
|
||||
},
|
||||
});
|
||||
|
||||
const Header = ({ classes, selectedTabIndex, handleTabChange }) => (
|
||||
<AppBar position="static">
|
||||
<Toolbar className={classes.styledToolbar}>
|
||||
<Tabs value={selectedTabIndex} onChange={handleTabChange}>
|
||||
<Tab label="Reportees" />
|
||||
<Tab label="Teams" />
|
||||
<Tab label="Components" />
|
||||
</Tabs>
|
||||
<CredentialsMenu />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
||||
Header.propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
selectedTabIndex: PropTypes.number.isRequired,
|
||||
handleTabChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withStyles(styles)(Header);
|
|
@ -1,126 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import BugzillaComponents from '../BugzillaComponents';
|
||||
import Reportees from '../Reportees';
|
||||
|
||||
const TabContainer = (props) => {
|
||||
const { children } = props;
|
||||
return (
|
||||
<Typography component="div" style={{ padding: 4 }}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
TabContainer.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
styledToolbar: {
|
||||
'min-height': 48,
|
||||
},
|
||||
});
|
||||
|
||||
class MainTabs extends React.Component {
|
||||
state = {
|
||||
selectedTabIndex: 0,
|
||||
};
|
||||
|
||||
handleChange = (event, selectedTabIndex) => {
|
||||
this.setState({ selectedTabIndex });
|
||||
};
|
||||
|
||||
renderTabContents(tabIndex) {
|
||||
const {
|
||||
ldapEmail, partialOrg, onPersonDetails, teamComponents,
|
||||
bugzillaComponents, onComponentDetails,
|
||||
} = this.props;
|
||||
switch (tabIndex) {
|
||||
case 0:
|
||||
return (
|
||||
<Reportees
|
||||
ldapEmail={ldapEmail}
|
||||
partialOrg={partialOrg}
|
||||
onPersonDetails={onPersonDetails}
|
||||
/>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<BugzillaComponents
|
||||
bugzillaComponents={teamComponents}
|
||||
onComponentDetails={onComponentDetails}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<BugzillaComponents
|
||||
bugzillaComponents={bugzillaComponents}
|
||||
onComponentDetails={onComponentDetails}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, partialOrg, ldapEmail } = this.props;
|
||||
const { selectedTabIndex } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar position="static">
|
||||
<Toolbar className={classes.styledToolbar}>
|
||||
<Tabs value={selectedTabIndex} onChange={this.handleChange}>
|
||||
<Tab label="Reportees" />
|
||||
<Tab label="Teams" />
|
||||
<Tab label="Components" />
|
||||
</Tabs>
|
||||
<div className={classes.grow} />
|
||||
<Typography variant="subtitle1" color="inherit">
|
||||
{partialOrg[ldapEmail].cn}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<TabContainer>
|
||||
{this.renderTabContents(selectedTabIndex)}
|
||||
</TabContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MainTabs.propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
ldapEmail: PropTypes.string.isRequired,
|
||||
partialOrg: PropTypes.shape({}).isRequired,
|
||||
bugzillaComponents: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
teamComponents: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
onComponentDetails: PropTypes.func.isRequired,
|
||||
onPersonDetails: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
MainTabs.defaultProps = {
|
||||
bugzillaComponents: [],
|
||||
teamComponents: [],
|
||||
};
|
||||
|
||||
export default withStyles(styles)(MainTabs);
|
|
@ -1,32 +1,50 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import MainTabs from '../MainTabs';
|
||||
import BugzillaComponents from '../BugzillaComponents';
|
||||
import Reportees from '../Reportees';
|
||||
|
||||
const styles = ({
|
||||
content: {
|
||||
display: 'flex',
|
||||
},
|
||||
header: {
|
||||
margin: '0.5rem 0 0 0',
|
||||
},
|
||||
});
|
||||
class MainView extends React.Component {
|
||||
renderTabContents() {
|
||||
const {
|
||||
ldapEmail, partialOrg, onPersonDetails, teamComponents,
|
||||
bugzillaComponents, onComponentDetails, selectedTabIndex,
|
||||
} = this.props;
|
||||
switch (selectedTabIndex) {
|
||||
case 0: {
|
||||
return (
|
||||
<Reportees
|
||||
ldapEmail={ldapEmail}
|
||||
partialOrg={partialOrg}
|
||||
onPersonDetails={onPersonDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 1: {
|
||||
return (
|
||||
<BugzillaComponents
|
||||
bugzillaComponents={teamComponents}
|
||||
onComponentDetails={onComponentDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 2: {
|
||||
return (
|
||||
<BugzillaComponents
|
||||
bugzillaComponents={bugzillaComponents}
|
||||
onComponentDetails={onComponentDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MainView = ({
|
||||
ldapEmail, partialOrg, bugzillaComponents, teamComponents,
|
||||
onComponentDetails, onPersonDetails,
|
||||
}) => (
|
||||
<div>
|
||||
<MainTabs
|
||||
ldapEmail={ldapEmail}
|
||||
partialOrg={partialOrg}
|
||||
onPersonDetails={onPersonDetails}
|
||||
teamComponents={teamComponents}
|
||||
onComponentDetails={onComponentDetails}
|
||||
bugzillaComponents={bugzillaComponents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
render() {
|
||||
return this.renderTabContents();
|
||||
}
|
||||
}
|
||||
|
||||
MainView.propTypes = {
|
||||
ldapEmail: PropTypes.string.isRequired,
|
||||
|
@ -35,6 +53,7 @@ MainView.propTypes = {
|
|||
teamComponents: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
onComponentDetails: PropTypes.func.isRequired,
|
||||
onPersonDetails: PropTypes.func.isRequired,
|
||||
selectedTabIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
MainView.defaultProps = {
|
||||
|
@ -42,4 +61,4 @@ MainView.defaultProps = {
|
|||
teamComponents: [],
|
||||
};
|
||||
|
||||
export default withStyles(styles)(MainView);
|
||||
export default MainView;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import React from 'react';
|
||||
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
|
||||
|
||||
export default () => <ErrorPanel error="The requested route was not found." />;
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
const PropsRoute = ({ component, ...props }) => (
|
||||
<Route
|
||||
{...props}
|
||||
render={routeProps => React.createElement(component, Object.assign({}, routeProps, props))}
|
||||
/>
|
||||
);
|
||||
|
||||
export default PropsRoute;
|
|
@ -0,0 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
const AuthContext = React.createContext();
|
||||
|
||||
export default AuthContext;
|
|
@ -0,0 +1,109 @@
|
|||
import mitt from 'mitt';
|
||||
import UserSession from './UserSession';
|
||||
|
||||
import { renew as auth0Renew } from './auth0';
|
||||
|
||||
/**
|
||||
* Controller for authentication-related pieces of the site.
|
||||
*
|
||||
* This encompasses knowledge of which authentication mechanisms are enabled, including
|
||||
* credentials menu items, ongoing expiration monitoring, and any additional required UI.
|
||||
* It also handles synchronizing sign-in status across tabs.
|
||||
*/
|
||||
export default class AuthController {
|
||||
constructor() {
|
||||
const events = mitt();
|
||||
|
||||
this.on = events.on;
|
||||
this.off = events.off;
|
||||
this.emit = events.emit;
|
||||
|
||||
this.renewalTimer = null;
|
||||
|
||||
window.addEventListener('storage', ({ storageArea, key }) => {
|
||||
if (storageArea === localStorage && key === 'userSession') {
|
||||
this.loadUserSession();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the renewal timer based on the given user session.
|
||||
*/
|
||||
resetRenewalTimer(userSession) {
|
||||
if (this.renewalTimer) {
|
||||
window.clearTimeout(this.renewalTimer);
|
||||
this.renewalTimer = null;
|
||||
}
|
||||
|
||||
if (userSession && userSession.renewAfter) {
|
||||
let timeout = Math.max(0, new Date(userSession.renewAfter) - new Date());
|
||||
|
||||
// if the timeout is in the future, apply up to a few minutes to it
|
||||
// randomly. This avoids multiple tabs all trying to renew at the
|
||||
// same time.
|
||||
if (timeout > 0) {
|
||||
timeout += Math.random() * 5 * 60 * 1000;
|
||||
}
|
||||
|
||||
this.renewalTimer = window.setTimeout(() => {
|
||||
this.renewalTimer = null;
|
||||
this.renew({ userSession });
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the current user session (from localStorage).
|
||||
*
|
||||
* This will emit the user-session-changed event, but does not
|
||||
* return the user session.
|
||||
*/
|
||||
loadUserSession() {
|
||||
const storedUserSession = localStorage.getItem('userSession');
|
||||
const userSession = storedUserSession
|
||||
? UserSession.deserialize(storedUserSession)
|
||||
: null;
|
||||
|
||||
this.userSession = userSession;
|
||||
this.resetRenewalTimer(userSession);
|
||||
this.emit('user-session-changed', userSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current userSession instance
|
||||
*/
|
||||
getUserSession() {
|
||||
return this.userSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current user session, or (if null) delete the current user session.
|
||||
*
|
||||
* This will change the user session in all open windows/tabs, eventually triggering
|
||||
* a call to any onSessionChanged callbacks.
|
||||
*/
|
||||
setUserSession = (userSession) => {
|
||||
if (!userSession) {
|
||||
localStorage.removeItem('userSession');
|
||||
} else {
|
||||
localStorage.setItem('userSession', userSession.serialize());
|
||||
}
|
||||
|
||||
// localStorage updates do not trigger event listeners on the current window/tab,
|
||||
// so invoke it directly
|
||||
this.loadUserSession();
|
||||
};
|
||||
|
||||
/**
|
||||
* Renew the user session. This is not possible for all auth methods, and will trivially succeed
|
||||
* for methods that do not support it. If it fails, the user will be logged out.
|
||||
*/
|
||||
async renew({ userSession }) {
|
||||
try {
|
||||
await auth0Renew({ userSession, authController: this });
|
||||
} catch (err) {
|
||||
this.setUserSession(null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
import { OIDCCredentialAgent, Secrets } from 'taskcluster-client-web';
|
||||
import { withRootUrl } from 'taskcluster-lib-urls';
|
||||
|
||||
const urls = withRootUrl('https://taskcluster.net');
|
||||
/**
|
||||
* An object representing a user session. Tools supports a variety of login methods,
|
||||
* so this combines them all in a single representation.
|
||||
*
|
||||
* UserSessions are immutable -- when anything about the session changes, a new instance
|
||||
* replaces the old. The `userChanged` method is useful to distinguish changes to the
|
||||
* user identity from mere token renewals.
|
||||
*
|
||||
* Common properties are:
|
||||
*
|
||||
* - type - 'oidc' or 'credentials'
|
||||
* - name - user name
|
||||
* - clientArgs - arguments to pass to taskcluster-client-web Client constructors
|
||||
* - renewAfter - date (Date or string) after which this session should be renewed,
|
||||
* if applicable
|
||||
*
|
||||
* When type is 'oidc':
|
||||
*
|
||||
* - oidcProvider -- the provider (see taskcluster-login)
|
||||
* - accessToken -- the accessToken to pass to taskcluster-login
|
||||
* - fullName -- user's full name
|
||||
* - picture -- URL of an image of the user
|
||||
* - oidcSubject -- the 'sub' field of the id_token (useful for debugging user issues)
|
||||
*
|
||||
* When the type is 'credentials':
|
||||
*
|
||||
* - credentials -- the Taskcluster credentials (with or without a certificate)
|
||||
*
|
||||
* To fetch Taskcluster credentials for the user regardless of type, use the getCredentials
|
||||
* method.
|
||||
*/
|
||||
export default class UserSession {
|
||||
constructor(options) {
|
||||
Object.assign(this, options);
|
||||
|
||||
if (this.accessToken) {
|
||||
this.credentialAgent = new OIDCCredentialAgent({
|
||||
accessToken: this.accessToken,
|
||||
url: urls.api('login', 'v1', `/oidc-credentials/${this.oidcProvider}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static fromCredentials(credentials) {
|
||||
return new UserSession({ type: 'credentials', credentials });
|
||||
}
|
||||
|
||||
static fromOIDC(options) {
|
||||
return new UserSession({ type: 'oidc', ...options });
|
||||
}
|
||||
|
||||
// determine whether the user changed from old to new; this is used by other components
|
||||
// to determine when to update in response to a sign-in/sign-out event
|
||||
static userChanged(oldUser, newUser) {
|
||||
if (!oldUser && !newUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!oldUser || !newUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return oldUser.type !== newUser.type || oldUser.name !== newUser.name;
|
||||
}
|
||||
|
||||
// get the user's name
|
||||
get name() {
|
||||
return (
|
||||
this.fullName
|
||||
|| (this.credentials && this.credentials.clientId)
|
||||
|| 'unknown'
|
||||
);
|
||||
}
|
||||
|
||||
// get the args used to create a new client object
|
||||
get clientArgs() {
|
||||
return this.credentialAgent
|
||||
? { credentialAgent: this.credentialAgent }
|
||||
: { credentials: this.credentials };
|
||||
}
|
||||
|
||||
// load Taskcluster credentials for this user
|
||||
getCredentials() {
|
||||
return this.credentials
|
||||
? Promise.resolve(this.credentials)
|
||||
: this.credentialAgent.getCredentials({});
|
||||
}
|
||||
|
||||
static deserialize(value) {
|
||||
return new UserSession(JSON.parse(value));
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return JSON.stringify({ ...this, credentialAgent: undefined });
|
||||
}
|
||||
|
||||
getTaskClusterSecretsClient = () => new Secrets({ ...this.clientArgs, rootUrl: 'https://taskcluster.net' });
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { fromNow } from 'taskcluster-client-web';
|
||||
import { WebAuth } from 'auth0-js';
|
||||
import UserSession from './UserSession';
|
||||
import config from '../../config';
|
||||
|
||||
export const webAuth = new WebAuth(config.auth0Options);
|
||||
|
||||
export function userSessionFromAuthResult(authResult) {
|
||||
return UserSession.fromOIDC({
|
||||
oidcProvider: 'mozilla-auth0',
|
||||
accessToken: authResult.accessToken,
|
||||
fullName: authResult.idTokenPayload.name,
|
||||
email: authResult.idTokenPayload.email,
|
||||
picture: authResult.idTokenPayload.picture,
|
||||
oidcSubject: authResult.idTokenPayload.sub,
|
||||
// per https://wiki.mozilla.org/Security/Guidelines/OpenID_connect#Session_handling
|
||||
renewAfter: fromNow('15 minutes'),
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable consistent-return */
|
||||
export async function renew({ userSession, authController }) {
|
||||
if (
|
||||
!userSession
|
||||
|| userSession.type !== 'oidc'
|
||||
|| userSession.oidcProvider !== 'mozilla-auth0'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((accept, reject) => webAuth.renewAuth({}, (err, authResult) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
} if (!authResult) {
|
||||
return reject(new Error('no authResult'));
|
||||
}
|
||||
authController.setUserSession(userSessionFromAuthResult(authResult));
|
||||
accept();
|
||||
}));
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
const loginCallbackRoute = '/callback';
|
||||
|
||||
const config = {
|
||||
redirectRoute: loginCallbackRoute,
|
||||
taskclusterSecrets: {
|
||||
orgData: 'project/bugzilla-management-dashboard/realOrg',
|
||||
},
|
||||
auth0Options: {
|
||||
domain: 'auth.mozilla.auth0.com',
|
||||
clientID: 'DGloMN2BXb0AC7lF5eRyOe1GXweqBAiI',
|
||||
redirectUri: new URL(loginCallbackRoute, window.location).href,
|
||||
scope: 'taskcluster-credentials full-user-credentials openid profile email',
|
||||
audience: 'login.taskcluster.net',
|
||||
responseType: 'token id_token',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,210 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MainView from '../../components/MainView';
|
||||
import BugzillaComponentDetails from '../../components/BugzillaComponentDetails';
|
||||
import PersonDetails from '../../components/PersonDetails';
|
||||
import getAllReportees from '../../utils/getAllReportees';
|
||||
import getBugzillaOwners from '../../utils/getBugzillaOwners';
|
||||
import getBugsCountAndLink from '../../utils/bugzilla/getBugsCountAndLink';
|
||||
import METRICS from '../../utils/bugzilla/metrics';
|
||||
import TEAMS_CONFIG from '../../teamsConfig';
|
||||
|
||||
class MainContainer extends Component {
|
||||
state = {
|
||||
ldapEmail: '',
|
||||
bugzillaComponents: {},
|
||||
partialOrg: undefined,
|
||||
teamComponents: {},
|
||||
showComponent: undefined,
|
||||
showPerson: undefined,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
ldapEmail: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
ldapEmail: '',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { ldapEmail } = this.props;
|
||||
this.state.ldapEmail = ldapEmail;
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleShowComponentDetails = this.handleShowComponentDetails.bind(this);
|
||||
this.handleShowPersonDetails = this.handleShowPersonDetails.bind(this);
|
||||
this.handleComponentBackToMenu = this.handleComponentBackToMenu.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { ldapEmail } = this.state;
|
||||
if (ldapEmail !== '') {
|
||||
this.retrieveData(ldapEmail);
|
||||
}
|
||||
}
|
||||
|
||||
async getReportees(ldapEmail) {
|
||||
const partialOrg = await getAllReportees(ldapEmail);
|
||||
this.setState({ partialOrg });
|
||||
return partialOrg;
|
||||
}
|
||||
|
||||
async bugzillaComponents(bzOwners, partialOrg) {
|
||||
// bzOwners uses the bugzilla email address as the key
|
||||
// while partialOrg uses the LDAP email address
|
||||
/* eslint-disable no-param-reassign */
|
||||
const bugzillaComponents = Object.values(partialOrg)
|
||||
.reduce((result, { bugzillaEmail, mail }) => {
|
||||
const componentsOwned = bzOwners[bugzillaEmail] || bzOwners[mail];
|
||||
if (componentsOwned) {
|
||||
componentsOwned.forEach(({ product, component }) => {
|
||||
if (!result[`${product}::${component}`]) {
|
||||
result[`${product}::${component}`] = {};
|
||||
}
|
||||
result[`${product}::${component}`] = {
|
||||
label: `${product}::${component}`,
|
||||
bugzillaEmail: bugzillaEmail || mail,
|
||||
product,
|
||||
component,
|
||||
metrics: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
/* eslint-enable no-param-reassign */
|
||||
// This will list the components but will not show metrics
|
||||
this.setState({ bugzillaComponents });
|
||||
|
||||
// Let's fetch the metrics for each component
|
||||
Object.values(bugzillaComponents)
|
||||
.map(async ({ product, component }) => {
|
||||
const { metrics } = bugzillaComponents[`${product}::${component}`];
|
||||
await Promise.all(Object.keys(METRICS).map(async (metric) => {
|
||||
metrics[metric] = await getBugsCountAndLink(product, component, metric);
|
||||
metrics[metric].label = METRICS[metric].label;
|
||||
}));
|
||||
this.setState({ bugzillaComponents });
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveData(ldapEmail) {
|
||||
const [bzOwners, partialOrg] = await Promise.all([
|
||||
getBugzillaOwners(),
|
||||
this.getReportees(ldapEmail),
|
||||
]);
|
||||
this.teamsData();
|
||||
this.bugzillaComponents(bzOwners, partialOrg);
|
||||
}
|
||||
|
||||
async teamsData() {
|
||||
const teamComponents = Object.assign({}, TEAMS_CONFIG);
|
||||
// This will cause the teams to be displayed before having any metrics
|
||||
this.setState({ teamComponents });
|
||||
Object.entries(teamComponents).map(async ([teamKey, teamInfo]) => {
|
||||
const team = {
|
||||
teamKey,
|
||||
...teamInfo,
|
||||
metrics: {},
|
||||
};
|
||||
const { product, component } = teamInfo;
|
||||
await Promise.all(Object.keys(METRICS).map(async (metric) => {
|
||||
team.metrics[metric] = await getBugsCountAndLink(product, component, metric);
|
||||
}));
|
||||
teamComponents[teamKey] = team;
|
||||
this.setState({ teamComponents });
|
||||
});
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
ldapEmail: event.target.selectedTabIndex,
|
||||
bugzillaComponents: undefined,
|
||||
partialOrg: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
const { ldapEmail } = this.state;
|
||||
this.retrieveData(ldapEmail);
|
||||
}
|
||||
|
||||
handleShowComponentDetails(event, properties) {
|
||||
event.preventDefault();
|
||||
const { componentKey, teamKey } = properties;
|
||||
// IDEA: In the future we could unify bugzilla components and teams into
|
||||
// the same data structure and make this logic simpler. We could use a
|
||||
// property 'team' to distinguish a component from a set of components
|
||||
if (teamKey) {
|
||||
this.setState(prevState => ({
|
||||
showComponent: {
|
||||
title: prevState.teamComponents[teamKey].label,
|
||||
...prevState.teamComponents[teamKey],
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
showComponent: {
|
||||
title: componentKey,
|
||||
...prevState.bugzillaComponents[componentKey],
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleShowPersonDetails(event, properties) {
|
||||
event.preventDefault();
|
||||
const { partialOrg } = this.state;
|
||||
this.setState({
|
||||
showPerson: partialOrg[properties.ldapEmail],
|
||||
});
|
||||
}
|
||||
|
||||
handleComponentBackToMenu(event) {
|
||||
event.preventDefault();
|
||||
this.setState({
|
||||
showComponent: undefined,
|
||||
showPerson: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
ldapEmail, showComponent, showPerson, bugzillaComponents, partialOrg, teamComponents,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showComponent && (
|
||||
<BugzillaComponentDetails
|
||||
{...showComponent}
|
||||
title={showComponent.title}
|
||||
onGoBack={this.handleComponentBackToMenu}
|
||||
/>
|
||||
)}
|
||||
{showPerson && (
|
||||
<PersonDetails
|
||||
person={showPerson}
|
||||
bugzillaComponents={Object.values(bugzillaComponents)}
|
||||
onGoBack={this.handleComponentBackToMenu}
|
||||
/>
|
||||
)}
|
||||
{!showComponent && !showPerson && partialOrg && (
|
||||
<MainView
|
||||
ldapEmail={ldapEmail}
|
||||
partialOrg={partialOrg}
|
||||
bugzillaComponents={Object.values(bugzillaComponents)}
|
||||
teamComponents={Object.values(teamComponents)}
|
||||
onComponentDetails={this.handleShowComponentDetails}
|
||||
onPersonDetails={this.handleShowPersonDetails}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MainContainer;
|
|
@ -1,9 +1,45 @@
|
|||
import getOrgChart from './getOrgChart';
|
||||
import config from '../config';
|
||||
|
||||
const findReportees = (completeOrg, ldapEmail) => {
|
||||
const buildOrgChartData = (people) => {
|
||||
const org = {};
|
||||
people.forEach((person) => {
|
||||
const { mail } = person;
|
||||
if (!org[mail]) {
|
||||
org[mail] = person;
|
||||
org[mail].reportees = [];
|
||||
} else {
|
||||
org[mail] = {
|
||||
...person,
|
||||
reportees: org[mail].reportees,
|
||||
};
|
||||
}
|
||||
const { manager } = person;
|
||||
if (manager) {
|
||||
const managerLDAPemail = manager.dn.split('mail=')[1].split(',o=')[0];
|
||||
if (org[managerLDAPemail]) {
|
||||
org[managerLDAPemail].reportees.push(mail);
|
||||
} else {
|
||||
org[managerLDAPemail] = {
|
||||
reportees: [mail],
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!org[mail].bugzillaEmail) {
|
||||
org[mail].bugzillaEmail = mail;
|
||||
}
|
||||
});
|
||||
return org;
|
||||
};
|
||||
|
||||
const getOrgChart = async (secretsClient) => {
|
||||
const { secret } = await await secretsClient.get(config.taskclusterSecrets.orgData);
|
||||
return buildOrgChartData(secret.employees);
|
||||
};
|
||||
|
||||
const findReportees = (completeOrg, email) => {
|
||||
let allReportees = {};
|
||||
allReportees[ldapEmail] = completeOrg[ldapEmail];
|
||||
const { reportees } = completeOrg[ldapEmail];
|
||||
allReportees[email] = completeOrg[email];
|
||||
const { reportees } = completeOrg[email];
|
||||
if (reportees.length !== 0) {
|
||||
reportees.forEach((reporteeEmail) => {
|
||||
const partialOrg = findReportees(completeOrg, reporteeEmail);
|
||||
|
@ -13,8 +49,8 @@ const findReportees = (completeOrg, ldapEmail) => {
|
|||
return allReportees;
|
||||
};
|
||||
|
||||
const getAllReportees = async (ldapEmail) => {
|
||||
const completeOrg = await getOrgChart();
|
||||
const getAllReportees = async (secretsClient, ldapEmail) => {
|
||||
const completeOrg = await getOrgChart(secretsClient);
|
||||
return findReportees(completeOrg, ldapEmail);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
const orgChart = (people) => {
|
||||
const org = {};
|
||||
people.forEach((person) => {
|
||||
const { mail } = person;
|
||||
if (!org[mail]) {
|
||||
org[mail] = person;
|
||||
org[mail].reportees = [];
|
||||
} else {
|
||||
org[mail] = {
|
||||
...person,
|
||||
reportees: org[mail].reportees,
|
||||
};
|
||||
}
|
||||
const { manager } = person;
|
||||
if (manager) {
|
||||
const managerLDAPemail = manager.dn.split('mail=')[1].split(',o=')[0];
|
||||
if (org[managerLDAPemail]) {
|
||||
org[managerLDAPemail].reportees.push(mail);
|
||||
} else {
|
||||
org[managerLDAPemail] = {
|
||||
reportees: [mail],
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!org[mail].bugzillaEmail) {
|
||||
org[mail].bugzillaEmail = mail;
|
||||
}
|
||||
});
|
||||
return org;
|
||||
};
|
||||
|
||||
const getOrgChart = async () => {
|
||||
const people = await (await fetch('people.json')).json();
|
||||
return orgChart(people);
|
||||
};
|
||||
|
||||
export default getOrgChart;
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ErrorPanel from '@mozilla-frontend-infra/components/ErrorPanel';
|
||||
import { webAuth, userSessionFromAuthResult } from '../../components/auth/auth0';
|
||||
|
||||
export default class Auth0Login extends React.PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.shape({}).isRequired,
|
||||
setUserSession: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
const { history, setUserSession } = this.props;
|
||||
|
||||
if (!window.location.hash) {
|
||||
webAuth.authorize();
|
||||
} else if (window !== window.top) {
|
||||
// for silent renewal, auth0-js opens this page in an iframe, and expects
|
||||
// a postMessage back, and that's it.
|
||||
window.parent.postMessage(window.location.hash, window.origin);
|
||||
} else {
|
||||
webAuth.parseHash(window.location.hash, (loginError, authResult) => {
|
||||
if (loginError) {
|
||||
this.setState({ loginError });
|
||||
} else {
|
||||
setUserSession(userSessionFromAuthResult(authResult));
|
||||
if (window.opener) {
|
||||
window.close();
|
||||
} else {
|
||||
history.push('/');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loginError } = this.state;
|
||||
if (loginError) {
|
||||
return <ErrorPanel error={loginError} />;
|
||||
}
|
||||
|
||||
if (window.location.hash) {
|
||||
return <p>Logging in..</p>;
|
||||
}
|
||||
|
||||
return <p>Redirecting..</p>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
|
||||
import AuthContext from '../../components/auth/AuthContext';
|
||||
import config from '../../config';
|
||||
|
||||
const styles = theme => ({
|
||||
button: {
|
||||
margin: theme.spacing.unit,
|
||||
},
|
||||
});
|
||||
|
||||
class CredentialsMenu extends React.PureComponent {
|
||||
static contextType = AuthContext;
|
||||
|
||||
static propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
static handleLoginRequest() {
|
||||
const loginView = new URL(config.redirectRoute, window.location);
|
||||
window.open(loginView, '_blank');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { context } = this;
|
||||
if (context) {
|
||||
context.on(
|
||||
'user-session-changed',
|
||||
this.handleUserSessionChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { context } = this.context;
|
||||
if (context) {
|
||||
context.off(
|
||||
'user-session-changed',
|
||||
this.handleUserSessionChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleUserSessionChanged = () => {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
// note: an update to the userSession will cause a forceUpdate
|
||||
const { context } = this;
|
||||
const { classes } = this.props;
|
||||
const userSession = context && context.getUserSession();
|
||||
|
||||
return (
|
||||
userSession ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
onClick={() => context.setUserSession(null)}
|
||||
>
|
||||
Sign out
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
onClick={CredentialsMenu.handleLoginRequest}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(CredentialsMenu);
|
|
@ -1,12 +1,239 @@
|
|||
import React from 'react';
|
||||
import MainContainer from '../../containers/MainContainer';
|
||||
import React, { Component } from 'react';
|
||||
import AuthContext from '../../components/auth/AuthContext';
|
||||
import Header from '../../components/Header';
|
||||
import MainView from '../../components/MainView';
|
||||
import BugzillaComponentDetails from '../../components/BugzillaComponentDetails';
|
||||
import PersonDetails from '../../components/PersonDetails';
|
||||
import getAllReportees from '../../utils/getAllReportees';
|
||||
import getBugzillaOwners from '../../utils/getBugzillaOwners';
|
||||
import getBugsCountAndLink from '../../utils/bugzilla/getBugsCountAndLink';
|
||||
import METRICS from '../../utils/bugzilla/metrics';
|
||||
import TEAMS_CONFIG from '../../teamsConfig';
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const Main = ({ location }) => (
|
||||
<MainContainer
|
||||
// XXX: Until we have SSO + real org access
|
||||
ldapEmail={new URLSearchParams(location.search).get('ldapEmail') || 'manager@mozilla.com'}
|
||||
/>
|
||||
);
|
||||
const DEFAULT_STATE = {
|
||||
bugzillaComponents: {},
|
||||
partialOrg: undefined,
|
||||
teamComponents: {},
|
||||
selectedTabIndex: 0,
|
||||
showComponent: undefined,
|
||||
showPerson: undefined,
|
||||
};
|
||||
|
||||
export default Main;
|
||||
class MainContainer extends Component {
|
||||
static contextType = AuthContext;
|
||||
|
||||
state = DEFAULT_STATE;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleShowComponentDetails = this.handleShowComponentDetails.bind(this);
|
||||
this.handleShowPersonDetails = this.handleShowPersonDetails.bind(this);
|
||||
this.handleComponentBackToMenu = this.handleComponentBackToMenu.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { context } = this;
|
||||
if (context) {
|
||||
context.on(
|
||||
'user-session-changed',
|
||||
this.handleUserSessionChanged,
|
||||
);
|
||||
this.fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { context } = this.context;
|
||||
if (context) {
|
||||
context.off(
|
||||
'user-session-changed',
|
||||
this.handleUserSessionChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getReportees(userSession, ldapEmail) {
|
||||
const secretsClient = userSession.getTaskClusterSecretsClient();
|
||||
const partialOrg = await getAllReportees(secretsClient, ldapEmail);
|
||||
this.setState({ partialOrg });
|
||||
return partialOrg;
|
||||
}
|
||||
|
||||
handleChangeSelectedTab = (event, selectedTabIndex) => {
|
||||
this.setState({ selectedTabIndex });
|
||||
};
|
||||
|
||||
handleUserSessionChanged = () => {
|
||||
this.fetchData();
|
||||
};
|
||||
|
||||
fetchData() {
|
||||
const { context } = this;
|
||||
const userSession = context && context.getUserSession();
|
||||
if (userSession) {
|
||||
const { location } = this.props;
|
||||
const ldapEmail = new URLSearchParams(location.search).get('ldapEmail') || (userSession && userSession.email);
|
||||
this.setState({ ldapEmail });
|
||||
this.retrieveData(userSession, ldapEmail);
|
||||
} else {
|
||||
this.setState(DEFAULT_STATE);
|
||||
}
|
||||
}
|
||||
|
||||
async bugzillaComponents(bzOwners, partialOrg) {
|
||||
// bzOwners uses the bugzilla email address as the key
|
||||
// while partialOrg uses the LDAP email address
|
||||
/* eslint-disable no-param-reassign */
|
||||
const bugzillaComponents = Object.values(partialOrg)
|
||||
.reduce((result, { bugzillaEmail, mail }) => {
|
||||
const componentsOwned = bzOwners[bugzillaEmail] || bzOwners[mail];
|
||||
if (componentsOwned) {
|
||||
componentsOwned.forEach(({ product, component }) => {
|
||||
if (!result[`${product}::${component}`]) {
|
||||
result[`${product}::${component}`] = {};
|
||||
}
|
||||
result[`${product}::${component}`] = {
|
||||
label: `${product}::${component}`,
|
||||
bugzillaEmail: bugzillaEmail || mail,
|
||||
product,
|
||||
component,
|
||||
metrics: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
/* eslint-enable no-param-reassign */
|
||||
// This will list the components but will not show metrics
|
||||
this.setState({ bugzillaComponents });
|
||||
|
||||
// Let's fetch the metrics for each component
|
||||
Object.values(bugzillaComponents)
|
||||
.map(async ({ product, component }) => {
|
||||
const { metrics } = bugzillaComponents[`${product}::${component}`];
|
||||
await Promise.all(Object.keys(METRICS).map(async (metric) => {
|
||||
metrics[metric] = await getBugsCountAndLink(product, component, metric);
|
||||
metrics[metric].label = METRICS[metric].label;
|
||||
}));
|
||||
this.setState({ bugzillaComponents });
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveData(userSession, ldapEmail) {
|
||||
const [bzOwners, partialOrg] = await Promise.all([
|
||||
getBugzillaOwners(),
|
||||
this.getReportees(userSession, ldapEmail),
|
||||
]);
|
||||
this.teamsData();
|
||||
this.bugzillaComponents(bzOwners, partialOrg);
|
||||
}
|
||||
|
||||
async teamsData() {
|
||||
const teamComponents = Object.assign({}, TEAMS_CONFIG);
|
||||
// This will cause the teams to be displayed before having any metrics
|
||||
this.setState({ teamComponents });
|
||||
Object.entries(teamComponents).map(async ([teamKey, teamInfo]) => {
|
||||
const team = {
|
||||
teamKey,
|
||||
...teamInfo,
|
||||
metrics: {},
|
||||
};
|
||||
const { product, component } = teamInfo;
|
||||
await Promise.all(Object.keys(METRICS).map(async (metric) => {
|
||||
team.metrics[metric] = await getBugsCountAndLink(product, component, metric);
|
||||
}));
|
||||
teamComponents[teamKey] = team;
|
||||
this.setState({ teamComponents });
|
||||
});
|
||||
}
|
||||
|
||||
handleShowComponentDetails(event, properties) {
|
||||
event.preventDefault();
|
||||
const { componentKey, teamKey } = properties;
|
||||
// IDEA: In the future we could unify bugzilla components and teams into
|
||||
// the same data structure and make this logic simpler. We could use a
|
||||
// property 'team' to distinguish a component from a set of components
|
||||
if (teamKey) {
|
||||
this.setState(prevState => ({
|
||||
showComponent: {
|
||||
title: prevState.teamComponents[teamKey].label,
|
||||
...prevState.teamComponents[teamKey],
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
this.setState(prevState => ({
|
||||
showComponent: {
|
||||
title: componentKey,
|
||||
...prevState.bugzillaComponents[componentKey],
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleShowPersonDetails(event, properties) {
|
||||
event.preventDefault();
|
||||
const { partialOrg } = this.state;
|
||||
this.setState({
|
||||
showPerson: partialOrg[properties.ldapEmail],
|
||||
});
|
||||
}
|
||||
|
||||
handleComponentBackToMenu(event) {
|
||||
event.preventDefault();
|
||||
this.setState({
|
||||
showComponent: undefined,
|
||||
showPerson: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
showComponent,
|
||||
showPerson,
|
||||
bugzillaComponents,
|
||||
ldapEmail,
|
||||
partialOrg,
|
||||
teamComponents,
|
||||
selectedTabIndex,
|
||||
} = this.state;
|
||||
const { context } = this;
|
||||
const userSession = context.getUserSession();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header
|
||||
selectedTabIndex={selectedTabIndex}
|
||||
handleTabChange={this.handleChangeSelectedTab}
|
||||
/>
|
||||
{!userSession && <h3>Please sign in</h3>}
|
||||
{showComponent && (
|
||||
<BugzillaComponentDetails
|
||||
{...showComponent}
|
||||
title={showComponent.title}
|
||||
onGoBack={this.handleComponentBackToMenu}
|
||||
/>
|
||||
)}
|
||||
{showPerson && (
|
||||
<PersonDetails
|
||||
person={showPerson}
|
||||
bugzillaComponents={Object.values(bugzillaComponents)}
|
||||
onGoBack={this.handleComponentBackToMenu}
|
||||
/>
|
||||
)}
|
||||
{!showComponent && !showPerson && partialOrg && userSession && (
|
||||
<MainView
|
||||
ldapEmail={ldapEmail}
|
||||
partialOrg={partialOrg}
|
||||
bugzillaComponents={Object.values(bugzillaComponents)}
|
||||
teamComponents={Object.values(teamComponents)}
|
||||
onComponentDetails={this.handleShowComponentDetails}
|
||||
onPersonDetails={this.handleShowPersonDetails}
|
||||
selectedTabIndex={selectedTabIndex}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MainContainer;
|
||||
|
|
|
@ -15,6 +15,7 @@ it('renders Someone with no reportees', () => {
|
|||
teams={{}}
|
||||
onComponentDetails={() => null}
|
||||
onPersonDetails={() => null}
|
||||
selectedTabIndex={0}
|
||||
/>
|
||||
))
|
||||
.toJSON();
|
||||
|
@ -31,6 +32,7 @@ it('renders Manager who has reportees', () => {
|
|||
teams={{}}
|
||||
onComponentDetails={() => null}
|
||||
onPersonDetails={() => null}
|
||||
selectedTabIndex={0}
|
||||
/>
|
||||
))
|
||||
.toJSON();
|
||||
|
@ -47,6 +49,7 @@ it('renders Manager who has reportees and teams', () => {
|
|||
teams={Object.values(teamsConfig)}
|
||||
onComponentDetails={() => null}
|
||||
onPersonDetails={() => null}
|
||||
selectedTabIndex={0}
|
||||
/>
|
||||
))
|
||||
.toJSON();
|
||||
|
|
|
@ -1,745 +1,223 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders Manager who has reportees 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="Reportees-root-1"
|
||||
>
|
||||
<div
|
||||
className="MainTabs-root-3"
|
||||
height="1rem"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-4"
|
||||
>
|
||||
<header
|
||||
className="MuiPaper-root-15 MuiPaper-elevation4-21 MuiAppBar-root-6 MuiAppBar-positionStatic-10 MuiAppBar-colorPrimary-13"
|
||||
>
|
||||
<div
|
||||
className="MuiToolbar-root-42 MuiToolbar-regular-44 MuiToolbar-gutters-43 MainTabs-styledToolbar-5"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root-46"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-flexContainer-47"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-scroller-49 MuiTabs-fixed-50"
|
||||
onScroll={[Function]}
|
||||
role="tablist"
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-flexContainer-47"
|
||||
>
|
||||
<button
|
||||
aria-selected={true}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57 MuiTab-selected-60"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Reportees
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-selected={false}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Teams
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-selected={false}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Components
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
className="MuiPrivateTabIndicator-root-70 MuiPrivateTabIndicator-colorSecondary-72 MuiTabs-indicator-54"
|
||||
style={
|
||||
Object {
|
||||
"left": 0,
|
||||
"width": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="MainTabs-grow-4"
|
||||
/>
|
||||
<h6
|
||||
className="MuiTypography-root-73 MuiTypography-subtitle1-91 MuiTypography-colorInherit-102"
|
||||
>
|
||||
Manager
|
||||
</h6>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="MuiTypography-root-73 MuiTypography-body1-82"
|
||||
style={
|
||||
Object {
|
||||
"padding": 4,
|
||||
}
|
||||
}
|
||||
className="DrilldownIcon-svgWrapper-5"
|
||||
name="manager@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<div
|
||||
className="Reportees-root-109"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<div
|
||||
height="1rem"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-112"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-113"
|
||||
name="manager@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Manager
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-112"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-113"
|
||||
name="someone@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Someone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Manager
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-4"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-5"
|
||||
name="someone@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Someone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders Manager who has reportees and teams 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="Reportees-root-1"
|
||||
>
|
||||
<div
|
||||
className="MainTabs-root-3"
|
||||
height="1rem"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-4"
|
||||
>
|
||||
<header
|
||||
className="MuiPaper-root-15 MuiPaper-elevation4-21 MuiAppBar-root-6 MuiAppBar-positionStatic-10 MuiAppBar-colorPrimary-13"
|
||||
>
|
||||
<div
|
||||
className="MuiToolbar-root-42 MuiToolbar-regular-44 MuiToolbar-gutters-43 MainTabs-styledToolbar-5"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root-46"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-flexContainer-47"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-scroller-49 MuiTabs-fixed-50"
|
||||
onScroll={[Function]}
|
||||
role="tablist"
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-flexContainer-47"
|
||||
>
|
||||
<button
|
||||
aria-selected={true}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57 MuiTab-selected-60"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Reportees
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-selected={false}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Teams
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-selected={false}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Components
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
className="MuiPrivateTabIndicator-root-70 MuiPrivateTabIndicator-colorSecondary-72 MuiTabs-indicator-54"
|
||||
style={
|
||||
Object {
|
||||
"left": 0,
|
||||
"width": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="MainTabs-grow-4"
|
||||
/>
|
||||
<h6
|
||||
className="MuiTypography-root-73 MuiTypography-subtitle1-91 MuiTypography-colorInherit-102"
|
||||
>
|
||||
Manager
|
||||
</h6>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="MuiTypography-root-73 MuiTypography-body1-82"
|
||||
style={
|
||||
Object {
|
||||
"padding": 4,
|
||||
}
|
||||
}
|
||||
className="DrilldownIcon-svgWrapper-5"
|
||||
name="manager@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<div
|
||||
className="Reportees-root-109"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<div
|
||||
height="1rem"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-112"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-113"
|
||||
name="manager@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Manager
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-112"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-113"
|
||||
name="someone@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Someone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Manager
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-4"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-5"
|
||||
name="someone@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Someone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders Someone with no reportees 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="Reportees-root-1"
|
||||
>
|
||||
<div
|
||||
className="MainTabs-root-3"
|
||||
height="1rem"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-4"
|
||||
>
|
||||
<header
|
||||
className="MuiPaper-root-15 MuiPaper-elevation4-21 MuiAppBar-root-6 MuiAppBar-positionStatic-10 MuiAppBar-colorPrimary-13"
|
||||
>
|
||||
<div
|
||||
className="MuiToolbar-root-42 MuiToolbar-regular-44 MuiToolbar-gutters-43 MainTabs-styledToolbar-5"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root-46"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-flexContainer-47"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-scroller-49 MuiTabs-fixed-50"
|
||||
onScroll={[Function]}
|
||||
role="tablist"
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-flexContainer-47"
|
||||
>
|
||||
<button
|
||||
aria-selected={true}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57 MuiTab-selected-60"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Reportees
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-selected={false}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Teams
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-selected={false}
|
||||
className="MuiButtonBase-root-67 MuiTab-root-55 MuiTab-textColorInherit-57"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseUp={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
onTouchMove={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
role="tab"
|
||||
tabIndex="0"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-wrapper-63"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-labelContainer-64"
|
||||
>
|
||||
<span
|
||||
className="MuiTab-label-65"
|
||||
>
|
||||
Components
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="MuiTouchRipple-root-124"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
className="MuiPrivateTabIndicator-root-70 MuiPrivateTabIndicator-colorSecondary-72 MuiTabs-indicator-54"
|
||||
style={
|
||||
Object {
|
||||
"left": 0,
|
||||
"width": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="MainTabs-grow-4"
|
||||
/>
|
||||
<h6
|
||||
className="MuiTypography-root-73 MuiTypography-subtitle1-91 MuiTypography-colorInherit-102"
|
||||
>
|
||||
Someone
|
||||
</h6>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="MuiTypography-root-73 MuiTypography-body1-82"
|
||||
style={
|
||||
Object {
|
||||
"padding": 4,
|
||||
}
|
||||
}
|
||||
className="DrilldownIcon-svgWrapper-5"
|
||||
name="manager@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<div
|
||||
className="Reportees-root-109"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<div
|
||||
height="1rem"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-112"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-113"
|
||||
name="manager@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Manager
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-112"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-113"
|
||||
name="someone@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-115 DrilldownIcon-icon-114"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Someone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Manager
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="Reportees-person-4"
|
||||
>
|
||||
<div
|
||||
className="DrilldownIcon-svgWrapper-5"
|
||||
name="someone@mozilla.com"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="MuiSvgIcon-root-7 DrilldownIcon-icon-6"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
/>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
Someone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
216
yarn.lock
216
yarn.lock
|
@ -743,17 +743,6 @@
|
|||
"@babel/runtime" "7.0.0"
|
||||
recompose "^0.29.0"
|
||||
|
||||
"@material-ui/lab@^3.0.0-alpha.30":
|
||||
version "3.0.0-alpha.30"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-3.0.0-alpha.30.tgz#c6c64d0ff2b28410a09e4009f3677499461f3df8"
|
||||
integrity sha512-d8IXbkQO92Ln7f/Tzy8Q5cLi/sMWH/Uz1xrOO5NKUgg42whwyCuoT9ErddDPFNQmPi9d1C7A5AG8ONjEAbAIyQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.2.0"
|
||||
"@material-ui/utils" "^3.0.0-alpha.2"
|
||||
classnames "^2.2.5"
|
||||
keycode "^2.1.9"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
"@material-ui/system@^3.0.0-alpha.0":
|
||||
version "3.0.0-alpha.2"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-3.0.0-alpha.2.tgz#096e80c8bb0f70aea435b9e38ea7749ee77b4e46"
|
||||
|
@ -822,13 +811,12 @@
|
|||
babel-loader "^8.0.4"
|
||||
babel-merge "^2.0.1"
|
||||
|
||||
"@neutrinojs/copy@^8.3.0":
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@neutrinojs/copy/-/copy-8.3.0.tgz#d41a7124b677103134063ef1a4f2a671da1b7758"
|
||||
integrity sha1-1BpxJLZ3EDE0Bj7xpPKmcdobd1g=
|
||||
"@neutrinojs/copy@^9.0.0-beta.1":
|
||||
version "9.0.0-rc.0"
|
||||
resolved "https://registry.yarnpkg.com/@neutrinojs/copy/-/copy-9.0.0-rc.0.tgz#9463e6557338a8433afff58fbaba206d38f2039b"
|
||||
integrity sha512-/Ucrl+jHi1lCbVtQ9tVPijLgP/vEjxWSORjTE9DF22BsyVv0HUPcWiWBj+Ahv3ZFS7B2gmMx6CK056OK+uqMmg==
|
||||
dependencies:
|
||||
copy-webpack-plugin "^4.5.1"
|
||||
deepmerge "^1.5.2"
|
||||
copy-webpack-plugin "^4.6.0"
|
||||
|
||||
"@neutrinojs/dev-server@9.0.0-beta.1":
|
||||
version "9.0.0-beta.1"
|
||||
|
@ -1430,6 +1418,18 @@ atob@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||
|
||||
auth0-js@9.2.3:
|
||||
version "9.2.3"
|
||||
resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-9.2.3.tgz#f98650a742c83567e887703c5972d0c7275e403d"
|
||||
integrity sha1-+YZQp0LINWfoh3A8WXLQxydeQD0=
|
||||
dependencies:
|
||||
base64-js "^1.2.0"
|
||||
idtoken-verifier "^1.1.1"
|
||||
qs "^6.4.0"
|
||||
superagent "^3.8.2"
|
||||
url-join "^1.1.0"
|
||||
winchan "^0.2.0"
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
|
@ -1447,6 +1447,13 @@ axobject-query@^2.0.1:
|
|||
dependencies:
|
||||
ast-types-flow "0.0.7"
|
||||
|
||||
b64@4.x.x:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/b64/-/b64-4.1.2.tgz#7015372ba8101f7fb18da070717a93c28c8580d8"
|
||||
integrity sha512-+GUspBxlH3CJaxMUGUE1EBoWM6RKgWiYwUDal0qdf8m3ArnXNN1KzKVo5HOnE/FSq4HHyWf3TlHLsZI8PKQgrQ==
|
||||
dependencies:
|
||||
hoek "6.x.x"
|
||||
|
||||
babel-code-frame@^6.26.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||
|
@ -1649,7 +1656,7 @@ balanced-match@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
base64-js@^1.0.2:
|
||||
base64-js@^1.0.2, base64-js@^1.2.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
|
||||
integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==
|
||||
|
@ -1732,6 +1739,21 @@ boolbase@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
||||
boom@7.x.x:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/boom/-/boom-7.3.0.tgz#733a6d956d33b0b1999da3fe6c12996950d017b9"
|
||||
integrity sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==
|
||||
dependencies:
|
||||
hoek "6.x.x"
|
||||
|
||||
bounce@1.x.x:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/bounce/-/bounce-1.2.3.tgz#2b286d36eb21d5f08fe672dd8cd37a109baad121"
|
||||
integrity sha512-3G7B8CyBnip5EahCZJjnvQ1HLyArC6P5e+xcolo13BVI9ogFaDOsNMAE7FIWliHtIkYI8/nTRCvCY9tZa3Mu4g==
|
||||
dependencies:
|
||||
boom "7.x.x"
|
||||
hoek "6.x.x"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
|
@ -2249,7 +2271,7 @@ commondir@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
|
||||
|
||||
component-emitter@^1.2.1:
|
||||
component-emitter@^1.2.0, component-emitter@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
|
||||
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
|
||||
|
@ -2343,6 +2365,11 @@ cookie@0.3.1:
|
|||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
|
||||
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
|
||||
|
||||
cookiejar@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
|
||||
integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==
|
||||
|
||||
copy-concurrently@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
|
||||
|
@ -2360,7 +2387,7 @@ copy-descriptor@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||
|
||||
copy-webpack-plugin@^4.5.1:
|
||||
copy-webpack-plugin@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae"
|
||||
integrity sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==
|
||||
|
@ -2459,6 +2486,13 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
|||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cryptiles@4.x.x:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-4.1.3.tgz#2461d3390ea0b82c643a6ba79f0ed491b0934c25"
|
||||
integrity sha512-gT9nyTMSUC1JnziQpPbxKGBbUg8VL7Zn2NB4E1cJYvuXdElHrwxrV9bmltZGDzet45zSDGyYceueke1TjynGzw==
|
||||
dependencies:
|
||||
boom "7.x.x"
|
||||
|
||||
crypto-browserify@^3.11.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
||||
|
@ -2476,6 +2510,11 @@ crypto-browserify@^3.11.0:
|
|||
randombytes "^2.0.0"
|
||||
randomfill "^1.0.3"
|
||||
|
||||
crypto-js@^3.1.9-1:
|
||||
version "3.1.9-1"
|
||||
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8"
|
||||
integrity sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=
|
||||
|
||||
css-loader@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
|
||||
|
@ -3473,7 +3512,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
|||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@~3.0.2:
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
@ -3766,7 +3805,7 @@ forever-agent@~0.6.1:
|
|||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
||||
|
||||
form-data@~2.3.2:
|
||||
form-data@^2.3.1, form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
|
||||
|
@ -3775,6 +3814,11 @@ form-data@~2.3.2:
|
|||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formidable@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659"
|
||||
integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==
|
||||
|
||||
forwarded@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
||||
|
@ -4127,6 +4171,17 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
|||
inherits "^2.0.3"
|
||||
minimalistic-assert "^1.0.1"
|
||||
|
||||
hawk@^7.0.7:
|
||||
version "7.0.10"
|
||||
resolved "https://registry.yarnpkg.com/hawk/-/hawk-7.0.10.tgz#960f72edac9c6b9114c8387886d7278fba9119eb"
|
||||
integrity sha512-3RWF4SXN9CdZ1VDAe6Pn3Rd0tC3Lw+GV+esX5oKCrXoScZK3Ri6dl5Wt986M/hlzU+GuapTGiB0rBhGeRIBQsw==
|
||||
dependencies:
|
||||
b64 "4.x.x"
|
||||
boom "7.x.x"
|
||||
cryptiles "4.x.x"
|
||||
hoek "6.x.x"
|
||||
sntp "3.x.x"
|
||||
|
||||
he@1.2.x:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
|
@ -4157,6 +4212,11 @@ hmac-drbg@^1.0.0:
|
|||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
hoek@6.x.x:
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c"
|
||||
integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==
|
||||
|
||||
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
|
||||
version "2.5.5"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
|
||||
|
@ -4357,6 +4417,17 @@ icss-utils@^2.1.0:
|
|||
dependencies:
|
||||
postcss "^6.0.1"
|
||||
|
||||
idtoken-verifier@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-1.2.0.tgz#4654f1f07ab7a803fc9b1b8b36057e2a87ad8b09"
|
||||
integrity sha512-8jmmFHwdPz8L73zGNAXHHOV9yXNC+Z0TUBN5rafpoaFaLFltlIFr1JkQa3FYAETP23eSsulVw0sBiwrE8jqbUg==
|
||||
dependencies:
|
||||
base64-js "^1.2.0"
|
||||
crypto-js "^3.1.9-1"
|
||||
jsbn "^0.1.0"
|
||||
superagent "^3.8.2"
|
||||
url-join "^1.1.0"
|
||||
|
||||
ieee754@^1.1.4:
|
||||
version "1.1.12"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
|
||||
|
@ -5279,7 +5350,7 @@ js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0:
|
|||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
jsbn@~0.1.0:
|
||||
jsbn@^0.1.0, jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
||||
|
@ -5442,11 +5513,6 @@ jsx-ast-utils@^2.0.1:
|
|||
dependencies:
|
||||
array-includes "^3.0.3"
|
||||
|
||||
keycode@^2.1.9:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
|
||||
integrity sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=
|
||||
|
||||
killable@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
|
||||
|
@ -5866,7 +5932,7 @@ merge@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
|
||||
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
|
||||
|
||||
methods@~1.1.2:
|
||||
methods@^1.1.1, methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
|
||||
|
@ -5934,6 +6000,11 @@ mime@1.4.1:
|
|||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
|
||||
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
|
||||
|
||||
mime@^1.4.1:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
mime@^2.0.3, mime@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
|
||||
|
@ -6039,6 +6110,11 @@ mississippi@^3.0.0:
|
|||
stream-each "^1.1.0"
|
||||
through2 "^2.0.0"
|
||||
|
||||
mitt@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.1.3.tgz#528c506238a05dce11cd914a741ea2cc332da9b8"
|
||||
integrity sha512-mUDCnVNsAi+eD6qA0HkRkwYczbLHJ49z17BGe2PYRhZL4wpZUFZGJHU7/5tmvohoma+Hdn0Vh/oJTiPEmgSruA==
|
||||
|
||||
mixin-deep@^1.2.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
|
||||
|
@ -7054,6 +7130,20 @@ qs@6.5.2, qs@~6.5.2:
|
|||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||
|
||||
qs@^6.4.0, qs@^6.5.1:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
||||
|
||||
query-string@^6.1.0:
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.4.2.tgz#8be1dbd105306aebf86022144f575a29d516b713"
|
||||
integrity sha512-DfJqAen17LfLA3rQ+H5S4uXphrF+ANU1lT2ijds4V/Tj4gZxA3gx5/tg1bz7kYCmwna7LyJNCYqO7jNRzo3aLw==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
query-string@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.2.0.tgz#468edeb542b7e0538f9f9b1aeb26f034f19c86e1"
|
||||
|
@ -7291,7 +7381,7 @@ read-pkg@^4.0.1:
|
|||
parse-json "^4.0.0"
|
||||
pify "^3.0.0"
|
||||
|
||||
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
|
||||
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
||||
|
@ -7943,6 +8033,16 @@ snapdragon@^0.8.1:
|
|||
source-map-resolve "^0.5.0"
|
||||
use "^3.1.0"
|
||||
|
||||
sntp@3.x.x:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-3.0.2.tgz#3f0b5de6115681dce82a9478691f0e5c552de5a3"
|
||||
integrity sha512-MCAPpBPFjNp1fwDVCLSRuWuH9gONtb2R+lS1esC6Mp8lP6jy60FVUtP/Qr0jBvcWAVbhzx06y1b6ptXiy32dug==
|
||||
dependencies:
|
||||
boom "7.x.x"
|
||||
bounce "1.x.x"
|
||||
hoek "6.x.x"
|
||||
teamwork "3.x.x"
|
||||
|
||||
sockjs-client@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177"
|
||||
|
@ -8072,6 +8172,11 @@ spdy@^3.4.1:
|
|||
select-hose "^2.0.0"
|
||||
spdy-transport "^2.0.18"
|
||||
|
||||
split-on-first@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.0.0.tgz#648af4ce9a28fbcaadd43274455f298b55025fc6"
|
||||
integrity sha512-mjA57TQtdWztVZ9THAjGNpgbuIrNfsNrGa5IyK94NoPaT4N14M+GI4jD7t4arLjFkYRQWdETC5RxFzLWouoB3A==
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
|
@ -8288,6 +8393,22 @@ style-loader@^0.23.1:
|
|||
loader-utils "^1.1.0"
|
||||
schema-utils "^1.0.0"
|
||||
|
||||
superagent@^3.8.2:
|
||||
version "3.8.3"
|
||||
resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128"
|
||||
integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==
|
||||
dependencies:
|
||||
component-emitter "^1.2.0"
|
||||
cookiejar "^2.1.0"
|
||||
debug "^3.1.0"
|
||||
extend "^3.0.0"
|
||||
form-data "^2.3.1"
|
||||
formidable "^1.2.0"
|
||||
methods "^1.1.1"
|
||||
mime "^1.4.1"
|
||||
qs "^6.5.1"
|
||||
readable-stream "^2.3.5"
|
||||
|
||||
supports-color@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
||||
|
@ -8350,6 +8471,31 @@ tar@^4:
|
|||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.2"
|
||||
|
||||
taskcluster-client-web@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/taskcluster-client-web/-/taskcluster-client-web-9.0.0.tgz#f160f264852683e36cc4f7bcaa57895ad91b3f6e"
|
||||
integrity sha512-cuAk5JLtX9SSngsfUiIARWWYj0uGKt2vB6yqT3w2gSR3mXReLb9AlQHsgpL51VgdpbXw2YMopUV8U8auCmSBmQ==
|
||||
dependencies:
|
||||
crypto-js "^3.1.9-1"
|
||||
hawk "^7.0.7"
|
||||
query-string "^6.1.0"
|
||||
taskcluster-lib-urls "^10.0.0"
|
||||
|
||||
taskcluster-lib-urls@^10.0.0:
|
||||
version "10.1.1"
|
||||
resolved "https://registry.yarnpkg.com/taskcluster-lib-urls/-/taskcluster-lib-urls-10.1.1.tgz#67d5b9449b947e5234eafdd15c46267dde29bf74"
|
||||
integrity sha512-tdrK++rCX73FMXk/cXwS6RLTjA3pX8hJlxg1ECLs3L3llCOPMNhQ4wi6lb6yMgHc/s5on/Edj6AlAH7gkxzgPg==
|
||||
|
||||
taskcluster-lib-urls@^12.0.0:
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/taskcluster-lib-urls/-/taskcluster-lib-urls-12.0.0.tgz#f56190eec9e9597d37a42ad0e7f461e8e0e6732b"
|
||||
integrity sha512-OrEFE0m3p/+mGsmIwjttLhSKg3io6MpJLhYtPNjVSZA9Ix8Y5tprN3vM6a3MjWt5asPF6AKZsfT43cgpGwJB0g==
|
||||
|
||||
teamwork@3.x.x:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/teamwork/-/teamwork-3.2.0.tgz#27916edab815459c1a4686252eb18fb5925f49fa"
|
||||
integrity sha512-xAmJ8PIVjRZMXAHgUuOP8ITsv0SedyWAit2UWiNImXgg/F+BxrsG46ZegElNBM0Dwp+iMfbigg/Ll/M2oDRYww==
|
||||
|
||||
terser-webpack-plugin@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz#cf7c25a1eee25bf121f4a587bb9e004e3f80e528"
|
||||
|
@ -8643,6 +8789,11 @@ urix@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
|
||||
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
|
||||
|
||||
url-join@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78"
|
||||
integrity sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=
|
||||
|
||||
url-loader@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8"
|
||||
|
@ -9002,6 +9153,11 @@ wide-align@^1.1.0:
|
|||
dependencies:
|
||||
string-width "^1.0.2 || 2"
|
||||
|
||||
winchan@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.1.tgz#19b334e49f7c07c0849f921f405fad87dfc8a1da"
|
||||
integrity sha512-QrG9q+ObfmZBxScv0HSCqFm/owcgyR5Sgpiy1NlCZPpFXhbsmNHhTiLWoogItdBUi0fnU7Io/5ABEqRta5/6Dw==
|
||||
|
||||
wordwrap@~0.0.2:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
||||
|
|
Загрузка…
Ссылка в новой задаче