Merge branch 'master' into alund/components

This commit is contained in:
Stefan Ålund
2020-04-17 12:16:38 +02:00
71 changed files with 1491 additions and 1098 deletions
+20 -11
View File
@@ -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
+63
View File
@@ -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 cant 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.
+14 -7
View File
@@ -6,6 +6,7 @@
![](https://github.com/spotify/backstage/workflows/Frontend%20CI/badge.svg)
[![Discord](https://img.shields.io/discord/687207715902193673)](https://discord.gg/EBHEGzX)
![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)
[![](https://img.shields.io/npm/v/@backstage/core?label=Version)](https://github.com/spotify/backstage/releases)
## What is Backstage?
@@ -59,23 +60,25 @@ To run a Backstage app, you will need to have the following installed:
- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12
- [yarn](https://classic.yarnpkg.com/en/docs/install)
Open a terminal window and start the web app using the following commands from the project root:
After cloning this repo, open a terminal window and start the web app using the following commands from the project root:
```bash
$ yarn install # may take a while
$ yarn start
yarn install
yarn start
```
The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.
For more complex development environment configuration, see the
[Development Environment](docs/getting-started/development-environment.md) section of the Getting Started docs.
And thats it! You are good to go 👍
### Next step
Take a look at the [Getting Started](docs/getting-started/README.md) guide to learn more about how to extend the functionality with Plugins.
## Documentation
- [FAQs](docs/FAQ.md)
- [Getting Started](docs/getting-started/README.md)
- [Create a Backstage App](docs/create-an-app.md)
- [Architecture](docs/architecture-terminology.md)
- [API references](docs/reference/README.md)
- [Designing for Backstage](docs/design.md)
@@ -95,6 +98,10 @@ For more complex development environment configuration, see the
Or, if you are an open source developer and are interested in joining our team, please reach out to [foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com)
## Contributing
We would love your help in building Backstage! See [CONTRIBUTING](CONTRIBUTING.md) for more information.
## License
Copyright 2020 Spotify AB.
@@ -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

+3 -2
View File
@@ -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
View File
@@ -5,5 +5,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.1.1-alpha.3"
"version": "0.1.1-alpha.4"
}
+3 -2
View File
@@ -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",
+8 -7
View File
@@ -1,14 +1,14 @@
{
"name": "example-app",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": true,
"dependencies": {
"@backstage/cli": "^0.1.1-alpha.3",
"@backstage/core": "^0.1.1-alpha.3",
"@backstage/theme": "^0.1.1-alpha.3",
"@backstage/plugin-home-page": "^0.1.1-alpha.3",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.3",
"@backstage/plugin-welcome": "^0.1.1-alpha.3",
"@backstage/cli": "^0.1.1-alpha.4",
"@backstage/core": "^0.1.1-alpha.4",
"@backstage/plugin-home-page": "^0.1.1-alpha.4",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.4",
"@backstage/plugin-welcome": "^0.1.1-alpha.4",
"@backstage/theme": "^0.1.1-alpha.4",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -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",
+7 -9
View File
@@ -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;
}
+2 -1
View File
@@ -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.
+29
View File
@@ -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,
},
],
},
};
+81
View File
@@ -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);
+44
View File
@@ -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;
+44
View File
@@ -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,
};
+5 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -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"
+33
View File
@@ -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));
+32 -1
View File
@@ -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),
};
+4
View File
@@ -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,
+13
View File
@@ -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,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}}",
@@ -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>,
);
+9 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -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>
);
@@ -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();
});
@@ -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';
@@ -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;
+1
View File
@@ -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';
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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>
</>
+2 -2
View File
@@ -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>
));
+1 -1
View File
@@ -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(
+4
View File
@@ -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`.
+8 -4
View File
@@ -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 -1
View File
@@ -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"
+6 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -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>
);
+4 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
"version": "0.1.1-alpha.3",
"version": "0.1.1-alpha.4",
"private": false,
"publishConfig": {
"access": "public"
@@ -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": {
-260
View File
@@ -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;
-273
View File
@@ -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;
-274
View File
@@ -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;
+197
View File
@@ -0,0 +1,197 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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;
}
+4 -3
View File
@@ -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';
+96
View File
@@ -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',
});
+12 -5
View File
@@ -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;
}
+6 -5
View File
@@ -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>,
);
+8 -9
View File
@@ -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] || []}
/>
+6 -5
View File
@@ -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>,