bring in Page, Header, InfoCard, ErrorBoundary, BackstageTheme, Navigation, SupportButtons, HeaderAction, HeaderLabel components from backstage frontend
@@ -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';
|
||||
@@ -13,7 +13,10 @@
|
||||
"@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"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "web-scripts lint",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
.burst {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* base style of burst shape SVGs */
|
||||
.burst-shape {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
opacity:0.1;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="484" height="379" xmlns="http://www.w3.org/2000/svg"><path d="M30.247 297.817C14.615 298.6 1.242 286.77.377 271.393c-.857-15.376 11.116-28.478 26.749-29.264 15.631-.78 29.001 11.046 29.866 26.423.86 15.38-11.113 28.479-26.745 29.265zm453.662-114.73c2.595 47.204-33.961 87.45-81.645 89.899-38.163 1.956-71.863-20.898-85.145-54.293-15.635 8.935-33.571 14.474-52.86 15.463-3.194.164-6.36.166-9.503.082 6.91 12.422 11.177 26.532 12.005 41.629 2.947 53.63-38.582 99.355-92.758 102.134-54.179 2.78-100.485-38.442-103.432-92.069-2.899-52.794 37.31-97.9 90.25-101.942-12.222-17.322-19.891-38.098-21.133-60.721C136.138 58.682 186.157 3.606 251.404.26c63.068-3.234 117.267 43.036 124.031 104.452a87.321 87.321 0 0 1 17.436-2.664c47.687-2.447 88.446 33.835 91.038 81.038z" fill-rule="evenodd" /></svg>
|
||||
|
After Width: | Height: | Size: 803 B |
@@ -0,0 +1 @@
|
||||
<svg width="373" height="418" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M50.77 275.72c9.87-12.14 8.15-29.8-3.86-39.46-12-9.64-29.74-7.62-39.62 4.52-9.87 12.14-8.15 29.8 3.85 39.46 12 9.64 29.75 7.62 39.63-4.52"/><path d="M341.06 19.81c-36.8-29.66-91.04-23.64-121.13 13.43a87.32 87.32 0 0 0-9.7 14.74C160.08 11.91 89.43 21.23 49.64 70.26 8.44 121 15.9 195.02 66.27 235.6a116.01 116.01 0 0 0 59.27 24.93c-32.42 42.05-25.8 102.1 15.37 135.28 41.82 33.7 103.44 26.86 137.63-15.26 34.18-42.12 28-103.58-13.84-137.27a96.26 96.26 0 0 0-38.97-18.94 118.45 118.45 0 0 0 30.18-56.75 86.8 86.8 0 0 0 97.32-26.94c30.09-37.07 24.64-91.17-12.18-120.83"/></g></svg>
|
||||
|
After Width: | Height: | Size: 676 B |
@@ -0,0 +1 @@
|
||||
<svg width="491" height="458" xmlns="http://www.w3.org/2000/svg"><path d="M0 246.48l266.02 100.18-148.35-40.13-10.57 39.65L490.6 457.9V138.2L367 0h-.01L298.5 61.24l48.13 53.82-118.57-72.34-93.33 153.03 173.63 105.9L20.1 193.1z" fill-rule="evenodd" /></svg>
|
||||
|
After Width: | Height: | Size: 257 B |
@@ -0,0 +1 @@
|
||||
<svg width="702" height="620" xmlns="http://www.w3.org/2000/svg"><path d="M482.48.34L250.4 291.06l-120.27-83.27L.45 391.43l326.48 228.6L702.05 312.1l.01-.01L586.8 171.68l-51.08 41.94-61.25-74.63-130.12 106.82L517.8 28.54z" fill-rule="evenodd" /></svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1 @@
|
||||
<svg width="702" height="620" xmlns="http://www.w3.org/2000/svg"><path d="M482.48.34L250.4 291.06l-120.27-83.27L.45 391.43l326.48 228.6L702.05 312.1l.01-.01L586.8 171.68l-51.08 41.94-61.25-74.63-130.12 106.82L517.8 28.54z" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 251 B |
@@ -0,0 +1 @@
|
||||
<svg width="1050" height="510" xmlns="http://www.w3.org/2000/svg"><path d="M646.72 234.41l67.93-93.6-169.42 83.96L740.73 9.12.36 439.99l851 70L997.7 244.7l-178.68 157.9L1049.97-.02z" fill-rule="evenodd" /></svg>
|
||||
|
After Width: | Height: | Size: 212 B |
@@ -0,0 +1 @@
|
||||
<svg width="655" height="442" xmlns="http://www.w3.org/2000/svg"><path d="M348.86 202.96L290.11 122l146.53 72.62L267.55 8.1l386.7 228.44-482.4 204.77-158.4-259.26 83.43 29.82L.08.2z" fill-rule="evenodd" /></svg>
|
||||
|
After Width: | Height: | Size: 212 B |
@@ -0,0 +1 @@
|
||||
<svg width="502" height="330" xmlns="http://www.w3.org/2000/svg"><path d="M501.57.14L341.2 329.8H.74L104.6 133.17l4.82 118.26 57.13-54-8 53.86z" fill-rule="evenodd" /></svg>
|
||||
|
After Width: | Height: | Size: 174 B |
@@ -1,2 +1,12 @@
|
||||
export * from './types';
|
||||
export { default as createPlugin } from './createPlugin';
|
||||
export { default as Page } from '../src/layout/Page';
|
||||
export { shapes, 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,16 @@
|
||||
import React, { Component } from 'react';
|
||||
import '../../assets/bursts/bursts.css';
|
||||
|
||||
class Burst extends Component {
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<div className="burst" style={{ backgroundImage: theme.gradient }}>
|
||||
<div className="burst-shape" 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 'shared/components/layout';
|
||||
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 { KebabMenuIcon } from 'shared/icons';
|
||||
|
||||
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,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 to={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 'shared/components/layout';
|
||||
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,65 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tooltip, withStyles } from '@material-ui/core';
|
||||
|
||||
import { StatusError } from 'shared/components/Status';
|
||||
import Link from 'shared/components/Link';
|
||||
import HeaderLabel from 'shared/components/layout/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 to="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 } from '@testing-library/react';
|
||||
import { OwnerHeaderLabel } from 'shared/components/layout';
|
||||
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 to={link} onClick={onClick} highlight="none">
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ArrowIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{title}</ListItemText>
|
||||
</ListItem>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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,168 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { matchPath } from 'react-router';
|
||||
import { ListItem, ListItemIcon, ListItemText, Typography, withStyles, Tooltip, IconButton } from '@material-ui/core';
|
||||
import { AlphaLabel, BetaLabel } from 'shared/components/Lifecycle';
|
||||
import Link from 'shared/components/Link';
|
||||
import { Theme } from 'shared/components/layout/Page';
|
||||
import ArrowDropUp from '@material-ui/icons/ArrowDropUp';
|
||||
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
display: 'block',
|
||||
overflow: 'hidden',
|
||||
'&:first-child': {
|
||||
borderTopRightRadius: 6,
|
||||
borderTopLeftRadius: 6,
|
||||
},
|
||||
'&:last-child': {
|
||||
borderBottomRightRadius: 6,
|
||||
borderBottomLeftRadius: 6,
|
||||
},
|
||||
},
|
||||
child: {
|
||||
paddingLeft: theme.spacing(7),
|
||||
'&:last-child': {
|
||||
borderBottom: '1px solid #727272',
|
||||
},
|
||||
},
|
||||
boldLabel: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bolder',
|
||||
},
|
||||
childLabel: {
|
||||
color: theme.palette.textVerySubtle,
|
||||
fontSize: 14,
|
||||
},
|
||||
label: {
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.0,
|
||||
},
|
||||
iconImg: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
icon: {
|
||||
margin: theme.spacing(0.5, 2, 0.5, 0),
|
||||
minWidth: 0,
|
||||
},
|
||||
expand: {
|
||||
color: 'white',
|
||||
},
|
||||
});
|
||||
|
||||
class NavItem extends Component {
|
||||
static defaultProps = {
|
||||
title: 'Mystery Link',
|
||||
href: '/',
|
||||
exact: true, // Only used for react-router matchPath to see if this sidebar item is active
|
||||
icon: null,
|
||||
isAlpha: false,
|
||||
isBeta: false,
|
||||
type: 'other', // Provided by Navigation
|
||||
child: false, // Whether this is a child of another NavItem (for indenting)
|
||||
};
|
||||
|
||||
// If NavItem has React children, the sub-navigation can be expanded or collapsed
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
static isActive(href, exact) {
|
||||
return matchPath(window.location.pathname, { path: href, exact: exact });
|
||||
}
|
||||
|
||||
maybeActiveNavStyle(theme) {
|
||||
return NavItem.isActive(this.props.href, this.props.exact)
|
||||
? {
|
||||
background: `linear-gradient(to right, ${theme.activeNavLinkColor} 0px, ${theme.activeNavLinkColor} 6px, #4F4F4F 6px, #4F4F4F 100%)`,
|
||||
}
|
||||
: {};
|
||||
}
|
||||
|
||||
renderIcon = (condensed, iconElement, title, classes) => {
|
||||
if (!iconElement) return null;
|
||||
const listItemIcon = <ListItemIcon className={classes.icon}>{iconElement}</ListItemIcon>;
|
||||
return condensed ? (
|
||||
<Tooltip title={title} placement="right">
|
||||
{listItemIcon}
|
||||
</Tooltip>
|
||||
) : (
|
||||
listItemIcon
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, href, icon, isAlpha, isBeta, classes, condensed, child, children } = this.props;
|
||||
const { expanded } = this.state;
|
||||
const isSection = !!children;
|
||||
|
||||
// Automatically expand nested nav if one of the child elements is active
|
||||
const isChildActive = React.Children.toArray(children).some(c => NavItem.isActive(c.props.href, c.props.exact));
|
||||
const isExpanded = expanded || isChildActive;
|
||||
const selfAwareChildren = children && React.cloneElement(children, { child: true });
|
||||
|
||||
let iconElement;
|
||||
|
||||
if (icon) {
|
||||
iconElement = typeof icon === 'object' ? icon : <img src={icon} className={classes.iconImg} alt={title} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Theme.Consumer>
|
||||
{theme => (
|
||||
<>
|
||||
<Link className={classes.root} style={this.maybeActiveNavStyle(theme)} to={href} gaprops={{ label: title }}>
|
||||
<ListItem button className={`${classes.listItemGutters} ${child && classes.child}`}>
|
||||
{this.renderIcon(condensed, iconElement, title, classes)}
|
||||
{!condensed && (
|
||||
<>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
className={`${child ? classes.childLabel : classes.boldLabel} ${classes.label}`}
|
||||
>
|
||||
{title}
|
||||
{isAlpha && (
|
||||
<Fragment>
|
||||
|
||||
<AlphaLabel isShorthand />
|
||||
</Fragment>
|
||||
)}
|
||||
{isBeta && (
|
||||
<Fragment>
|
||||
|
||||
<BetaLabel isShorthand />
|
||||
</Fragment>
|
||||
)}
|
||||
</Typography>
|
||||
}
|
||||
disableTypography
|
||||
/>
|
||||
{isSection && (
|
||||
<IconButton
|
||||
onClick={() => this.setState(prev => ({ expanded: !prev.expanded }))}
|
||||
style={{ padding: 4 }}
|
||||
data-testid="expand-toggle"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ArrowDropUp className={classes.expand} />
|
||||
) : (
|
||||
<ArrowDropDown className={classes.expand} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListItem>
|
||||
</Link>
|
||||
{isExpanded && selfAwareChildren}
|
||||
</>
|
||||
)}
|
||||
</Theme.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(NavItem);
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { matchPath } from 'react-router';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import NavItem from 'shared/components/layout/NavItem';
|
||||
import { wrapInThemedTestApp } from 'testUtils';
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
matchPath: jest.fn(),
|
||||
}));
|
||||
|
||||
const minProps = {
|
||||
title: 'A link title',
|
||||
href: '/mocked',
|
||||
};
|
||||
|
||||
const minChildProps = {
|
||||
title: 'A child title',
|
||||
href: '/child',
|
||||
};
|
||||
|
||||
describe('<NavLink />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<NavItem {...minProps} />));
|
||||
expect(rendered.getByText('A link title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have an anchor with correct href', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<NavItem {...minProps} />));
|
||||
const anchor = rendered.container.querySelector('a');
|
||||
expect(anchor.href).toBe('http://localhost/mocked');
|
||||
});
|
||||
|
||||
it('renders with icon src', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<NavItem icon="mocked-icon.png" {...minProps} />));
|
||||
const icon = rendered.getByAltText('A link title');
|
||||
expect(icon.src).toBe('http://localhost/mocked-icon.png');
|
||||
});
|
||||
|
||||
it('renders with icon component', () => {
|
||||
const Falafel = () => <div>falafel</div>;
|
||||
const rendered = render(wrapInThemedTestApp(<NavItem icon={<Falafel />} {...minProps} />));
|
||||
const icon = rendered.getByText('falafel');
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows alpha icon', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<NavItem isAlpha {...minProps} />));
|
||||
expect(rendered.getByText('α')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows beta icon', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<NavItem isBeta {...minProps} />));
|
||||
expect(rendered.getByText('β')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nested navigation without exploding', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
wrapInThemedTestApp(
|
||||
<NavItem {...minProps}>
|
||||
<NavItem {...minChildProps} />
|
||||
</NavItem>,
|
||||
),
|
||||
);
|
||||
expect(getByText('A link title')).toBeInTheDocument();
|
||||
expect(getByTestId('expand-toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles nested navigation', () => {
|
||||
const { getByText, getByTestId, queryByText } = render(
|
||||
wrapInThemedTestApp(
|
||||
<NavItem {...minProps}>
|
||||
<NavItem {...minChildProps} />
|
||||
</NavItem>,
|
||||
),
|
||||
);
|
||||
expect(queryByText('A child link')).not.toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('expand-toggle'));
|
||||
expect(getByText('A child title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows nested navigation automatically if the child is active', () => {
|
||||
matchPath.mockReturnValue(true);
|
||||
const { getByText } = render(
|
||||
wrapInThemedTestApp(
|
||||
<NavItem {...minProps}>
|
||||
<NavItem {...minChildProps} />
|
||||
</NavItem>,
|
||||
),
|
||||
);
|
||||
expect(getByText('A link title')).toBeInTheDocument();
|
||||
expect(getByText('A child title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { List, withStyles } from '@material-ui/core';
|
||||
import ChevronLeft from '@material-ui/icons/ChevronLeft';
|
||||
import ChevronRight from '@material-ui/icons/ChevronRight';
|
||||
|
||||
import { IconButton } from 'shared/components';
|
||||
import { getFromStore, saveToStore } from 'shared/apis/storage/persistentStore';
|
||||
|
||||
const styles = theme => ({
|
||||
nav: {
|
||||
gridArea: 'pageNav',
|
||||
width: 62,
|
||||
marginLeft: theme.spacing(3),
|
||||
marginTop: theme.spacing(3),
|
||||
transition: 'width 0.07s, height 0s',
|
||||
transitionTimingFunction: 'ease-in',
|
||||
},
|
||||
item: {
|
||||
display: 'block',
|
||||
},
|
||||
list: {
|
||||
padding: 0,
|
||||
border: '0px solid #333333',
|
||||
borderRadius: 6,
|
||||
backgroundColor: '#333333',
|
||||
},
|
||||
buttonContainer: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: theme.spacing(0.5),
|
||||
width: 48,
|
||||
},
|
||||
button: {
|
||||
color: '#888',
|
||||
},
|
||||
});
|
||||
|
||||
class Navigation extends Component {
|
||||
static propTypes = {
|
||||
condensable: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
condensed: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const condensedPref = getFromStore('navigation', 'condensed', false);
|
||||
this.state.condensed = this.props.condensable && condensedPref;
|
||||
}
|
||||
|
||||
buttonClicked = () => {
|
||||
const condensed = !this.state.condensed;
|
||||
saveToStore('navigation', 'condensed', condensed);
|
||||
this.setState({ condensed });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, condensable } = this.props;
|
||||
const { condensed } = this.state;
|
||||
|
||||
const ExpandButton = condensable && (
|
||||
<div className={classes.buttonContainer}>
|
||||
<IconButton className={classes.button} onClick={this.buttonClicked} data-testid="nav-expand-toggle">
|
||||
{condensed ? <ChevronRight alt="nav-expand" /> : <ChevronLeft alt="nav-reduce" />}
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
const children = React.Children.map(this.props.children, child => {
|
||||
if (!child) return null;
|
||||
return React.cloneElement(child, {
|
||||
condensed: condensed,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<nav className={classes.nav} style={condensed ? {} : { width: 220 }}>
|
||||
<List className={classes.list} gacontext="Navigation">
|
||||
{children}
|
||||
</List>
|
||||
{ExpandButton}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Navigation);
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { Navigation, NavItem } from 'shared/components/layout';
|
||||
import { wrapInThemedTestApp } from 'testUtils';
|
||||
|
||||
import { GcpProjectIcon, OverviewIcon } from 'shared/icons';
|
||||
|
||||
describe('<Navigation />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const nav = (
|
||||
<Navigation>
|
||||
<NavItem title="Overview" icon={<OverviewIcon />} href={'/'} />
|
||||
<NavItem title="Projects" icon={<GcpProjectIcon />} href={'/projects'} />
|
||||
</Navigation>
|
||||
);
|
||||
const rendered = render(wrapInThemedTestApp(nav));
|
||||
expect(rendered.getByText('Overview')).toBeInTheDocument();
|
||||
expect(rendered.getByText('Projects')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a condenseable navigation bar', () => {
|
||||
const nav = (
|
||||
<Navigation condensable={true}>
|
||||
<NavItem title="Overview" icon={<OverviewIcon />} href={'/'} />
|
||||
<NavItem title="Projects" icon={<GcpProjectIcon />} href={'/projects'} />
|
||||
</Navigation>
|
||||
);
|
||||
const rendered = render(wrapInThemedTestApp(nav));
|
||||
const btn = rendered.getByTestId('nav-expand-toggle');
|
||||
expect(btn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is condenseable', () => {
|
||||
const nav = (
|
||||
<Navigation condensable={true}>
|
||||
<NavItem title="Overview" icon={<OverviewIcon />} href={'/'} />
|
||||
<NavItem title="Projects" icon={<GcpProjectIcon />} href={'/projects'} />
|
||||
</Navigation>
|
||||
);
|
||||
const rendered = render(wrapInThemedTestApp(nav));
|
||||
const btn = rendered.getByTestId('nav-expand-toggle');
|
||||
fireEvent.click(btn);
|
||||
expect(rendered.queryByText('Overview')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have an anchor with correct href', () => {
|
||||
const nav = (
|
||||
<Navigation>
|
||||
<NavItem title="Projects" icon={<GcpProjectIcon />} href={'/projects'} />
|
||||
</Navigation>
|
||||
);
|
||||
const rendered = render(wrapInThemedTestApp(nav));
|
||||
const anchor = rendered.container.querySelector('a');
|
||||
expect(anchor.href).toBe('http://localhost/projects');
|
||||
});
|
||||
});
|
||||
@@ -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,172 @@
|
||||
import '../../assets/bursts/bursts.css';
|
||||
import circle1 from '../../assets/bursts/circle-1.svg';
|
||||
import circle2 from '../../assets/bursts/circle-2.svg';
|
||||
import triangle1 from '../../assets/bursts/triangle-1.svg';
|
||||
import triangle2 from '../../assets/bursts/triangle-2.svg';
|
||||
import triangle3 from '../../assets/bursts/triangle-3.svg';
|
||||
import rectangle1 from '../../assets/bursts/rectangle-1.svg';
|
||||
import rectangle2 from '../../assets/bursts/rectangle-2.svg';
|
||||
|
||||
// TODO move this file to shared and out of core
|
||||
|
||||
export const shapes = {
|
||||
circle1: {
|
||||
backgroundImage: `url(${circle1})`,
|
||||
width: 'calc(400px + 20%)',
|
||||
backgroundPosition: '100px 60%',
|
||||
},
|
||||
circle2: {
|
||||
backgroundImage: `url(${circle2})`,
|
||||
width: 'calc(400px + 20%)',
|
||||
backgroundPosition: '100px 58%',
|
||||
},
|
||||
rectangle1: {
|
||||
backgroundImage: `url(${rectangle1})`,
|
||||
width: 'calc(400px + 20%)',
|
||||
backgroundPosition: '100px 60%',
|
||||
},
|
||||
rectangle2: {
|
||||
backgroundImage: `url(${rectangle2})`,
|
||||
width: 'calc(400px + 20%)',
|
||||
backgroundPosition: '30px 35%',
|
||||
},
|
||||
triangle1: {
|
||||
backgroundImage: `url(${triangle1})`,
|
||||
width: 'calc(500px + 30%)',
|
||||
backgroundPosition: '100px calc(50px + 45%)',
|
||||
},
|
||||
triangle2: {
|
||||
backgroundImage: `url(${triangle2})`,
|
||||
width: 'calc(400px + 20%)',
|
||||
backgroundPosition: '100px 38%',
|
||||
},
|
||||
triangle3: {
|
||||
backgroundImage: `url(${triangle3})`,
|
||||
width: 'calc(400px + 20%)',
|
||||
backgroundPosition: '100px 76%',
|
||||
},
|
||||
};
|
||||
|
||||
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: shapes.rectangle2,
|
||||
},
|
||||
appFeature: {
|
||||
activeNavLinkColor: '#FF6437',
|
||||
gradient: gradients.orangeYellow,
|
||||
burstShape: shapes.triangle1,
|
||||
},
|
||||
app: {
|
||||
activeNavLinkColor: '#A72525',
|
||||
gradient: gradients.redOrange,
|
||||
burstShape: shapes.triangle1,
|
||||
},
|
||||
endpoint: {
|
||||
activeNavLinkColor: '#9315b0',
|
||||
gradient: gradients.purpleBlue,
|
||||
burstShape: shapes.circle1,
|
||||
},
|
||||
pipeline: {
|
||||
activeNavLinkColor: '#9315b0',
|
||||
gradient: gradients.purpleBlue,
|
||||
burstShape: shapes.circle1,
|
||||
},
|
||||
website: {
|
||||
activeNavLinkColor: '#765d90',
|
||||
gradient: gradients.purple,
|
||||
burstShape: shapes.rectangle2,
|
||||
},
|
||||
workflow: {
|
||||
activeNavLinkColor: '#AF2996',
|
||||
secondary: '#9315b0',
|
||||
gradient: gradients.purpleBlue,
|
||||
burstShape: shapes.circle1,
|
||||
},
|
||||
library: {
|
||||
activeNavLinkColor: '#B39AC8',
|
||||
gradient: gradients.sunset,
|
||||
burstShape: shapes.triangle3,
|
||||
},
|
||||
other: {
|
||||
activeNavLinkColor: '#8f6858',
|
||||
gradient: gradients.brown,
|
||||
burstShape: shapes.rectangle1,
|
||||
},
|
||||
system: {
|
||||
activeNavLinkColor: '#526C90',
|
||||
gradient: gradients.darkBlue,
|
||||
burstShape: shapes.triangle2,
|
||||
},
|
||||
project: {
|
||||
activeNavLinkColor: '#F13DA2',
|
||||
gradient: gradients.pinkOrange,
|
||||
burstShape: shapes.triangle3,
|
||||
},
|
||||
tool: {
|
||||
activeNavLinkColor: '#B39AC8',
|
||||
gradient: gradients.violetPeach,
|
||||
burstShape: shapes.rectangle1,
|
||||
},
|
||||
home: {
|
||||
activeNavLinkColor: '#00814e',
|
||||
gradient: gradients.green,
|
||||
burstShape: shapes.triangle2,
|
||||
},
|
||||
org: {
|
||||
activeNavLinkColor: '#6044ef',
|
||||
gradient: gradients.violetGreen,
|
||||
burstShape: shapes.triangle3,
|
||||
},
|
||||
documentation: {
|
||||
activeNavLinkColor: '#04c2ba',
|
||||
gradient: gradients.tpm,
|
||||
burstShape: shapes.circle2,
|
||||
},
|
||||
blogPost: {
|
||||
activeNavLinkColor: '#FFF',
|
||||
gradient: 'linear-gradient(90deg, #033A45 0%, #033A45 100%)',
|
||||
burstShape: shapes.circle2,
|
||||
},
|
||||
machineLearning: {
|
||||
activeNavLinkColor: '#0033DD',
|
||||
gradient: gradients.royalBlue,
|
||||
burstShape: shapes.circle1,
|
||||
},
|
||||
partnership: {
|
||||
activeNavLinkColor: '#000000',
|
||||
gradient: gradients.grey,
|
||||
burstShape: shapes.rectangle2,
|
||||
},
|
||||
ml: {
|
||||
activeNavLinkColor: '#B39AC8',
|
||||
gradient: gradients.blue,
|
||||
burstShape: shapes.circle1,
|
||||
},
|
||||
scienceBox: {
|
||||
activeNavLinkColor: '#69B9FF', // matches sky gradient beginning point
|
||||
gradient: gradients.sky,
|
||||
burstShape: shapes.circle1,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './Page';
|
||||
export { shapes, gradients, theme } from './PageThemeProvider';
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose } from 'recompose';
|
||||
import { List, ListItem, ListItemIcon, Popover, Typography, withStyles } from '@material-ui/core';
|
||||
import { withPluginContext } from 'core/app/withPluginContext';
|
||||
import { EmailIcon, SlackIcon, SupportIcon } from 'shared/icons';
|
||||
import { Button, Link } from 'shared/components';
|
||||
import { StackOverflow, StackOverflowTag } from 'shared/components/layout';
|
||||
|
||||
class SupportButton extends Component {
|
||||
static defaultProps = {
|
||||
slackChannel: '#backstage',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
slackChannel: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
|
||||
};
|
||||
|
||||
state = {
|
||||
popoverOpen: false,
|
||||
anchorEl: null,
|
||||
};
|
||||
|
||||
button_clickHandler = event => {
|
||||
this.setState({ anchorEl: event.currentTarget, popoverOpen: true });
|
||||
};
|
||||
|
||||
popover_closeHandler = () => {
|
||||
this.setState({ popoverOpen: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { slackChannel, email, children, classes, plugin } = this.props;
|
||||
const tags = plugin ? plugin.stackoverflowTags : undefined; // plugin is undefined on base routes
|
||||
const { popoverOpen, anchorEl } = this.state;
|
||||
const slackChannels = slackChannel ? (Array.isArray(slackChannel) ? slackChannel : [slackChannel]) : null;
|
||||
const contactEmails = email ? (Array.isArray(email) ? email : [email]) : null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Button color="primary" onClick={this.button_clickHandler}>
|
||||
<SupportIcon className={classes.leftIcon} />
|
||||
Support
|
||||
</Button>
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
||||
onClose={this.popover_closeHandler}
|
||||
gacontext="Support"
|
||||
>
|
||||
<List className={classes.popoverList}>
|
||||
{React.Children.map(children, (child, index) => (
|
||||
<ListItem alignItems="flex-start" key={index}>
|
||||
{child}
|
||||
</ListItem>
|
||||
))}
|
||||
{tags && tags.length > 0 && (
|
||||
<ListItem alignItems="flex-start">
|
||||
<StackOverflow>
|
||||
{tags.map((tag, idx) => (
|
||||
<StackOverflowTag key={idx} tag={tag} />
|
||||
))}
|
||||
</StackOverflow>
|
||||
</ListItem>
|
||||
)}
|
||||
{slackChannels && (
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemIcon>
|
||||
<SlackIcon />
|
||||
</ListItemIcon>
|
||||
<div>
|
||||
<Typography variant="subtitle1">Support</Typography>
|
||||
{slackChannels.map((channel, index) => (
|
||||
<Typography key={index}>
|
||||
<Link slackChannel={channel}>{channel}</Link>
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
{contactEmails && (
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemIcon>
|
||||
<EmailIcon />
|
||||
</ListItemIcon>
|
||||
<div>
|
||||
<Typography variant="subtitle1">Contact</Typography>
|
||||
{contactEmails.map((email, index) => (
|
||||
<Typography key={index}>
|
||||
<Link email={email}>{email}</Link>
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Popover>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = theme => ({
|
||||
leftIcon: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
popoverList: {
|
||||
minWidth: 260,
|
||||
maxWidth: 320,
|
||||
},
|
||||
});
|
||||
|
||||
export default compose(withStyles(styles), withPluginContext)(SupportButton);
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitForElement } from '@testing-library/react';
|
||||
|
||||
import SupportButton from './SupportButton';
|
||||
import { wrapInThemedTestApp, Keyboard } from 'testUtils';
|
||||
|
||||
describe('<SupportButton />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const rendered = render(<SupportButton />);
|
||||
rendered.getByText('Support');
|
||||
|
||||
fireEvent.click(rendered.baseElement);
|
||||
});
|
||||
|
||||
it('should show popover', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<SupportButton email="a@b.com" plugin={{ stackoverflowTags: ['a-tag'] }} />),
|
||||
);
|
||||
|
||||
expect(rendered.queryByText('Contact')).not.toBeInTheDocument();
|
||||
fireEvent.click(rendered.getByText('Support'));
|
||||
expect(rendered.queryByText('Contact')).toBeInTheDocument();
|
||||
rendered.getByText('a@b.com');
|
||||
rendered.getByText('a-tag');
|
||||
});
|
||||
|
||||
it('should close popover when hitting escape', async () => {
|
||||
const rendered = render(wrapInThemedTestApp(<SupportButton email="a@b.com" />));
|
||||
|
||||
fireEvent.click(rendered.getByText('Support'));
|
||||
expect(rendered.queryByText('Contact')).toBeInTheDocument();
|
||||
await Keyboard.type(rendered, '<Esc>');
|
||||
await waitForElement(() => !rendered.queryByText('Contact'));
|
||||
expect(rendered.queryByText('Contact')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -7504,6 +7526,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 +7664,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 +7685,7 @@ 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@^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==
|
||||
@@ -10352,7 +10391,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 +10751,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"
|
||||
@@ -12053,6 +12101,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"
|
||||
@@ -12982,7 +13037,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 +13247,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 +13309,62 @@ 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-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"
|
||||
@@ -13765,6 +13876,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"
|
||||
@@ -14218,6 +14334,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"
|
||||
@@ -15184,16 +15305,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"
|
||||
@@ -15715,6 +15846,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"
|
||||
|
||||