Merge branch 'master' into alund/components
This commit is contained in:
+20
-11
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -6,6 +6,7 @@
|
||||

|
||||
[](https://discord.gg/EBHEGzX)
|
||||

|
||||
[](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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
<p align='center'>
|
||||
<img src='https://github.com/spotify/backstage/raw/master/docs/getting-started/create-app_output.png' width='600' alt='create app'>
|
||||
<img src='https://github.com/spotify/backstage/raw/master/docs/create-app_output.png' width='600' alt='create app'>
|
||||
</p>
|
||||
|
||||
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)
|
||||
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 303 KiB |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.1-alpha.3"
|
||||
"version": "0.1.1-alpha.4"
|
||||
}
|
||||
|
||||
+3
-2
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ export default {
|
||||
json(),
|
||||
typescript({
|
||||
include: `${paths.resolveTarget('src')}/**/*.{js,jsx,ts,tsx}`,
|
||||
clean: true,
|
||||
}),
|
||||
],
|
||||
} as RollupWatchOptions;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 🤔');
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<CssBaseline>
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Router>
|
||||
<AppComponent />
|
||||
</Router>
|
||||
|
||||
+2
-2
@@ -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(
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<WelcomePage />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
+2
-2
@@ -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(
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ExampleComponent />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -17,7 +17,7 @@ import React from 'react';
|
||||
import { AlphaLabel } from './Lifecycle';
|
||||
|
||||
export default {
|
||||
title: 'Alpha Lifecycle',
|
||||
title: 'Lifecycle - Alpha',
|
||||
component: AlphaLabel,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import React from 'react';
|
||||
import { BetaLabel } from './Lifecycle';
|
||||
|
||||
export default {
|
||||
title: 'Beta Lifecycle',
|
||||
title: 'Lifecycle - Beta',
|
||||
component: BetaLabel,
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { BackstageTheme } from '@backstage/theme';
|
||||
import { Circle } from 'rc-progress';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
const useStyles = makeStyles<typeof BackstageTheme>(theme => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
lineHeight: 0,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { BackstageTheme } from '@backstage/theme';
|
||||
import classNames from 'classnames';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
const useStyles = makeStyles<typeof BackstageTheme>(theme => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
status: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
|
||||
@@ -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 = () => (
|
||||
<div style={{ width }}>
|
||||
<TrendLine data={[0.1, 0.7, 0.5, 0.8]} title="Trend over time" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const TrendingUp = () => (
|
||||
<div style={{ width }}>
|
||||
<TrendLine data={[0.1, 0.5, 0.9, 1.0]} title="Trend over time" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const TrendingDown = () => (
|
||||
<div style={{ width }}>
|
||||
<TrendLine data={[0.8, 0.7, 0.5, 0.1]} title="Trend over time" />
|
||||
</div>
|
||||
);
|
||||
+7
-16
@@ -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(<CategoryTrendline data={[]} title="sparkline" />),
|
||||
wrapInThemedTestApp(<TrendLine data={[]} title="sparkline" />),
|
||||
);
|
||||
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(
|
||||
<CategoryTrendline data={[0.5]} title="sparkline" />,
|
||||
),
|
||||
wrapInThemedTestApp(<TrendLine data={[0.5]} title="sparkline" />),
|
||||
);
|
||||
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(
|
||||
<CategoryTrendline data={[0.5, 0.95]} title="sparkline" />,
|
||||
),
|
||||
wrapInThemedTestApp(<TrendLine data={[0.5, 0.95]} title="sparkline" />),
|
||||
);
|
||||
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(
|
||||
<CategoryTrendline data={[0.5, 0.65]} title="sparkline" />,
|
||||
),
|
||||
wrapInThemedTestApp(<TrendLine data={[0.5, 0.65]} title="sparkline" />),
|
||||
);
|
||||
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(
|
||||
<CategoryTrendline data={[0.5, 0.4]} title="sparkline" />,
|
||||
),
|
||||
wrapInThemedTestApp(<TrendLine data={[0.5, 0.4]} title="sparkline" />),
|
||||
);
|
||||
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
|
||||
});
|
||||
+5
-7
@@ -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<SparklinesProps & { title?: string }> = props => {
|
||||
const theme = useTheme<typeof BackstageTheme>();
|
||||
const Trendline: FC<SparklinesProps & { title?: string }> = props => {
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
|
||||
if (!props.data) return null;
|
||||
return (
|
||||
@@ -41,4 +39,4 @@ const CategoryTrendline: FC<SparklinesProps & { title?: string }> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryTrendline;
|
||||
export default Trendline;
|
||||
@@ -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';
|
||||
+28
-27
@@ -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<BackstageTheme>(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 (
|
||||
<div className={classes.message}>
|
||||
<div className={classes.header}>
|
||||
<ErrorOutlineStyled />
|
||||
<Typography className={classes.headerText} variant="subtitle1">
|
||||
{title}
|
||||
</Typography>
|
||||
</div>
|
||||
{message && (
|
||||
<Typography className={classes.messageText}>{message}</Typography>
|
||||
)}
|
||||
{children}
|
||||
type Props = {
|
||||
message?: React.ReactNode;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const WarningPanel: FC<Props> = props => {
|
||||
const classes = useStyles(props);
|
||||
const { title, message, children } = props;
|
||||
return (
|
||||
<div className={classes.message}>
|
||||
<div className={classes.header}>
|
||||
<ErrorOutlineStyled />
|
||||
<Typography className={classes.headerText} variant="subtitle1">
|
||||
{title}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
{message && (
|
||||
<Typography className={classes.messageText}>{message}</Typography>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(WarningPanel);
|
||||
export default WarningPanel;
|
||||
@@ -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';
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Theme } from 'layout/Page/Page';
|
||||
// import { Link } from 'shared/components';
|
||||
import Waves from './Waves';
|
||||
|
||||
const useStyles = makeStyles<typeof BackstageTheme>(theme => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
header: {
|
||||
gridArea: 'pageHeader',
|
||||
padding: theme.spacing(3),
|
||||
|
||||
@@ -20,7 +20,7 @@ import React, { FC, useRef, useState } from 'react';
|
||||
import { sidebarConfig, SidebarContext } from './config';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<typeof BackstageTheme>(theme => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
root: {
|
||||
zIndex: 1000,
|
||||
position: 'relative',
|
||||
|
||||
@@ -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 => (
|
||||
<Grid container spacing={4}>
|
||||
<Grid item>{storyFn()}</Grid>
|
||||
</Grid>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<TabbedCard title="Default Example Header">
|
||||
<CardTab label="Option 1">some content 1</CardTab>
|
||||
<CardTab label="Option 2">some content 2</CardTab>
|
||||
<CardTab label="Option 3">some content 3</CardTab>
|
||||
<CardTab label="Option 4">some content 4</CardTab>
|
||||
<CardTab label="Option 1">
|
||||
<div style={cardContentStyle}>Some content</div>
|
||||
</CardTab>
|
||||
<CardTab label="Option 2">
|
||||
<div style={cardContentStyle}>Some content 2</div>
|
||||
</CardTab>
|
||||
<CardTab label="Option 3">
|
||||
<div style={cardContentStyle}>Some content 3</div>
|
||||
</CardTab>
|
||||
<CardTab label="Option 4">
|
||||
<div style={cardContentStyle}>Some content 4</div>
|
||||
</CardTab>
|
||||
</TabbedCard>
|
||||
);
|
||||
};
|
||||
@@ -37,10 +55,18 @@ const linkInfo = { title: 'Go to XYZ Location', link: '#' };
|
||||
export const WithFooterLink = () => {
|
||||
return (
|
||||
<TabbedCard title="Footer Link Example Header" deepLink={linkInfo}>
|
||||
<CardTab label="Option 1">some content 1</CardTab>
|
||||
<CardTab label="Option 2">some content 2</CardTab>
|
||||
<CardTab label="Option 3">some content 3</CardTab>
|
||||
<CardTab label="Option 4">some content 4</CardTab>
|
||||
<CardTab label="Option 1">
|
||||
<div style={cardContentStyle}>Some content</div>
|
||||
</CardTab>
|
||||
<CardTab label="Option 2">
|
||||
<div style={cardContentStyle}>Some content 2</div>
|
||||
</CardTab>
|
||||
<CardTab label="Option 3">
|
||||
<div style={cardContentStyle}>Some content 3</div>
|
||||
</CardTab>
|
||||
<CardTab label="Option 4">
|
||||
<div style={cardContentStyle}>Some content 4</div>
|
||||
</CardTab>
|
||||
</TabbedCard>
|
||||
);
|
||||
};
|
||||
@@ -60,16 +86,16 @@ export const WithControlledTabValue = () => {
|
||||
title="Controlled Value Example"
|
||||
>
|
||||
<CardTab value="one" label="Option 1">
|
||||
some content 1
|
||||
<div style={cardContentStyle}>Some content</div>
|
||||
</CardTab>
|
||||
<CardTab value="two" label="Option 2">
|
||||
some content 2
|
||||
<div style={cardContentStyle}>Some content 2</div>
|
||||
</CardTab>
|
||||
<CardTab value="three" label="Option 3">
|
||||
some content 3
|
||||
<div style={cardContentStyle}>Some content 3</div>
|
||||
</CardTab>
|
||||
<CardTab value="four" label="Option 4">
|
||||
some content 4
|
||||
<div style={cardContentStyle}>Some content 4</div>
|
||||
</CardTab>
|
||||
</TabbedCard>
|
||||
</>
|
||||
|
||||
@@ -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 => (
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<CssBaseline>{story()}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = (
|
||||
<ThemeProvider theme={BackstageTheme}>{component}</ThemeProvider>
|
||||
);
|
||||
const themed = <ThemeProvider theme={lightTheme}>{component}</ThemeProvider>;
|
||||
return wrapInTestApp(themed, initialRouterEntries);
|
||||
}
|
||||
|
||||
export const wrapInTheme = (component: ReactNode, theme = BackstageTheme) => (
|
||||
export const wrapInTheme = (component: ReactNode, theme = lightTheme) => (
|
||||
<ThemeProvider theme={theme}>{component}</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<BackstageMuiTheme> => {
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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<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 BackstageThemeLight = {
|
||||
...extendedTheme,
|
||||
...createOverrides(extendedTheme),
|
||||
};
|
||||
|
||||
// Temporary workaround for files incorrectly importing the theme directly
|
||||
export const V1 = BackstageThemeLight;
|
||||
|
||||
export default BackstageThemeLight;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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<BackstageMuiPalette>;
|
||||
export interface BackstageThemeOptions extends ThemeOptions {
|
||||
palette: BackstagePaletteOptions;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<HomePage />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<CategoryTrendline
|
||||
<TrendLine
|
||||
title={`trendline for ${CATEGORY_LABELS[category]} category of ${website.url}`}
|
||||
data={categorySparklines[website.url][category] || []}
|
||||
/>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.from([[errorApiRef, { post: jest.fn() }]])}
|
||||
>
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<WelcomePage />
|
||||
</ThemeProvider>
|
||||
</ApiProvider>,
|
||||
|
||||
Reference in New Issue
Block a user