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
+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;
+139
View File
@@ -0,0 +1,139 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const childProcess = require('child_process');
const { spawn } = childProcess;
const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/;
function spawnPiped(cmd, options) {
function pipeWithPrefix(stream, prefix = '') {
return data => {
const prefixedMsg = data
.toString('utf8')
.trimRight()
.replace(/^/gm, prefix);
stream.write(`${prefixedMsg}\n`, 'utf8');
};
}
const child = spawn(cmd[0], cmd.slice(1), {
stdio: 'pipe',
shell: true,
...options,
});
child.on('error', handleError);
child.on('exit', code => {
if (code) {
print(`Child '${cmd.join(' ')}' exited with code ${code}`);
process.exit(code);
}
});
child.stdout.on(
'data',
pipeWithPrefix(process.stdout, `[${cmd.join(' ')}].out: `),
);
child.stderr.on(
'data',
pipeWithPrefix(process.stderr, `[${cmd.join(' ')}].err: `),
);
return child;
}
function handleError(err) {
process.stdout.write(`${err.name}: ${err.stack || err.message}\n`);
if (typeof err.code === 'number') {
process.exit(err.code);
} else {
process.exit(1);
}
}
function waitFor(fn) {
return new Promise(resolve => {
const handle = setInterval(() => {
if (fn()) {
clearInterval(handle);
resolve();
return;
}
}, 100);
});
}
async function waitForExit(child) {
if (child.exitCode !== null) {
throw new Error(`Child already exited with code ${child.exitCode}`);
}
await new Promise((resolve, reject) =>
child.once('exit', code => {
if (code) {
reject(new Error(`Child exited with code ${code}`));
} else {
print('Child finished');
resolve();
}
}),
);
}
async function waitForPageWithText(
browser,
path,
text,
{ intervalMs = 1000, maxAttempts = 240 } = {},
) {
let attempts = 0;
for (;;) {
try {
await new Promise(resolve => setTimeout(resolve, intervalMs));
await browser.visit(path);
break;
} catch (error) {
if (error.message.match(EXPECTED_LOAD_ERRORS)) {
attempts++;
if (attempts > maxAttempts) {
throw new Error(
`Failed to load page '${path}', max number of attempts reached`,
);
}
} else {
throw error;
}
}
}
const escapedText = text.replace(/"/g, '\\"');
browser.assert.evaluate(
`Array.from(document.querySelectorAll("*")).some(el => el.textContent === "${escapedText}")`,
true,
`expected to find text ${text}`,
);
}
function print(msg) {
return process.stdout.write(`${msg}\n`);
}
module.exports = {
spawnPiped,
handleError,
waitFor,
waitForExit,
waitForPageWithText,
print,
};
+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>
);
@@ -0,0 +1,69 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable jest/no-disabled-tests */
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from '@backstage/test-utils';
import TrendLine from '.';
describe('TrendLine', () => {
describe('when no data is present', () => {
it('renders null without throwing', () => {
const rendered = render(
wrapInThemedTestApp(<TrendLine data={[]} title="sparkline" />),
);
expect(rendered.queryByTitle('sparkline')).not.toBeInTheDocument();
});
});
describe('when one datapoint is present', () => {
it('renders as a straight line', () => {
const rendered = render(
wrapInThemedTestApp(<TrendLine data={[0.5]} title="sparkline" />),
);
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
});
});
describe.skip('when the data finishes above the success threshold', () => {
it('renders with the correct color', () => {
const rendered = render(
wrapInThemedTestApp(<TrendLine data={[0.5, 0.95]} title="sparkline" />),
);
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
});
});
describe.skip('when the data finishes within the the warning threshold', () => {
it('renders with the correct color', () => {
const rendered = render(
wrapInThemedTestApp(<TrendLine data={[0.5, 0.65]} title="sparkline" />),
);
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
});
});
describe.skip('when the data finishes within the the error threshold', () => {
it('renders with the correct color', () => {
const rendered = render(
wrapInThemedTestApp(<TrendLine data={[0.5, 0.4]} title="sparkline" />),
);
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
});
});
});
@@ -0,0 +1,42 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } 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: BackstageTheme): string | undefined {
const lastNum = data[data.length - 1];
if (!lastNum) return undefined;
if (lastNum >= 0.9) return theme.palette.status.ok;
if (lastNum >= 0.5) return theme.palette.status.warning;
return theme.palette.status.error;
}
const Trendline: FC<SparklinesProps & { title?: string }> = props => {
const theme = useTheme<BackstageTheme>();
if (!props.data) return null;
return (
<Sparklines width={120} height={30} min={0} max={1} {...props}>
{props.title && <title>{props.title}</title>}
<SparklinesLine color={color(props.data, theme)} />
</Sparklines>
);
};
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;
}