Merge pull request #40 from spotify/soapraj/layout-changes

Update layout, header
This commit is contained in:
Raghunandan Balachandran
2020-02-05 11:38:13 +01:00
committed by GitHub
42 changed files with 2214 additions and 71 deletions
-1
View File
@@ -6,7 +6,6 @@ describe('App', () => {
it('renders learn react link', () => {
const rendered = render(<App />);
rendered.getByText('This is Backstage!');
rendered.getByText('Backstage is an open platform for building developer portals');
rendered.getByText('…with plugin hello-world:');
});
});
+47 -25
View File
@@ -1,9 +1,9 @@
import React, { FC, Fragment } from 'react';
import React, { FC } from 'react';
import helloWorld, { MyComponent } from '@backstage/plugin-hello-world';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import SideBar from './components/SideBar';
import PageHeader from './components/PageHeader';
//import PageHeader from './components/PageHeader';
import { Header, Page, InfoCard } from '@backstage/core';
import { LoginComponent } from '@backstage/plugin-login';
import {
BrowserRouter as Router,
@@ -11,6 +11,15 @@ import {
Route,
Link as RouterLink,
} from 'react-router-dom';
import { BackstageTheme, withGlobalStyles, theme } from '@backstage/core';
import { CssBaseline, MuiThemeProvider, makeStyles } from '@material-ui/core';
import { ThemeProvider } from '@material-ui/styles';
import { StylesProvider, createGenerateClassName } from '@material-ui/styles';
import HomePageTimer from './components/HomepageTimer';
const generateClassName = createGenerateClassName({
productionPrefix: 'stajl',
});
const useStyles = makeStyles(theme => ({
root: {
@@ -26,8 +35,7 @@ const useStyles = makeStyles(theme => ({
overflowY: 'auto',
},
pageBody: {
paddingLeft: theme.spacing(2),
paddingTop: theme.spacing(2),
padding: theme.spacing(2),
},
avatarButton: {
padding: theme.spacing(2),
@@ -36,24 +44,32 @@ const useStyles = makeStyles(theme => ({
const App: FC<{}> = () => {
return (
<AppShell>
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/login">
<Login />
</Route>
</Switch>
</Router>
</AppShell>
<CssBaseline>
<MuiThemeProvider theme={BackstageTheme}>
<StylesProvider generateClassName={generateClassName}>
<ThemeProvider theme={BackstageTheme}>
<AppContent>
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/login">
<Login />
</Route>
</Switch>
</Router>
</AppContent>
</ThemeProvider>
</StylesProvider>
</MuiThemeProvider>
</CssBaseline>
);
};
const Home: FC<{}> = () => {
return (
<Fragment>
<InfoCard title="Home Page">
<Typography variant="body1">
{' '}
with plugin {helloWorld?.id ?? 'wat'}:
@@ -62,18 +78,18 @@ const Home: FC<{}> = () => {
<div>
<RouterLink to="/login">Go to Login</RouterLink>
</div>
</Fragment>
</InfoCard>
);
};
const Login: FC<{}> = () => {
return (
<Fragment>
<InfoCard title="Login Page">
<LoginComponent />
<div>
<RouterLink to="/">Go to Home</RouterLink>
</div>
</Fragment>
</InfoCard>
);
};
@@ -83,12 +99,18 @@ const AppShell: FC<{}> = ({ children }) => {
return (
<div className={classes.root}>
<SideBar />
<div className={classes.mainContentArea}>
<PageHeader />
<div className={classes.pageBody}>{children}</div>
</div>
<Page theme={theme.home}>
<div className={classes.mainContentArea}>
<Header title="This is Backstage!">
<HomePageTimer />
</Header>
<div className={classes.pageBody}>{children}</div>
</div>
</Page>
</div>
);
};
const AppContent = withGlobalStyles(AppShell);
export default App;
@@ -0,0 +1,58 @@
import React, { FC } from 'react';
import { HeaderLabel } from '@backstage/core';
const timeFormat = { hour: '2-digit', minute: '2-digit' };
const nycOptions = { timeZone: 'America/New_York', ...timeFormat };
const utcOptions = { timeZone: 'UTC', ...timeFormat };
const lonOptions = { timeZone: 'Europe/London', ...timeFormat };
const stoOptions = { timeZone: 'Europe/Stockholm', ...timeFormat };
const defaultTimes = {
timeNY: '',
timeUTC: '',
timeLON: '',
timeSTO: '',
};
function getTimes() {
const d = new Date();
const lang = window.navigator.language;
// Using the browser native toLocaleTimeString instead of huge moment-tz
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString
const timeNY = d.toLocaleTimeString(lang, nycOptions);
const timeUTC = d.toLocaleTimeString(lang, utcOptions);
const timeLON = d.toLocaleTimeString(lang, lonOptions);
const timeSTO = d.toLocaleTimeString(lang, stoOptions);
return { timeNY, timeUTC, timeLON, timeSTO };
}
const HomePageTimer: FC<{}> = () => {
const [{ timeNY, timeUTC, timeLON, timeSTO }, setTimes] = React.useState(
defaultTimes,
);
React.useEffect(() => {
setTimes(getTimes());
const intervalId = setInterval(() => {
setTimes(getTimes());
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<>
<HeaderLabel label="NYC" value={timeNY} />
<HeaderLabel label="UTC" value={timeUTC} />
<HeaderLabel label="LON" value={timeLON} />
<HeaderLabel label="STO" value={timeSTO} />
</>
);
};
export default HomePageTimer;
@@ -0,0 +1 @@
export { default } from './HomepageTimer';
@@ -1,28 +0,0 @@
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import React, { FC } from 'react';
const useStyles = makeStyles(theme => ({
root: {
background: 'linear-gradient(262.63deg, #19D15A 4.2%, #1CAB5B 72.01%)',
height: '120px',
paddingTop: theme.spacing(2),
paddingLeft: theme.spacing(2),
color: 'white',
},
}));
const SideBar: FC<{}> = () => {
const classes = useStyles();
return (
<header className={classes.root}>
<Typography variant="h2">This is Backstage!</Typography>
<Typography variant="caption">
Backstage is an open platform for building developer portals
</Typography>
</header>
);
};
export default SideBar;
@@ -1 +0,0 @@
export { default } from './PageHeader';
+5 -1
View File
@@ -13,7 +13,11 @@
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"react": "^16.12.0",
"react-dom": "^16.12.0"
"react-dom": "^16.12.0",
"react-helmet":"5.2.1",
"tinycolor2": "1.4.1",
"react-addons-text-content": "0.0.4",
"recompose": "0.30.0"
},
"scripts": {
"lint": "web-scripts lint",
@@ -0,0 +1,44 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Lifecycle extends Component {
static propTypes = {
isShorthand: PropTypes.bool,
fontSize: PropTypes.string,
};
}
const styles = {
alpha: {
color: '#d00150',
fontFamily: 'serif',
fontWeight: 'normal',
fontStyle: 'italic',
},
beta: {
color: '#4d65cc',
fontFamily: 'serif',
fontWeight: 'normal',
fontStyle: 'italic',
},
};
export class AlphaLabel extends Lifecycle {
render() {
const style = fontSize => ({ ...styles.alpha, fontSize, ...this.props.style });
return this.props.isShorthand ? (
<span style={style('120%')}>&alpha;</span>
) : (
<span style={style('100%')}>Alpha</span>
);
}
}
export class BetaLabel extends Lifecycle {
render() {
const fontSize = this.props.fontSize ? this.props.fontSize : this.props.isShorthand ? '120%' : '100%';
const style = { ...styles.beta, fontSize, ...this.props.style };
return this.props.isShorthand ? <span style={style}>&beta;</span> : <span style={style}>Beta</span>;
}
}
@@ -0,0 +1 @@
export { AlphaLabel, BetaLabel } from './Lifecycle';
@@ -0,0 +1,79 @@
import React from 'react';
import { pure } from 'recompose';
import { withStyles } from '@material-ui/core';
const styles = theme => ({
status: {
width: 12,
height: 12,
display: 'inline-block',
marginRight: 1,
},
ok: {
backgroundColor: theme.palette.status.ok,
borderRadius: '50%',
},
warning: {
backgroundColor: theme.palette.status.warning,
},
error: {
width: '0',
height: '0',
borderLeft: '7px solid transparent',
borderRight: '7px solid transparent',
borderBottom: `14px solid ${theme.palette.status.error}`,
},
pending: {
backgroundColor: theme.palette.status.pending,
},
failed: {
backgroundColor: 'rgba(245, 155, 35, 0.5)',
},
running: {
animation: 'blink 0.8s step-start 0s infinite',
backgroundColor: theme.palette.status.running,
},
'@keyframes blink': {
'50%': {
backgroundColor: theme.palette.status.background,
},
},
});
export const StatusOK = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.ok}`} aria-label="Status OK" {...props} />
)),
);
export const StatusWarning = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.warning}`} aria-label="Status warning" {...props} />
)),
);
export const StatusError = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.error}`} aria-label="Status error" {...props} />
)),
);
export const StatusNA = pure(() => <span aria-label="Status N/A">N/A</span>);
export const StatusPending = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.pending}`} aria-label="Status pending" {...props} />
)),
);
export const StatusRunning = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.running}`} aria-label="Status running" {...props} />
)),
);
export const StatusFailed = pure(
withStyles(styles)(props => (
<span className={`${props.classes.status} ${props.classes.failed}`} aria-label="Status failed" {...props} />
)),
);
@@ -0,0 +1 @@
export { StatusError, StatusFailed, StatusNA, StatusOK, StatusPending, StatusRunning, StatusWarning } from './Status';
+10
View File
@@ -1,2 +1,12 @@
export * from './types';
export { default as createPlugin } from './createPlugin';
export { default as Page } from '../src/layout/Page';
export { gradients, theme } from '../src/layout/Page';
export { default as Header } from '../src/layout/Header/Header';
export { default as HeaderLabel } from '../src/layout/HeaderLabel';
export { default as InfoCard } from '../src/layout/InfoCard';
export { default as ErrorBoundary } from '../src/layout/ErrorBoundary';
export { default as BackstageTheme } from '../src/theme/BackstageTheme';
export { withGlobalStyles } from '../src/theme/BackstageTheme';
@@ -0,0 +1,69 @@
import React, { Component, Fragment } from 'react';
import { Typography, withStyles } from '@material-ui/core';
import Helmet from 'react-helmet';
import FavoriteButton from 'shared/components/layout/FavoriteButton';
const styles = theme => ({
container: {
width: '100%',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-end',
alignItems: 'center',
},
leftItemsBox: {
flex: '1 1 auto',
marginBottom: theme.spacing(1),
minWidth: 0,
overflow: 'visible',
},
rightItemsBox: {
flex: '0 1 auto',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
marginLeft: theme.spacing(1),
marginBottom: theme.spacing(1),
minWidth: 0,
overflow: 'visible',
},
description: {},
title: {
display: 'inline-flex',
},
});
class ContentHeader extends Component {
static defaultProps = {
favoriteable: true,
title: 'Unknown page',
};
render() {
const { title, description, favoriteable, children, classes } = this.props;
return (
<Fragment>
<Helmet title={title} />
<div className={classes.container}>
<div className={classes.leftItemsBox}>
<Typography variant="h3" className={classes.title} data-testid="header-title">
{title}
</Typography>
{favoriteable && <FavoriteButton />}
{description && (
<Typography className={classes.description} variant="body2">
{description}
</Typography>
)}
</div>
<div className={classes.rightItemsBox}>{children}</div>
</div>
</Fragment>
);
}
}
export default withStyles(styles)(ContentHeader);
@@ -0,0 +1,46 @@
import React, { Component } from 'react';
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
error: null,
errorInfo: null,
onError: props.onError,
};
}
componentDidCatch(error, errorInfo) {
console.error(`ErrorBoundary, error: ${error}, info: ${errorInfo}`);
this.setState({ error, errorInfo });
// Exposed for testing
if (ErrorBoundary.onError) {
ErrorBoundary.onError(error, errorInfo);
}
}
render() {
const { slackChannel } = this.props;
const { error, errorInfo } = this.state;
if (!errorInfo) {
return this.props.children;
}
return (
<Error error={error} errorInfo={errorInfo} slackChannel={slackChannel} />
);
}
}
// Importing Error would mean importing a lot of stuff
// will take it up in a separate PR
const Error = ({ slackChannel }) => {
return (
<div>
Something went wrong here. Please contact {slackChannel} for help.
</div>
);
};
@@ -0,0 +1 @@
export { default } from './ErrorBoundary';
@@ -0,0 +1,34 @@
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
burst: {
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
'z-index': -1,
},
/* base style of burst shape SVGs */
burstShape: {
position: 'absolute',
right: 0,
height: '100%',
'background-repeat': 'no-repeat',
'background-size': 'cover',
opacity:0.1,
}
}));
const Burst = ({theme}) => {
const classes = useStyles();
return (
<div className={classes.burst} style={{ backgroundImage: theme.gradient }}>
<div className={classes.burstShape} style={theme.burstShape} />
</div>
);
}
export default Burst;
@@ -0,0 +1,141 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Typography, withStyles, Tooltip } from '@material-ui/core';
import { Theme } from '../Page/Page';
// import { Link } from 'shared/components';
import Burst from './Burst';
import Helmet from 'react-helmet';
class Header extends Component {
static propTypes = {
type: PropTypes.string,
typeLink: PropTypes.string,
title: PropTypes.node.isRequired,
tooltip: PropTypes.string,
subtitle: PropTypes.node,
pageTitleOverride: PropTypes.string,
style: PropTypes.object,
component: PropTypes.object,
};
typeFragment() {
const { type, typeLink, classes } = this.props;
if (!type) {
return null;
}
return typeLink ? (
// <Link to={typeLink}>
<Typography className={classes.type}>{type}</Typography>
// </Link>
) : (
<Typography className={classes.type}>{type}</Typography>
);
}
titleFragment() {
const { title, pageTitleOverride, classes, tooltip } = this.props;
const FinalTitle = (
<Typography className={classes.title} variant="h4">
{title || pageTitleOverride}
</Typography>
);
if (tooltip) {
return (
<Tooltip title={tooltip} placement="top-start">
{FinalTitle}
</Tooltip>
);
}
return FinalTitle;
}
subtitleFragment() {
const { subtitle, classes } = this.props;
if (!subtitle) {
return null;
} else if (typeof subtitle !== 'string') {
return subtitle;
} else {
return (
<Typography className={classes.subtitle} variant="subtitle1">
{subtitle}
</Typography>
);
}
}
render() {
const { title, pageTitleOverride, children, style, classes } = this.props;
const pageTitle = pageTitleOverride || title;
if (typeof pageTitle !== 'string') {
console.warn('<Header/> title prop is not a string, pageTitleOverride should be provided.');
}
return (
<Fragment>
<Helmet titleTemplate={`${pageTitle} | %s | Backstage`} defaultTitle={`${pageTitle} | Backstage`} />
<Theme.Consumer>
{theme => (
<header style={style} className={classes.header}>
<Burst theme={theme} />
<div className={classes.leftItemsBox}>
{this.typeFragment()}
{this.titleFragment()}
{this.subtitleFragment()}
</div>
<div className={classes.rightItemsBox}>{children}</div>
</header>
)}
</Theme.Consumer>
</Fragment>
);
}
}
const styles = theme => ({
header: {
gridArea: 'pageHeader',
padding: theme.spacing(3),
minHeight: 118,
width: 'calc(100vw - 224px)',
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
position: 'relative',
zIndex: 100,
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-end',
alignItems: 'center',
},
leftItemsBox: {
flex: '1 1 auto',
},
rightItemsBox: {
flex: '0 1 auto',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
},
title: {
color: theme.palette.bursts.fontColor,
lineHeight: '1.0em',
wordBreak: 'break-all',
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
marginBottom: theme.spacing(1),
},
subtitle: {
color: 'rgba(255, 255, 255, 0.8)',
lineHeight: '1.0em',
},
type: {
textTransform: 'uppercase',
fontSize: 9,
opacity: 0.8,
marginBottom: 10,
color: theme.palette.bursts.fontColor,
},
});
export default withStyles(styles)(Header);
@@ -0,0 +1,37 @@
import React from 'react';
import { render } from '@testing-library/react';
import Header from './Header';
import { wrapInThemedTestApp } from '../../testUtils';
jest.mock('react-helmet', () => {
return ({ defaultTitle }) => <div>defaultTitle: {defaultTitle}</div>;
});
describe('<Header/>', () => {
it('should render with title', () => {
const rendered = render(wrapInThemedTestApp(<Header title="Title" />));
rendered.getByText('Title');
});
it('should set document title', () => {
const rendered = render(wrapInThemedTestApp(<Header title="Title1" />));
rendered.getByText('Title1');
rendered.getByText('defaultTitle: Title1 | Backstage');
});
it('should override document title', () => {
const rendered = render(wrapInThemedTestApp(<Header title="Title1" pageTitleOverride="Title2" />));
rendered.getByText('Title1');
rendered.getByText('defaultTitle: Title2 | Backstage');
});
it('should have subtitle', () => {
const rendered = render(wrapInThemedTestApp(<Header title="Title" subtitle="Subtitle" />));
rendered.getByText('Subtitle');
});
it('should have type rendered', () => {
const rendered = render(wrapInThemedTestApp(<Header title="Title" type="service" />));
rendered.getByText('service');
});
});
@@ -0,0 +1 @@
export { default } from './Header';
@@ -0,0 +1,56 @@
import React, { Fragment } from 'react';
import { IconButton, List, ListItem, ListItemIcon, ListItemText, Popover } from '@material-ui/core';
import { default as KebabMenuIcon } from './MenuVertical';
const ActionItem = ({ label, secondaryLabel, icon, disabled = false, onClick, WrapperComponent = React.Fragment }) => {
return (
<WrapperComponent>
<ListItem
data-testid="header-action-item"
disabled={disabled}
button
onClick={event => {
if (onClick) {
onClick(event);
}
}}
>
{icon && <ListItemIcon>{icon}</ListItemIcon>}
<ListItemText primary={label} secondary={secondaryLabel} />
</ListItem>
</WrapperComponent>
);
};
const HeaderActionMenu = ({ actionItems }) => {
const [open, setOpen] = React.useState(false);
const anchorElRef = React.useRef(null);
return (
<Fragment>
<IconButton
onClick={() => setOpen(true)}
data-testid="header-action-menu"
ref={anchorElRef}
style={{ color: 'white', height: 56, width: 56, marginRight: -4, padding: 0 }}
>
<KebabMenuIcon titleAccess={'menu'} style={{ fontSize: 40 }} />
</IconButton>
<Popover
open={open}
anchorEl={anchorElRef.current}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
onClose={() => setOpen(false)}
>
<List>
{actionItems.map(actionItem => {
return <ActionItem key={actionItem.label} {...actionItem} />;
})}
</List>
</Popover>
</Fragment>
);
};
export default HeaderActionMenu;
@@ -0,0 +1,70 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInThemedTestApp, Keyboard } from '../../testUtils';
import HeaderActionMenu from './HeaderActionMenu';
describe('<ComponentContextMenu />', () => {
it('renders without any items and without exploding', () => {
render(wrapInThemedTestApp(<HeaderActionMenu actionItems={[]} />));
});
it('can open the menu and click menu items', () => {
const onClickFunction = jest.fn();
const rendered = render(
wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label', onClick: onClickFunction }]} />),
);
expect(rendered.queryByText('Some label')).not.toBeInTheDocument();
expect(onClickFunction).not.toHaveBeenCalled();
fireEvent.click(rendered.getByTestId('header-action-menu'));
expect(onClickFunction).not.toHaveBeenCalled();
expect(rendered.getByTestId('header-action-item')).not.toHaveAttribute('aria-disabled', 'true');
fireEvent.click(rendered.queryByText('Some label'));
expect(onClickFunction).toHaveBeenCalled();
// We do not expect the dropdown to disappear after click
expect(rendered.queryByText('Some label')).toBeInTheDocument();
});
it('Disabled', async () => {
const rendered = render(
wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label', disabled: true }]} />),
);
fireEvent.click(rendered.getByTestId('header-action-menu'));
expect(rendered.getByTestId('header-action-item')).toHaveAttribute('aria-disabled', 'true');
});
it('Test wrapper, and secondary label', () => {
const onClickFunction = jest.fn();
const rendered = render(
wrapInThemedTestApp(
<HeaderActionMenu
actionItems={[
{
label: 'Some label',
secondaryLabel: 'Secondary label',
WrapperComponent: ({ children }) => <button onClick={onClickFunction}>{children}</button>,
},
]}
/>,
),
);
expect(onClickFunction).not.toHaveBeenCalled();
fireEvent.click(rendered.getByTestId('header-action-menu'));
expect(onClickFunction).not.toHaveBeenCalled();
fireEvent.click(rendered.queryByText('Secondary label'));
expect(onClickFunction).toHaveBeenCalled();
// We do not expect the dropdown to disappear after click
expect(rendered.queryByText('Some label')).toBeInTheDocument();
});
it('should close when hitting escape', async () => {
const rendered = render(wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label' }]} />));
expect(rendered.container.getAttribute('aria-hidden')).toBeNull();
fireEvent.click(rendered.getByTestId('header-action-menu'));
expect(rendered.container.getAttribute('aria-hidden')).toBe('true');
await Keyboard.type(rendered, '<Esc>');
expect(rendered.container.getAttribute('aria-hidden')).toBeNull();
});
});
@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
const SvgMenuVertical = (props: SvgIconProps) =>
React.createElement(
SvgIcon,
props,
<path d="M11 3a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1V4a1 1 0 00-1-1h-2zm0 7a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2zm0 7a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2z" />,
);
export default SvgMenuVertical;
@@ -0,0 +1,50 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Typography, withStyles } from '@material-ui/core';
import { Link } from '@material-ui/core';
const style = theme => ({
root: {
textAlign: 'left',
margin: theme.spacing(2),
display: 'inline-block',
},
label: {
color: '#FFFFFF',
fontWeight: 'bold',
lineHeight: '16px',
letterSpacing: 0,
fontSize: 14,
height: '16px',
marginBottom: 2,
},
value: {
color: 'rgba(255, 255, 255, 0.8)',
lineHeight: '16px',
fontSize: 14,
height: '16px',
},
});
class HeaderLabel extends Component {
static propTypes = {
label: PropTypes.string.isRequired,
value: PropTypes.node,
url: PropTypes.string,
};
render() {
const { label, value, url, classes } = this.props;
const content = (
<Typography className={classes.value}>{value || '<Unknown>'}</Typography>
);
return (
<span className={classes.root}>
<Typography className={classes.label}>{label}</Typography>
{url ? <Link href={url}>{content}</Link> : content}
</span>
);
}
}
export default withStyles(style)(HeaderLabel);
@@ -0,0 +1,28 @@
import React from 'react';
import { render } from '@testing-library/react';
import HeaderLabel from './HeaderLabel';
import { wrapInThemedTestApp } from '../../testUtils';
describe('<HeaderLabel />', () => {
it('should have a label', () => {
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" />));
expect(rendered.getByText('Label')).toBeInTheDocument();
});
it('should say unknown', () => {
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" />));
expect(rendered.getByText('<Unknown>')).toBeInTheDocument();
});
it('should have value', () => {
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" value="Value" />));
expect(rendered.getByText('Value')).toBeInTheDocument();
});
it('should have a link', () => {
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" value="Value" url="/test" />));
const anchor = rendered.container.querySelector('a');
expect(rendered.getByText('Value')).toBeInTheDocument();
expect(anchor.href).toBe('http://localhost/test');
});
});
@@ -0,0 +1,64 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Tooltip, Link, withStyles } from '@material-ui/core';
import { StatusError } from '../../components/Status';
import HeaderLabel from './HeaderLabel';
const style = theme => ({
notVerified: {
color: theme.palette.status.error,
borderRadius: 4,
padding: '3px 6px',
fontSize: '8pt',
opacity: 0.8,
fontWeight: 'bold',
position: 'relative',
top: -4,
backgroundColor: 'pink',
float: 'right',
marginLeft: 14,
},
label: { float: 'left' },
});
class OwnerHeaderLabel extends Component {
static propTypes = {
owner: PropTypes.object.isRequired,
};
render() {
const { owner, classes } = this.props;
const isBadSquad = owner.type !== 'squad';
const notVerified = isBadSquad && (
<Link href="https://spotify.stackenterprise.co/a/4412/23">
<span className={classes.notVerified}>
<StatusError style={{ position: 'relative', top: 2 }} /> Squad not verified!
</span>
</Link>
);
const label = (
<Tooltip
title="This component is not owned by an existing squad. Click the badge to learn how to fix this."
placement="bottom"
>
<span>
<span className={classes.label}>{owner.name}</span>
</span>
</Tooltip>
);
return (
<>
<HeaderLabel
label="Owner"
value={isBadSquad ? label : owner.name}
url={owner.name ? `/org/${owner.name}` : ''}
/>
{notVerified}
</>
);
}
}
export default withStyles(style)(OwnerHeaderLabel);
@@ -0,0 +1,37 @@
import React from 'react';
import { render, prettyDOM } from '@testing-library/react';
import OwnerHeaderLabel from './OwnerHeaderLabel';
import { wrapInThemedTestApp } from '../../testUtils';
const properOwner = { id: 'tools', name: 'tools', type: 'squad' };
const badOwner = { id: 'tools-xxx', name: 'tools-xxx' };
describe('<OwnerHeaderLabel />', () => {
it('should have a label', () => {
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={properOwner} />));
expect(rendered.getByText('Owner')).toBeInTheDocument();
expect(rendered.getByText('tools')).toBeInTheDocument();
expect(rendered.queryByText('Squad not verified!')).not.toBeInTheDocument();
});
it('should have an org link', () => {
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={properOwner} />));
const anchor = rendered.container.querySelector('a');
expect(anchor.href).toBe('http://localhost/org/tools');
});
it('should have WARNING label', () => {
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={badOwner} />));
expect(rendered.getByText('Squad not verified!')).toBeInTheDocument();
});
it('should have status error label', () => {
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={badOwner} />));
expect(rendered.getByLabelText('Status error')).toBeInTheDocument();
});
it('should handle empty input', () => {
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={{}} />));
expect(rendered.getByLabelText('Status error')).toBeInTheDocument();
});
});
@@ -0,0 +1 @@
export { default } from './HeaderLabel';
@@ -0,0 +1,31 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from '@material-ui/core';
import { Divider, ListItemText } from '@material-ui/core';
import { ListItem, ListItemIcon } from '@material-ui/core';
import ArrowIcon from '@material-ui/icons/ArrowForward';
export default class BottomLink extends Component {
static propTypes = {
link: PropTypes.string,
title: PropTypes.string,
onClick: PropTypes.func,
};
render() {
const { link, title, onClick } = this.props;
return (
<div>
<Divider />
<Link href={link} onClick={onClick} highlight="none">
<ListItem>
<ListItemIcon>
<ArrowIcon />
</ListItemIcon>
<ListItemText>{title}</ListItemText>
</ListItem>
</Link>
</div>
);
}
}
@@ -0,0 +1,16 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '../../testUtils';
import BottomLink from './BottomLink';
const minProps = {
title: 'A deepLink title',
link: '/mocked',
};
describe('<BottomLink />', () => {
it('renders without exploding', () => {
const rendered = render(wrapInTestApp(<BottomLink {...minProps} />));
expect(rendered.getByText('A deepLink title')).toBeInTheDocument();
});
});
@@ -0,0 +1,197 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
Card,
CardActions,
CardContent,
CardHeader,
Divider,
withStyles,
} from '@material-ui/core';
import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
import BottomLink from './BottomLink';
import textContent from 'react-addons-text-content';
const BoldHeader = withStyles({ title: { fontWeight: '700' } })(CardHeader);
const CardActionsTopRight = withStyles({
root: {
display: 'inline-block',
paddingRight: '16px',
paddingTop: '16px',
float: 'right',
},
})(CardActions);
const VARIANT_STYLES = {
card: {
flex: {
display: 'flex',
flexDirection: 'column',
},
widget: {
height: 430,
},
fullHeight: {
height: '100%',
},
height100: {
display: 'flex',
flexDirection: 'column',
height: 'calc(100% - 10px)', // for pages without content header
marginBottom: '10px',
},
contentheader: {
height: 'calc(100% - 40px)', // for pages with content header
},
contentheadertabs: {
height: 'calc(100% - 97px)', // for pages with content header and tabs (Tingle)
},
noShrink: {
flexShrink: 0,
},
minheight300: {
minHeight: 300,
overflow: 'initial',
},
},
cardContent: {
widget: {
overflowY: 'auto',
height: 332,
width: '100%',
},
fullHeight: {
height: 'calc(100% - 50px)',
},
height100: {
height: 'calc(100% - 50px)',
},
contentRow: {
display: 'flex',
flexDirection: 'row',
},
},
};
/**
* InfoCard is used to display a paper-styled block on the screen, similar to a panel.
*
* You can custom style an InfoCard with the 'style' (outer container) and 'cardStyle' (inner container)
* styles.
*
* The InfoCard serves as an error boundary. As a result, if you provide a 'slackChannel' property this
* specifies the channel to display in the error component that is displayed if an error occurs
* in any descendent components.
*
* By default the InfoCard has no custom layout of its children, but is treated as a block element. A
* couple common variants are provided and can be specified via the variant property:
*
* Display the card full height suitable for DataGrid:
*
* <InfoCard variant="height100">...</InfoCard>
*
* Variants can be combined in a whitespace delimited list like so:
*
* <InfoCard variant="noShrink">...</InfoCard>
*/
class InfoCard extends Component {
static propTypes = {
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
subheader: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
divider: PropTypes.bool,
deepLink: PropTypes.object,
slackChannel: PropTypes.string,
variant: PropTypes.string,
};
render() {
const {
title,
subheader,
divider,
deepLink,
children,
actions,
actionsTopRight,
headerStyle,
headerProps,
classes,
slackChannel,
variant,
} = this.props;
if (this.props.style) {
console.warn(
'InfoCard: using `style` property directly, consider migrating your style to variant in InfoCard',
);
}
if (this.props.cardStyle) {
console.warn(
'InfoCard: using `cardStyle` property directly, consider migrating your style to variant in InfoCard',
);
}
/**
* If variant is specified, we build up styles for that particular variant for both
* the Card and the CardContent (since these need to be synced)
*/
let calculatedStyle = {};
let calculatedCardStyle = {};
if (variant) {
let variants = variant.split(/[\s]+/g);
variants.forEach(name => {
calculatedStyle = { ...calculatedStyle, ...VARIANT_STYLES.card[name] };
calculatedCardStyle = {
...calculatedCardStyle,
...VARIANT_STYLES.cardContent[name],
};
});
}
// Apply the passed styles on top
const computedStyle = { ...calculatedStyle, ...this.props.style };
const computedCardStyle = {
...calculatedCardStyle,
...this.props.cardStyle,
};
return (
<Card
style={computedStyle}
classes={classes}
gacontext={textContent(title)}
>
<ErrorBoundary slackChannel={slackChannel}>
{title && (
<BoldHeader
title={title}
subheader={subheader}
style={{ display: 'inline-block', ...headerStyle }}
{...headerProps}
/>
)}
{actionsTopRight && (
<CardActionsTopRight>{actionsTopRight}</CardActionsTopRight>
)}
{divider && <Divider />}
<CardContent
className={this.props.cardClassName}
style={computedCardStyle}
>
{children}
</CardContent>
{actions && (
<CardActions className={this.props.actionsClassName}>
{actions}
</CardActions>
)}
{deepLink && <BottomLink {...deepLink} />}
</ErrorBoundary>
</Card>
);
}
}
export default InfoCard;
@@ -0,0 +1,24 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '../../testUtils';
import InfoCard from './InfoCard';
const minProps = {
title: 'Some title',
deepLink: {
title: 'A deepLink title',
link: '/mocked',
},
};
describe('<InfoCard />', () => {
it('renders without exploding', () => {
const rendered = render(wrapInTestApp(<InfoCard {...minProps} />));
expect(rendered.getByText('Some title')).toBeInTheDocument();
});
it('renders a deepLink when prop is set', () => {
const rendered = render(wrapInTestApp(<InfoCard {...minProps} />));
expect(rendered.getByText('A deepLink title')).toBeInTheDocument();
});
});
@@ -0,0 +1 @@
export { default } from './InfoCard';
@@ -0,0 +1,37 @@
import React, { Component } from 'react';
import { withStyles } from '@material-ui/core';
import { theme } from './PageThemeProvider';
const styles = theme => ({
root: {
display: 'grid',
gridTemplateAreas:
"'pageHeader pageHeader pageHeader' 'pageSubheader pageSubheader pageSubheader' 'pageNav pageContent pageSidebar'",
gridTemplateRows: 'auto auto 1fr',
gridTemplateColumns: 'auto 1fr auto',
minHeight: '100%',
paddingBottom: theme.spacing(3),
},
});
export const Theme = React.createContext({});
class Page extends Component {
static defaultProps = {
theme: theme.other,
};
render() {
const { theme, backgroundColor, classes, children, styles = {}, ...otherProps } = this.props;
return (
<Theme.Provider value={theme}>
<div style={{ backgroundColor, ...styles }} className={classes.root} {...otherProps}>
{children}
</div>
</Theme.Provider>
);
}
}
export default withStyles(styles)(Page);
@@ -0,0 +1,47 @@
export const gradients = {
blue: 'linear-gradient(135deg, #2D46B9 0%, #509BF5 100%)',
darkBlue: 'linear-gradient(44deg, #1E3264 0%, #A0C3D2 100%)',
brown: 'linear-gradient(44deg, #674638 0%, #C39887 100%)',
green: 'linear-gradient(-90deg, #1DB954 0%, #006350 100%)',
orangeYellow: 'linear-gradient(37deg, #FF6437 0%, #FFC864 100%)',
redOrange: 'linear-gradient(37deg, #A72525 0%, #E6542D 100%)',
pinkOrange: 'linear-gradient(43deg, #F13DA2 0%, #FF8A48 100%)',
purpleBlue: 'linear-gradient(-137deg, #4100F4 0%, #AF2996 100%)',
tealGreen: 'linear-gradient(-137deg, #19E68C 0%, #1D7F6E 100%)',
violetPeach: 'linear-gradient(44deg, #B39AC8 0%, #FCCBD3 100%)',
violetGreen: 'linear-gradient(44deg, #4302F4 0%, #C3EFC8 100%)',
purple: 'linear-gradient(-90deg, #a186bd 0%, #7c5c92 100%)',
tpm: 'linear-gradient(-137deg, #00FFF2 0%, #035355 100%)',
royalBlue: 'linear-gradient(45deg, #000044 0%, #0000DD 61.47%, #0033DD 74%, #4B80D4 100%)',
grey: 'linear-gradient(45deg, #111111 0%, #777777 100%)',
sunset: 'linear-gradient(148deg, #cf8022 0%, #4e6ec7 100%)',
sky: 'linear-gradient(135deg, #69B9FF 0%, #ACCEEC 100%)',
};
export const theme = {
service: {
activeNavLinkColor: '#1D7F6E',
gradient: gradients.tealGreen,
burstShape: null,
},
website: {
activeNavLinkColor: '#765d90',
gradient: gradients.purple,
burstShape: null,
},
home: {
activeNavLinkColor: '#00814e',
gradient: gradients.green,
burstShape: null,
},
org: {
activeNavLinkColor: '#6044ef',
gradient: gradients.violetGreen,
burstShape: null,
},
documentation: {
activeNavLinkColor: '#04c2ba',
gradient: gradients.tpm,
burstShape: null,
},
};
@@ -0,0 +1,2 @@
export { default } from './Page';
export { gradients, theme } from './PageThemeProvider';
@@ -0,0 +1,188 @@
import { act, fireEvent } from '@testing-library/react';
const codes = {
Tab: 9,
Enter: 10,
Click: 17 /* This keyboard can click, deal with it */,
Esc: 27,
};
export default class Keyboard {
static async type(target, input) {
await new Keyboard(target).type(input);
}
static async typeDebug(target, input) {
await new Keyboard(target, { debug: true }).type(input);
}
static toReadableInput(chars) {
return chars.split('').map(char => {
switch (char.charCodeAt(0)) {
case codes.Tab:
return '<Tab>';
case codes.Enter:
return '<Enter>';
case codes.Click:
return '<Click>';
case codes.Esc:
return '<Esc>';
default:
return char;
}
});
}
static fromReadableInput(input) {
return input.trim().replace(/\s*<([a-zA-Z]+)>\s*/g, (match, name) => {
if (name in codes) {
return String.fromCharCode(codes[name]);
}
throw new Error(`Unknown char name: '${name}'`);
});
}
constructor(target, { debug = false } = {}) {
this.debug = debug;
if (target.ownerDocument) {
this.document = target.ownerDocument;
} else if (target.baseElement) {
this.document = target.baseElement.ownerDocument;
} else {
throw TypeError('Keyboard(target): target must be DOM node or react-testing-library render() output');
}
}
toString() {
return `Keyboard{document=${this.document}, debug=${this.debug}}`;
}
_log(message, ...args) {
if (this.debug) {
console.log(`[Keyboard] ${message}`, ...args);
}
}
_pretty(element) {
const attrs = [...element.attributes].map(attr => `${attr.name}="${attr.value}"`).join(' ');
return `<${element.nodeName.toLowerCase()} ${attrs}>`;
}
get focused() {
return this.document.activeElement;
}
async type(input) {
this._log(`sending sequence '${input}' with initial focus ${this._pretty(this.focused)}`);
await this.send(Keyboard.fromReadableInput(input));
}
async send(chars) {
for (const key of chars.split('')) {
const charCode = key.charCodeAt(0);
if (charCode === codes.Tab) {
await this.tab();
continue;
}
const focused = this.focused;
if (!focused || focused === this.document.body) {
throw Error(`No element focused in document while trying to type '${Keyboard.toReadableInput(chars)}'`);
}
const nextValue = (focused.value || '') + key;
if (charCode >= 32) {
await this._sendKey(key, charCode, () => {
this._log(`sending +${key} = '${nextValue}' to ${this._pretty(focused)}`);
fireEvent.change(focused, {
target: { value: nextValue },
bubbles: true,
cancelable: true,
});
});
} else if (charCode === codes.Enter) {
await this.enter(focused.value || '');
} else if (charCode === codes.Esc) {
await this.escape();
} else if (charCode === codes.Click) {
await this.click();
} else {
throw new Error(`Unsupported char code, ${charCode}`);
}
}
}
async click() {
this._log(`clicking ${this._pretty(this.focused)}`);
await act(async () => fireEvent.click(this.focused));
}
async tab() {
await this._sendKey('Tab', codes.Tab, () => {
const focusable = this.document.querySelectorAll(
[
'a[href]',
'area[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'button:not([disabled])',
'iframe',
'object',
'embed',
'*[tabindex]',
'*[contenteditable]',
].join(','),
);
const tabbable = [...focusable].filter(el => {
return el.tabIndex >= 0;
});
const focused = this.document.activeElement;
const focusedIndex = tabbable.indexOf(focused);
const nextFocus = tabbable[focusedIndex + (1 % tabbable.length)];
this._log(`tabbing to ${this._pretty(nextFocus)} ${this.focused.textContent}`);
nextFocus.focus();
});
}
async enter(value) {
this._log(`submitting '${value}' via ${this._pretty(this.focused)}`);
await act(() =>
this._sendKey('Enter', codes.Enter, () => {
if (this.focused.type === 'button') {
fireEvent.click(this.focused, { target: { value } });
} else {
fireEvent.submit(this.focused, {
target: { value },
bubbles: true,
cancelable: true,
});
}
}),
);
}
async escape() {
this._log(`escape from ${this._pretty(this.focused)}`);
await act(async () => this._sendKey('Escape', codes.Esc));
}
async _sendKey(key, charCode, action) {
const event = { key, charCode, keyCode: charCode, which: charCode };
const focused = this.focused;
if (fireEvent.keyDown(focused, event)) {
if (fireEvent.keyPress(focused, event)) {
if (action) {
action();
}
}
}
fireEvent.keyUp(focused, event);
}
}
@@ -0,0 +1,79 @@
import React from 'react';
import Keyboard from './Keyboard';
import { render } from '@testing-library/react';
describe('testUtils.Keyboard', () => {
it('types into some inputs with focus and submits a form', async () => {
const typed1 = [];
const typed2 = [];
const typed3 = [];
let submitted = false;
const handleSubmit = event => {
event.preventDefault();
submitted = true;
};
const rendered = render(
<form onSubmit={handleSubmit}>
<input onChange={({ target: { value } }) => typed1.push(value)} />
<input onChange={({ target: { value } }) => typed2.push(value)} autoFocus />
<input onChange={({ target: { value } }) => typed3.push(value)} />
</form>,
);
const keyboard = new Keyboard(rendered);
await keyboard.send('xy');
await keyboard.tab();
await keyboard.send('abc');
await keyboard.enter();
expect(typed1).toEqual([]);
expect(typed2).toEqual(['x', 'xy']);
expect(typed3).toEqual(['a', 'ab', 'abc']);
expect(submitted).toBe(true);
});
it('can use Keyboard.type to send readable input', async () => {
const typed1 = [];
const typed2 = [];
const typed3 = [];
let submitted = false;
const handleSubmit = event => {
event.preventDefault();
submitted = true;
};
const rendered = render(
<form onSubmit={handleSubmit}>
<input defaultValue="1" onChange={({ target: { value } }) => typed1.push(value)} />
<input defaultValue="2" onChange={({ target: { value } }) => typed2.push(value)} />
<input defaultValue="3" onChange={({ target: { value } }) => typed3.push(value)} />
</form>,
);
await Keyboard.type(rendered, '<Tab> a <Tab> b <Tab> c <Enter>');
expect(typed1).toEqual(['1a']);
expect(typed2).toEqual(['2b']);
expect(typed3).toEqual(['3c']);
expect(submitted).toBe(true);
});
it('should be able to navigate a radio input with click', async () => {
const selections = [];
const rendered = render(
<div onChange={({ target: { value } }) => selections.push(value)}>
<input type="radio" name="group" value="a" />
<input type="radio" name="group" value="b" />
<input type="radio" name="group" value="c" />
</div>,
);
await Keyboard.type(rendered, '<Tab> <Click> <Tab> <Tab> <Click>');
expect(selections).toEqual(['a', 'c']);
});
});
@@ -0,0 +1,45 @@
/**
* Helpers for testing components
*/
import React from 'react';
import { MuiThemeProvider } from '@material-ui/core';
import { ThemeProvider } from '@material-ui/styles';
import { MemoryRouter } from 'react-router';
import { Route } from 'react-router-dom';
import { V1 } from '../theme/BackstageTheme';
import ErrorBoundary from '../layout/ErrorBoundary';
export { default as Keyboard } from './Keyboard';
export { default as mockBreakpoint } from './mockBreakpoint';
export function wrapInTestApp(
Component,
initialRouterEntries,
) {
const Wrapper = Component instanceof Function ? Component : () => Component;
return (
<MemoryRouter initialEntries={initialRouterEntries || ['/']}>
<ErrorBoundary>
<Route component={Wrapper} />
</ErrorBoundary>
</MemoryRouter>
);
}
export function wrapInThemedTestApp(component, initialRouterEntries) {
const themed = (
<MuiThemeProvider theme={V1}>
<ThemeProvider theme={V1}>{component}</ThemeProvider>
</MuiThemeProvider>
);
return wrapInTestApp(themed, initialRouterEntries);
}
export const wrapInTheme = (component, theme = V1) => (
<MuiThemeProvider theme={theme}>
<ThemeProvider theme={theme}>{component}</ThemeProvider>
</MuiThemeProvider>
);
@@ -0,0 +1,73 @@
import { act } from '@testing-library/react';
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const queryToBreakpoint = {
'(min-width:1920px)': 'xl',
'(min-width:1280px)': 'lg',
'(min-width:960px)': 'md',
'(min-width:600px)': 'sm',
'(min-width:0px)': 'xs',
} as Record<string, Breakpoint>;
function toBreakpoint(query: string) {
const breakpoint = queryToBreakpoint[query];
if (!breakpoint) {
throw new Error(`received unknown media query in breakpoint mock: '${query}'`);
}
return breakpoint;
}
type Listener = (event: { matches: boolean }) => void;
interface QueryList {
addListener(listener: Listener): void;
removeListener(listener: Listener): void;
matches: boolean;
}
interface Query {
query: string;
queryList: QueryList;
listeners: Set<Listener>;
}
export default function mockBreakpoint(initialBreakpoint: Breakpoint = 'xl') {
let currentBreakpoint = initialBreakpoint;
const queries = Array<Query>();
(window as any).matchMedia = (query: string): QueryList => {
const listeners = new Set<Listener>();
const queryList: QueryList = {
addListener(listener) {
listeners.add(listener);
},
removeListener(listener) {
listeners.delete(listener);
},
matches: toBreakpoint(query) === currentBreakpoint,
};
queries.push({ query, queryList, listeners });
return queryList;
};
return {
set(breakpoint: Breakpoint) {
currentBreakpoint = breakpoint;
act(() => {
queries.forEach(({ query, queryList, listeners }) => {
const matches = toBreakpoint(query) === breakpoint;
queryList.matches = matches;
listeners.forEach(listener => listener({ matches }));
});
});
},
remove() {
delete window.matchMedia;
},
};
}
@@ -0,0 +1,358 @@
import { createMuiTheme, withStyles } from '@material-ui/core';
import { darken, lighten } from '@material-ui/core/styles/colorManipulator';
import { blue, yellow } from '@material-ui/core/colors';
import tinycolor from 'tinycolor2';
export const COLORS = {
PAGE_BACKGROUND: '#F8F8F8',
DEFAULT_PAGE_THEME_COLOR: '#7C3699',
DEFAULT_PAGE_THEME_LIGHT_COLOR: tinycolor('#7C3699')
.lighten(50)
.toString(),
ERROR_BACKGROUND_COLOR: '#FFEBEE',
ERROR_TEXT_COLOR: '#CA001B',
INFO_TEXT_COLOR: '#004e8a',
LINK_TEXT: '#0A6EBE',
LINK_TEXT_HOVER: '#2196F3',
COMPONENT_TYPES: {
SERVICE: '#39732E',
ENDPOINT: '#B71C1C',
PROJECT: '#E657AA',
WORKFLOW: '#c15013',
},
NAMED: {
WHITE: '#FEFEFE',
},
STATUS: {
OK: '#1db855',
WARNING: '#f49b20',
ERROR: '#CA001B',
},
};
const extendedThemeConfig = {
props: {
MuiGrid: {
spacing: 2,
},
MuiSwitch: {
color: 'primary',
},
},
palette: {
background: {
default: COLORS.PAGE_BACKGROUND,
informational: '#60a3cb',
},
status: {
ok: COLORS.STATUS.OK,
warning: COLORS.STATUS.WARNING,
error: COLORS.STATUS.ERROR,
running: '#BEBEBE',
pending: '#5BC0DE',
background: COLORS.NAMED.WHITE,
},
bursts: {
fontColor: COLORS.NAMED.WHITE,
slackChannelText: '#ddd',
backgroundColor: {
service: COLORS.COMPONENT_TYPES.SERVICE,
endpoint: COLORS.COMPONENT_TYPES.ENDPOINT,
project: COLORS.COMPONENT_TYPES.PROJECT,
default: COLORS.DEFAULT_PAGE_THEME_COLOR,
},
},
primary: {
main: blue[500],
},
border: '#E6E6E6',
textVerySubtle: '#DDD',
textSubtle: '#6E6E6E',
highlight: '#FFFBCC',
errorBackground: COLORS.ERROR_BACKGROUND_COLOR,
warningBackground: '#F59B23',
infoBackground: '#ebf5ff',
errorText: COLORS.ERROR_TEXT_COLOR,
infoText: COLORS.INFO_TEXT_COLOR,
warningText: COLORS.NAMED.WHITE,
linkHover: COLORS.LINK_TEXT_HOVER,
link: COLORS.LINK_TEXT,
gold: yellow.A700,
},
navigation: {
width: 220,
background: '#333333',
},
typography: {
h4: {
// Page name/heading | Dialog titles
fontSize: 48,
color: '#2D2D2D',
fontWeight: 'bold',
},
h3: {
// Page titles
fontSize: 32,
color: '#2D2D2D',
fontWeight: 'bold',
},
h2: {
// Card titles | Sub headings in dialogs
fontSize: 24,
color: '#2D2D2D',
fontWeight: 'bold',
lineHeight: 1.2,
marginBottom: 6,
},
header3: {
// Table headers (ALL CAPS!)
fontSize: 12,
color: '#9E9E9E',
},
menuItem1: {
// Sidebar menu item
fontSize: 16,
color: '#828282',
fontWeight: 'bold',
},
menuItem2: {
// Dropdown menu items | Form labels | Deep links from cards (Go to...)
fontSize: 16,
color: '#2D2D2D',
},
text: {
// Table entries | Information/error text/messages | General copy/paragraphs
fontSize: 13,
color: '#2D2D2D',
},
links: {
// Table entries | Information/error text/messages | General copy/paragraphs
color: '#509BF5',
},
tabSelected: {
// Selected tab
fontSize: 18,
color: '#2D2D2D',
},
tabUnselected: {
// Unselected tab
fontSize: 18,
color: '#9E9E9E',
},
code: {
fontFamily: 'monospace',
whiteSpace: 'pre',
fontSize: '16px',
},
caption: {
// Restores caption to display: block to match MUI3 behavior; this global override should
// be removed if possible.
display: 'block',
},
},
};
const createOverrides = theme => {
const overrides = {
MuiAppBar: {
root: {
zIndex: 1049,
height: 44,
},
colorPrimary: {
backgroundColor: '#212121',
},
},
MuiToolbar: {
root: {
'@media (min-width:600px)': {
minHeight: 44,
},
},
},
MuiTableRow: {
root: {
height: 'auto',
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.background.default,
},
},
hover: {
'&:hover': {
cursor: 'pointer',
},
},
head: {
height: 'auto',
'&:nth-of-type(odd)': {
backgroundColor: COLORS.NAMED.WHITE,
},
},
},
MuiTableCell: {
root: {
wordBreak: 'break-word',
overflow: 'hidden',
verticalAlign: 'middle',
lineHeight: '1',
margin: 0,
padding: '8px',
borderBottom: 0,
},
head: {
wordBreak: 'break-word',
overflow: 'hidden',
color: 'rgb(179, 179, 179)',
fontWeight: 'normal',
lineHeight: '1',
},
},
MuiTabs: {
root: {
minHeight: 24,
},
},
MuiTab: {
root: {
color: theme.palette.link,
minHeight: 24,
textTransform: 'initial',
'&:hover': {
color: darken(theme.palette.link, 0.3),
background: lighten(theme.palette.link, 0.95),
},
[theme.breakpoints.up('md')]: {
minWidth: 120,
fontSize: theme.typography.pxToRem(14),
fontWeight: 500,
},
},
textColorPrimary: {
color: theme.palette.link,
},
},
MuiTableSortLabel: {
root: {
color: 'inherit',
'&:hover': {
color: 'inherit',
},
'&:focus': {
color: 'inherit',
},
},
active: {
fontWeight: 'bold',
color: 'inherit',
},
},
MuiListItemText: {
dense: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
},
MuiButton: {
text: {
padding: '6px 16px',
},
},
MuiChip: {
root: {
marginRight: theme.spacing(1),
marginBottom: theme.spacing(1),
},
},
MuiDivider: {
light: {
backgroundColor: '#727272',
},
},
MuiDialogTitle: {
root: {
minWidth: 600,
},
},
MuiCardHeader: {
root: {
// Remove bottom padding on titles; there is always a CardContent below that also has padding,
// so without this fix there will be too much space below the title.
paddingBottom: '0',
// Mui 1.2.1 introduced more padding left and right with media queries.
// Question is if we should override them or just go with the defaults
'@media (min-width:600px)': {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
},
},
MuiCardContent: {
root: {
'&:last-child': {
// There is some odd extra whitespace at the bottom of card content, which makes it look
// bottom-heavy and uneven; set it to the same as the other sides
paddingBottom: theme.spacing(2),
},
// Mui 1.2.1 introduced more padding left and right with media queries.
// Question is if we should override them or just go with the defaults
'@media (min-width:600px)': {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
},
},
MuiCardActions: {
root: {
justifyContent: 'flex-end',
},
},
MuiListSubheader: {
sticky: {
// Sticky subheaders need to be opaque so that they overwrite list items as you scroll down.
backgroundColor: theme.palette.background.paper,
},
},
Toolbar: {
toolbar: {
// Override the toolbar in dx-react-grid table
marginTop: '-64px',
},
},
};
return {
overrides,
};
};
const extendedTheme = createMuiTheme(extendedThemeConfig);
// V1 theming
// https://material-ui-next.com/customization/themes/
// For CSS it is advised to use JSS, see https://material-ui-next.com/customization/css-in-js/
const BackstageTheme = { ...extendedTheme, ...createOverrides(extendedTheme) };
// Temporary workaround for files incorrectly importing the theme directly
export const V1 = BackstageTheme;
// Default styles for the application other than the built in mui components
const styles = theme => ({
'@global': {
html: {
height: '100%',
fontFamily: theme.typography.fontFamily,
},
body: {
height: '100%',
fontFamily: theme.typography.fontFamily,
},
a: {
color: 'inherit',
textDecoration: 'none',
},
},
});
export const withGlobalStyles = withStyles(styles);
export default BackstageTheme;
@@ -1,8 +1,8 @@
import React, { FC, useState, useEffect } from 'react';
import React, { FC, useState, useEffect, Fragment } from 'react';
import Button from '@material-ui/core/Button';
import { HelloPromiseClient } from '../../proto/hello_grpc_web_pb';
import { HelloRequest } from '../../proto/hello_pb';
import { Paper, Typography } from '@material-ui/core';
import { Typography } from '@material-ui/core';
const MyComponent: FC<{}> = () => {
const [message, setMessage] = useState<string>('');
@@ -25,7 +25,7 @@ const MyComponent: FC<{}> = () => {
}, []);
return (
<Paper square>
<Fragment>
<Typography variant="body1">{message}</Typography>
<Typography variant="body1" color="error">
{error}
@@ -37,7 +37,7 @@ const MyComponent: FC<{}> = () => {
>
Hello!
</Button>
</Paper>
</Fragment>
);
};
+220 -11
View File
@@ -876,7 +876,7 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.2"
"@babel/runtime@7.8.4", "@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
"@babel/runtime@7.8.4", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
version "7.8.4"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308"
integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==
@@ -2850,6 +2850,11 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/history@*":
version "4.7.5"
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.1"
resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -2932,6 +2937,23 @@
dependencies:
"@types/react" "*"
"@types/react-router-dom@^5.1.3":
version "5.1.3"
resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196"
integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*":
version "5.1.4"
resolved "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.4.tgz#7d70bd905543cb6bcbdcc6bd98902332054f31a6"
integrity sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-transition-group@^4.2.0":
version "4.2.3"
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.3.tgz#4924133f7268694058e415bf7aea2d4c21131470"
@@ -3574,7 +3596,7 @@ arrify@^1.0.1:
resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
asap@^2.0.0, asap@~2.0.6:
asap@^2.0.0, asap@~2.0.3, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
@@ -4430,6 +4452,11 @@ chalk@^1.0.0, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
change-emitter@^0.1.2:
version "0.1.6"
resolved "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
integrity sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@@ -5101,6 +5128,11 @@ core-js-pure@^3.0.0:
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
core-js@^2.4.0, core-js@^2.5.0:
version "2.6.11"
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
@@ -6754,6 +6786,19 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fbjs@^0.8.1:
version "0.8.17"
resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
dependencies:
core-js "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
version "3.5.1"
resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@@ -7504,6 +7549,11 @@ grpc-web@^1.0.7:
resolved "https://registry.npmjs.org/grpc-web/-/grpc-web-1.0.7.tgz#9e4fbcf63d3734515332ab59e42baa7d0d290015"
integrity sha512-Fkbz1nyvvt6GC6ODcxh9Fen6LLB3OTCgGHzHwM2Eni44SUhzqPz1UQgFp9sfBEfInOhx3yBdwo9ZLjZAmJ+TtA==
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
gzip-size@5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
@@ -7637,6 +7687,18 @@ hex-color-regex@^1.1.0:
resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
history@^4.9.0:
version "4.10.1"
resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -7646,7 +7708,12 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^2.3.1:
version "2.5.5"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -8533,7 +8600,7 @@ is-ssh@^1.3.0:
dependencies:
protocols "^1.1.0"
is-stream@^1.0.0, is-stream@^1.1.0:
is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
@@ -8626,6 +8693,14 @@ isobject@^4.0.0:
resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
dependencies:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -10352,7 +10427,7 @@ longest@^2.0.1:
resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8"
integrity sha1-eB4YMpaqlPbU2RbcM10NF676I/g=
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -10712,6 +10787,15 @@ min-indent@^1.0.0:
resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256"
integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=
mini-create-react-context@^0.3.0:
version "0.3.2"
resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==
dependencies:
"@babel/runtime" "^7.4.0"
gud "^1.0.0"
tiny-warning "^1.0.2"
mini-css-extract-plugin@0.8.0:
version "0.8.0"
resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1"
@@ -11005,6 +11089,14 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1"
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
node-fetch@^2.3.0, node-fetch@^2.5.0:
version "2.6.0"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
@@ -12053,6 +12145,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -12960,6 +13059,13 @@ promise-retry@^1.1.1:
err-code "^1.0.0"
retry "^0.10.0"
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
dependencies:
asap "~2.0.3"
promise@^8.0.3:
version "8.0.3"
resolved "https://registry.npmjs.org/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6"
@@ -12982,7 +13088,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -13192,6 +13298,11 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-addons-text-content@0.0.4:
version "0.0.4"
resolved "https://registry.npmjs.org/react-addons-text-content/-/react-addons-text-content-0.0.4.tgz#d2e259fdc951d1d8906c08902002108dce8792e5"
integrity sha1-0uJZ/clR0diQbAiQIAIQjc6HkuU=
react-app-polyfill@^1.0.5:
version "1.0.6"
resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0"
@@ -13249,11 +13360,67 @@ react-error-overlay@^6.0.5:
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.5.tgz#55d59c2a3810e8b41922e0b4e5f85dcf239bd533"
integrity sha512-+DMR2k5c6BqMDSMF8hLH0vYKtKTeikiFW+fj0LClN+XZg4N9b8QUAdHC62CGWNLTi/gnuuemNcNcTFrCvK1f+A==
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4:
react-fast-compare@^2.0.2:
version "2.0.4"
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-helmet@5.2.1:
version "5.2.1"
resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz#16a7192fdd09951f8e0fe22ffccbf9bb3e591ffa"
integrity sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==
dependencies:
object-assign "^4.1.1"
prop-types "^15.5.4"
react-fast-compare "^2.0.2"
react-side-effect "^1.1.0"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4:
version "16.12.0"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
react-lifecycles-compat@^3.0.2:
version "3.0.4"
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-router-dom@^5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.1.2"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418"
integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.3.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-side-effect@^1.1.0:
version "1.2.0"
resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz#0e940c78faba0c73b9b0eba9cd3dda8dfb7e7dae"
integrity sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==
dependencies:
shallowequal "^1.0.1"
react-transition-group@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683"
@@ -13472,6 +13639,18 @@ rechoir@^0.6.2:
dependencies:
resolve "^1.1.6"
recompose@0.30.0:
version "0.30.0"
resolved "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0"
integrity sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==
dependencies:
"@babel/runtime" "^7.0.0"
change-emitter "^0.1.2"
fbjs "^0.8.1"
hoist-non-react-statics "^2.3.1"
react-lifecycles-compat "^3.0.2"
symbol-observable "^1.0.4"
recursive-readdir@2.2.2:
version "2.2.2"
resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@@ -13765,6 +13944,11 @@ resolve-global@1.0.0, resolve-global@^1.0.0:
dependencies:
global-dirs "^0.1.1"
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url-loader@3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz#28931895fa1eab9be0647d3b2958c100ae3c0bf0"
@@ -14171,7 +14355,7 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
setimmediate@^1.0.4:
setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -14218,6 +14402,11 @@ shallow-clone@^3.0.0:
dependencies:
kind-of "^6.0.2"
shallowequal@^1.0.1:
version "1.1.0"
resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -14972,7 +15161,7 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
symbol-observable@^1.1.0:
symbol-observable@^1.0.4, symbol-observable@^1.1.0:
version "1.2.0"
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -15184,16 +15373,26 @@ timsort@^0.3.0:
resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-relative-date@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07"
integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==
tiny-warning@^1.0.2:
tiny-warning@^1.0.0, tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -15417,6 +15616,11 @@ typescript@^3.7.4, typescript@^3.7.5:
resolved "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
ua-parser-js@^0.7.18:
version "0.7.21"
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
uglify-js@3.4.x:
version "3.4.10"
resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
@@ -15715,6 +15919,11 @@ validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0:
dependencies:
builtins "^1.0.3"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -15921,7 +16130,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
dependencies:
iconv-lite "0.4.24"
whatwg-fetch@^3.0.0:
whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==