Merge branch 'master' into feature/400-info-card-storybook

This commit is contained in:
Mateus Marquezini
2020-03-30 10:17:44 -03:00
26 changed files with 950 additions and 242 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
# @backstage/app
# example-app
This package is an example of a Backstage application.
+5
View File
@@ -0,0 +1,5 @@
{
"baseUrl": "http://localhost:3000",
"fixturesFolder": false,
"pluginsFile": false
}
+12
View File
@@ -0,0 +1,12 @@
{
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"jest/expect-expect": [
"error",
{
"assertFunctionNames": ["expect", "cy.contains"]
}
]
}
}
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
describe('App', () => {
it('should render the welcome page', () => {
cy.visit('/');
cy.contains('Welcome to Backstage');
cy.contains('Getting Started');
cy.contains('Quick Links');
cy.contains('APIs');
});
it('should display support info when clicking the button', () => {
cy.visit('/');
cy.findByTestId('support-button').click({ force: true });
cy.contains('#backstage');
});
it('should display error message when triggering it', () => {
cy.visit('/');
cy.findByTestId('error-button').click({ force: true });
cy.contains('Error: Oh no!');
cy.findByTestId('error-button-close').click({ force: true });
});
it('should be able to login and logout', () => {
const name = 'test-name';
Cypress.on('window:before:load', win => {
win.fetch = cy.stub().resolves({
status: 200,
json: () => ({ username: 'test name', token: 'token', name }),
});
});
cy.visit('/');
cy.get('a[href="/login"]').click({ force: true });
cy.url().should('include', '/login');
cy.contains('Welcome, guest!');
cy.contains('Username')
.get('input[name=github-username-tf]')
.type(name, { force: true });
cy.contains('Token')
.get('input[name=github-auth-tf]')
.type('password', { force: true });
cy.findByTestId('github-auth-button').click({ force: true });
cy.contains(`Welcome, ${name}!`);
cy.contains('Logout').click({ force: true });
cy.contains('Welcome, guest!');
});
});
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/cypress/add-commands';
+14 -3
View File
@@ -1,5 +1,5 @@
{
"name": "@backstage/app",
"name": "example-app",
"version": "0.1.1-alpha.0",
"private": true,
"dependencies": {
@@ -28,7 +28,11 @@
"start": "backstage-cli app:serve",
"build": "backstage-cli app:build",
"test": "backstage-cli test",
"lint": "backstage-cli lint"
"test:e2e": "start-server-and-test start http://localhost:3000 cy:dev",
"test:e2e:ci": "start-server-and-test start http://localhost:3000 cy:run",
"lint": "backstage-cli lint",
"cy:dev": "cypress open",
"cy:run": "cypress run"
},
"browserslist": {
"production": [
@@ -42,5 +46,12 @@
"last 1 safari version"
]
},
"license": "Apache-2.0"
"license": "Apache-2.0",
"devDependencies": {
"@testing-library/cypress": "^6.0.0",
"@types/jquery": "^3.3.34",
"cypress": "^4.2.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
}
}
@@ -16,7 +16,8 @@
import React, { FC, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Snackbar } from '@material-ui/core';
import { Snackbar, IconButton } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import { Alert } from '@material-ui/lab';
import { ErrorApi, ErrorContext } from '@backstage/core';
@@ -73,7 +74,19 @@ const ErrorDisplay: FC<Props> = ({ forwarder }) => {
message={firstError.toString()}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert onClose={handleClose} severity="error">
<Alert
action={
<IconButton
color="inherit"
size="small"
onClick={handleClose}
data-testid="error-button-close"
>
<CloseIcon />
</IconButton>
}
severity="error"
>
{firstError.toString()}
</Alert>
</Snackbar>
@@ -27,7 +27,7 @@ import {
TextField,
List,
ListItem,
Link
Link,
} from '@material-ui/core';
import InfoCard from '../../../layout/InfoCard/InfoCard';
@@ -37,7 +37,9 @@ enum AuthType {
const LoginPage: FC<{}> = () => {
const [githubUsername, setGithubUsername] = useState(String);
const [githubPersonalAuthToken, setGithubPersonalAuthToken] = useState(String);
const [githubPersonalAuthToken, setGithubPersonalAuthToken] = useState(
String,
);
const [loginDetails, setLoginDetails] = useState(Object);
const saveGithubInfo = (info: {}) => {
@@ -70,11 +72,11 @@ const LoginPage: FC<{}> = () => {
'Content-Type': 'application/x-www-form-urlencoded',
}),
})
.then((response) => {
.then(response => {
if (response.status === 200) return response.json();
throw Error(`${response.status} ${response.statusText}`);
})
.then((data) => {
.then(data => {
const info = {
username: username,
token: token,
@@ -158,6 +160,7 @@ const LoginPage: FC<{}> = () => {
</ListItem>
<ListItem>
<Button
data-testid="github-auth-button"
variant="outlined"
color="primary"
onClick={() => authenticate(AuthType.GitHub)}
@@ -174,4 +177,4 @@ const LoginPage: FC<{}> = () => {
);
};
export default LoginPage;
export default LoginPage;
@@ -78,7 +78,11 @@ const SupportButton: FC<Props> = ({
return (
<Fragment>
<Button color="primary" onClick={onClickHandler}>
<Button
data-testid="support-button"
color="primary"
onClick={onClickHandler}
>
<HelpIcon className={classes.leftIcon} />
Support
</Button>
-157
View File
@@ -1,157 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 Waves from './Waves';
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;
}
return (
<Typography className={classes.subtitle} variant="subtitle1">
{subtitle}
</Typography>
);
}
render() {
const { title, pageTitleOverride, children, style, classes } = this.props;
const pageTitle = pageTitleOverride || title;
return (
<Fragment>
<Helmet
titleTemplate={`${pageTitle} | %s | Backstage`}
defaultTitle={`${pageTitle} | Backstage`}
/>
<Theme.Consumer>
{theme => (
<header style={style} className={classes.header}>
<Waves 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: '100%',
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',
marginRight: theme.spacing(6),
},
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);
@@ -36,18 +36,24 @@ describe('<Header/>', () => {
});
it('should override document title', () => {
const rendered = render(wrapInThemedTestApp(<Header title="Title1" pageTitleOverride="Title2" />));
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" />));
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="tool" />));
const rendered = render(
wrapInThemedTestApp(<Header title="Title" type="tool" />),
);
rendered.getByText('tool');
});
});
+197
View File
@@ -0,0 +1,197 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { Fragment, ReactNode, CSSProperties, FC } from 'react';
import Helmet from 'react-helmet';
import { Typography, Tooltip, makeStyles } from '@material-ui/core';
import { Theme } from '../Page/Page';
// import { Link } from 'shared/components';
import { BackstageTheme } from '../../theme/theme';
import Waves from './Waves';
const useStyles = makeStyles<BackstageTheme>(theme => ({
header: {
gridArea: 'pageHeader',
padding: theme.spacing(3),
minHeight: 118,
width: '100%',
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',
marginRight: theme.spacing(6),
},
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,
},
}));
type HeaderStyles = ReturnType<typeof useStyles>;
type Props = {
component?: ReactNode;
pageTitleOverride?: string;
style?: CSSProperties;
subtitle?: ReactNode;
title: ReactNode;
tooltip?: string;
type?: string;
typeLink?: string;
};
type TypeFragmentProps = {
classes: HeaderStyles;
type?: Props['title'];
typeLink?: Props['typeLink'];
};
type TitleFragmentProps = {
classes: HeaderStyles;
pageTitle: string | ReactNode;
tooltip?: Props['tooltip'];
};
type SubtitleFragmentProps = {
classes: HeaderStyles;
subtitle?: Props['subtitle'];
};
const TypeFragment: FC<TypeFragmentProps> = ({ type, typeLink, classes }) => {
if (!type) {
return null;
}
if (!typeLink) {
return (
// </Link>
<Typography className={classes.type}>{type}</Typography>
);
}
return (
// <Link to={typeLink}>
<Typography className={classes.type}>{type}</Typography>
);
};
const TitleFragment: FC<TitleFragmentProps> = ({
pageTitle,
classes,
tooltip,
}) => {
const FinalTitle = (
<Typography className={classes.title} variant="h4">
{pageTitle}
</Typography>
);
if (!tooltip) {
return FinalTitle;
}
return (
<Tooltip title={tooltip} placement="top-start">
{FinalTitle}
</Tooltip>
);
};
const SubtitleFragment: FC<SubtitleFragmentProps> = ({ classes, subtitle }) => {
if (!subtitle) {
return null;
}
if (typeof subtitle !== 'string') {
return <>{subtitle}</>;
}
return (
<Typography className={classes.subtitle} variant="subtitle1">
{subtitle}
</Typography>
);
};
export const Header: FC<Props> = ({
children,
pageTitleOverride,
style,
subtitle,
title,
tooltip,
type,
typeLink,
}) => {
const classes = useStyles();
const documentTitle = pageTitleOverride || title;
const pageTitle = title || pageTitleOverride;
const titleTemplate = `${documentTitle} | %s | Backstage`;
const defaultTitle = `${documentTitle} | Backstage`;
return (
<Fragment>
<Helmet titleTemplate={titleTemplate} defaultTitle={defaultTitle} />
<Theme.Consumer>
{theme => (
<header style={style} className={classes.header}>
<Waves theme={theme} />
<div className={classes.leftItemsBox}>
<TypeFragment classes={classes} type={type} typeLink={typeLink} />
<TitleFragment
classes={classes}
pageTitle={pageTitle}
tooltip={tooltip}
/>
<SubtitleFragment classes={classes} subtitle={subtitle} />
</div>
<div className={classes.rightItemsBox}>{children}</div>
</header>
)}
</Theme.Consumer>
</Fragment>
);
};
export default Header;
-1
View File
@@ -84,7 +84,6 @@ const Waves = ({ theme }) => {
gradientUnits="userSpaceOnUse"
>
<stop stopColor={color1} />
<stop offset="1" stopColor={color2} />
</linearGradient>
<linearGradient
id="paint1_linear"
@@ -75,7 +75,7 @@ export const gradients: Record<string, Gradient> = {
colors: ['#69B9FF', '#ACCEEC'],
},
teal: {
colors: ['#1F8A77', 'rgba(155, 240, 225, 1.0)'],
colors: ['#005E4D', '#9BF0E1'],
},
};
+1 -1
View File
@@ -94,7 +94,7 @@ export const SidebarItem: FC<SidebarItemProps> = ({
onClick={onClick}
underline="none"
>
<div className={classes.iconContainer}>
<div data-testid="login-button" className={classes.iconContainer}>
<Icon fontSize="small" />
</div>
<Typography variant="subtitle1" className={classes.label}>
+7
View File
@@ -39,5 +39,12 @@ export type BackstageTheme = Theme & {
linkHover: string;
link: string;
gold: string;
bursts: {
fontColor: string;
slackChannelText: string;
backgroundColor: {
default: string;
};
};
};
};