diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index d571703294..dad8437613 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -6,6 +6,7 @@ on: - '.github/workflows/cli.yml' - 'packages/cli/**' - 'packages/core/**' + - 'scripts/**' jobs: build: @@ -40,19 +41,31 @@ jobs: - name: yarn install run: yarn install --frozen-lockfile - run: yarn build - # This creates a new plugin and pollutes the workspace, so it should be run last. - - name: verify app serve and plugin creation on Windows + # generate temp directory + - name: generate tempdir + id: generate_tempdir + run: echo "::set-output name=tempdir::$(node scripts/generateTempDir.js)" + # This creates a new app and plugin which pollutes the workspace, so it should be run last. + - name: verify app and plugin creation on Windows + working-directory: ${{ steps.generate_tempdir.outputs.tempdir }} if: runner.os == 'Windows' - run: node scripts/cli-e2e-test.js - - name: verify app serve and plugin creation on Linux + run: node ${{ github.workspace }}/scripts/cli-e2e-test.js + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: verify app and plugin creation on Linux + working-directory: ${{ steps.generate_tempdir.outputs.tempdir }} if: runner.os == 'Linux' run: | sudo sysctl fs.inotify.max_user_watches=524288 - node scripts/cli-e2e-test.js - - name: yarn lint, test after plugin creation - working-directory: plugins/test-plugin - run: | - yarn lint - yarn test + node ${{ github.workspace }}/scripts/cli-e2e-test.js env: - CI: true + BACKSTAGE_E2E_CLI_TEST: true + # This should lint and test both an app and a plugin + - name: yarn lint, test after creation + working-directory: ${{ steps.generate_tempdir.outputs.tempdir }} + run: | + cd test-app + yarn lint:all + yarn test:all + env: + BACKSTAGE_E2E_CLI_TEST: true diff --git a/package.json b/package.json index ff38d7f9ad..f5c6fdcf60 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "yarn workspace example-app start", "bundle": "yarn build && yarn workspace example-app bundle", "build": "lerna run build", + "clean": "lerna run clean", "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 --", diff --git a/packages/app/package.json b/packages/app/package.json index 409abb6e46..fd9aa88323 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -30,6 +30,7 @@ "scripts": { "start": "backstage-cli app:serve", "bundle": "backstage-cli app:build", + "clean": "backstage-cli clean", "test": "backstage-cli test", "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", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 7f203db196..3b0a8c894b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -20,7 +20,7 @@ import { Theme, ThemeProvider, } from '@material-ui/core'; -import { BackstageThemeLight, BackstageThemeDark } from '@backstage/theme'; +import { lightTheme, darkTheme } from '@backstage/theme'; import { createApp } from '@backstage/core'; import React, { FC } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -53,30 +53,28 @@ app.registerApis(apis); app.registerPlugin(...Object.values(plugins)); const AppComponent = app.build(); -type T = typeof BackstageThemeLight | typeof BackstageThemeDark; - const App: FC<{}> = () => { useStyles(); const [theme, toggleTheme] = useThemeType( localStorage.getItem('theme') || 'auto', ); - let backstageTheme: T = BackstageThemeLight; + let backstageTheme = lightTheme; switch (theme) { case 'light': - backstageTheme = BackstageThemeLight; + backstageTheme = lightTheme; break; case 'dark': - backstageTheme = BackstageThemeDark; + backstageTheme = darkTheme; break; default: if (!window.matchMedia) { - backstageTheme = BackstageThemeLight; + backstageTheme = lightTheme; break; } backstageTheme = window.matchMedia('(prefers-color-scheme: dark)').matches - ? BackstageThemeDark - : BackstageThemeLight; + ? darkTheme + : lightTheme; break; } diff --git a/packages/cli/bin/backstage-cli b/packages/cli/bin/backstage-cli index a8ba74f28a..5288f913d9 100755 --- a/packages/cli/bin/backstage-cli +++ b/packages/cli/bin/backstage-cli @@ -19,7 +19,8 @@ const path = require('path'); // Figure out whether we're running inside the backstage repo or as an installed dependency const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src')); -if (!isLocal) { + +if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { // src-relative imports are a pain to get to work with plain tsc compilation, as the // transpiled code will maintain the imports as they are in the source. Which means an // import for `helpers/paths` will start like that in the output, which won't work in NodeJS. diff --git a/packages/cli/package.json b/packages/cli/package.json index 1808b1239e..2a2adba6a8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,6 +22,7 @@ "build": "backstage-cli build-cache -- tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", + "clean": "backstage-cli clean", "start": "nodemon ." }, "devDependencies": { diff --git a/packages/cli/src/commands/build-cache/index.ts b/packages/cli/src/commands/build-cache/index.ts index a8a1a27e9d..a825a7f415 100644 --- a/packages/cli/src/commands/build-cache/index.ts +++ b/packages/cli/src/commands/build-cache/index.ts @@ -30,7 +30,7 @@ export async function withCache( buildFunc: () => Promise, ): Promise { const key = await Cache.readInputKey(options.inputs); - if (!key) { + if (!key || process.env.BACKSTAGE_E2E_CLI_TEST) { print('input directory is dirty, skipping cache'); await fs.remove(options.output); await buildFunc(); diff --git a/packages/cli/src/commands/clean/clean.ts b/packages/cli/src/commands/clean/clean.ts new file mode 100644 index 0000000000..f18250d210 --- /dev/null +++ b/packages/cli/src/commands/clean/clean.ts @@ -0,0 +1,33 @@ +/* + * 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 fs from 'fs-extra'; +import { resolve as resolvePath, relative as relativePath } from 'path'; +import { getDefaultCacheOptions } from 'commands/build-cache/options'; +import { paths } from 'helpers/paths'; + +export default async function clean() { + const cacheOptions = getDefaultCacheOptions(); + const packagePath = getPackagePath(cacheOptions.cacheDir); + await fs.remove(cacheOptions.output); + await fs.remove(packagePath); +} + +function getPackagePath(cacheDir: string) { + const relativePackagePath = relativePath(paths.targetRoot, paths.targetDir); + const packagePath = resolvePath(cacheDir, relativePackagePath); + return packagePath; +} diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index 5863d9e705..219406704d 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -86,6 +86,34 @@ export async function moveApp( }); } +async function addPackageResolutions(rootDir: string, appDir: string) { + process.chdir(appDir); + + const packageFileContent = await fs.readFile('package.json', 'utf-8'); + const packageFileJson = JSON.parse(packageFileContent); + + if (packageFileJson.resolutions) { + throw new Error('package.json already contains resolutions'); + } + packageFileJson.resolutions = {}; + + const packages = ['cli', 'core', 'test-utils', 'test-utils-core', 'theme']; + + for (const pkg of packages) { + await Task.forItem('adding', `${pkg} link to package.json`, async () => { + const pkgPath = require('path').join(rootDir, 'packages', pkg); + packageFileJson.resolutions[`@backstage/${pkg}`] = `file:${pkgPath}`; + const newContents = `${JSON.stringify(packageFileJson, null, 2)}\n`; + + await fs.writeFile('package.json', newContents, 'utf-8').catch(error => { + throw new Error( + `Failed to add resolutions to package.json: ${error.message}`, + ); + }); + }); + } +} + export default async () => { const questions: Question[] = [ { @@ -126,6 +154,15 @@ export default async () => { Task.section('Moving to final location'); await moveApp(tempDir, appDir, answers.name); + // e2e testing needs special treatment + if (process.env.BACKSTAGE_E2E_CLI_TEST) { + Task.section('Linking packages locally for e2e tests'); + const rootDir = process.env.CI + ? resolvePath(process.env.GITHUB_WORKSPACE!) + : resolvePath(__dirname, '..', '..', '..'); + await addPackageResolutions(rootDir, appDir); + } + Task.section('Building the app'); await buildApp(appDir); @@ -134,6 +171,7 @@ export default async () => { chalk.green(`🥇 Successfully created ${chalk.cyan(answers.name)}`), ); Task.log(); + Task.exit(); } catch (error) { Task.error(error.message); @@ -143,5 +181,6 @@ export default async () => { Task.section('Cleanup'); await cleanUp(tempDir); Task.error('🔥 Failed to create app!'); + Task.exit(1); } }; diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 54aafd69bf..ac200d01f1 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -268,6 +268,7 @@ export default async () => { )}`, ); Task.log(); + Task.exit(); } catch (error) { Task.error(error.message); @@ -277,5 +278,6 @@ export default async () => { Task.section('Cleanup'); await cleanUp(tempDir); Task.error('🔥 Failed to create plugin!'); + Task.exit(1); } }; diff --git a/packages/cli/src/commands/plugin/rollup.config.ts b/packages/cli/src/commands/plugin/rollup.config.ts index fe2675e0e0..c991fa3efb 100644 --- a/packages/cli/src/commands/plugin/rollup.config.ts +++ b/packages/cli/src/commands/plugin/rollup.config.ts @@ -46,6 +46,7 @@ export default { json(), typescript({ include: `${paths.resolveTarget('src')}/**/*.{js,jsx,ts,tsx}`, + clean: true, }), ], } as RollupWatchOptions; diff --git a/packages/cli/src/commands/watch-deps/index.ts b/packages/cli/src/commands/watch-deps/index.ts index 11093d7387..68d2417e36 100644 --- a/packages/cli/src/commands/watch-deps/index.ts +++ b/packages/cli/src/commands/watch-deps/index.ts @@ -23,6 +23,7 @@ import { startCompiler } from './compiler'; import { startChild } from './child'; import { waitForExit, run } from 'helpers/run'; import { paths } from 'helpers/paths'; +import { Command } from 'commander'; const PACKAGE_BLACKLIST = [ // We never want to watch for changes in the cli, but all packages will depend on it. @@ -88,8 +89,14 @@ export async function watchDeps(options: Options = {}) { * and instead start up watch mode for that package. Starting watch mode means running the first * available yarn script out of "build:watch", "watch", or "build" --watch. */ -export default async (_command: any, args: string[]) => { - await watchDeps(); +export default async (cmd: Command, args: string[]) => { + const options: Options = {}; + + if (cmd.build) { + options.build = true; + } + + await watchDeps(options); if (args?.length) { await waitForExit(startChild(args)); diff --git a/packages/cli/src/helpers/paths.ts b/packages/cli/src/helpers/paths.ts index 746b3f6ea8..173e77e1c5 100644 --- a/packages/cli/src/helpers/paths.ts +++ b/packages/cli/src/helpers/paths.ts @@ -53,7 +53,7 @@ export function findRootPath(topPath: string): string { try { const contents = fs.readFileSync(packagePath, 'utf8'); const data = JSON.parse(contents); - if (data.name === 'root') { + if (data.name === 'root' || data.name.includes('backstage-e2e')) { return path; } } catch (error) { diff --git a/packages/cli/src/helpers/tasks.ts b/packages/cli/src/helpers/tasks.ts index 3cba4d262f..0753301b78 100644 --- a/packages/cli/src/helpers/tasks.ts +++ b/packages/cli/src/helpers/tasks.ts @@ -37,6 +37,10 @@ export class Task { process.stdout.write(`\n ${title}\n`); } + static exit(code: number = 0) { + process.exit(code); + } + static async forItem( task: string, item: string, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 531561acce..3ad43faa76 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -77,6 +77,7 @@ const main = (argv: string[]) => { program .command('watch-deps') + .option('--build', 'Build all dependencies on startup') .description('Watch all dependencies while running another command') .action(actionHandler(() => require('commands/watch-deps'))); @@ -97,6 +98,11 @@ const main = (argv: string[]) => { ) .action(actionHandler(() => require('commands/build-cache'))); + program + .command('clean') + .description('Delete cache directories') + .action(actionHandler(() => require('commands/clean/clean'))); + program.on('command:*', () => { console.log(); console.log( diff --git a/packages/cli/templates/default-app/package.json.hbs b/packages/cli/templates/default-app/package.json.hbs index 7b5fab5221..231194a54e 100644 --- a/packages/cli/templates/default-app/package.json.hbs +++ b/packages/cli/templates/default-app/package.json.hbs @@ -13,7 +13,8 @@ "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" + "create-plugin": "backstage-cli create-plugin", + "clean": "lerna run clean" }, "workspaces": { "packages": [ diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs index a5b2b07b96..940091380a 100644 --- a/packages/cli/templates/default-app/packages/app/package.json.hbs +++ b/packages/cli/templates/default-app/packages/app/package.json.hbs @@ -5,6 +5,7 @@ "dependencies": { "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", "@backstage/cli": "^{{version}}", "@backstage/core": "^{{version}}", "@backstage/theme": "^{{version}}", @@ -17,7 +18,8 @@ "plugin-welcome": "0.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-router-dom": "^5.1.2" + "react-router-dom": "^5.1.2", + "react-use": "^13.24.0" }, "scripts": { "start": "backstage-cli app:serve", diff --git a/packages/cli/templates/default-app/packages/app/src/App.tsx b/packages/cli/templates/default-app/packages/app/src/App.tsx index ec8d8d435a..5fa952239a 100644 --- a/packages/cli/templates/default-app/packages/app/src/App.tsx +++ b/packages/cli/templates/default-app/packages/app/src/App.tsx @@ -1,6 +1,6 @@ import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core'; import { createApp } from '@backstage/core'; -import { BackstageTheme } from '@backstage/theme'; +import { lightTheme } from '@backstage/theme'; import React, { FC } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import * as plugins from './plugins'; @@ -31,7 +31,7 @@ const App: FC<{}> = () => { useStyles(); return ( - + diff --git a/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx index 6d9268fadd..27e44a3f75 100644 --- a/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx +++ b/packages/cli/templates/default-app/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx @@ -2,12 +2,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import WelcomePage from './WelcomePage'; import { ThemeProvider } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; +import { lightTheme } from '@backstage/theme'; describe('WelcomePage', () => { it('should render', () => { const rendered = render( - + , ); diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 49d8d20d71..32479b879b 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -8,7 +8,8 @@ "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", - "test": "backstage-cli test" + "test": "backstage-cli test", + "clean": "backstage-cli clean" }, "devDependencies": { "@backstage/cli": "^{{version}}", diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 80b97169bf..dff57e66a4 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -19,13 +19,13 @@ import { render } from '@testing-library/react'; import mockFetch from 'jest-fetch-mock'; import ExampleComponent from './ExampleComponent'; import { ThemeProvider } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; +import { lightTheme } from '@backstage/theme'; describe('ExampleComponent', () => { it('should render', () => { mockFetch.mockResponse(() => new Promise(() => {})); const rendered = render( - + , ); diff --git a/packages/core/package.json b/packages/core/package.json index 94195942f1..609f00e2d0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -21,7 +21,8 @@ "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", - "test": "backstage-cli test" + "test": "backstage-cli test", + "clean": "backstage-cli clean" }, "dependencies": { "@material-ui/core": "^4.9.1", @@ -42,9 +43,9 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.4", + "@backstage/test-utils": "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", diff --git a/packages/core/src/components/CircleProgress.tsx b/packages/core/src/components/CircleProgress.tsx index 4d9c5c3340..9290132eec 100644 --- a/packages/core/src/components/CircleProgress.tsx +++ b/packages/core/src/components/CircleProgress.tsx @@ -19,7 +19,7 @@ import { BackstageTheme } from '@backstage/theme'; import { Circle } from 'rc-progress'; import React, { FC } from 'react'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles(theme => ({ root: { position: 'relative', lineHeight: 0, diff --git a/packages/core/src/components/Status/Status.tsx b/packages/core/src/components/Status/Status.tsx index d03dc52b6e..0d02d86fb1 100644 --- a/packages/core/src/components/Status/Status.tsx +++ b/packages/core/src/components/Status/Status.tsx @@ -19,7 +19,7 @@ import { BackstageTheme } from '@backstage/theme'; import classNames from 'classnames'; import React, { FC } from 'react'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles(theme => ({ status: { width: 12, height: 12, diff --git a/packages/core/src/components/WarningPanel/WarningPanel.test.js b/packages/core/src/components/WarningPanel/WarningPanel.test.tsx similarity index 100% rename from packages/core/src/components/WarningPanel/WarningPanel.test.js rename to packages/core/src/components/WarningPanel/WarningPanel.test.tsx diff --git a/packages/core/src/components/WarningPanel/WarningPanel.js b/packages/core/src/components/WarningPanel/WarningPanel.tsx similarity index 64% rename from packages/core/src/components/WarningPanel/WarningPanel.js rename to packages/core/src/components/WarningPanel/WarningPanel.tsx index 679f7511d8..1d3b3e03db 100644 --- a/packages/core/src/components/WarningPanel/WarningPanel.js +++ b/packages/core/src/components/WarningPanel/WarningPanel.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Typography, withStyles } from '@material-ui/core'; +import React, { FC } from 'react'; +import { Typography, withStyles, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; const errorOutlineStyles = theme => ({ @@ -27,7 +27,7 @@ const errorOutlineStyles = theme => ({ }); const ErrorOutlineStyled = withStyles(errorOutlineStyles)(ErrorOutline); -const styles = theme => ({ +const useStyles = makeStyles(theme => ({ message: { display: 'flex', flexDirection: 'column', @@ -47,34 +47,35 @@ const styles = theme => ({ 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 ( -
-
- - - {title} - -
- {message && ( - {message} - )} - {children} +type Props = { + message?: React.ReactNode; + title?: string; +}; + +const WarningPanel: FC = props => { + const classes = useStyles(props); + const { title, message, children } = props; + return ( +
+
+ + + {title} +
- ); - } -} + {message && ( + {message} + )} + {children} +
+ ); +}; -export default withStyles(styles)(WarningPanel); +export default WarningPanel; diff --git a/packages/core/src/components/WarningPanel/index.js b/packages/core/src/components/WarningPanel/index.ts similarity index 100% rename from packages/core/src/components/WarningPanel/index.js rename to packages/core/src/components/WarningPanel/index.ts diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index 5ed929d9a0..94de84ad8c 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -23,7 +23,7 @@ import { Theme } from 'layout/Page/Page'; // import { Link } from 'shared/components'; import Waves from './Waves'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles(theme => ({ header: { gridArea: 'pageHeader', padding: theme.spacing(3), diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx index 4cf3db63ab..b32101a270 100644 --- a/packages/core/src/layout/Sidebar/Bar.tsx +++ b/packages/core/src/layout/Sidebar/Bar.tsx @@ -20,7 +20,7 @@ import React, { FC, useRef, useState } from 'react'; import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles(theme => ({ root: { zIndex: 1000, position: 'relative', diff --git a/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx b/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx index d670b0b908..cf9fa6e6c6 100644 --- a/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx +++ b/packages/core/src/layout/TabbedCard/TabbedCard.stories.tsx @@ -15,19 +15,37 @@ */ import React, { useState } from 'react'; import { TabbedCard, CardTab } from '.'; +import { Grid } from '@material-ui/core'; + +const cardContentStyle = { height: 200, width: 500 }; export default { title: 'Tabbed Card', component: TabbedCard, + decorators: [ + storyFn => ( + + {storyFn()} + + ), + ], }; export const Default = () => { return ( - some content 1 - some content 2 - some content 3 - some content 4 + +
Some content
+
+ +
Some content 2
+
+ +
Some content 3
+
+ +
Some content 4
+
); }; @@ -37,10 +55,18 @@ const linkInfo = { title: 'Go to XYZ Location', link: '#' }; export const WithFooterLink = () => { return ( - some content 1 - some content 2 - some content 3 - some content 4 + +
Some content
+
+ +
Some content 2
+
+ +
Some content 3
+
+ +
Some content 4
+
); }; @@ -60,16 +86,16 @@ export const WithControlledTabValue = () => { title="Controlled Value Example" > - some content 1 +
Some content
- some content 2 +
Some content 2
- some content 3 +
Some content 3
- some content 4 +
Some content 4
diff --git a/packages/storybook/.storybook/config.js b/packages/storybook/.storybook/config.js index b083bc0e9e..b9962b5bd3 100644 --- a/packages/storybook/.storybook/config.js +++ b/packages/storybook/.storybook/config.js @@ -1,10 +1,10 @@ import React from 'react'; import { addDecorator } from '@storybook/react'; -import { BackstageTheme } from '@backstage/theme'; +import { lightTheme } from '@backstage/theme'; import { CssBaseline, ThemeProvider } from '@material-ui/core'; addDecorator(story => ( - + {story()} )); diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index f086fa0d4a..530cd161ec 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -13,7 +13,7 @@ module.exports = { webpackFinal: async config => { config.resolve.alias = { ...config.resolve.alias, - '@backstage/theme': path.resolve(__dirname, '../../theme/src'), + '@backstage/theme': path.resolve(__dirname, '../../theme'), }; config.resolve.modules.push(path.resolve(__dirname, '../../core/src')); config.module.rules.push( diff --git a/packages/storybook/README.md b/packages/storybook/README.md index 2e716a5466..66768c0fe5 100644 --- a/packages/storybook/README.md +++ b/packages/storybook/README.md @@ -1,3 +1,7 @@ # storybook This package provides a storybook build for Backstage. See [storybook.backstage.io](http://storybook.backstage.io) + +## Why is this not part of `@backstage/core`? + +This separate storybook package exists because of dependency conflicts with `@backstage/cli`. It uses nohoist to avoid the conflicts, and since you can only use that in private packages it has to be separated out of `@backstage/core`. diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 8b54b2b8e1..56ac5c6264 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -4,14 +4,18 @@ "description": "Storybook build for core package", "private": true, "scripts": { - "start": "start-storybook -p 6006", - "build-storybook": "build-storybook --output-dir dist" + "start": "backstage-cli watch-deps --build -- start-storybook -p 6006", + "build-storybook": "backstage-cli watch-deps --build -- build-storybook --output-dir dist" }, "workspaces": { "nohoist": [ - "@storybook/**" + "@storybook/react/**", + "@storybook/addons/**" ] }, + "dependencies": { + "@backstage/theme": "0.1.1-alpha.4" + }, "devDependencies": { "@storybook/addon-actions": "^5.3.17", "@storybook/addon-links": "^5.3.17", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 5b328f2a58..9eca38eeb5 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -21,7 +21,8 @@ "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", - "test": "backstage-cli test" + "test": "backstage-cli test", + "clean": "backstage-cli clean" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.4", diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 93f92ec537..06d8781669 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -18,7 +18,7 @@ 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'; +import { lightTheme } from '@backstage/theme'; export function wrapInTestApp( Component: ComponentType | ReactNode, @@ -42,12 +42,10 @@ export function wrapInThemedTestApp( component: ReactNode, initialRouterEntries: string[] = ['/'], ) { - const themed = ( - {component} - ); + const themed = {component}; return wrapInTestApp(themed, initialRouterEntries); } -export const wrapInTheme = (component: ReactNode, theme = BackstageTheme) => ( +export const wrapInTheme = (component: ReactNode, theme = lightTheme) => ( {component} ); diff --git a/packages/theme/package.json b/packages/theme/package.json index 2f8a213478..6504d7dea5 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -20,7 +20,8 @@ "types": "dist/index.d.ts", "scripts": { "build": "backstage-cli plugin:build", - "lint": "backstage-cli lint" + "lint": "backstage-cli lint", + "clean": "backstage-cli clean" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.4", diff --git a/packages/theme/src/BackstageTheme.ts b/packages/theme/src/BackstageTheme.ts deleted file mode 100644 index 4f61437400..0000000000 --- a/packages/theme/src/BackstageTheme.ts +++ /dev/null @@ -1,260 +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 { createMuiTheme } from '@material-ui/core'; -import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; -import { blue, yellow } from '@material-ui/core/colors'; - -import { BackstageMuiTheme, BackstageMuiThemeOptions } from './types'; - -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', - LINK_TEXT: '#0A6EBE', - LINK_TEXT_HOVER: '#2196F3', - NAMED: { - WHITE: '#FEFEFE', - }, - STATUS: { - OK: '#1db855', - WARNING: '#f49b20', - ERROR: '#CA001B', - }, -}; - -const extendedThemeConfig: BackstageMuiThemeOptions = { - props: { - MuiGrid: { - spacing: 2, - }, - MuiSwitch: { - color: 'primary', - }, - }, - palette: { - background: { - default: COLORS.PAGE_BACKGROUND, - // @ts-ignore - 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: { - default: COLORS.DEFAULT_PAGE_THEME_COLOR, - }, - }, - // @ts-ignore - 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, - sidebar: COLORS.SIDEBAR_BACKGROUND_COLOR, - }, - navigation: { - width: 220, - background: '#333333', - }, - typography: { - fontFamily: '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif', - h5: { - fontWeight: 700, - }, - h4: { - fontWeight: 700, - fontSize: 28, - marginBottom: 6, - }, - h3: { - fontSize: 32, - fontWeight: 700, - marginBottom: 6, - }, - h2: { - fontSize: 40, - fontWeight: 700, - marginBottom: 8, - }, - h1: { - fontSize: 54, - fontWeight: 700, - marginBottom: 10, - }, - }, -}; - -const createOverrides = ( - theme: BackstageMuiTheme, -): Partial => { - return { - overrides: { - MuiTableRow: { - // Alternating row backgrounds - root: { - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.background.default, - }, - }, - // Use pointer for hoverable rows - hover: { - '&:hover': { - cursor: 'pointer', - }, - }, - // Alternating head backgrounds - head: { - '&:nth-of-type(odd)': { - backgroundColor: COLORS.NAMED.WHITE, - }, - }, - }, - // Tables are more dense than default mui tables - 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: { - // Tabs are smaller than default mui tab rows - root: { - minHeight: 24, - }, - }, - MuiTab: { - // Tabs are smaller and have a hover background - 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: { - // No color change on hover, just rely on the arrow showing up instead. - root: { - color: 'inherit', - '&:hover': { - color: 'inherit', - }, - '&:focus': { - color: 'inherit', - }, - }, - // Bold font for highlighting selected column - active: { - fontWeight: 'bold', - color: 'inherit', - }, - }, - MuiListItemText: { - dense: { - // Default dense list items to adding ellipsis for really long str... - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, - MuiButton: { - text: { - // Text buttons have less padding by default, but we want to keep the original padding - padding: undefined, - }, - }, - MuiChip: { - root: { - // By default there's no margin, but it's usually wanted, so we add some trailing margin - marginRight: theme.spacing(1), - marginBottom: theme.spacing(1), - }, - }, - MuiCardHeader: { - root: { - // Reduce padding between header and content - paddingBottom: 0, - }, - }, - MuiCardActions: { - root: { - // We default to putting the card actions at the end - justifyContent: 'flex-end', - }, - }, - }, - }; -}; - -const extendedTheme = createMuiTheme(extendedThemeConfig) as BackstageMuiTheme; - -// 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: BackstageMuiTheme = { - ...extendedTheme, - ...createOverrides(extendedTheme), -}; - -// Temporary workaround for files incorrectly importing the theme directly -export const V1 = BackstageTheme; -export default BackstageTheme; diff --git a/packages/theme/src/BackstageThemeDark.ts b/packages/theme/src/BackstageThemeDark.ts deleted file mode 100644 index 9952a5eeaa..0000000000 --- a/packages/theme/src/BackstageThemeDark.ts +++ /dev/null @@ -1,273 +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 { createMuiTheme } from '@material-ui/core'; -import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; -import { blue, yellow } from '@material-ui/core/colors'; - -import { BackstageMuiTheme, BackstageMuiThemeOptions } from './types'; - -const COLORS = { - PAGE_BACKGROUND: '#282828', - 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', - LINK_TEXT: '#0A6EBE', - LINK_TEXT_HOVER: '#2196F3', - NAMED: { - WHITE: '#FEFEFE', - }, - STATUS: { - OK: '#1db855', - WARNING: '#f49b20', - ERROR: '#CA001B', - }, -}; - -const extendedThemeConfig: BackstageMuiThemeOptions = { - props: { - MuiGrid: { - spacing: 2, - }, - MuiSwitch: { - color: 'primary', - }, - }, - palette: { - background: { - default: COLORS.PAGE_BACKGROUND, - // @ts-ignore - informational: '#60a3cb', - }, - color: { - default: '#fff', - }, - type: 'dark', - 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: { - default: COLORS.DEFAULT_PAGE_THEME_COLOR, - }, - }, - // @ts-ignore - 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, - sidebar: COLORS.SIDEBAR_BACKGROUND_COLOR, - }, - navigation: { - width: 220, - background: '#333333', - }, - typography: { - fontFamily: '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif', - h5: { - fontWeight: 700, - }, - h4: { - fontWeight: 700, - fontSize: 28, - marginBottom: 6, - }, - h3: { - fontSize: 32, - fontWeight: 700, - marginBottom: 6, - }, - h2: { - fontSize: 40, - fontWeight: 700, - marginBottom: 8, - }, - h1: { - fontSize: 54, - fontWeight: 700, - marginBottom: 10, - }, - }, -}; - -const createOverrides = (theme: BackstageMuiTheme): BackstageMuiTheme => { - return { - overrides: { - // @ts-ignore - MuiCSSBaseline: { - '@global': { - body: { - backgroundColor: theme.palette.background.default, - // @ts-ignore - color: theme.palette.color.default, - }, - }, - }, - MuiTableRow: { - // Alternating row backgrounds - root: { - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.background.default, - }, - }, - // Use pointer for hoverable rows - hover: { - '&:hover': { - cursor: 'pointer', - }, - }, - // Alternating head backgrounds - head: { - '&:nth-of-type(odd)': { - backgroundColor: COLORS.NAMED.WHITE, - }, - }, - }, - // Tables are more dense than default mui tables - 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: { - // Tabs are smaller than default mui tab rows - root: { - minHeight: 24, - }, - }, - MuiTab: { - // Tabs are smaller and have a hover background - 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: { - // No color change on hover, just rely on the arrow showing up instead. - root: { - color: 'inherit', - '&:hover': { - color: 'inherit', - }, - '&:focus': { - color: 'inherit', - }, - }, - // Bold font for highlighting selected column - active: { - fontWeight: 'bold', - color: 'inherit', - }, - }, - MuiListItemText: { - dense: { - // Default dense list items to adding ellipsis for really long str... - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, - MuiButton: { - text: { - // Text buttons have less padding by default, but we want to keep the original padding - padding: undefined, - }, - }, - MuiChip: { - root: { - // By default there's no margin, but it's usually wanted, so we add some trailing margin - marginRight: theme.spacing(1), - marginBottom: theme.spacing(1), - }, - }, - MuiCardHeader: { - root: { - // Reduce padding between header and content - paddingBottom: 0, - }, - }, - MuiCardActions: { - root: { - // We default to putting the card actions at the end - justifyContent: 'flex-end', - }, - }, - }, - }; -}; - -const extendedTheme = createMuiTheme(extendedThemeConfig) as BackstageMuiTheme; - -// 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 BackstageThemeDark = { - ...extendedTheme, - ...createOverrides(extendedTheme), -}; - -// Temporary workaround for files incorrectly importing the theme directly -export const V1 = BackstageThemeDark; - -export default BackstageThemeDark; diff --git a/packages/theme/src/BackstageThemeLight.ts b/packages/theme/src/BackstageThemeLight.ts deleted file mode 100644 index a0a063ad24..0000000000 --- a/packages/theme/src/BackstageThemeLight.ts +++ /dev/null @@ -1,274 +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 { createMuiTheme } from '@material-ui/core'; -import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; -import { blue, yellow } from '@material-ui/core/colors'; - -import { BackstageMuiTheme, BackstageMuiThemeOptions } from './types'; - -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', - LINK_TEXT: '#0A6EBE', - LINK_TEXT_HOVER: '#2196F3', - NAMED: { - WHITE: '#FEFEFE', - }, - STATUS: { - OK: '#1db855', - WARNING: '#f49b20', - ERROR: '#CA001B', - }, -}; - -const extendedThemeConfig: BackstageMuiThemeOptions = { - props: { - MuiGrid: { - spacing: 2, - }, - MuiSwitch: { - color: 'primary', - }, - }, - palette: { - background: { - default: COLORS.PAGE_BACKGROUND, - // @ts-ignore - informational: '#60a3cb', - }, - color: { - default: '#000', - }, - 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: { - default: COLORS.DEFAULT_PAGE_THEME_COLOR, - }, - }, - // @ts-ignore - 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, - sidebar: COLORS.SIDEBAR_BACKGROUND_COLOR, - }, - navigation: { - width: 220, - background: '#333333', - }, - typography: { - fontFamily: '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif', - h5: { - fontWeight: 700, - }, - h4: { - fontWeight: 700, - fontSize: 28, - marginBottom: 6, - }, - h3: { - fontSize: 32, - fontWeight: 700, - marginBottom: 6, - }, - h2: { - fontSize: 40, - fontWeight: 700, - marginBottom: 8, - }, - h1: { - fontSize: 54, - fontWeight: 700, - marginBottom: 10, - }, - }, -}; - -const createOverrides = ( - theme: BackstageMuiTheme, -): Partial => { - return { - overrides: { - // @ts-ignore - MuiCSSBaseline: { - '@global': { - body: { - backgroundColor: theme.palette.background.default, - // @ts-ignore - color: theme.palette.color.default, - }, - }, - }, - MuiTableRow: { - // Alternating row backgrounds - root: { - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.background.default, - }, - }, - // Use pointer for hoverable rows - hover: { - '&:hover': { - cursor: 'pointer', - }, - }, - // Alternating head backgrounds - head: { - '&:nth-of-type(odd)': { - backgroundColor: COLORS.NAMED.WHITE, - }, - }, - }, - // Tables are more dense than default mui tables - 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: { - // Tabs are smaller than default mui tab rows - root: { - minHeight: 24, - }, - }, - MuiTab: { - // Tabs are smaller and have a hover background - 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: { - // No color change on hover, just rely on the arrow showing up instead. - root: { - color: 'inherit', - '&:hover': { - color: 'inherit', - }, - '&:focus': { - color: 'inherit', - }, - }, - // Bold font for highlighting selected column - active: { - fontWeight: 'bold', - color: 'inherit', - }, - }, - MuiListItemText: { - dense: { - // Default dense list items to adding ellipsis for really long str... - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, - MuiButton: { - text: { - // Text buttons have less padding by default, but we want to keep the original padding - padding: undefined, - }, - }, - MuiChip: { - root: { - // By default there's no margin, but it's usually wanted, so we add some trailing margin - marginRight: theme.spacing(1), - marginBottom: theme.spacing(1), - }, - }, - MuiCardHeader: { - root: { - // Reduce padding between header and content - paddingBottom: 0, - }, - }, - MuiCardActions: { - root: { - // We default to putting the card actions at the end - justifyContent: 'flex-end', - }, - }, - }, - }; -}; - -const extendedTheme = createMuiTheme(extendedThemeConfig) as BackstageMuiTheme; - -// 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 BackstageThemeLight = { - ...extendedTheme, - ...createOverrides(extendedTheme), -}; - -// Temporary workaround for files incorrectly importing the theme directly -export const V1 = BackstageThemeLight; - -export default BackstageThemeLight; diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts new file mode 100644 index 0000000000..da9b3299a3 --- /dev/null +++ b/packages/theme/src/baseTheme.ts @@ -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 { createMuiTheme } from '@material-ui/core'; +import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; +import { Overrides } from '@material-ui/core/styles/overrides'; + +import { + BackstageTheme, + BackstageThemeOptions, + BackstagePaletteOptions, +} from './types'; + +export function createThemeOptions( + palette: BackstagePaletteOptions, +): BackstageThemeOptions { + return { + palette, + props: { + MuiGrid: { + spacing: 2, + }, + MuiSwitch: { + color: 'primary', + }, + }, + typography: { + fontFamily: '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif', + h5: { + fontWeight: 700, + }, + h4: { + fontWeight: 700, + fontSize: 28, + marginBottom: 6, + }, + h3: { + fontSize: 32, + fontWeight: 700, + marginBottom: 6, + }, + h2: { + fontSize: 40, + fontWeight: 700, + marginBottom: 8, + }, + h1: { + fontSize: 54, + fontWeight: 700, + marginBottom: 10, + }, + }, + }; +} + +export function createThemeOverrides(theme: BackstageTheme): Overrides { + return { + MuiTableRow: { + // Alternating row backgrounds + root: { + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.background.default, + }, + }, + // Use pointer for hoverable rows + hover: { + '&:hover': { + cursor: 'pointer', + }, + }, + // Alternating head backgrounds + head: { + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.background.paper, + }, + }, + }, + // Tables are more dense than default mui tables + 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: { + // Tabs are smaller than default mui tab rows + root: { + minHeight: 24, + }, + }, + MuiTab: { + // Tabs are smaller and have a hover background + 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: { + // No color change on hover, just rely on the arrow showing up instead. + root: { + color: 'inherit', + '&:hover': { + color: 'inherit', + }, + '&:focus': { + color: 'inherit', + }, + }, + // Bold font for highlighting selected column + active: { + fontWeight: 'bold', + color: 'inherit', + }, + }, + MuiListItemText: { + dense: { + // Default dense list items to adding ellipsis for really long str... + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, + MuiButton: { + text: { + // Text buttons have less padding by default, but we want to keep the original padding + padding: undefined, + }, + }, + MuiChip: { + root: { + // By default there's no margin, but it's usually wanted, so we add some trailing margin + marginRight: theme.spacing(1), + marginBottom: theme.spacing(1), + }, + }, + MuiCardHeader: { + root: { + // Reduce padding between header and content + paddingBottom: 0, + }, + }, + MuiCardActions: { + root: { + // We default to putting the card actions at the end + justifyContent: 'flex-end', + }, + }, + }; +} + +// Creates a Backstage MUI theme using a palette. +// The theme is created with the common Backstage options and component styles. +export function createTheme(palette: BackstagePaletteOptions): BackstageTheme { + const themeOptions = createThemeOptions(palette); + const baseTheme = createMuiTheme(themeOptions) as BackstageTheme; + const overrides = createThemeOverrides(baseTheme); + const theme = { ...baseTheme, overrides }; + return theme; +} diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts index 8aa7e30ef5..862f9b7755 100644 --- a/packages/theme/src/index.ts +++ b/packages/theme/src/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { default as BackstageThemeLight } from './BackstageThemeLight'; -export { default as BackstageThemeDark } from './BackstageThemeDark'; -export { default as BackstageTheme } from './BackstageTheme'; + +export * from './themes'; +export * from './baseTheme'; +export * from './types'; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts new file mode 100644 index 0000000000..eb36a37bf5 --- /dev/null +++ b/packages/theme/src/themes.ts @@ -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. + */ + +import { createTheme } from 'baseTheme'; +import { blue, yellow } from '@material-ui/core/colors'; + +export const lightTheme = createTheme({ + type: 'light', + background: { + default: '#F8F8F8', + }, + status: { + ok: '#1db855', + warning: '#f49b20', + error: '#CA001B', + running: '#BEBEBE', + pending: '#5BC0DE', + background: '#FEFEFE', + }, + bursts: { + fontColor: '#FEFEFE', + slackChannelText: '#ddd', + backgroundColor: { + default: '#7C3699', + }, + }, + primary: { + main: blue[500], + }, + border: '#E6E6E6', + textVerySubtle: '#DDD', + textSubtle: '#6E6E6E', + highlight: '#FFFBCC', + errorBackground: '#FFEBEE', + warningBackground: '#F59B23', + infoBackground: '#ebf5ff', + errorText: '#CA001B', + infoText: '#004e8a', + warningText: '#FEFEFE', + linkHover: '#2196F3', + link: '#0A6EBE', + gold: yellow.A700, + sidebar: '#171717', +}); + +export const darkTheme = createTheme({ + type: 'dark', + background: { + default: '#282828', + }, + status: { + ok: '#1db855', + warning: '#f49b20', + error: '#CA001B', + running: '#BEBEBE', + pending: '#5BC0DE', + background: '#FEFEFE', + }, + bursts: { + fontColor: '#FEFEFE', + slackChannelText: '#ddd', + backgroundColor: { + default: '#7C3699', + }, + }, + primary: { + main: blue[500], + }, + border: '#E6E6E6', + textVerySubtle: '#DDD', + textSubtle: '#6E6E6E', + highlight: '#FFFBCC', + errorBackground: '#FFEBEE', + warningBackground: '#F59B23', + infoBackground: '#ebf5ff', + errorText: '#CA001B', + infoText: '#004e8a', + warningText: '#FEFEFE', + linkHover: '#2196F3', + link: '#0A6EBE', + gold: yellow.A700, + sidebar: '#424242', +}); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index cbd3de4efa..f0e43c54d1 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -15,8 +15,12 @@ */ import { Theme, ThemeOptions } from '@material-ui/core'; +import { + PaletteOptions, + Palette, +} from '@material-ui/core/styles/createPalette'; -export type BackstageMuiPalette = Theme['palette'] & { +type PaletteAdditions = { status: { ok: string; warning: string; @@ -48,10 +52,13 @@ export type BackstageMuiPalette = Theme['palette'] & { }; }; -export interface BackstageMuiTheme extends Theme { - palette: BackstageMuiPalette; +export type BackstagePalette = Palette & PaletteAdditions; +export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions; + +export interface BackstageTheme extends Theme { + palette: BackstagePalette; } -export interface BackstageMuiThemeOptions extends ThemeOptions { - palette: Partial; +export interface BackstageThemeOptions extends ThemeOptions { + palette: BackstagePaletteOptions; } diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json index a25e0cdc58..65fb3cf9da 100644 --- a/plugins/home-page/package.json +++ b/plugins/home-page/package.json @@ -8,7 +8,8 @@ "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", - "test": "backstage-cli test" + "test": "backstage-cli test", + "clean": "backstage-cli clean" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.4", diff --git a/plugins/home-page/src/components/HomePage/HomePage.test.tsx b/plugins/home-page/src/components/HomePage/HomePage.test.tsx index 713f0afe1e..e7df794a00 100644 --- a/plugins/home-page/src/components/HomePage/HomePage.test.tsx +++ b/plugins/home-page/src/components/HomePage/HomePage.test.tsx @@ -18,12 +18,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import HomePage from './HomePage'; import { ThemeProvider } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; +import { lightTheme } from '@backstage/theme'; describe('HomePage', () => { it('should render', () => { const rendered = render( - + , ); diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index a342c45b82..442211a38c 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -9,7 +9,8 @@ "build:watch": "backstage-cli plugin:build --watch", "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", - "test": "backstage-cli test" + "test": "backstage-cli test", + "clean": "backstage-cli clean" }, "dependencies": { "react-markdown": "^4.3.1", diff --git a/plugins/lighthouse/src/components/CategoryTrendline/index.tsx b/plugins/lighthouse/src/components/CategoryTrendline/index.tsx index a46aff52e5..e3dbd186e0 100644 --- a/plugins/lighthouse/src/components/CategoryTrendline/index.tsx +++ b/plugins/lighthouse/src/components/CategoryTrendline/index.tsx @@ -18,10 +18,7 @@ import { Sparklines, SparklinesLine, SparklinesProps } from 'react-sparklines'; import { useTheme } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; -function color( - data: number[], - theme: typeof BackstageTheme, -): string | undefined { +function color(data: number[], theme: BackstageTheme): string | undefined { const lastNum = data[data.length - 1]; if (!lastNum) return undefined; if (lastNum >= 0.9) return theme.palette.status.ok; @@ -30,7 +27,7 @@ function color( } const CategoryTrendline: FC = props => { - const theme = useTheme(); + const theme = useTheme(); if (!props.data) return null; return ( diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index e0186f1cce..86cb0e3f45 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -8,7 +8,8 @@ "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", - "test": "backstage-cli test" + "test": "backstage-cli test", + "clean": "backstage-cli clean" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.4", diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx index 9b78726f0a..9cba76fddc 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import WelcomePage from './WelcomePage'; import { ThemeProvider } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; +import { lightTheme } from '@backstage/theme'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; describe('WelcomePage', () => { @@ -28,7 +28,7 @@ describe('WelcomePage', () => { - + , diff --git a/scripts/cli-e2e-test.js b/scripts/cli-e2e-test.js index 48058550d9..4af26e2e41 100644 --- a/scripts/cli-e2e-test.js +++ b/scripts/cli-e2e-test.js @@ -15,168 +15,75 @@ */ const { resolve: resolvePath } = require('path'); -const childProcess = require('child_process'); -const { spawn } = childProcess; const Browser = require('zombie'); -const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/; +const { + spawnPiped, + handleError, + waitForPageWithText, + waitForExit, + print, +} = require('./helpers'); + +const createTestApp = require('./createTestApp'); +const createTestPlugin = require('./createTestPlugin'); +const generateTempDir = require('./generateTempDir.js'); Browser.localhost('localhost', 3000); async function main() { - process.env.CI = 'true'; + process.env.BACKSTAGE_E2E_CLI_TEST = 'true'; - const projectDir = resolvePath(__dirname, '..'); - process.chdir(projectDir); + const rootDir = process.env.CI + ? resolvePath(process.env.GITHUB_WORKSPACE) + : resolvePath(__dirname, '..'); - const start = spawnPiped(['yarn', 'start']); + const tempDir = process.env.CI ? process.cwd() : await generateTempDir(); + + process.stdout.write(`Initial directory: ${process.cwd()}\n`); + process.chdir(tempDir); + process.stdout.write(`Temp directory: ${process.cwd()}\n`); + + await waitForExit(spawnPiped(['yarn', 'init --yes'])); + + const createCmdPath = require('path').join( + rootDir, + 'packages', + 'cli', + 'bin', + 'backstage-cli', + ); + await createTestApp(`${createCmdPath} create-app`); + + const appDir = resolvePath(tempDir, 'test-app'); + process.chdir(appDir); + process.stdout.write(`App directory: ${appDir}\n`); + + await createTestPlugin(); + + print('Starting the app'); + const startApp = spawnPiped(['yarn', 'start']); try { const browser = new Browser(); await waitForPageWithText(browser, '/', 'Welcome to Backstage'); - print('Backstage loaded correctly, creating plugin'); - - const createPlugin = spawnPiped(['yarn', 'create-plugin']); - - let stdout = ''; - createPlugin.stdout.on('data', data => { - stdout = stdout + data.toString('utf8'); - }); - - await waitFor(() => stdout.includes('Enter an ID for the plugin')); - createPlugin.stdin.write('test-plugin\n'); - - await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); - createPlugin.stdin.write('@someuser\n'); - - print('Waiting for plugin create script to be done'); - await waitForExit(createPlugin); - - print('Plugin create script is done, waiting for plugin page to load'); await waitForPageWithText( browser, '/test-plugin', 'Welcome to test-plugin!', ); - print('Test plugin loaded correctly, exiting'); + print('Both App and Plugin loaded correctly'); } finally { - start.kill(); + startApp.kill(); } - await waitForExit(start); + await waitForExit(startApp); + + print('All tests done'); process.exit(0); } -function waitFor(fn) { - return new Promise(resolve => { - const handle = setInterval(() => { - if (fn()) { - clearInterval(handle); - resolve(); - return; - } - }, 100); - }); -} - -function print(msg) { - return process.stdout.write(`${msg}\n`); -} - -async function waitForExit(child) { - if (child.exitCode !== null) { - throw new Error(`Child already exited with code ${child.exitCode}`); - } - await new Promise((resolve, reject) => - child.once('exit', code => { - if (code) { - reject(new Error(`Child exited with code ${code}`)); - } else { - resolve(); - } - }), - ); -} - -function spawnPiped(cmd, options) { - function pipeWithPrefix(stream, prefix = '') { - return data => { - const prefixedMsg = data - .toString('utf8') - .trimRight() - .replace(/^/gm, prefix); - stream.write(`${prefixedMsg}\n`, 'utf8'); - }; - } - - const child = spawn(cmd[0], cmd.slice(1), { - stdio: 'pipe', - shell: true, - ...options, - }); - child.on('error', handleError); - child.on('exit', code => { - if (code) { - print(`Child '${cmd.join(' ')}' exited with code ${code}`); - process.exit(code); - } - }); - child.stdout.on( - 'data', - pipeWithPrefix(process.stdout, `[${cmd.join(' ')}].out: `), - ); - child.stderr.on( - 'data', - pipeWithPrefix(process.stderr, `[${cmd.join(' ')}].err: `), - ); - - return child; -} - -async function waitForPageWithText( - browser, - path, - text, - { intervalMs = 1000, maxAttempts = 120 } = {}, -) { - let attempts = 0; - for (;;) { - try { - await new Promise(resolve => setTimeout(resolve, intervalMs)); - await browser.visit(path); - break; - } catch (error) { - if (error.message.match(EXPECTED_LOAD_ERRORS)) { - attempts++; - if (attempts > maxAttempts) { - throw new Error( - `Failed to load page '${path}', max number of attempts reached`, - ); - } - } else { - throw error; - } - } - } - - const escapedText = text.replace(/"/g, '\\"'); - browser.assert.evaluate( - `Array.from(document.querySelectorAll("*")).some(el => el.textContent === "${escapedText}")`, - true, - `expected to find text ${text}`, - ); -} - -function handleError(err) { - process.stdout.write(`${err.name}: ${err.stack || err.message}\n`); - if (typeof err.code === 'number') { - process.exit(err.code); - } else { - process.exit(1); - } -} - process.on('unhandledRejection', handleError); main(process.argv.slice(2)).catch(handleError); diff --git a/scripts/createTestApp.js b/scripts/createTestApp.js new file mode 100644 index 0000000000..80f7bf3b18 --- /dev/null +++ b/scripts/createTestApp.js @@ -0,0 +1,41 @@ +/* + * 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. + */ + +const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); + +async function createTestApp(cmd) { + print('Creating a Backstage App'); + const createApp = spawnPiped(['node', cmd]); + + try { + let stdout = ''; + createApp.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + + await waitFor(() => stdout.includes('Enter a name for the app')); + createApp.stdin.write('test-app\n'); + + print('Waiting for app create script to be done'); + await waitForExit(createApp); + + print('Test app created'); + } finally { + createApp.kill(); + } +} + +module.exports = createTestApp; diff --git a/scripts/createTestPlugin.js b/scripts/createTestPlugin.js new file mode 100644 index 0000000000..59e8d6dd77 --- /dev/null +++ b/scripts/createTestPlugin.js @@ -0,0 +1,44 @@ +/* + * 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. + */ + +const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); + +async function createTestPlugin() { + print('Creating a Backstage Plugin'); + const createPlugin = spawnPiped(['yarn', 'create-plugin']); + + try { + let stdout = ''; + createPlugin.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + + await waitFor(() => stdout.includes('Enter an ID for the plugin')); + createPlugin.stdin.write('test-plugin\n'); + + // await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); + // createPlugin.stdin.write('@someuser\n'); + + print('Waiting for plugin create script to be done'); + await waitForExit(createPlugin); + + print('Test plugin created'); + } finally { + createPlugin.kill(); + } +} + +module.exports = createTestPlugin; diff --git a/scripts/generateTempDir.js b/scripts/generateTempDir.js new file mode 100644 index 0000000000..e455a29320 --- /dev/null +++ b/scripts/generateTempDir.js @@ -0,0 +1,30 @@ +/* + * 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. + */ + +const { handleError } = require('./helpers'); + +async function generateTempDir() { + const tempDir = await require('fs-extra').mkdtemp( + require('path').join(require('os').tmpdir(), 'backstage-e2e-'), + ); + process.stdout.write(tempDir); + return tempDir; +} + +module.exports = generateTempDir; + +process.on('unhandledRejection', handleError); +generateTempDir().catch(handleError); diff --git a/scripts/helpers.js b/scripts/helpers.js new file mode 100644 index 0000000000..7e34f10a63 --- /dev/null +++ b/scripts/helpers.js @@ -0,0 +1,139 @@ +/* + * 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. + */ + +const childProcess = require('child_process'); +const { spawn } = childProcess; + +const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/; + +function spawnPiped(cmd, options) { + function pipeWithPrefix(stream, prefix = '') { + return data => { + const prefixedMsg = data + .toString('utf8') + .trimRight() + .replace(/^/gm, prefix); + stream.write(`${prefixedMsg}\n`, 'utf8'); + }; + } + + const child = spawn(cmd[0], cmd.slice(1), { + stdio: 'pipe', + shell: true, + ...options, + }); + child.on('error', handleError); + child.on('exit', code => { + if (code) { + print(`Child '${cmd.join(' ')}' exited with code ${code}`); + process.exit(code); + } + }); + child.stdout.on( + 'data', + pipeWithPrefix(process.stdout, `[${cmd.join(' ')}].out: `), + ); + child.stderr.on( + 'data', + pipeWithPrefix(process.stderr, `[${cmd.join(' ')}].err: `), + ); + + return child; +} + +function handleError(err) { + process.stdout.write(`${err.name}: ${err.stack || err.message}\n`); + if (typeof err.code === 'number') { + process.exit(err.code); + } else { + process.exit(1); + } +} + +function waitFor(fn) { + return new Promise(resolve => { + const handle = setInterval(() => { + if (fn()) { + clearInterval(handle); + resolve(); + return; + } + }, 100); + }); +} + +async function waitForExit(child) { + if (child.exitCode !== null) { + throw new Error(`Child already exited with code ${child.exitCode}`); + } + await new Promise((resolve, reject) => + child.once('exit', code => { + if (code) { + reject(new Error(`Child exited with code ${code}`)); + } else { + print('Child finished'); + resolve(); + } + }), + ); +} + +async function waitForPageWithText( + browser, + path, + text, + { intervalMs = 1000, maxAttempts = 240 } = {}, +) { + let attempts = 0; + for (;;) { + try { + await new Promise(resolve => setTimeout(resolve, intervalMs)); + await browser.visit(path); + break; + } catch (error) { + if (error.message.match(EXPECTED_LOAD_ERRORS)) { + attempts++; + if (attempts > maxAttempts) { + throw new Error( + `Failed to load page '${path}', max number of attempts reached`, + ); + } + } else { + throw error; + } + } + } + + const escapedText = text.replace(/"/g, '\\"'); + browser.assert.evaluate( + `Array.from(document.querySelectorAll("*")).some(el => el.textContent === "${escapedText}")`, + true, + `expected to find text ${text}`, + ); +} + +function print(msg) { + return process.stdout.write(`${msg}\n`); +} + +module.exports = { + spawnPiped, + handleError, + waitFor, + waitForExit, + waitForPageWithText, + print, +};