Merge branch 'master' into feature/yarn-clean

This commit is contained in:
Leonel Mendez Jimenez
2020-04-15 22:13:37 -05:00
committed by GitHub
86 changed files with 1404 additions and 357 deletions
+7 -7
View File
@@ -1,14 +1,14 @@
{
"name": "example-app",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": true,
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.3",
"@backstage/core": "^0.1.1-alpha.3",
"@backstage/theme": "^0.1.1-alpha.3",
"@backstage/plugin-home-page": "^0.1.1-alpha.3",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.3",
"@backstage/plugin-welcome": "^0.1.1-alpha.3",
"@backstage/cli": "^0.1.1-alpha.4",
"@backstage/core": "^0.1.1-alpha.4",
"@backstage/plugin-home-page": "^0.1.1-alpha.4",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.4",
"@backstage/plugin-welcome": "^0.1.1-alpha.4",
"@backstage/theme": "^0.1.1-alpha.4",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
+2 -2
View File
@@ -24,8 +24,8 @@ describe('App', () => {
value: jest.fn(() => {
return {
matches: true,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
addListener: jest.fn(),
removeListener: jest.fn(),
};
}),
});
+2 -2
View File
@@ -41,9 +41,9 @@ export function useThemeType(themeId: string): [string, () => void] {
setTheme('auto');
}
};
mql.addEventListener('change', darkListener);
mql.addListener(darkListener);
return () => {
mql.removeEventListener('change', darkListener);
mql.removeListener(darkListener);
};
});
function toggleTheme() {
+3 -4
View File
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { default as HomePagePlugin } from '@backstage/plugin-home-page';
import { default as WelcomePlugin } from '@backstage/plugin-welcome';
import { default as LighthousePlugin } from '@backstage/plugin-lighthouse';
export { HomePagePlugin, WelcomePlugin, LighthousePlugin };
export { plugin as HomePagePlugin } from '@backstage/plugin-home-page';
export { plugin as WelcomePlugin } from '@backstage/plugin-welcome';
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
+4
View File
@@ -51,6 +51,10 @@ module.exports = {
bundledDependencies: true,
},
],
'@typescript-eslint/no-unused-vars': [
'warn',
{ vars: 'all', args: 'after-used', ignoreRestSiblings: true },
],
},
overrides: [
{
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -122,7 +122,7 @@ export async function addPluginToApp(rootDir: string, pluginName: string) {
.split('-')
.map(name => capitalize(name))
.join('');
const pluginImport = `import { default as ${pluginNameCapitalized} } from '${pluginPackage}';`;
const pluginImport = `import { plugin as ${pluginNameCapitalized} } from '${pluginPackage}';`;
const pluginExport = `export { ${pluginNameCapitalized} };`;
const pluginsFilePath = 'packages/app/src/plugins.ts';
const pluginsFile = resolvePath(rootDir, pluginsFilePath);
@@ -1,13 +1,20 @@
{
"name": "root",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=12.0.0"
},
"scripts": {
"start": "yarn build && yarn workspace app start",
"start": "yarn workspace app start",
"bundle": "yarn build && yarn workspace app bundle",
"build": "lerna run build",
"test": "cross-env CI=true lerna run test -- --coverage",
"test": "yarn build && lerna run test --since origin/master -- --coverage",
"test:all": "yarn build && lerna run test -- --coverage",
"lint": "lerna run lint --since origin/master --",
"lint:all": "lerna run lint --",
"create-plugin": "backstage-cli create-plugin",
"clean": "lerna run clean",
"lint": "lerna run lint"
"clean": "lerna run clean"
},
"workspaces": {
"packages": [
@@ -15,10 +22,8 @@
"plugins/*"
]
},
"version": "1.0.0",
"devDependencies": {
"@backstage/cli": "^{{version}}",
"cross-env": "^7.0.0",
"lerna": "^3.20.2",
"prettier": "^1.19.1"
}
@@ -21,7 +21,7 @@
},
"scripts": {
"start": "backstage-cli app:serve",
"build": "backstage-cli app:build",
"bundle": "backstage-cli app:build",
"test": "backstage-cli test",
"lint": "backstage-cli lint"
},
@@ -1 +1 @@
export { default as WelcomePlugin } from 'plugin-welcome';
export { plugin as WelcomePlugin } from 'plugin-welcome';
@@ -1 +1 @@
export { default } from './plugin';
export { plugin } from './plugin';
@@ -1,4 +1,4 @@
import plugin from './plugin';
import { plugin } from './plugin';
describe('welcome', () => {
it('should export plugin', () => {
@@ -1,7 +1,7 @@
import { createPlugin } from '@backstage/core';
import WelcomePage from './components/WelcomePage';
export default createPlugin({
export const plugin = createPlugin({
id: 'welcome',
register({ router }) {
router.registerRoute('/', WelcomePage);
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './plugin';
export { plugin } from './plugin';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import plugin from './plugin';
import { plugin } from './plugin';
describe('{{ id }}', () => {
it('should export plugin', () => {
@@ -17,7 +17,7 @@
import { createPlugin } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export default createPlugin({
export const plugin = createPlugin({
id: '{{ id }}',
register({ router }) {
router.registerRoute('/{{ id }}', ExampleComponent);
-22
View File
@@ -1,22 +0,0 @@
module.exports = {
stories: [
'../src/layout/**/*.stories.tsx',
'../src/components/**/*.stories.tsx',
],
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
webpackFinal: async config => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
{
loader: require.resolve('ts-loader'),
options: {
transpileOnly: true,
},
},
],
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
},
};
+5 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -42,9 +42,10 @@
"recompose": "0.30.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.3",
"@backstage/test-utils": "^0.1.1-alpha.3",
"@backstage/theme": "^0.1.1-alpha.3",
"@backstage/cli": "^0.1.1-alpha.4",
"@backstage/test-utils-core": "^0.1.1-alpha.4",
"@backstage/theme": "^0.1.1-alpha.4",
"@storybook/addon-storysource": "^5.3.18",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
@@ -19,7 +19,7 @@ import ApiProvider, { useApi, withApis } from './ApiProvider';
import ApiRef from './ApiRef';
import ApiRegistry from './ApiRegistry';
import { render } from '@testing-library/react';
import { withLogCollector } from '@backstage/test-utils';
import { withLogCollector } from '@backstage/test-utils-core';
describe('ApiProvider', () => {
type Api = () => string;
@@ -16,7 +16,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import { COLORS } from '@backstage/theme';
import { wrapInThemedTestApp } from '@backstage/test-utils';
import CircleProgress, { getProgressColor } from './CircleProgress';
@@ -52,17 +51,22 @@ describe('<CircleProgress />', () => {
getByText('10m');
});
const ok = '#111';
const warning = '#222';
const error = '#333';
const palette = { status: { ok, warning, error } };
it('colors the progress correctly', () => {
expect(getProgressColor()).toBe('#ddd');
expect(getProgressColor(10)).toBe(COLORS.STATUS.ERROR);
expect(getProgressColor(50)).toBe(COLORS.STATUS.WARNING);
expect(getProgressColor(90)).toBe(COLORS.STATUS.OK);
expect(getProgressColor(palette)).toBe('#ddd');
expect(getProgressColor(palette, 10)).toBe(error);
expect(getProgressColor(palette, 50)).toBe(warning);
expect(getProgressColor(palette, 90)).toBe(ok);
});
it('colors the inverse progress correctly', () => {
expect(getProgressColor()).toBe('#ddd');
expect(getProgressColor(10, true)).toBe(COLORS.STATUS.OK);
expect(getProgressColor(50, true)).toBe(COLORS.STATUS.WARNING);
expect(getProgressColor(90, true)).toBe(COLORS.STATUS.ERROR);
expect(getProgressColor(palette)).toBe('#ddd');
expect(getProgressColor(palette, 10, true)).toBe(ok);
expect(getProgressColor(palette, 50, true)).toBe(warning);
expect(getProgressColor(palette, 90, true)).toBe(error);
});
});
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { makeStyles } from '@material-ui/core';
import { BackstageTheme, COLORS } from '@backstage/theme';
import { makeStyles, useTheme } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { Circle } from 'rc-progress';
import React, { FC } from 'react';
@@ -55,7 +55,7 @@ const defaultProps = {
max: 100,
};
export function getProgressColor(value, inverse, max) {
export function getProgressColor(palette, value, inverse, max) {
if (isNaN(value)) {
return '#ddd';
}
@@ -64,16 +64,17 @@ export function getProgressColor(value, inverse, max) {
const actualValue = inverse ? actualMax - value : value;
if (actualValue < actualMax / 3) {
return COLORS.STATUS.ERROR;
return palette.status.error;
} else if (actualValue < actualMax * (2 / 3)) {
return COLORS.STATUS.WARNING;
return palette.status.warning;
}
return COLORS.STATUS.OK;
return palette.status.ok;
}
const CircleProgress: FC<Props> = props => {
const classes = useStyles(props);
const theme = useTheme();
const { value, fractional, inverse, unit, max } = {
...defaultProps,
...props,
@@ -89,7 +90,7 @@ const CircleProgress: FC<Props> = props => {
percent={asPercentage}
strokeWidth={12}
trailWidth={12}
strokeColor={getProgressColor(asActual, inverse, max)}
strokeColor={getProgressColor(theme.palette, asActual, inverse, max)}
className={classes.circle}
/>
<div className={classes.overlay}>
@@ -16,9 +16,8 @@
import React, { FC } from 'react';
import { makeStyles } from '@material-ui/core';
import InfoCard from 'layout/InfoCard';
import { Props as BottomLinkProps } from 'layout/InfoCard/BottomLink';
import { Props as BottomLinkProps } from '../layout/BottomLink';
import CircleProgress from './CircleProgress';
type Props = {
@@ -171,7 +171,7 @@ const DataTableRow = pure(({ row, columns, handleRowClick, style }) => {
* @example
* render {
* const data = [
* { id: 'buffalos', amount: 1, status: <Error />, statusValue: 2 }
* { id: 'buffalos', amount: 1, status: <Error />, statusValue: 2 },
* { id: 'milk', amount: 3, status: <Warning />, statusValue: 1 }
* ];
* const columns = [
@@ -0,0 +1,57 @@
/*
* 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 from 'react';
import { StatusError, StatusOK, StatusWarning } from './Status';
import SortableTable from './SortableTable';
export default {
title: 'Sortable Table',
component: SortableTable,
};
const containerStyle = { width: 600, padding: 20 };
const data = [
{ id: 'buffalos', amount: 1, status: <StatusError />, statusValue: 2 },
{ id: 'milk', amount: 3, status: <StatusWarning />, statusValue: 1 },
{ id: 'cheese', amount: 8, status: <StatusWarning />, statusValue: 1 },
{ id: 'bread', amount: 2, status: <StatusOK />, statusValue: 0 },
];
const columns = [
{ id: 'id', label: 'ID' },
{ id: 'amount', disablePadding: false, numeric: true, label: 'AMOUNT' },
{ id: 'status', label: 'STATUS', sortValue: row => row.statusValue },
];
const footerData = [
{ id: 'total', amount: 4, statusValue: 2, status: <StatusError /> },
];
export const Default = () => (
<div style={containerStyle}>
<SortableTable orderBy="desc" data={data} columns={columns} />
</div>
);
export const WithFooter = () => (
<div style={containerStyle}>
<SortableTable
orderBy="asc"
data={data}
columns={columns}
footerData={footerData}
/>
</div>
);
@@ -0,0 +1,67 @@
/*
* 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 from 'react';
import {
StatusError,
StatusFailed,
StatusNA,
StatusOK,
StatusPending,
StatusRunning,
StatusWarning,
} from './Status';
export default {
title: 'Status',
component: StatusOK,
};
export const statusOK = () => (
<>
<StatusOK /> Status OK
</>
);
export const statusWarning = () => (
<>
<StatusWarning /> Status Warning
</>
);
export const statusError = () => (
<>
<StatusError /> Status Error
</>
);
export const statusFailed = () => (
<>
<StatusFailed /> Status Failed
</>
);
export const statusPending = () => (
<>
<StatusPending /> Status Pending
</>
);
export const statusRunning = () => (
<>
<StatusRunning /> Status Running
</>
);
export const statusNA = () => (
<>
<StatusNA /> Status NA
</>
);
@@ -0,0 +1,80 @@
/*
* 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 } from 'react';
import PropTypes from 'prop-types';
import { Typography, withStyles } from '@material-ui/core';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
const errorOutlineStyles = theme => ({
root: {
marginRight: theme.spacing(1),
fill: theme.palette.warningText,
},
});
const ErrorOutlineStyled = withStyles(errorOutlineStyles)(ErrorOutline);
const styles = theme => ({
message: {
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(1.5),
backgroundColor: theme.palette.warningBackground,
color: theme.palette.warningText,
verticalAlign: 'middle',
},
header: {
display: 'flex',
flexDirection: 'row',
marginBottom: theme.spacing(1),
},
headerText: {
color: theme.palette.warningText,
},
messageText: {
color: theme.palette.warningText,
},
});
/**
* WarningPanel. Show a user friendly error message to a user similar to ErrorPanel except that the warning panel
* only shows the warning message to the user
*/
class WarningPanel extends Component {
static propTypes = {
message: PropTypes.node.isRequired,
};
render() {
const { classes, title, message, children } = this.props;
return (
<div className={classes.message}>
<div className={classes.header}>
<ErrorOutlineStyled />
<Typography className={classes.headerText} variant="subtitle1">
{title}
</Typography>
</div>
{message && (
<Typography className={classes.messageText}>{message}</Typography>
)}
{children}
</div>
);
}
}
export default withStyles(styles)(WarningPanel);
@@ -0,0 +1,45 @@
/*
* 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 from 'react';
import WarningPanel from '.';
import { Link, Button } from '@material-ui/core';
export default {
title: 'Warning Panel',
component: WarningPanel,
};
export const Default = () => (
<WarningPanel
title="Example Warning Title"
message={
<>
This example entity is missing something. If this is unexpected, please
make sure you have set up everything correctly by following{' '}
<Link href="http://example.com">this guide</Link>.
</>
}
/>
);
export const Children = () => (
<WarningPanel title="Example Warning Title">
<Button variant="outlined" color="primary">
Supports custom children - for example this button
</Button>
</WarningPanel>
);
@@ -0,0 +1,40 @@
/*
* 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 from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from '@backstage/test-utils';
import WarningPanel from './WarningPanel';
const minProps = { title: 'Mock title', message: 'Some more info' };
describe('<WarningPanel />', () => {
it('renders without exploding', () => {
const { getByText } = render(
wrapInThemedTestApp(<WarningPanel {...minProps} />),
);
expect(getByText('Mock title')).toBeInTheDocument();
});
it('renders message and children', () => {
const { getByText } = render(
wrapInThemedTestApp(<WarningPanel {...minProps}>children</WarningPanel>),
);
expect(getByText('Some more info')).toBeInTheDocument();
expect(getByText('children')).toBeInTheDocument();
});
});
@@ -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.
*/
export { default } from './WarningPanel';
+1
View File
@@ -33,3 +33,4 @@ export { default as SupportButton } from './components/SupportButton';
export { default as SortableTable } from './components/SortableTable';
export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular';
export * from './components/Status';
export { default as WarningPanel } from './components/WarningPanel';
@@ -14,18 +14,18 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import {
Divider,
Link,
ListItem,
ListItemIcon,
Divider,
ListItemText,
makeStyles,
} from '@material-ui/core';
import Box from '@material-ui/core/Box';
import grey from '@material-ui/core/colors/grey';
import ArrowIcon from '@material-ui/icons/ArrowForward';
import React, { FC } from 'react';
import grey from '@material-ui/core/colors/grey';
import Box from '@material-ui/core/Box';
const useStyles = makeStyles(theme => ({
root: {
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { default, Props } from './BottomLink';
@@ -1,81 +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 } from 'react';
import PropTypes from 'prop-types';
import { Tooltip, Link, withStyles } from '@material-ui/core';
import { StatusError } from 'components/Status';
import HeaderLabel from './HeaderLabel';
const style = theme => ({
notVerified: {
color: theme.palette.status.error,
borderRadius: 4,
padding: '3px 6px',
fontSize: '8pt',
opacity: 0.8,
fontWeight: 'bold',
position: 'relative',
top: -4,
backgroundColor: 'pink',
float: 'right',
marginLeft: 14,
},
label: { float: 'left' },
});
class OwnerHeaderLabel extends Component {
static propTypes = {
owner: PropTypes.object.isRequired,
};
render() {
const { owner, classes } = this.props;
const isBadSquad = owner.type !== 'squad';
const notVerified = isBadSquad && (
<Link href="https://spotify.stackenterprise.co/a/4412/23">
<span className={classes.notVerified}>
<StatusError style={{ position: 'relative', top: 2 }} /> Squad not
verified!
</span>
</Link>
);
const label = (
<Tooltip
title="This component is not owned by an existing squad. Click the badge to learn how to fix this."
placement="bottom"
>
<span>
<span className={classes.label}>{owner.name}</span>
</span>
</Tooltip>
);
return (
<>
<HeaderLabel
label="Owner"
value={isBadSquad ? label : owner.name}
url={owner.name ? `/org/${owner.name}` : ''}
/>
{notVerified}
</>
);
}
}
export default withStyles(style)(OwnerHeaderLabel);
@@ -25,7 +25,7 @@ import {
makeStyles,
} from '@material-ui/core';
import ErrorBoundary from 'layout/ErrorBoundary/ErrorBoundary';
import BottomLink, { Props as BottomLinkProps } from './BottomLink';
import BottomLink, { Props as BottomLinkProps } from '../BottomLink';
const useStyles = makeStyles(theme => ({
header: {
+3 -2
View File
@@ -18,8 +18,9 @@ import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import React, { FC, useRef, useState } from 'react';
import { sidebarConfig, SidebarContext } from './config';
import { BackstageTheme } from '@backstage/theme';
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles<typeof BackstageTheme>(theme => ({
root: {
zIndex: 1000,
position: 'relative',
@@ -35,7 +36,7 @@ const useStyles = makeStyles(theme => ({
top: 0,
bottom: 0,
padding: 0,
background: '#171717',
background: theme.palette.sidebar,
overflowX: 'hidden',
width: sidebarConfig.drawerWidthClosed,
transition: theme.transitions.create('width', {
@@ -0,0 +1,77 @@
/*
* 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, { useState } from 'react';
import { TabbedCard, CardTab } from '.';
export default {
title: 'Tabbed Card',
component: TabbedCard,
};
export const Default = () => {
return (
<TabbedCard title="Default Example Header">
<CardTab label="Option 1">some content 1</CardTab>
<CardTab label="Option 2">some content 2</CardTab>
<CardTab label="Option 3">some content 3</CardTab>
<CardTab label="Option 4">some content 4</CardTab>
</TabbedCard>
);
};
const linkInfo = { title: 'Go to XYZ Location', link: '#' };
export const WithFooterLink = () => {
return (
<TabbedCard title="Footer Link Example Header" deepLink={linkInfo}>
<CardTab label="Option 1">some content 1</CardTab>
<CardTab label="Option 2">some content 2</CardTab>
<CardTab label="Option 3">some content 3</CardTab>
<CardTab label="Option 4">some content 4</CardTab>
</TabbedCard>
);
};
export const WithControlledTabValue = () => {
const [selectedTab, setSelectedTab] = useState('one');
const handleChange = (_ev, newSelectedTab) => setSelectedTab(newSelectedTab);
return (
<>
<span>Selected tab is {selectedTab}</span>
<TabbedCard
value={selectedTab}
onChange={handleChange}
title="Controlled Value Example"
>
<CardTab value="one" label="Option 1">
some content 1
</CardTab>
<CardTab value="two" label="Option 2">
some content 2
</CardTab>
<CardTab value="three" label="Option 3">
some content 3
</CardTab>
<CardTab value="four" label="Option 4">
some content 4
</CardTab>
</TabbedCard>
</>
);
};
@@ -0,0 +1,105 @@
/*
* 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 from 'react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { TabbedCard, CardTab } from '.';
const minProps = {
title: 'Some title',
deepLink: {
title: 'A deepLink title',
link: '/mocked',
},
};
describe('<TabbedCard />', () => {
it('renders without exploding', () => {
const rendered = render(
wrapInTestApp(
<TabbedCard title={minProps.title}>
<CardTab label="Test 1">Test Content</CardTab>
<CardTab label="Test 2">Test Content</CardTab>
</TabbedCard>,
),
);
expect(rendered.getByText('Some title')).toBeInTheDocument();
});
it('renders a deepLink when prop is set', () => {
const rendered = render(
wrapInTestApp(
<TabbedCard deepLink={minProps.deepLink}>
<CardTab label="Test 1">Test Content</CardTab>
<CardTab label="Test 2">Test Content</CardTab>
</TabbedCard>,
),
);
expect(rendered.getByText('A deepLink title')).toBeInTheDocument();
});
it('switches tabs when clicking', () => {
const rendered = render(
wrapInTestApp(
<TabbedCard>
<CardTab label="Test 1">Test Content 1</CardTab>
<CardTab label="Test 2">Test Content 2</CardTab>
</TabbedCard>,
),
);
expect(rendered.getByText('Test Content 1')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Test 2'));
expect(rendered.getByText('Test Content 2')).toBeInTheDocument();
});
it('switches tabs when clicking in controlled mode', () => {
let selectedTab = 'one';
const handleTabChange = jest.fn(
(_ev, newSelectedTab) => (selectedTab = newSelectedTab),
);
const rendered = render(
wrapInTestApp(
<TabbedCard value={selectedTab} onChange={handleTabChange}>
<CardTab value="one" label="Test 1">
Test Content 1
</CardTab>
<CardTab value="two" label="Test 2">
Test Content 2
</CardTab>
</TabbedCard>,
),
);
expect(rendered.getByText('Test Content 1')).toBeInTheDocument();
fireEvent.click(rendered.getByText('Test 2'));
expect(handleTabChange.mock.calls.length).toBe(1);
rendered.rerender(
<TabbedCard value={selectedTab} onChange={handleTabChange}>
<CardTab value="one" label="Test 1">
Test Content 1
</CardTab>
<CardTab value="two" label="Test 2">
Test Content 2
</CardTab>
</TabbedCard>,
);
expect(rendered.getByText('Test Content 2')).toBeInTheDocument();
});
});
@@ -0,0 +1,127 @@
/*
* 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, { FC, useState, ReactElement, ReactNode } from 'react';
import {
Card,
CardContent,
CardHeader,
Divider,
withStyles,
makeStyles,
Tabs,
Tab,
TabProps,
} from '@material-ui/core';
import BottomLink, { Props as BottomLinkProps } from '../BottomLink';
import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
const useTabsStyles = makeStyles(theme => ({
root: {
padding: theme.spacing(0, 2, 0, 2.5),
minHeight: theme.spacing(3),
},
indicator: {
backgroundColor: theme.palette.info.main,
height: theme.spacing(0.3),
},
}));
const BoldHeader = withStyles(theme => ({
root: { padding: theme.spacing(2, 2, 2, 2.5), display: 'inline-block' },
title: { fontWeight: 700 },
subheader: { paddingTop: theme.spacing(1) },
}))(CardHeader);
type Props = {
slackChannel?: string;
children?: ReactElement<TabProps>[];
onChange?: (event: React.ChangeEvent<{}>, value: number | string) => void;
title?: string;
value?: number | string;
deepLink?: BottomLinkProps;
};
const TabbedCard: FC<Props> = ({
slackChannel = '#backstage',
children,
title,
deepLink,
value,
onChange,
}) => {
const tabsClasses = useTabsStyles();
const [selectedIndex, selectIndex] = useState(0);
const handleChange = onChange
? onChange
: (_ev, newSelectedIndex: number) => selectIndex(newSelectedIndex);
let selectedTabContent: ReactNode;
if (!value) {
React.Children.map(children, (child, index) => {
if (index === selectedIndex) selectedTabContent = child?.props.children;
});
} else {
React.Children.map(children, child => {
if (child?.props.value === value)
selectedTabContent = child?.props.children;
});
}
return (
<Card>
<ErrorBoundary slackChannel={slackChannel}>
{title && <BoldHeader title={title} />}
<Tabs
classes={tabsClasses}
value={value || selectedIndex}
onChange={handleChange}
>
{children}
</Tabs>
<Divider />
<CardContent>{selectedTabContent}</CardContent>
{deepLink && <BottomLink {...deepLink} />}
</ErrorBoundary>
</Card>
);
};
const useCardTabStyles = makeStyles(theme => ({
root: {
minWidth: theme.spacing(6),
minHeight: theme.spacing(3),
margin: theme.spacing(0, 2, 0, 0),
padding: theme.spacing(0.5, 0, 0.5, 0),
textTransform: 'none',
},
selected: {
fontWeight: 'bold',
},
}));
type CardTabProps = TabProps & {
children: ReactNode;
};
const CardTab: FC<CardTabProps> = ({ children, ...props }) => {
const classes = useCardTabStyles();
return <Tab disableRipple classes={classes} {...props} />;
};
export { TabbedCard, CardTab };
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './TabbedCard';
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import { addDecorator } from '@storybook/react';
import { BackstageTheme } from '@backstage/theme';
import { CssBaseline, ThemeProvider } from '@material-ui/core';
addDecorator(story => (
<ThemeProvider theme={BackstageTheme}>
<CssBaseline>{story()}</CssBaseline>
</ThemeProvider>
));
+9 -1
View File
@@ -5,8 +5,16 @@ module.exports = {
'../../core/src/layout/**/*.stories.tsx',
'../../core/src/components/**/*.stories.tsx',
],
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
addons: [
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-storysource',
],
webpackFinal: async config => {
config.resolve.alias = {
...config.resolve.alias,
'@backstage/theme': path.resolve(__dirname, '../../theme/src'),
};
config.resolve.modules.push(path.resolve(__dirname, '../../core/src'));
config.module.rules.push(
{
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "storybook",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"description": "Storybook build for core package",
"private": true,
"scripts": {
@@ -15,6 +15,7 @@
"devDependencies": {
"@storybook/addon-actions": "^5.3.17",
"@storybook/addon-links": "^5.3.17",
"@storybook/addon-storysource": "^5.3.18",
"@storybook/addons": "^5.3.17",
"@storybook/react": "^5.3.17"
}
+1
View File
@@ -0,0 +1 @@
registry=https://registry.npmjs.org/
+15
View File
@@ -0,0 +1,15 @@
# @backstage/test-utils-core
This package provides utilities for testing the Backstage core packages.
## Installation
This package should not be used directly, use `@backstage/test-utils` instead. All exports from this
package are re-exported by `@backstage/test-utils`.
The reason this package exists is to allow the Backstage core packages to use the testing utils exposed in this package. Since `@backstage/test-utils` needs to depend on `@backstage/core`, core is not able to use those test-utils. We put any test-utils that don't need to depend on other Backstage packages in this package, in order for them to be usable by any other `@backstage` packages.
## Documentation
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@backstage/test-utils-core",
"description": "Utilities to test Backstage core",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/spotify/backstage",
"directory": "packages/test-utils-core"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "backstage-cli plugin:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test"
},
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"react": "^16.12.0",
"react-dom": "^16.12.0"
}
}
+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.
*/
export * from './testUtils';
@@ -0,0 +1,17 @@
/*
* 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/jest-dom/extend-expect';
@@ -23,7 +23,7 @@ const codes = {
Esc: 27,
};
export default class Keyboard {
export class Keyboard {
static async type(target, input) {
await new Keyboard(target).type(input);
}
@@ -15,7 +15,7 @@
*/
import React from 'react';
import Keyboard from './Keyboard';
import { Keyboard } from './Keyboard';
import { render } from '@testing-library/react';
describe('testUtils.Keyboard', () => {
@@ -0,0 +1,19 @@
/*
* 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.
*/
export * from './Keyboard';
export * from './logCollector';
export * from './testingLibrary';
@@ -0,0 +1,96 @@
/*
* 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.
*/
/* eslint-disable no-console */
import { withLogCollector } from './logCollector';
describe('logCollector', () => {
it('should collect some logs synchronously', () => {
const logs = withLogCollector(() => {
console.log('a');
console.warn('b');
console.error('c');
console.error('3');
console.warn('2');
console.log('1');
});
expect(logs.log).toEqual(['a', '1']);
expect(logs.warn).toEqual(['b', '2']);
expect(logs.error).toEqual(['c', '3']);
});
it('should collect some logs asynchrnously', async () => {
const logs = await withLogCollector(async () => {
console.log('a');
console.warn('b');
console.error('c');
console.error('3');
console.warn('2');
console.log('1');
});
expect(logs.log).toEqual(['a', '1']);
expect(logs.warn).toEqual(['b', '2']);
expect(logs.error).toEqual(['c', '3']);
});
it('should collect specific logs synchronously', () => {
const missedLogs = withLogCollector(() => {
const logs = withLogCollector(['warn', 'log'], () => {
console.log('a');
console.warn('b');
console.error('c');
console.error('3');
console.warn('2');
console.log('1');
});
expect(logs.log).toEqual(['a', '1']);
expect(logs.warn).toEqual(['b', '2']);
// @ts-ignore
expect(logs.error).toEqual([]);
});
expect(missedLogs.log).toEqual([]);
expect(missedLogs.warn).toEqual([]);
expect(missedLogs.error).toEqual(['c', '3']);
});
it('should collect specific logs asynchrnously', async () => {
const missedLogs = await withLogCollector(async () => {
const logs = await withLogCollector(['error'], async () => {
console.log('a');
console.warn('b');
console.error('c');
console.error('3');
console.warn('2');
console.log('1');
});
// @ts-ignore
expect(logs.log).toEqual([]);
// @ts-ignore
expect(logs.warn).toEqual([]);
expect(logs.error).toEqual(['c', '3']);
});
expect(missedLogs.log).toEqual(['a', '1']);
expect(missedLogs.warn).toEqual(['b', '2']);
expect(missedLogs.error).toEqual([]);
});
});
@@ -0,0 +1,111 @@
/*
* 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.
*/
/* eslint-disable no-console */
export type LogFuncs = 'log' | 'warn' | 'error';
export type AsyncLogCollector = () => Promise<void>;
export type SyncLogCollector = () => void;
export type LogCollector = AsyncLogCollector | SyncLogCollector;
export type CollectedLogs<T extends LogFuncs> = { [key in T]: string[] };
const allCategories = ['log', 'warn', 'error'];
// Asynchronous log collector with that collects all categories
export function withLogCollector(
callback: AsyncLogCollector,
): Promise<CollectedLogs<LogFuncs>>;
// Synchronous log collector with that collects all categories
export function withLogCollector(
callback: SyncLogCollector,
): CollectedLogs<LogFuncs>;
// Asynchronous log collector with that only collects selected categories
export function withLogCollector<T extends LogFuncs>(
logsToCollect: T[],
callback: AsyncLogCollector,
): Promise<CollectedLogs<T>>;
// Synchronous log collector with that only collects selected categories
export function withLogCollector<T extends LogFuncs>(
logsToCollect: T[],
callback: SyncLogCollector,
): CollectedLogs<T>;
export function withLogCollector(
logsToCollect: LogFuncs[] | LogCollector,
callback?: LogCollector,
): CollectedLogs<LogFuncs> | Promise<CollectedLogs<LogFuncs>> {
const oneArg = !callback;
const actualCallback = (oneArg ? logsToCollect : callback) as LogCollector;
const categories = (oneArg ? allCategories : logsToCollect) as LogFuncs[];
const logs = {
log: new Array<string>(),
warn: new Array<string>(),
error: new Array<string>(),
};
const origLog = console.log;
const origWarn = console.warn;
const origError = console.error;
if (categories.includes('log')) {
console.log = (message: string) => {
logs.log.push(message);
};
}
if (categories.includes('warn')) {
console.warn = (message: string) => {
logs.warn.push(message);
};
}
if (categories.includes('error')) {
console.error = (message: string) => {
logs.error.push(message);
};
}
const restore = () => {
console.log = origLog;
console.warn = origWarn;
console.error = origError;
};
try {
const ret = actualCallback();
if (!ret || !ret.then) {
restore();
return logs;
}
return ret.then(
() => {
restore();
return logs;
},
error => {
restore();
throw error;
},
);
} catch (error) {
restore();
throw error;
}
}
@@ -0,0 +1,35 @@
/*
* 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 { ReactElement } from 'react';
import { act } from 'react-dom/test-utils';
import { render, RenderResult } from '@testing-library/react';
// Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async
// act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit
// cleaner, since act doesn't return the result of the evaluated function.
// https://github.com/testing-library/react-testing-library/issues/281
// https://github.com/facebook/react/pull/14853
export async function renderWithEffects(
nodes: ReactElement,
): Promise<RenderResult> {
let value: RenderResult;
await act(() => {
value = render(nodes);
});
// @ts-ignore
return value;
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {
"baseUrl": "src"
}
}
+4 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -25,8 +25,8 @@
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.3",
"@backstage/theme": "^0.1.1-alpha.3",
"@backstage/cli": "^0.1.1-alpha.4",
"@backstage/theme": "^0.1.1-alpha.4",
"@material-ui/core": "^4.9.1",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
@@ -39,6 +39,7 @@
"react-router-dom": "^5.1.2"
},
"peerDependencies": {
"@backstage/test-utils-core": "^0.1.1-alpha.3",
"@backstage/theme": "^0.1.1-alpha.3",
"@material-ui/core": "^4.9.1",
"@testing-library/jest-dom": "^4.2.4",
+1
View File
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export * from './testUtils';
export * from '@backstage/test-utils-core';
@@ -0,0 +1,35 @@
/*
* 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 from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from './appWrappers';
import { Route } from 'react-router';
describe('wrapInTestApp', () => {
it('should provide routing', () => {
const rendered = render(
wrapInTestApp(
<>
<Route path="/route1">Route 1</Route>
<Route path="/route2">Route 2</Route>
</>,
['/route2'],
),
);
expect(rendered.getByText('Route 2')).toBeInTheDocument();
});
});
@@ -0,0 +1,53 @@
/*
* 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, { ComponentType, ReactNode, FunctionComponent } from 'react';
import { ThemeProvider } from '@material-ui/core';
import { MemoryRouter } from 'react-router';
import { Route } from 'react-router-dom';
import { BackstageTheme } from '@backstage/theme';
export function wrapInTestApp(
Component: ComponentType | ReactNode,
initialRouterEntries: string[] = ['/'],
) {
let Wrapper: ComponentType;
if (Component instanceof Function) {
Wrapper = Component;
} else {
Wrapper = (() => Component) as FunctionComponent;
}
return (
<MemoryRouter initialEntries={initialRouterEntries}>
<Route component={Wrapper} />
</MemoryRouter>
);
}
export function wrapInThemedTestApp(
component: ReactNode,
initialRouterEntries: string[] = ['/'],
) {
const themed = (
<ThemeProvider theme={BackstageTheme}>{component}</ThemeProvider>
);
return wrapInTestApp(themed, initialRouterEntries);
}
export const wrapInTheme = (component: ReactNode, theme = BackstageTheme) => (
<ThemeProvider theme={theme}>{component}</ThemeProvider>
);
+1 -63
View File
@@ -14,67 +14,5 @@
* limitations under the License.
*/
import React, {
ComponentType,
ReactNode,
FunctionComponent,
ReactElement,
} from 'react';
import { ThemeProvider } from '@material-ui/core';
import { act } from 'react-dom/test-utils';
import { render, RenderResult } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import { Route } from 'react-router-dom';
import { BackstageTheme } from '@backstage/theme';
export { default as Keyboard } from './Keyboard';
export { default as mockBreakpoint } from './mockBreakpoint';
export * from './logCollector';
export function wrapInTestApp(
Component: ComponentType | ReactNode,
initialRouterEntries: string[] = ['/'],
) {
let Wrapper: ComponentType;
if (Component instanceof Function) {
Wrapper = Component;
} else {
Wrapper = (() => Component) as FunctionComponent;
}
return (
<MemoryRouter initialEntries={initialRouterEntries}>
<Route component={Wrapper} />
</MemoryRouter>
);
}
export function wrapInThemedTestApp(
component: ReactNode,
initialRouterEntries: string[] = ['/'],
) {
const themed = (
<ThemeProvider theme={BackstageTheme}>{component}</ThemeProvider>
);
return wrapInTestApp(themed, initialRouterEntries);
}
export const wrapInTheme = (component: ReactNode, theme = BackstageTheme) => (
<ThemeProvider theme={theme}>{component}</ThemeProvider>
);
// Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async
// act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit
// cleaner, since act doesn't return the result of the evaluated function.
// https://github.com/testing-library/react-testing-library/issues/281
// https://github.com/facebook/react/pull/14853
export async function renderWithEffects(
nodes: ReactElement,
): Promise<RenderResult> {
let value: RenderResult;
await act(() => {
value = render(nodes);
});
// @ts-ignore
return value;
}
export * from './appWrappers';
@@ -1,80 +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.
*/
/* eslint-disable no-console */
/* eslint-disable no-param-reassign */
// If the callback function is async this one will be too.
export function withLogCollector(logsToCollect, callback) {
if (typeof logsToCollect === 'function') {
callback = logsToCollect;
logsToCollect = ['log', 'warn', 'error'];
}
const logs = {
log: [],
warn: [],
error: [],
};
const origLog = console.log;
const origWarn = console.warn;
const origError = console.error;
if (logsToCollect.includes('log')) {
console.log = message => {
logs.log.push(message);
};
}
if (logsToCollect.includes('warn')) {
console.warn = message => {
logs.warn.push(message);
};
}
if (logsToCollect.includes('error')) {
console.error = message => {
logs.error.push(message);
};
}
const restore = () => {
console.log = origLog;
console.warn = origWarn;
console.error = origError;
};
try {
const ret = callback();
if (!ret || !ret.then) {
restore();
return logs;
}
return ret.then(
() => {
restore();
return logs;
},
error => {
restore();
throw error;
},
);
} catch (error) {
restore();
throw error;
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -24,7 +24,7 @@
"clean": "backstage-cli clean"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.3",
"@backstage/cli": "^0.1.1-alpha.4",
"@material-ui/core": "^4.9.1"
},
"peerDependencies": {
+3 -1
View File
@@ -20,10 +20,11 @@ import { blue, yellow } from '@material-ui/core/colors';
import { BackstageMuiTheme, BackstageMuiThemeOptions } from './types';
export const COLORS = {
const COLORS = {
PAGE_BACKGROUND: '#F8F8F8',
DEFAULT_PAGE_THEME_COLOR: '#7C3699',
DEFAULT_PAGE_THEME_LIGHT_COLOR: '#ECDBF2',
SIDEBAR_BACKGROUND_COLOR: '#171717',
ERROR_BACKGROUND_COLOR: '#FFEBEE',
ERROR_TEXT_COLOR: '#CA001B',
INFO_TEXT_COLOR: '#004e8a',
@@ -86,6 +87,7 @@ const extendedThemeConfig: BackstageMuiThemeOptions = {
linkHover: COLORS.LINK_TEXT_HOVER,
link: COLORS.LINK_TEXT,
gold: yellow.A700,
sidebar: COLORS.SIDEBAR_BACKGROUND_COLOR,
},
navigation: {
width: 220,
+3 -2
View File
@@ -20,11 +20,11 @@ import { blue, yellow } from '@material-ui/core/colors';
import { BackstageMuiTheme, BackstageMuiThemeOptions } from './types';
export const COLORS = {
const COLORS = {
PAGE_BACKGROUND: '#282828',
EFAULT_PAGE_THEME_COLOR: '#232323',
DEFAULT_PAGE_THEME_COLOR: '#7C3699',
DEFAULT_PAGE_THEME_LIGHT_COLOR: '#ECDBF2',
SIDEBAR_BACKGROUND_COLOR: '#424242',
ERROR_BACKGROUND_COLOR: '#FFEBEE',
ERROR_TEXT_COLOR: '#CA001B',
INFO_TEXT_COLOR: '#004e8a',
@@ -91,6 +91,7 @@ const extendedThemeConfig: BackstageMuiThemeOptions = {
linkHover: COLORS.LINK_TEXT_HOVER,
link: COLORS.LINK_TEXT,
gold: yellow.A700,
sidebar: COLORS.SIDEBAR_BACKGROUND_COLOR,
},
navigation: {
width: 220,
+3 -1
View File
@@ -20,10 +20,11 @@ import { blue, yellow } from '@material-ui/core/colors';
import { BackstageMuiTheme, BackstageMuiThemeOptions } from './types';
export const COLORS = {
const COLORS = {
PAGE_BACKGROUND: '#F8F8F8',
DEFAULT_PAGE_THEME_COLOR: '#7C3699',
DEFAULT_PAGE_THEME_LIGHT_COLOR: '#ECDBF2',
SIDEBAR_BACKGROUND_COLOR: '#171717',
ERROR_BACKGROUND_COLOR: '#FFEBEE',
ERROR_TEXT_COLOR: '#CA001B',
INFO_TEXT_COLOR: '#004e8a',
@@ -89,6 +90,7 @@ const extendedThemeConfig: BackstageMuiThemeOptions = {
linkHover: COLORS.LINK_TEXT_HOVER,
link: COLORS.LINK_TEXT,
gold: yellow.A700,
sidebar: COLORS.SIDEBAR_BACKGROUND_COLOR,
},
navigation: {
width: 220,
+1 -1
View File
@@ -15,4 +15,4 @@
*/
export { default as BackstageThemeLight } from './BackstageThemeLight';
export { default as BackstageThemeDark } from './BackstageThemeDark';
export { default as BackstageTheme, COLORS } from './BackstageTheme';
export { default as BackstageTheme } from './BackstageTheme';
+1
View File
@@ -38,6 +38,7 @@ export type BackstageMuiPalette = Theme['palette'] & {
linkHover: string;
link: string;
gold: string;
sidebar: string;
bursts: {
fontColor: string;
slackChannelText: string;