diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index d571703294..612375ca1c 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,27 @@ 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 + - name: verify app and plugin creation on Windows + working-directory: ${{ runner.temp }} if: runner.os == 'Windows' - run: node scripts/cli-e2e-test.js - - name: verify app serve and plugin creation on Linux + run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: verify app and plugin creation on Linux + working-directory: ${{ runner.temp }} 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 }}/packages/cli/e2e-test/cli-e2e-test.js env: - CI: true + BACKSTAGE_E2E_CLI_TEST: true + - name: lint newly created app and plugin + run: yarn lint:all + working-directory: ${{ runner.temp }}/test-app + env: + BACKSTAGE_E2E_CLI_TEST: true + - name: test newly created app and plugin + run: yarn test:all + working-directory: ${{ runner.temp }}/test-app + env: + BACKSTAGE_E2E_CLI_TEST: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..68fb208caa --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing + +Our vision for Backstage is for it to become the trusted standard toolbox (read: UX layer) for the open source infrastructure landscape. Think of it like Kubernetes for developer experience. We realize this is an ambitious goal. We can’t do it alone. + +Therefore we want to create strong community of contributors -- all working together to create the kind of delightful experience that our developers deserve. + +Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ❀️ + +# Types of Contributions + +## Report bugs + +No one likes bugs. Report bugs as an issue [here](https://github.com/spotify/backstage/issues/new?template=bug_template.md). + +## Fix bugs or build new features + +Look through the GitHub issues for [bugs](https://github.com/spotify/backstage/labels/bugs), [good first issues](https://github.com/spotify/backstage/labels/good%20first%20issue) or [help wanted](https://github.com/spotify/backstage/labels/help%20wanted). + +## Build a plugin + +The value of Backstage grows with every new plugin that gets added. Wouldn't it be fantastic if there was a plugin for every infrastructure project out there? We think so. And we would love your help. + +What kind of plugins should/could be created? Some inspiration from the 120+ plugins that we have developed inside Spotify can be found [here](https://backstage.io/demos), but we will keep a running list of suggestions labeled with [[plugin]](https://github.com/spotify/backstage/labels/plugin). + +A great reference example of a plugin can be found on [our blog](https://backstage.io/blog/2020/04/06/lighthouse-plugin) (thanks [@fastfrwrd](https://github.com/fastfrwrd)!) + +## Write Documentation + +The current documentation is very limited. Help us make the `/docs` folder come alive. + +## Contribute to Storybook + +We think the best way to ensure different plugins provide a consistent experience is through a solid set of reusable UI/UX components. Backstage uses [Storybook](http://storybook.backstage.io). + +Either help us [create new components](https://github.com/spotify/backstage/labels/help%20wanted) or improve stories for the existing ones (look for files with `*.stories.tsx`). + +## Submit Feedback + +The best way to send feedback is to file [an issue](https://github.com/spotify/backstage/issues). + +If you are proposing a feature: + +- Explain in detail how it would work. +- Keep the scope as narrow as possible, to make it easier to implement. +- Use appropriate labels +- Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +# Get Started! + +So...feel ready to jump in? Let's do this. Head over to the [Getting Started guide](https://github.com/spotify/backstage#getting-started) πŸ‘πŸ»πŸ’― + +If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2). + +# Code of Conduct + +This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code. + +[code-of-conduct]: https://github.com/spotify/backstage/blob/master/CODE_OF_CONDUCT.md + +# Security Issues? + +Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub. diff --git a/README.md b/README.md index c012638e7d..87a3de6138 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ ![](https://github.com/spotify/backstage/workflows/Frontend%20CI/badge.svg) [![Discord](https://img.shields.io/discord/687207715902193673)](https://discord.gg/EBHEGzX) ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) +[![](https://img.shields.io/npm/v/@backstage/core?label=Version)](https://github.com/spotify/backstage/releases) ## What is Backstage? @@ -59,23 +60,25 @@ To run a Backstage app, you will need to have the following installed: - [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12 - [yarn](https://classic.yarnpkg.com/en/docs/install) -Open a terminal window and start the web app using the following commands from the project root: +After cloning this repo, open a terminal window and start the web app using the following commands from the project root: ```bash -$ yarn install # may take a while - -$ yarn start +yarn install +yarn start ``` The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. -For more complex development environment configuration, see the -[Development Environment](docs/getting-started/development-environment.md) section of the Getting Started docs. +And thats it! You are good to go πŸ‘ + +### Next step + +Take a look at the [Getting Started](docs/getting-started/README.md) guide to learn more about how to extend the functionality with Plugins. ## Documentation -- [FAQs](docs/FAQ.md) - [Getting Started](docs/getting-started/README.md) +- [Create a Backstage App](docs/create-an-app.md) - [Architecture](docs/architecture-terminology.md) - [API references](docs/reference/README.md) - [Designing for Backstage](docs/design.md) @@ -95,6 +98,10 @@ For more complex development environment configuration, see the Or, if you are an open source developer and are interested in joining our team, please reach out to [foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com) +## Contributing + +We would love your help in building Backstage! See [CONTRIBUTING](CONTRIBUTING.md) for more information. + ## License Copyright 2020 Spotify AB. diff --git a/docs/getting-started/create-an-app.md b/docs/create-an-app.md similarity index 92% rename from docs/getting-started/create-an-app.md rename to docs/create-an-app.md index 703d1d59a5..8c8050c2ff 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/create-an-app.md @@ -17,7 +17,7 @@ npx @backstage/cli create-app This will create a new Backstage App inside the current folder. The name of the app-folder is the name that was provided when prompted.

- create app + create app

Inside that directory, it will generate all the files and folder structure needed for you to run your app. @@ -70,7 +70,3 @@ yarn start ``` _When `yarn start` is ready it should open up a browser window displaying your app, if not you can navigate to `http://localhost:3000`._ - -[Next Step - Create a Backstage plugin](create-a-plugin.md) - -[Back to Docs](README.md) diff --git a/docs/getting-started/create-app_output.png b/docs/create-app_output.png similarity index 100% rename from docs/getting-started/create-app_output.png rename to docs/create-app_output.png diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 163e52b950..064776e712 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -1,9 +1,10 @@ # Getting started with Backstage -Here is a collection of tutorials that will guide you through setting up and extending an instance of Backstage with your own plugins. +## Creating a Plugin + +The value of Backstage grows with every new plugin that gets added. Here is a collection of tutorials that will guide you through setting up and extending an instance of Backstage with your own plugins. - [Development Environment](development-environment.md) -- [Create a Backstage App](create-an-app.md) - [Create a Backstage plugin](create-a-plugin.md) - [Structure of a plugin](structure-of-a-plugin.md) - Using Backstage components (TODO) diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index e4fe69af68..7f6e2bcb44 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -34,4 +34,6 @@ Then open http://localhost/ on your browser. > See [package.json](/package.json) for other yarn commands/options. +[Next Step - Create a Backstage plugin](create-a-plugin.md) + [Back to Docs](README.md) diff --git a/lerna.json b/lerna.json index 2cc439d190..1d77db92fb 100644 --- a/lerna.json +++ b/lerna.json @@ -5,5 +5,5 @@ ], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.3" + "version": "0.1.1-alpha.4" } diff --git a/package.json b/package.json index 0387a4e7ce..8a6c58618f 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "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 --", "lint:all": "lerna run lint --", "docker-build": "yarn bundle && docker build . -t spotify/backstage", "create-plugin": "backstage-cli create-plugin", + "remove-plugin": "backstage-cli remove-plugin", "release": "if [ \"$(git symbolic-ref --short HEAD)\" = master ]; then echo \"don't try to release master\"; exit 1; else lerna version --no-push; fi", "lerna": "lerna", "storybook": "yarn workspace storybook start" @@ -32,8 +34,7 @@ "lerna": "^3.20.2", "lint-staged": "^10.1.0", "prettier": "^1.19.1", - "typescript": "^3.7.5", - "zombie": "^6.1.4" + "typescript": "^3.7.5" }, "dependencies": { "@types/classnames": "^2.2.9", diff --git a/packages/app/package.json b/packages/app/package.json index 232a00416d..fd9aa88323 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", @@ -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/e2e-test/.eslintrc.js b/packages/cli/e2e-test/.eslintrc.js new file mode 100644 index 0000000000..274c7426b8 --- /dev/null +++ b/packages/cli/e2e-test/.eslintrc.js @@ -0,0 +1,29 @@ +/* + * 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. + */ + +module.exports = { + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + }, +}; diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js new file mode 100644 index 0000000000..c8374af0eb --- /dev/null +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -0,0 +1,81 @@ +/* + * 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 os = require('os'); +const fs = require('fs-extra'); +const { resolve: resolvePath } = require('path'); +const Browser = require('zombie'); + +const { + spawnPiped, + handleError, + waitForPageWithText, + waitForExit, + print, +} = require('./helpers'); + +const createTestApp = require('./createTestApp'); +const createTestPlugin = require('./createTestPlugin'); + +Browser.localhost('localhost', 3000); + +async function createTempDir() { + return fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); +} + +async function main() { + process.env.BACKSTAGE_E2E_CLI_TEST = 'true'; + + const workDir = process.env.CI ? process.cwd() : await createTempDir(); + + process.stdout.write(`Initial directory: ${process.cwd()}\n`); + process.chdir(workDir); + process.stdout.write(`Working directory: ${process.cwd()}\n`); + + await createTestApp(); + + const appDir = resolvePath(workDir, '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'); + await waitForPageWithText( + browser, + '/test-plugin', + 'Welcome to test-plugin!', + ); + + print('Both App and Plugin loaded correctly'); + } finally { + startApp.kill(); + } + + await waitForExit(startApp); + + print('All tests done'); + process.exit(0); +} + +process.on('unhandledRejection', handleError); +main(process.argv.slice(2)).catch(handleError); diff --git a/packages/cli/e2e-test/createTestApp.js b/packages/cli/e2e-test/createTestApp.js new file mode 100644 index 0000000000..5437804975 --- /dev/null +++ b/packages/cli/e2e-test/createTestApp.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 { resolve: resolvePath } = require('path'); +const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); + +async function createTestApp() { + const cliPath = resolvePath(__dirname, '../bin/backstage-cli'); + + print('Creating a Backstage App'); + const createApp = spawnPiped(['node', cliPath, 'create-app']); + + 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/packages/cli/e2e-test/createTestPlugin.js b/packages/cli/e2e-test/createTestPlugin.js new file mode 100644 index 0000000000..59e8d6dd77 --- /dev/null +++ b/packages/cli/e2e-test/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/cli-e2e-test.js b/packages/cli/e2e-test/helpers.js similarity index 68% rename from scripts/cli-e2e-test.js rename to packages/cli/e2e-test/helpers.js index 48058550d9..7e34f10a63 100644 --- a/scripts/cli-e2e-test.js +++ b/packages/cli/e2e-test/helpers.js @@ -14,92 +14,11 @@ * limitations under the License. */ -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/; -Browser.localhost('localhost', 3000); - -async function main() { - process.env.CI = 'true'; - - const projectDir = resolvePath(__dirname, '..'); - process.chdir(projectDir); - - const start = 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'); - } finally { - start.kill(); - } - await waitForExit(start); - - 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 => { @@ -135,11 +54,48 @@ function spawnPiped(cmd, options) { 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 = 120 } = {}, + { intervalMs = 1000, maxAttempts = 240 } = {}, ) { let attempts = 0; for (;;) { @@ -169,14 +125,15 @@ async function waitForPageWithText( ); } -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 print(msg) { + return process.stdout.write(`${msg}\n`); } -process.on('unhandledRejection', handleError); -main(process.argv.slice(2)).catch(handleError); +module.exports = { + spawnPiped, + handleError, + waitFor, + waitForExit, + waitForPageWithText, + print, +}; diff --git a/packages/cli/package.json b/packages/cli/package.json index 1764d19bf2..faf8710d14 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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" @@ -22,6 +22,8 @@ "build": "backstage-cli build-cache -- tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", + "test:e2e": "node e2e-test/cli-e2e-test.js", + "clean": "backstage-cli clean", "start": "nodemon ." }, "devDependencies": { @@ -40,7 +42,8 @@ "del": "^5.1.0", "nodemon": "^2.0.2", "ts-node": "^8.6.2", - "tsconfig-paths": "^3.9.0" + "tsconfig-paths": "^3.9.0", + "zombie": "^6.1.4" }, "bin": { "backstage-cli": "bin/backstage-cli" 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..cb34dc0686 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -86,6 +86,30 @@ export async function moveApp( }); } +async function addPackageResolutions(appDir: string) { + const pkgJsonPath = resolvePath(appDir, 'package.json'); + const packageFileContent = await fs.readFile(pkgJsonPath, 'utf-8'); + const packageFileJson = JSON.parse(packageFileContent); + + packageFileJson.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 = paths.resolveOwnRoot('packages', pkg); + packageFileJson.resolutions[`@backstage/${pkg}`] = `file:${pkgPath}`; + const newContents = `${JSON.stringify(packageFileJson, null, 2)}\n`; + + await fs.writeFile(pkgJsonPath, 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 +150,12 @@ 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'); + await addPackageResolutions(appDir); + } + Task.section('Building the app'); await buildApp(appDir); @@ -134,6 +164,7 @@ export default async () => { chalk.green(`πŸ₯‡ Successfully created ${chalk.cyan(answers.name)}`), ); Task.log(); + Task.exit(); } catch (error) { Task.error(error.message); @@ -143,5 +174,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 df8b71f619..1af5832818 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -65,19 +65,18 @@ const sortObjectByKeys = (obj: { [name in string]: string }) => { }, {} as { [name in string]: string }); }; -const capitalize = (str: string): string => +export const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); -const addExportStatement = async ( +export const addExportStatement = async ( file: string, - importStatement: string, exportStatement: string, ) => { const newContents = fs .readFileSync(file, 'utf8') .split('\n') .filter(Boolean) // get rid of empty lines - .concat([importStatement, exportStatement]) + .concat([exportStatement]) .concat(['']) // newline at end of file .join('\n'); @@ -122,19 +121,16 @@ export async function addPluginToApp(rootDir: string, pluginName: string) { .split('-') .map(name => capitalize(name)) .join(''); - const pluginImport = `import { plugin as ${pluginNameCapitalized} } from '${pluginPackage}';`; - const pluginExport = `export { ${pluginNameCapitalized} };`; + const pluginExport = `export { plugin as ${pluginNameCapitalized} } from '${pluginPackage}';`; const pluginsFilePath = 'packages/app/src/plugins.ts'; const pluginsFile = resolvePath(rootDir, pluginsFilePath); await Task.forItem('processing', pluginsFilePath, async () => { - await addExportStatement(pluginsFile, pluginImport, pluginExport).catch( - error => { - throw new Error( - `Failed to import plugin in app: ${pluginsFile}: ${error.message}`, - ); - }, - ); + await addExportStatement(pluginsFile, pluginExport).catch(error => { + throw new Error( + `Failed to import plugin in app: ${pluginsFile}: ${error.message}`, + ); + }); }); } @@ -268,6 +264,7 @@ export default async () => { )}`, ); Task.log(); + Task.exit(); } catch (error) { Task.error(error.message); @@ -277,5 +274,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/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts new file mode 100644 index 0000000000..1cd47864c6 --- /dev/null +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -0,0 +1,193 @@ +/* + * 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 fse from 'fs-extra'; +import path from 'path'; +import os from 'os'; +import { paths } from '../../helpers/paths'; +import { + addExportStatement, + capitalize, + createTemporaryPluginFolder, +} from '../create-plugin/createPlugin'; +import { addCodeownersEntry } from '../create-plugin/lib/codeowners'; +import { + removeReferencesFromAppPackage, + removeReferencesFromPluginsFile, + removePluginDirectory, + removeSymLink, + removePluginFromCodeOwners, +} from './removePlugin'; + +// Some constant variables +const BACKSTAGE = `@backstage`; +const testPluginName = 'yarn-test-package'; +const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; +const tempDir = path.join(os.tmpdir(), 'remove-plugin-test'); + +const removeEmptyLines = (file: string): string => + file + .split('\n') + .filter(Boolean) + .join('\n'); + +const createTestPackageFile = async ( + testFilePath: string, + packageFile: string, +) => { + // Copy contents of package file for test + const packageFileContent = JSON.parse(fse.readFileSync(packageFile, 'utf8')); + + packageFileContent.dependencies[testPluginPackage] = '0.1.0'; + fse.createFileSync(testFilePath); + fse.writeFileSync( + testFilePath, + `${JSON.stringify(packageFileContent, null, 2)}\n`, + 'utf8', + ); + return; +}; + +const createTestPluginFile = async ( + testFilePath: string, + pluginsFilePath: string, +) => { + // Copy contents of package file for test + fse.copyFileSync(pluginsFilePath, testFilePath); + const pluginNameCapitalized = testPluginName + .split('-') + .map(name => capitalize(name)) + .join(''); + const exportStatement = `export { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; + addExportStatement(testFilePath, exportStatement); +}; + +const mkTestPluginDir = (testDirPath: string) => { + fse.mkdirSync(testDirPath); + for (let i = 0; i < 50; i++) + fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); +}; + +beforeAll(() => { + // Create temporary directory for all tests + createTemporaryPluginFolder(tempDir); +}); + +describe('removePlugin', () => { + describe('Remove Plugin Dependencies', () => { + const appPath = paths.resolveTargetRoot('packages', 'app'); + const githubDir = paths.resolveTargetRoot('.github'); + it('removes plugin references from /packages/app/package.json', async () => { + // Set up test + const packageFilePath = path.join(appPath, 'package.json'); + const testFilePath = path.join(tempDir, 'test.json'); + createTestPackageFile(testFilePath, packageFilePath); + try { + await removeReferencesFromAppPackage(testFilePath, testPluginName); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const packageFileContent = removeEmptyLines( + fse.readFileSync(packageFilePath, 'utf8'), + ); + expect(testFileContent === packageFileContent).toBe(true); + } finally { + fse.removeSync(testFilePath); + } + }); + it('removes plugin exports from /packages/app/src/packacge.json', async () => { + const testFilePath = path.join(tempDir, 'test.ts'); + const pluginsFilePaths = path.join(appPath, 'src', 'plugins.ts'); + createTestPluginFile(testFilePath, pluginsFilePaths); + try { + await removeReferencesFromPluginsFile(testFilePath, testPluginName); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const pluginsFileContent = removeEmptyLines( + fse.readFileSync(pluginsFilePaths, 'utf8'), + ); + expect(testFileContent === pluginsFileContent).toBe(true); + } finally { + fse.removeSync(testFilePath); + } + }); + it('removes codeOwners references', async () => { + const testFilePath = path.join(tempDir, 'test'); + const codeownersPath = path.join(githubDir, 'CODEOWNERS'); + try { + fse.copySync(codeownersPath, testFilePath); + const testFileContent = removeEmptyLines( + fse.readFileSync(testFilePath, 'utf8'), + ); + const codeOwnersFileContent = removeEmptyLines( + fse.readFileSync(codeownersPath, 'utf8'), + ); + await addCodeownersEntry(testFilePath!, `/plugins/${testPluginName}`, [ + '@thisIsAtestTeam', + 'test@gmail.com', + ]); + await removePluginFromCodeOwners(testFilePath, testPluginName); + expect(testFileContent === codeOwnersFileContent).toBeTruthy(); + } finally { + if (fse.existsSync(testFilePath)) fse.removeSync(testFilePath); + } + }); + }); + describe('Remove files', () => { + const testDirPath = path.join( + paths.resolveTargetRoot(), + 'plugins', + testPluginName, + ); + describe('Removes Plugin Directory', () => { + it('removes plugin directory from /plugins', async () => { + try { + mkTestPluginDir(testDirPath); + expect(fse.existsSync(testDirPath)).toBeTruthy(); + await removePluginDirectory(testDirPath); + expect(fse.existsSync(testDirPath)).toBeFalsy(); + } finally { + if (fse.existsSync(testDirPath)) fse.removeSync(testDirPath); + } + }); + }); + describe('Removes System Link', () => { + it('removes system link from @backstage', async () => { + const scopedDir = paths.resolveTargetRoot('node_modules', '@backstage'); + const testSymLinkPath = path.join( + scopedDir, + `plugin-${testPluginName}`, + ); + try { + mkTestPluginDir(testDirPath); + fse.ensureSymlinkSync(testSymLinkPath, testDirPath); + + await removeSymLink(testSymLinkPath); + expect(fse.existsSync(testSymLinkPath)).toBeFalsy(); + } finally { + if (fse.existsSync(testDirPath)) fse.removeSync(testDirPath); + if (fse.existsSync(testSymLinkPath)) fse.removeSync(testSymLinkPath); + } + }); + }); + }); +}); + +afterAll(() => { + // Remove temporary directory + fse.removeSync(tempDir); +}); diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts new file mode 100644 index 0000000000..76b8606973 --- /dev/null +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -0,0 +1,257 @@ +/* + * 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 fse from 'fs-extra'; +import path from 'path'; +import chalk from 'chalk'; +import inquirer, { Answers, Question } from 'inquirer'; +import { getCodeownersFilePath } from '../create-plugin/lib/codeowners'; +import { paths } from 'helpers/paths'; +import { Task } from 'helpers/tasks'; +// import os from 'os'; + +const BACKSTAGE = '@backstage'; + +export const checkExists = async (rootDir: string, pluginName: string) => { + await Task.forItem('checking', pluginName, async () => { + try { + const destination = path.join(rootDir, 'plugins', pluginName); + const pathExist = await fse.pathExists(destination); + + if (!pathExist) { + throw new Error( + chalk.red(` Plugin ${chalk.cyan(pluginName)} does not exist!`), + ); + } + } catch (e) { + throw new Error( + chalk.red( + ` There was an error removing plugin ${chalk.cyan(pluginName)}: ${ + e.message + }`, + ), + ); + } + }); +}; + +export const removePluginDirectory = async (destination: string) => { + await Task.forItem('removing', 'plugin files', async () => { + try { + await fse.remove(destination); + } catch (e) { + throw Error( + chalk.red( + ` There was a problem removing the plugin directory: ${e.message}`, + ), + ); + } + }); +}; + +export const removeSymLink = async (destination: string) => { + await Task.forItem('removing', 'symbolic link', async () => { + const symLinkExists = fse.pathExists(destination); + if (symLinkExists) { + try { + await fse.remove(destination); + } catch (e) { + throw Error( + chalk.red( + ` Could not remove symbolic link\t${chalk.cyan(destination)}: ${ + e.message + }`, + ), + ); + } + } + }); +}; + +const removeAllStatementsContainingID = async (file: string, ID: string) => { + const originalContent = await fse.readFile(file, 'utf8'); + const contentAfterRemoval = originalContent + .split('\n') + .filter(Boolean) // get rid of empty lines + .filter(statement => { + return !statement.includes(`${ID}`); + }) // get rid of lines with pluginName + .concat(['']) // newline at end of line + .join('\n'); + await fse.writeFile(file, contentAfterRemoval, 'utf8'); + const finalContent = await fse.readFile(file, 'utf8'); + if (finalContent === originalContent) + throw new Error(`File was not modified.`); +}; + +const capitalize = (str: string): string => + str.charAt(0).toUpperCase() + str.slice(1); + +export const removeReferencesFromPluginsFile = async ( + pluginsFile: string, + pluginName: string, +) => { + const pluginNameCapitalized = pluginName + .split('-') + .map(name => capitalize(name)) + .join(''); + + await Task.forItem('removing', 'export references', async () => { + try { + await removeAllStatementsContainingID(pluginsFile, pluginNameCapitalized); + } catch (e) { + throw new Error( + chalk.red( + ` There was an error removing export statement for plugin ${chalk.cyan( + pluginNameCapitalized, + )}: ${e.message}`, + ), + ); + } + }); +}; + +export const removePluginFromCodeOwners = async ( + codeOwnersFile: string, + pluginName: string, +) => { + await Task.forItem('removing', 'codeowners references', async () => { + try { + await removeAllStatementsContainingID(codeOwnersFile, pluginName); + } catch (e) { + throw new Error( + chalk.red( + ` There was an error removing code owners statement for plugin ${chalk.cyan( + pluginName, + )}: ${e.message}`, + ), + ); + } + }); +}; + +export const removeReferencesFromAppPackage = async ( + appPackageFile: string, + pluginName: string, +) => { + const pluginPackage = `${BACKSTAGE}/plugin-${pluginName}`; + + await Task.forItem('removing', 'plugin app dependency', async () => { + try { + const appPackageFileContent = await fse.readFile(appPackageFile, 'utf-8'); + const appPackageFileContentJSON = JSON.parse(appPackageFileContent); + const dependencies = appPackageFileContentJSON.dependencies; + + if (!dependencies[pluginPackage]) { + throw new Error( + chalk.red( + ` Plugin ${chalk.cyan( + pluginPackage, + )} does not exist in ${chalk.cyan(appPackageFile)}`, + ), + ); + } + + delete dependencies[pluginPackage]; + await fse.writeFile( + appPackageFile, + `${JSON.stringify(appPackageFileContentJSON, null, 2)}\n`, + 'utf-8', + ); + } catch (e) { + throw new Error( + chalk.red( + ` Failed to remove plugin as dependency in app: ${chalk.cyan( + appPackageFile, + )}: ${e.message}`, + ), + ); + } + }); +}; + +export default async () => { + const questions: Question[] = [ + { + type: 'input', + name: 'pluginName', + message: chalk.blue( + 'Enter the ID of the plugin to be removed [required]', + ), + validate: (value: any) => { + if (!value) { + return chalk.red('Please enter an ID for the plugin'); + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return chalk.red( + 'Plugin IDs must be kehbab-cased and contain only letters, digits and dashes.', + ); + } + return true; + }, + }, + ]; + + const answers: Answers = await inquirer.prompt(questions); + const pluginName: string = answers.pluginName; + const appPackage = paths.resolveTargetRoot('packages/app'); + const pluginDir = paths.resolveTargetRoot('plugins', answers.pluginName); + const codeOwnersFile = await getCodeownersFilePath(paths.targetRoot); + const appPackageFile = path.join(appPackage, 'package.json'); + const appPluginsFile = path.join(appPackage, 'src', 'plugins.ts'); + const pluginScopedDirectory = paths.resolveTargetRoot( + 'node_modules', + BACKSTAGE, + `plugin-${pluginName}`, + ); + + Task.log(); + Task.log('Removing the plugin...'); + + console.log(pluginScopedDirectory); + try { + Task.section('Checking the plugin exists.'); + await checkExists(paths.targetRoot, pluginName); + + Task.section('Removing plugin files.'); + await removePluginDirectory(pluginDir); + + Task.section('Removing symbolic link from @backstage.'); + await removeSymLink(pluginScopedDirectory); + + if (await fse.pathExists(appPackage)) { + Task.section('Removing references from plugins.ts.'); + await removeReferencesFromPluginsFile(appPluginsFile, pluginName); + + Task.section('Removing plugin dependency from app.'); + await removeReferencesFromAppPackage(appPackageFile, pluginName); + } + + if (codeOwnersFile) { + Task.section('Removing codeowners reference.'); + await removePluginFromCodeOwners(codeOwnersFile, pluginName); + } + + Task.log(); + Task.log( + `πŸ₯‡ Successfully removed ${chalk.cyan( + `@backstage/plugin-${answers.id}`, + )}`, + ); + Task.log(); + } catch (error) { + Task.error(error.message); + Task.log('It seems that something went wrong when removing the plugin πŸ€”'); + } +}; 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..e7b351ec16 100644 --- a/packages/cli/src/helpers/paths.ts +++ b/packages/cli/src/helpers/paths.ts @@ -25,6 +25,9 @@ export type Paths = { // Root dir of the cli itself, containing package.json ownDir: string; + // Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo. + ownRoot: string; + // The location of the app that the cli is being executed in targetDir: string; @@ -34,6 +37,9 @@ export type Paths = { // Resolve a path relative to own repo resolveOwn: ResolveFunc; + // Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo. + resolveOwnRoot: ResolveFunc; + // Resolve a path relative to the app resolveTarget: ResolveFunc; @@ -53,7 +59,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) { @@ -91,10 +97,31 @@ export function findOwnDir() { return resolvePath(__dirname, path); } +// Finds the root of the monorepo that the cli exists in. Only accessible when running inside Backstage repo. +export function findOwnRootPath(ownDir: string) { + const isLocal = fs.pathExistsSync(resolvePath(ownDir, 'src')); + if (!isLocal) { + throw new Error( + 'Tried to access monorepo package root dir outside of Backstage repository', + ); + } + + return resolvePath(ownDir, '../..'); +} + export function findPaths(): Paths { const ownDir = findOwnDir(); const targetDir = fs.realpathSync(process.cwd()); + // Lazy load this as it will throw an error if we're not inside the Backstage repo. + let ownRoot = ''; + const getOwnRoot = () => { + if (!ownRoot) { + ownRoot = findOwnRootPath(ownDir); + } + return ownRoot; + }; + // We're not always running in a monorepo, so we lazy init this to only crash commands // that require a monorepo when we're not in one. let targetRoot = ''; @@ -107,11 +134,15 @@ export function findPaths(): Paths { return { ownDir, + get ownRoot() { + return getOwnRoot(); + }, targetDir, get targetRoot() { return getTargetRoot(); }, resolveOwn: (...paths) => resolvePath(ownDir, ...paths), + resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths), resolveTarget: (...paths) => resolvePath(targetDir, ...paths), resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths), }; 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 ca39c1ac3b..3ad43faa76 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -44,6 +44,13 @@ const main = (argv: string[]) => { actionHandler(() => require('commands/create-plugin/createPlugin')), ); + program + .command('remove-plugin') + .description('Removes plugin in the current repository') + .action( + actionHandler(() => require('commands/remove-plugin/removePlugin')), + ); + program .command('plugin:build') .option('--watch', 'Enable watch mode') @@ -70,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'))); @@ -90,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 d60dcf91be..418bac8da9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" @@ -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", @@ -39,16 +40,18 @@ "react-dom": "^16.12.0", "react-helmet": "5.2.1", "react-router-dom": "^5.1.2", + "react-sparklines": "^1.7.0", "recompose": "0.30.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.3", - "@backstage/test-utils-core": "^0.1.1-alpha.3", - "@backstage/theme": "^0.1.1-alpha.3", - "@storybook/addon-storysource": "^5.3.18", + "@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", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", + "@types/react-sparklines": "^1.7.0", "react-router": "^5.1.2" }, "peerDependencies": { diff --git a/packages/core/src/components/Lifecycle/LifecycleAlpha.stories.tsx b/packages/core/src/components/Lifecycle/LifecycleAlpha.stories.tsx index 1b044fcd5f..072fb90a55 100644 --- a/packages/core/src/components/Lifecycle/LifecycleAlpha.stories.tsx +++ b/packages/core/src/components/Lifecycle/LifecycleAlpha.stories.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { AlphaLabel } from './Lifecycle'; export default { - title: 'Alpha Lifecycle', + title: 'Lifecycle - Alpha', component: AlphaLabel, }; diff --git a/packages/core/src/components/Lifecycle/LifecycleBeta.stories.tsx b/packages/core/src/components/Lifecycle/LifecycleBeta.stories.tsx index b74171b5ea..f8cce2a562 100644 --- a/packages/core/src/components/Lifecycle/LifecycleBeta.stories.tsx +++ b/packages/core/src/components/Lifecycle/LifecycleBeta.stories.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { BetaLabel } from './Lifecycle'; export default { - title: 'Beta Lifecycle', + title: 'Lifecycle - Beta', component: BetaLabel, }; diff --git a/packages/core/src/components/ProgressBars/CircleProgress.tsx b/packages/core/src/components/ProgressBars/CircleProgress.tsx index 4d9c5c3340..9290132eec 100644 --- a/packages/core/src/components/ProgressBars/CircleProgress.tsx +++ b/packages/core/src/components/ProgressBars/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/TrendLine/TrendLine.stories.tsx b/packages/core/src/components/TrendLine/TrendLine.stories.tsx new file mode 100644 index 0000000000..855b14207c --- /dev/null +++ b/packages/core/src/components/TrendLine/TrendLine.stories.tsx @@ -0,0 +1,43 @@ +/* + * 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 TrendLine from '.'; + +export default { + title: 'TrendLine', + component: TrendLine, +}; + +const width = 140; + +export const Default = () => ( +
+ +
+); + +export const TrendingUp = () => ( +
+ +
+); + +export const TrendingDown = () => ( +
+ +
+); diff --git a/plugins/lighthouse/src/components/CategoryTrendline/index.test.tsx b/packages/core/src/components/TrendLine/TrendLine.test.tsx similarity index 77% rename from plugins/lighthouse/src/components/CategoryTrendline/index.test.tsx rename to packages/core/src/components/TrendLine/TrendLine.test.tsx index c45db58b9e..985e5d2b31 100644 --- a/plugins/lighthouse/src/components/CategoryTrendline/index.test.tsx +++ b/packages/core/src/components/TrendLine/TrendLine.test.tsx @@ -15,18 +15,17 @@ */ /* eslint-disable jest/no-disabled-tests */ - import React from 'react'; import { render } from '@testing-library/react'; import { wrapInThemedTestApp } from '@backstage/test-utils'; -import CategoryTrendline from '.'; +import TrendLine from '.'; -describe('CategoryTrendline', () => { +describe('TrendLine', () => { describe('when no data is present', () => { it('renders null without throwing', () => { const rendered = render( - wrapInThemedTestApp(), + wrapInThemedTestApp(), ); expect(rendered.queryByTitle('sparkline')).not.toBeInTheDocument(); }); @@ -35,9 +34,7 @@ describe('CategoryTrendline', () => { describe('when one datapoint is present', () => { it('renders as a straight line', () => { const rendered = render( - wrapInThemedTestApp( - , - ), + wrapInThemedTestApp(), ); expect(rendered.getByTitle('sparkline')).toBeInTheDocument(); }); @@ -46,9 +43,7 @@ describe('CategoryTrendline', () => { describe.skip('when the data finishes above the success threshold', () => { it('renders with the correct color', () => { const rendered = render( - wrapInThemedTestApp( - , - ), + wrapInThemedTestApp(), ); expect(rendered.getByTitle('sparkline')).toBeInTheDocument(); }); @@ -57,9 +52,7 @@ describe('CategoryTrendline', () => { describe.skip('when the data finishes within the the warning threshold', () => { it('renders with the correct color', () => { const rendered = render( - wrapInThemedTestApp( - , - ), + wrapInThemedTestApp(), ); expect(rendered.getByTitle('sparkline')).toBeInTheDocument(); }); @@ -68,9 +61,7 @@ describe('CategoryTrendline', () => { describe.skip('when the data finishes within the the error threshold', () => { it('renders with the correct color', () => { const rendered = render( - wrapInThemedTestApp( - , - ), + wrapInThemedTestApp(), ); expect(rendered.getByTitle('sparkline')).toBeInTheDocument(); }); diff --git a/plugins/lighthouse/src/components/CategoryTrendline/index.tsx b/packages/core/src/components/TrendLine/TrendLine.tsx similarity index 83% rename from plugins/lighthouse/src/components/CategoryTrendline/index.tsx rename to packages/core/src/components/TrendLine/TrendLine.tsx index a46aff52e5..3ab9b67e53 100644 --- a/plugins/lighthouse/src/components/CategoryTrendline/index.tsx +++ b/packages/core/src/components/TrendLine/TrendLine.tsx @@ -13,15 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { FC } from 'react'; 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; @@ -29,8 +27,8 @@ function color( return theme.palette.status.error; } -const CategoryTrendline: FC = props => { - const theme = useTheme(); +const Trendline: FC = props => { + const theme = useTheme(); if (!props.data) return null; return ( @@ -41,4 +39,4 @@ const CategoryTrendline: FC = props => { ); }; -export default CategoryTrendline; +export default Trendline; diff --git a/packages/core/src/components/TrendLine/index.ts b/packages/core/src/components/TrendLine/index.ts new file mode 100644 index 0000000000..168c6e6d3f --- /dev/null +++ b/packages/core/src/components/TrendLine/index.ts @@ -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 } from './TrendLine'; 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/index.ts b/packages/core/src/index.ts index 93288b9b68..af5789ef92 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -34,6 +34,7 @@ export { AlphaLabel, BetaLabel } from './components/Lifecycle'; export { default as SupportButton } from './components/SupportButton'; export { default as SortableTable } from './components/SortableTable'; export { default as StructuredMetadataTable } from './components/StructuredMetadataTable'; +export { default as TrendLine } from './components/TrendLine'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status'; export { default as WarningPanel } from './components/WarningPanel'; 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 f884da5f07..56ac5c6264 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,17 +1,21 @@ { "name": "storybook", - "version": "0.1.1-alpha.3", + "version": "0.1.1-alpha.4", "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-core/package.json b/packages/test-utils-core/package.json index 79d36a3525..569622ed1c 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.1-alpha.3", + "version": "0.1.1-alpha.4", "private": false, "publishConfig": { "access": "public" diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ab52ca94a1..9eca38eeb5 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -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" @@ -21,11 +21,12 @@ "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.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", @@ -38,8 +39,8 @@ "react-router-dom": "^5.1.2" }, "peerDependencies": { - "@backstage/theme": "^0.1.1-alpha.3", "@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", "@testing-library/react": "^9.3.2", 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 b08014e9d6..6504d7dea5 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -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" @@ -20,10 +20,11 @@ "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.3", + "@backstage/cli": "^0.1.1-alpha.4", "@material-ui/core": "^4.9.1" }, "peerDependencies": { 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 7f78f1bf63..65fb3cf9da 100644 --- a/plugins/home-page/package.json +++ b/plugins/home-page/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-page", - "version": "0.1.1-alpha.3", + "version": "0.1.1-alpha.4", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", "license": "Apache-2.0", @@ -8,12 +8,13 @@ "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.3", - "@backstage/core": "^0.1.1-alpha.3", - "@backstage/theme": "^0.1.1-alpha.3", + "@backstage/cli": "^0.1.1-alpha.4", + "@backstage/core": "^0.1.1-alpha.4", + "@backstage/theme": "^0.1.1-alpha.4", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^4.2.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 721f20f802..d082b0be0f 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.1.1-alpha.3", + "version": "0.1.1-alpha.4", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", "license": "Apache-2.0", @@ -9,17 +9,17 @@ "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", - "react-sparklines": "^1.7.0" + "react-markdown": "^4.3.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.3", - "@backstage/core": "^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/core": "^0.1.1-alpha.4", + "@backstage/test-utils": "^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", @@ -28,7 +28,6 @@ "@testing-library/user-event": "^7.1.2", "@types/jest": "^24.0.0", "@types/node": "^12.0.0", - "@types/react-sparklines": "^1.7.0", "@types/testing-library__jest-dom": "5.0.2", "jest-fetch-mock": "^3.0.3", "react": "^16.13.1", diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx index 52f3eeb5e2..ba0e79f066 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx @@ -24,6 +24,7 @@ import { TableRow, } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; +import { TrendLine } from '@backstage/core'; import { Audit, @@ -32,7 +33,6 @@ import { Website, } from '../../api'; import { formatTime } from '../../utils'; -import CategoryTrendline from '../CategoryTrendline'; import AuditStatusIcon from '../AuditStatusIcon'; export const CATEGORIES: LighthouseCategoryId[] = [ @@ -132,7 +132,7 @@ export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => { key={`${website.url}|${category}`} className={classes.sparklinesCell} > - diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 3ee95e8898..86cb0e3f45 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.1.1-alpha.3", + "version": "0.1.1-alpha.4", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", "private": true, @@ -8,12 +8,13 @@ "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.3", - "@backstage/core": "^0.1.1-alpha.3", - "@backstage/theme": "^0.1.1-alpha.3", + "@backstage/cli": "^0.1.1-alpha.4", + "@backstage/core": "^0.1.1-alpha.4", + "@backstage/theme": "^0.1.1-alpha.4", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", 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', () => { - + ,