Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
||||
- 'packages/cli/**'
|
||||
- 'packages/core/**'
|
||||
- 'scripts/**'
|
||||
- 'yarn.lock'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -48,18 +48,31 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: check for yarn.lock changes
|
||||
id: yarn-lock
|
||||
run: git diff --quiet origin/master HEAD -- yarn.lock
|
||||
continue-on-error: true
|
||||
|
||||
- name: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: verify plugin template
|
||||
run: yarn lerna -- run diff -- --check
|
||||
|
||||
- name: lint
|
||||
run: yarn lerna -- run lint --since origin/master
|
||||
|
||||
- name: build
|
||||
run: yarn build
|
||||
|
||||
- name: test
|
||||
- name: test changed packages
|
||||
if: ${{ steps.yarn-lock.outcome == 'success' }}
|
||||
run: yarn lerna -- run test --since origin/master -- --coverage
|
||||
|
||||
- name: test all packages
|
||||
if: ${{ steps.yarn-lock.outcome == 'failure' }}
|
||||
run: yarn lerna -- run test -- --coverage
|
||||
|
||||
- name: yarn bundle, if app was changed
|
||||
run: git diff --quiet origin/master HEAD -- yarn.lock packages/app packages/core || yarn bundle
|
||||
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
Backstage
|
||||
Copyright 2020 Spotify AB
|
||||
|
||||
Portions of this software were developed by third-party software vendors:
|
||||
- Tech Radar Plugin (https://opensource.zalando.com/tech-radar/), Copyright (c) 2017 Zalando SE
|
||||
|
||||
@@ -95,7 +95,7 @@ We would love your help in building Backstage! See [CONTRIBUTING](CONTRIBUTING.m
|
||||
## Community
|
||||
|
||||
- [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the project
|
||||
- [Good First Issues](https://github.com/spotify/backstage/labels/good%20first%20issue) - Start here if you want to contribute
|
||||
- [Good First Issues](https://github.com/spotify/backstage/contribute) - Start here if you want to contribute
|
||||
- [RFCs](https://github.com/spotify/backstage/labels/rfc) - Help shape the technical direction
|
||||
- [FAQ](docs/FAQ.md) - Frequently Asked Questions
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
+1
-1
@@ -5,5 +5,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.1-alpha.4"
|
||||
"version": "0.1.1-alpha.5"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"bundle": "yarn build && yarn workspace example-app bundle",
|
||||
"build": "lerna run build",
|
||||
"clean": "lerna run clean",
|
||||
"diff": "lerna run diff --",
|
||||
"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 --",
|
||||
|
||||
+15
-14
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@backstage/core": "^0.1.1-alpha.4",
|
||||
"@backstage/cli": "^0.1.1-alpha.5",
|
||||
"@backstage/core": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-circleci": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-explore": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-home-page": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-inventory": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-lighthouse": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-tech-radar": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-welcome": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-explore": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-home-page": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-lighthouse": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-register-component": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-scaffolder": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-tech-radar": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-welcome": "^0.1.1-alpha.5",
|
||||
"@backstage/theme": "^0.1.1-alpha.5",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
@@ -21,8 +22,8 @@
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-use": "^13.24.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-use": "^14.2.0",
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -30,7 +31,7 @@
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/jquery": "^3.3.34",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
|
||||
@@ -85,7 +85,7 @@ const Root: FC<{}> = ({ children }) => (
|
||||
<SidebarItem icon={ExploreIcon} to="/explore" text="Explore" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="/create" text="Create..." />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={AccountTreeIcon} to="/inventory" text="Inventory" />
|
||||
<SidebarItem icon={AccountTreeIcon} to="/catalog" text="Catalog" />
|
||||
<SidebarItem icon={AccountCircle} to="/login" text="Login" />
|
||||
<SidebarDivider />
|
||||
<SidebarSpace />
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
export { plugin as HomePagePlugin } from '@backstage/plugin-home-page';
|
||||
export { plugin as WelcomePlugin } from '@backstage/plugin-welcome';
|
||||
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
export { plugin as InventoryPlugin } from '@backstage/plugin-inventory';
|
||||
export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
|
||||
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
export { plugin as TechRadar } from '@backstage/plugin-tech-radar';
|
||||
export { plugin as Explore } from '@backstage/plugin-explore';
|
||||
export { plugin as Circleci } from '@backstage/plugin-circleci';
|
||||
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@backstage/backend-common",
|
||||
"description": "Common functionality library for Backstage backends",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -21,6 +22,8 @@
|
||||
"build": "backstage-cli build-cache -- tsc",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -29,17 +32,18 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@backstage/cli": "^0.1.1-alpha.5",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/http-errors": "^1.6.3",
|
||||
"@types/morgan": "^1.9.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@types/yaml": "^1.9.7",
|
||||
"get-port": "^5.1.1",
|
||||
"http-errors": "^1.7.3",
|
||||
"jest": "^25.1.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"supertest": "^4.0.2",
|
||||
"typescript": "^3.8.3"
|
||||
"typescript": "^3.9.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 = {
|
||||
development: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: './dev.sqlite3',
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
staging: {
|
||||
client: 'postgresql',
|
||||
connection: {
|
||||
database: 'my_db',
|
||||
user: 'username',
|
||||
password: 'password',
|
||||
},
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10,
|
||||
},
|
||||
migrations: {
|
||||
tableName: 'knex_migrations',
|
||||
},
|
||||
},
|
||||
|
||||
production: {
|
||||
client: 'postgresql',
|
||||
connection: {
|
||||
database: 'my_db',
|
||||
user: 'username',
|
||||
password: 'password',
|
||||
},
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10,
|
||||
},
|
||||
migrations: {
|
||||
tableName: 'knex_migrations',
|
||||
},
|
||||
},
|
||||
*/
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
@@ -12,27 +12,31 @@
|
||||
"start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess nodemon",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean"
|
||||
"clean": "backstage-cli clean",
|
||||
"migrate:create": "knex migrate:make -x ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "0.1.1-alpha.4",
|
||||
"@backstage/plugin-inventory-backend": "0.1.1-alpha.4",
|
||||
"@backstage/plugin-scaffolder-backend": "0.1.1-alpha.4",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.5",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.5",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^3.22.0"
|
||||
"helmet": "^3.22.0",
|
||||
"knex": "^0.21.1",
|
||||
"sqlite3": "^4.2.0",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@backstage/cli": "^0.1.1-alpha.5",
|
||||
"@types/compression": "^1.7.0",
|
||||
"@types/cors": "^2.8.6",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
"@types/helmet": "^0.0.45",
|
||||
"@types/helmet": "^0.0.47",
|
||||
"jest": "^25.1.0",
|
||||
"tsc-watch": "^4.2.3",
|
||||
"typescript": "^3.8.3",
|
||||
"typescript": "^3.9.2",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
|
||||
@@ -28,19 +28,30 @@ import {
|
||||
notFoundHandler,
|
||||
requestLoggingHandler,
|
||||
} from '@backstage/backend-common';
|
||||
import inventory from './plugins/inventory';
|
||||
import scaffolder from './plugins/scaffolder';
|
||||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import knex from 'knex';
|
||||
import catalog from './plugins/catalog';
|
||||
import scaffolder from './plugins/scaffolder';
|
||||
import { PluginEnvironment } from './types';
|
||||
|
||||
const DEFAULT_PORT = 7000;
|
||||
const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT;
|
||||
const pluginEnvironment: PluginEnvironment = {
|
||||
logger: getRootLogger().child({ type: 'plugin' }),
|
||||
};
|
||||
|
||||
function createEnv(plugin: string): PluginEnvironment {
|
||||
const logger = getRootLogger().child({ type: 'plugin', plugin });
|
||||
const database = knex({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
return { logger, database };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const app = express();
|
||||
@@ -50,8 +61,8 @@ async function main() {
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/inventory', await inventory(pluginEnvironment));
|
||||
app.use('/scaffolder', await scaffolder(pluginEnvironment));
|
||||
app.use('/catalog', await catalog(createEnv('catalog')));
|
||||
app.use('/scaffolder', await scaffolder(createEnv('scaffolder')));
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 {
|
||||
DatabaseCatalog,
|
||||
createRouter,
|
||||
} from '@backstage/plugin-catalog-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function ({ logger, database }: PluginEnvironment) {
|
||||
const catalog = await DatabaseCatalog.create(database, logger);
|
||||
return await createRouter({ catalog, logger });
|
||||
}
|
||||
@@ -14,8 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Logger } from 'winston';
|
||||
import Knex from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export type PluginEnvironment = {
|
||||
logger: Logger;
|
||||
database: Knex;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmit": false,
|
||||
"declarationMap": true,
|
||||
"incremental": true,
|
||||
"target": "ES2019",
|
||||
"module": "ESNext",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/cli",
|
||||
"description": "CLI for developing Backstage plugins and apps",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -16,7 +16,7 @@
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "backstage-cli build-cache -- tsc --project tsconfig.build.json",
|
||||
"lint": "backstage-cli lint",
|
||||
@@ -43,7 +43,7 @@
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-monorepo": "^0.2.1",
|
||||
"fork-ts-checker-webpack-plugin": "^4.0.5",
|
||||
"fs-extra": "^8.1.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"handlebars": "^4.7.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"inquirer": "^7.0.4",
|
||||
@@ -55,14 +55,14 @@
|
||||
"react-dev-utils": "^10.2.0",
|
||||
"react-scripts": "^3.4.1",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"replace-in-file": "^5.0.2",
|
||||
"replace-in-file": "^6.0.0",
|
||||
"rollup": "^2.3.2",
|
||||
"rollup-plugin-image-files": "^1.4.2",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.2",
|
||||
"rollup-plugin-postcss": "^2.5.0",
|
||||
"rollup-plugin-postcss": "^3.1.1",
|
||||
"rollup-plugin-typescript2": "^0.26.0",
|
||||
"tar": "^6.0.1",
|
||||
"ts-loader": "^6.2.1",
|
||||
"ts-loader": "^7.0.4",
|
||||
"webpack": "^4.41.6",
|
||||
"webpack-dev-server": "^3.10.3"
|
||||
},
|
||||
@@ -89,7 +89,7 @@
|
||||
"templates",
|
||||
"config",
|
||||
"bin",
|
||||
"dist"
|
||||
"dist/**/*.js"
|
||||
],
|
||||
"nodemonConfig": {
|
||||
"watch": "./src",
|
||||
|
||||
@@ -88,25 +88,39 @@ 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);
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
|
||||
packageFileJson.resolutions = packageFileJson.resolutions || {};
|
||||
pkgJson.resolutions = pkgJson.resolutions || {};
|
||||
pkgJson.dependencies = pkgJson.dependencies || {};
|
||||
|
||||
const packages = ['cli', 'core', 'test-utils', 'test-utils-core', 'theme'];
|
||||
const depNames = [
|
||||
'cli',
|
||||
'core',
|
||||
'dev-utils',
|
||||
'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`;
|
||||
for (const name of depNames) {
|
||||
await Task.forItem(
|
||||
'adding',
|
||||
`@backstage/${name} link to package.json`,
|
||||
async () => {
|
||||
const pkgPath = paths.resolveOwnRoot('packages', name);
|
||||
// Add to both resolutions and dependencies, or transitive dependencies will still be fetched from the registry.
|
||||
pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
|
||||
await fs.writeFile(pkgJsonPath, newContents, 'utf-8').catch((error) => {
|
||||
throw new Error(
|
||||
`Failed to add resolutions to package.json: ${error.message}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
await fs
|
||||
.writeJSON(pkgJsonPath, pkgJson, { encoding: 'utf8', spaces: 2 })
|
||||
.catch((error) => {
|
||||
throw new Error(
|
||||
`Failed to add resolutions to package.json: ${error.message}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,23 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AggregatorInventory,
|
||||
createRouter,
|
||||
StaticInventory,
|
||||
} from '@backstage/plugin-inventory-backend';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
import fs from 'fs-extra';
|
||||
import { paths } from '../lib/paths';
|
||||
|
||||
export default async function ({ logger }: PluginEnvironment) {
|
||||
const inventory = new AggregatorInventory();
|
||||
inventory.enlist(
|
||||
new StaticInventory([
|
||||
{ id: 'component1' },
|
||||
{ id: 'component2' },
|
||||
{ id: 'component3' },
|
||||
{ id: 'component4' },
|
||||
]),
|
||||
);
|
||||
export const pre = async () => {
|
||||
const pkgPath = paths.resolveTarget('package.json');
|
||||
|
||||
return await createRouter({ inventory, logger });
|
||||
}
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
pkg.types = 'dist/index.d.ts';
|
||||
await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 });
|
||||
};
|
||||
|
||||
export const post = async () => {
|
||||
const pkgPath = paths.resolveTarget('package.json');
|
||||
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
pkg.types = 'src/index.ts';
|
||||
await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 });
|
||||
};
|
||||
@@ -54,6 +54,7 @@ class PackageJsonHandler {
|
||||
await this.syncField('types');
|
||||
await this.syncField('files');
|
||||
await this.syncScripts();
|
||||
await this.syncPublishConfig();
|
||||
await this.syncDependencies('dependencies');
|
||||
await this.syncDependencies('devDependencies');
|
||||
}
|
||||
@@ -105,6 +106,33 @@ class PackageJsonHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncPublishConfig() {
|
||||
const pkgPublishConf = this.pkg.publishConfig;
|
||||
const targetPublishConf = this.targetPkg.publishConfig;
|
||||
|
||||
// If template doesn't have a publish config we're done
|
||||
if (!pkgPublishConf) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Publish config can be removed the the target, skip in that case
|
||||
if (!targetPublishConf) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(pkgPublishConf)) {
|
||||
// Don't want to mess with peoples internal setup
|
||||
if (!['access', 'registry'].includes(key)) {
|
||||
await this.syncField(
|
||||
key,
|
||||
pkgPublishConf,
|
||||
targetPublishConf,
|
||||
'publishConfig',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async syncDependencies(fieldName: string) {
|
||||
const pkgDeps = this.pkg[fieldName];
|
||||
const targetDeps = (this.targetPkg[fieldName] =
|
||||
|
||||
@@ -82,6 +82,16 @@ const main = (argv: string[]) => {
|
||||
.description('Run tests, forwarding args to Jest, defaulting to watch mode')
|
||||
.action(actionHandler(() => require('./commands/testCommand')));
|
||||
|
||||
program
|
||||
.command('prepack')
|
||||
.description('Prepares a package for packaging before publishing')
|
||||
.action(actionHandler(() => require('./commands/pack').pre));
|
||||
|
||||
program
|
||||
.command('postpack')
|
||||
.description('Restores the changes made by the prepack command')
|
||||
.action(actionHandler(() => require('./commands/pack').post));
|
||||
|
||||
program
|
||||
.command('watch-deps')
|
||||
.option('--build', 'Build all dependencies on startup')
|
||||
@@ -129,11 +139,14 @@ const main = (argv: string[]) => {
|
||||
|
||||
// Wraps an action function so that it always exits and handles errors
|
||||
function actionHandler<T extends readonly any[]>(
|
||||
actionRequireFunc: () => { default(...args: T): Promise<any> },
|
||||
actionRequireFunc:
|
||||
| (() => { default(...args: T): Promise<any> })
|
||||
| (() => (...args: T) => Promise<any>),
|
||||
): (...args: T) => Promise<never> {
|
||||
return async (...args: T) => {
|
||||
try {
|
||||
const actionFunc = actionRequireFunc().default;
|
||||
const ret = actionRequireFunc();
|
||||
const actionFunc = typeof ret === 'function' ? ret : ret.default;
|
||||
await actionFunc(...args);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
|
||||
@@ -26,8 +26,5 @@
|
||||
"@backstage/cli": "^{{version}}",
|
||||
"lerna": "^3.20.2",
|
||||
"prettier": "^1.19.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/cypress": "4.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
"plugin-welcome": "0.0.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-use": "^13.24.0"
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "plugin-welcome",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
@@ -10,6 +10,8 @@
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -19,8 +21,8 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^13.24.0",
|
||||
"react-router-dom": "5.1.2"
|
||||
"react-use": "^14.2.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^{{version}}",
|
||||
@@ -31,6 +33,6 @@
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
"name": "@backstage/plugin-{{id}}",
|
||||
"version": "{{version}}",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"start": "backstage-cli plugin:serve",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -21,7 +26,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-use": "^13.24.0"
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^{{version}}",
|
||||
@@ -29,12 +34,12 @@
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
|
||||
+19
-13
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/core",
|
||||
"description": "Core API used by Backstage plugins and apps",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -17,15 +17,17 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.5",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"classnames": "^2.2.6",
|
||||
@@ -33,30 +35,34 @@
|
||||
"lodash": "^4.17.15",
|
||||
"material-table": "^1.57.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"rc-progress": "^2.5.2",
|
||||
"rc-progress": "^3.0.0",
|
||||
"react": "^16.12.0",
|
||||
"react-addons-text-content": "0.0.4",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-helmet": "5.2.1",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-sparklines": "^1.7.0"
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-sparklines": "^1.7.0",
|
||||
"react-syntax-highlighter": "^12.2.1",
|
||||
"react-use": "^14.2.0",
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@backstage/test-utils": "0.1.1-alpha.4",
|
||||
"@backstage/test-utils-core": "^0.1.1-alpha.4",
|
||||
"@backstage/cli": "^0.1.1-alpha.5",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.5",
|
||||
"@backstage/test-utils-core": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-helmet": "^5.0.15",
|
||||
"@types/react-sparklines": "^1.7.0"
|
||||
"@types/react-sparklines": "^1.7.0",
|
||||
"@types/zen-observable": "^0.8.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ApiProvider, { useApi, withApis } from './ApiProvider';
|
||||
import ApiRef from './ApiRef';
|
||||
import ApiRegistry from './ApiRegistry';
|
||||
import { ApiProvider, useApi, withApis } from './ApiProvider';
|
||||
import { ApiRef } from './ApiRef';
|
||||
import { ApiRegistry } from './ApiRegistry';
|
||||
import { render } from '@testing-library/react';
|
||||
import { withLogCollector } from '@backstage/test-utils-core';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React, { FC, createContext, useContext, ReactNode } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiRef } from './ApiRef';
|
||||
import { ApiHolder, TypesToApiRefs } from './types';
|
||||
|
||||
type Props = {
|
||||
@@ -26,7 +26,7 @@ type Props = {
|
||||
|
||||
const Context = createContext<ApiHolder | undefined>(undefined);
|
||||
|
||||
const ApiProvider: FC<Props> = ({ apis, children }) => {
|
||||
export const ApiProvider: FC<Props> = ({ apis, children }) => {
|
||||
return <Context.Provider value={apis} children={children} />;
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ export function withApis<T>(apis: TypesToApiRefs<T>) {
|
||||
return function withApisWrapper<P extends T>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
) {
|
||||
const Hoc: FC<Omit<P, keyof T>> = props => {
|
||||
const Hoc: FC<Omit<P, keyof T>> = (props) => {
|
||||
const apiHolder = useContext(Context);
|
||||
|
||||
if (!apiHolder) {
|
||||
@@ -84,5 +84,3 @@ export function withApis<T>(apis: TypesToApiRefs<T>) {
|
||||
return Hoc;
|
||||
};
|
||||
}
|
||||
|
||||
export default ApiProvider;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiRef } from './ApiRef';
|
||||
|
||||
describe('ApiRef', () => {
|
||||
it('should be created', () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export type ApiRefConfig = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
export default class ApiRef<T> {
|
||||
export class ApiRef<T> {
|
||||
constructor(private readonly config: ApiRefConfig) {
|
||||
if (!config.id.match(/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/)) {
|
||||
throw new Error(
|
||||
@@ -45,3 +45,7 @@ export default class ApiRef<T> {
|
||||
return `apiRef{${this.config.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiRef<T>(config: ApiRefConfig) {
|
||||
return new ApiRef<T>(config);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRegistry from './ApiRegistry';
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiRegistry } from './ApiRegistry';
|
||||
import { ApiRef } from './ApiRef';
|
||||
|
||||
describe('ApiRegistry', () => {
|
||||
const x1Ref = new ApiRef<number>({ id: 'x', description: '' });
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiRef } from './ApiRef';
|
||||
import { ApiHolder } from './types';
|
||||
|
||||
type ApiImpl<T = unknown> = readonly [ApiRef<T>, T];
|
||||
@@ -33,7 +33,7 @@ class ApiRegistryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
export default class ApiRegistry implements ApiHolder {
|
||||
export class ApiRegistry implements ApiHolder {
|
||||
static builder() {
|
||||
return new ApiRegistryBuilder();
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiTestRegistry from './ApiTestRegistry';
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiTestRegistry } from './ApiTestRegistry';
|
||||
import { ApiRef } from './ApiRef';
|
||||
|
||||
describe('ApiTestRegistry', () => {
|
||||
const aRef = new ApiRef<number>({ id: 'a', description: '' });
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiRef } from './ApiRef';
|
||||
import { TypesToApiRefs, AnyApiRef, ApiHolder, ApiFactory } from './types';
|
||||
|
||||
export default class ApiTestRegistry implements ApiHolder {
|
||||
export class ApiTestRegistry implements ApiHolder {
|
||||
private readonly apis = new Map<AnyApiRef, unknown>();
|
||||
private factories = new Map<
|
||||
AnyApiRef,
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import ApiRef from '../ApiRef';
|
||||
import { ApiRef } from '../ApiRef';
|
||||
|
||||
export type AlertMessage = {
|
||||
message: string;
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from '../ApiRef';
|
||||
import { ApiRef } from '../ApiRef';
|
||||
|
||||
/**
|
||||
* Mirrors the javascript Error class, for the purpose of
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from '../ApiRef';
|
||||
import { ApiRef } from '../ApiRef';
|
||||
import {
|
||||
UserFlags,
|
||||
FeatureFlagsRegistry,
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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 { IconComponent } from '../../../icons';
|
||||
import { Observable } from '../../types';
|
||||
import { ApiRef } from '../ApiRef';
|
||||
|
||||
export type OAuthScopes = {
|
||||
extend(scopes: OAuthScopeLike): OAuthScopes;
|
||||
hasScopes(scopes: OAuthScopeLike): boolean;
|
||||
toSet(): Set<string>;
|
||||
toString(): string;
|
||||
};
|
||||
|
||||
export type OAuthScopeLike =
|
||||
| string /** Space separated scope strings */
|
||||
| string[] /** Array of individual scope strings */
|
||||
| OAuthScopes;
|
||||
|
||||
/**
|
||||
* Options used to open a login popup.
|
||||
*/
|
||||
export type LoginPopupOptions = {
|
||||
/**
|
||||
* The URL that the auth popup should point to
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* The name of the popup, as in second argument to window.open
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The origin of the final popup page that will post a message to this window.
|
||||
*/
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* The width of the popup in pixels, defaults to 500
|
||||
*/
|
||||
width?: number;
|
||||
|
||||
/**
|
||||
* The height of the popup in pixels, defaults to 700
|
||||
*/
|
||||
height?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Information about the auth provider that we're requesting a login towards.
|
||||
*
|
||||
* This should be shown to the user so that they can be informed about what login is being requested
|
||||
* before a popup is shown.
|
||||
*/
|
||||
export type AuthProvider = {
|
||||
/**
|
||||
* Title for the auth provider, for example "GitHub"
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Icon for the auth provider.
|
||||
*/
|
||||
icon: IconComponent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes how to handle auth requests. Both how to show them to the user, and what to do when
|
||||
* the user accesses the auth request.
|
||||
*/
|
||||
export type AuthRequesterOptions<AuthResponse> = {
|
||||
/**
|
||||
* Information about the auth provider, which will be forwarded to auth requests.
|
||||
*/
|
||||
provider: AuthProvider;
|
||||
|
||||
/**
|
||||
* Implementation of the auth flow, which will be called synchronously when
|
||||
* trigger() is called on an auth requests.
|
||||
*/
|
||||
onAuthRequest(scope: OAuthScopes): Promise<AuthResponse>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function used to trigger new auth requests for a set of scopes.
|
||||
*
|
||||
* The returned promise will resolve to the same value returned by the onAuthRequest in the
|
||||
* AuthRequesterOptions. Or rejected, if the request is rejected.
|
||||
*
|
||||
* This function can be called multiple times before the promise resolves. All calls
|
||||
* will be merged into one request, and the scopes forwarded to the onAuthRequest will be the
|
||||
* union of all requested scopes.
|
||||
*/
|
||||
export type AuthRequester<AuthResponse> = (
|
||||
scope: OAuthScopes,
|
||||
) => Promise<AuthResponse>;
|
||||
|
||||
/**
|
||||
* An pending auth request for a single auth provider. The request will remain in this pending
|
||||
* state until either reject() or trigger() is called.
|
||||
*
|
||||
* Any new requests for the same provider are merged into the existing pending request, meaning
|
||||
* there will only ever be a single pending request for a given provider.
|
||||
*/
|
||||
export type PendingAuthRequest = {
|
||||
/**
|
||||
* Information about the auth provider, as given in the AuthRequesterOptions
|
||||
*/
|
||||
provider: AuthProvider;
|
||||
|
||||
/**
|
||||
* Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError".
|
||||
*/
|
||||
reject: () => void;
|
||||
|
||||
/**
|
||||
* Trigger the auth request to continue the auth flow, by for example showing a popup.
|
||||
*
|
||||
* Synchronously calls onAuthRequest with all scope currently in the request.
|
||||
*/
|
||||
trigger(): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides helpers for implemented OAuth login flows within Backstage.
|
||||
*/
|
||||
export type OAuthRequestApi = {
|
||||
/**
|
||||
* Show a popup pointing to a URL that starts an OAuth flow.
|
||||
*
|
||||
* The redirect handler of the flow should use postMessage to communicate back
|
||||
* to the app window.
|
||||
*
|
||||
* The returned promise resolves to the contents of the message that was posted from the auth popup.
|
||||
*/
|
||||
showLoginPopup(options: LoginPopupOptions): Promise<any>;
|
||||
|
||||
/**
|
||||
* A utility for showing login popups or similar things, and merging together multiple requests for
|
||||
* different scopes into one request that inclues all scopes.
|
||||
*
|
||||
* The passed in options provide information about the login provider, and how to handle auth requests.
|
||||
*
|
||||
* The returned AuthRequester function is used to request login with new scopes. These requests
|
||||
* are merged together and forwarded to the auth handler, as soon as a consumer of auth requests
|
||||
* triggers an auth flow.
|
||||
*
|
||||
* See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info.
|
||||
*/
|
||||
createAuthRequester<AuthResponse>(
|
||||
options: AuthRequesterOptions<AuthResponse>,
|
||||
): AuthRequester<AuthResponse>;
|
||||
|
||||
/**
|
||||
* Observers panding auth requests. The returned observable will emit all
|
||||
* current active auth request, at most one for each created auth requester.
|
||||
*
|
||||
* Each request has its own info about the login provider, forwarded from the auth requester options.
|
||||
*
|
||||
* Depending on user interaction, the request should either be rejected, or used to trigger the auth handler.
|
||||
* If the request is rejected, all pending AuthRequester calls will fail with a "RejectedError".
|
||||
* If a auth is triggered, and the auth handler resolves successfully, then all currently pending
|
||||
* AuthRequester calls will resolve to the value returned by the onAuthRequest call.
|
||||
*/
|
||||
authRequest$(): Observable<PendingAuthRequest[]>;
|
||||
};
|
||||
|
||||
export const oauthRequestApiRef = new ApiRef<OAuthRequestApi>({
|
||||
id: 'core.oauthrequest',
|
||||
description: 'An API for implementing unified OAuth flows in Backstage',
|
||||
});
|
||||
@@ -20,6 +20,7 @@
|
||||
//
|
||||
// If you think some API definition is missing, please open an Issue or send a PR!
|
||||
|
||||
export * from './alert';
|
||||
export * from './error';
|
||||
export * from './featureFlags';
|
||||
export * from './AlertApi';
|
||||
export * from './ErrorApi';
|
||||
export * from './FeatureFlagsApi';
|
||||
export * from './OAuthRequestApi';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 { OAuthScopes, OAuthScopeLike } from '../../definitions';
|
||||
|
||||
/**
|
||||
* The BasicOAuthScopes class is an implementation of OAuthScopes that
|
||||
* works for any simple comma- or space-separated format of scope.
|
||||
*/
|
||||
export class BasicOAuthScopes implements OAuthScopes {
|
||||
static from(scopes: OAuthScopeLike, normalizer?: (scope: string) => string) {
|
||||
const normalized = BasicOAuthScopes.asStrings(scopes, normalizer);
|
||||
return new BasicOAuthScopes(new Set(normalized), normalizer);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly scopes: Set<string>,
|
||||
private readonly normalizer?: (scope: string) => string,
|
||||
) {}
|
||||
|
||||
extend(requestedScopes: OAuthScopeLike): BasicOAuthScopes {
|
||||
const newScopes = new Set(this.scopes);
|
||||
BasicOAuthScopes.asStrings(requestedScopes, this.normalizer).forEach((s) =>
|
||||
newScopes.add(s),
|
||||
);
|
||||
return new BasicOAuthScopes(newScopes, this.normalizer);
|
||||
}
|
||||
|
||||
hasScopes(scopes: OAuthScopeLike): boolean {
|
||||
return BasicOAuthScopes.asStrings(scopes, this.normalizer).every((s) =>
|
||||
this.scopes.has(s),
|
||||
);
|
||||
}
|
||||
|
||||
toSet(): Set<string> {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return Array.from(this.scopes).join(' ');
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return Array.from(this.scopes);
|
||||
}
|
||||
|
||||
static asStrings(
|
||||
input: OAuthScopeLike,
|
||||
normalizer?: (scope: string) => string,
|
||||
): string[] {
|
||||
let scopeArray: string[];
|
||||
if (typeof input === 'string') {
|
||||
scopeArray = input.split(/[,\s]/).filter(Boolean);
|
||||
} else if (Array.isArray(input)) {
|
||||
scopeArray = input;
|
||||
} else {
|
||||
scopeArray = Array.from(input.toSet());
|
||||
}
|
||||
if (normalizer) {
|
||||
scopeArray = scopeArray.map((x) => normalizer(x));
|
||||
}
|
||||
return scopeArray;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 { wait } from '@testing-library/react';
|
||||
import { OAuthPendingRequests } from './OAuthPendingRequests';
|
||||
import { BasicOAuthScopes } from './BasicOAuthScopes';
|
||||
|
||||
describe('OAuthPendingRequests', () => {
|
||||
it('notifies new observers about current state', async () => {
|
||||
const target = new OAuthPendingRequests<string>();
|
||||
const next = jest.fn();
|
||||
const error = jest.fn();
|
||||
|
||||
const input = BasicOAuthScopes.from('a b');
|
||||
target.pending().subscribe({ next, error });
|
||||
target.request(input);
|
||||
|
||||
await wait(() => expect(next).toBeCalledTimes(2));
|
||||
expect(next.mock.calls[0][0].scopes).toBeUndefined();
|
||||
expect(next.mock.calls[1][0].scopes.toString()).toBe(input.toString());
|
||||
expect(error.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('resolves requests and notifies observers', async () => {
|
||||
const target = new OAuthPendingRequests<string>();
|
||||
const next = jest.fn();
|
||||
const error = jest.fn();
|
||||
|
||||
const request1 = target.request(BasicOAuthScopes.from('a'));
|
||||
const request2 = target.request(BasicOAuthScopes.from('a'));
|
||||
target.pending().subscribe({ next, error });
|
||||
target.resolve(BasicOAuthScopes.from('a'), 'session1');
|
||||
target.resolve(BasicOAuthScopes.from('a'), 'session2');
|
||||
|
||||
await expect(request1).resolves.toBe('session1');
|
||||
await expect(request2).resolves.toBe('session1');
|
||||
expect(next).toBeCalledTimes(3); // once on subscription, twice on resolve
|
||||
expect(error).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can resolve through the observable', async () => {
|
||||
const target = new OAuthPendingRequests<string>();
|
||||
const next = jest.fn((pendingRequest) => pendingRequest.resolve('done'));
|
||||
const error = jest.fn();
|
||||
|
||||
const request1 = target.request(BasicOAuthScopes.from('a'));
|
||||
target.pending().subscribe({ next, error });
|
||||
|
||||
await expect(request1).resolves.toBe('done');
|
||||
expect(next).toBeCalledTimes(2); // once with data on subscription, once empty after resolution
|
||||
expect(error).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('rejects requests and notifies observers only once', async () => {
|
||||
const target = new OAuthPendingRequests<string>();
|
||||
const next = jest.fn();
|
||||
const error = jest.fn();
|
||||
const rejection = new Error('eek');
|
||||
|
||||
const request1 = target.request(BasicOAuthScopes.from('a'));
|
||||
const request2 = target.request(BasicOAuthScopes.from('a'));
|
||||
target.pending().subscribe({ next, error });
|
||||
target.reject(rejection);
|
||||
target.resolve(BasicOAuthScopes.from('a'), 'session');
|
||||
|
||||
await expect(request1).rejects.toBe(rejection);
|
||||
await expect(request2).rejects.toBe(rejection);
|
||||
expect(next).toBeCalledTimes(3); // once on subscription, once or reject, once on resolve
|
||||
expect(error).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can reject through the observable', async () => {
|
||||
const target = new OAuthPendingRequests<string>();
|
||||
const rejection = new Error('nope');
|
||||
const next = jest.fn((pendingRequest) => pendingRequest.reject(rejection));
|
||||
const error = jest.fn();
|
||||
|
||||
const request1 = target.request(BasicOAuthScopes.from('a'));
|
||||
target.pending().subscribe({ next, error });
|
||||
|
||||
await expect(request1).rejects.toBe(rejection);
|
||||
expect(next).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 Observable from 'zen-observable';
|
||||
import { OAuthScopes } from '../../definitions';
|
||||
|
||||
type RequestQueueEntry<ResultType> = {
|
||||
scopes: OAuthScopes;
|
||||
resolve: (value?: ResultType | PromiseLike<ResultType> | undefined) => void;
|
||||
reject: (reason: Error) => void;
|
||||
};
|
||||
|
||||
export type PendingRequest<ResultType> = {
|
||||
scopes: OAuthScopes | undefined;
|
||||
resolve: (value: ResultType) => void;
|
||||
reject: (reason: Error) => void;
|
||||
};
|
||||
|
||||
export type OAuthPendingRequestsApi<ResultType> = {
|
||||
request(scopes: OAuthScopes): Promise<ResultType>;
|
||||
resolve(scopes: OAuthScopes, result: ResultType): void;
|
||||
reject(error: Error): void;
|
||||
pending(): Observable<PendingRequest<ResultType>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The OAuthPendingRequests class is a utility for managing and observing
|
||||
* a stream of requests for oauth scopes for a single provider, and resolving
|
||||
* them correctly once requests are fulfilled.
|
||||
*/
|
||||
export class OAuthPendingRequests<ResultType>
|
||||
implements OAuthPendingRequestsApi<ResultType> {
|
||||
private requests: RequestQueueEntry<ResultType>[] = [];
|
||||
private listeners: ZenObservable.SubscriptionObserver<
|
||||
PendingRequest<ResultType>
|
||||
>[] = [];
|
||||
|
||||
request(scopes: OAuthScopes): Promise<ResultType> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.requests.push({ scopes, resolve, reject });
|
||||
|
||||
const pending = this.getCurrentPending();
|
||||
this.listeners.forEach((listener) => listener.next(pending));
|
||||
});
|
||||
}
|
||||
|
||||
resolve(scopes: OAuthScopes, result: ResultType): void {
|
||||
this.requests = this.requests.filter((request) => {
|
||||
if (scopes.hasScopes(request.scopes)) {
|
||||
request.resolve(result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const pending = this.getCurrentPending();
|
||||
this.listeners.forEach((listener) => listener.next(pending));
|
||||
}
|
||||
|
||||
reject(error: Error) {
|
||||
this.requests.forEach((request) => request.reject(error));
|
||||
this.requests = [];
|
||||
|
||||
const pending = this.getCurrentPending();
|
||||
this.listeners.forEach((listener) => listener.next(pending));
|
||||
}
|
||||
|
||||
pending(): Observable<PendingRequest<ResultType>> {
|
||||
return new Observable((subscriber) => {
|
||||
this.listeners.push(subscriber);
|
||||
subscriber.next(this.getCurrentPending());
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter((l) => l !== subscriber);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getCurrentPending(): PendingRequest<ResultType> {
|
||||
const currentScopes =
|
||||
this.requests.length === 0
|
||||
? undefined
|
||||
: this.requests
|
||||
.slice(1)
|
||||
.reduce(
|
||||
(acc, current) => acc.extend(current.scopes),
|
||||
this.requests[0].scopes,
|
||||
);
|
||||
|
||||
return {
|
||||
scopes: currentScopes,
|
||||
resolve: (value: ResultType) => {
|
||||
if (currentScopes) {
|
||||
this.resolve(currentScopes, value);
|
||||
}
|
||||
},
|
||||
reject: (reason: Error) => {
|
||||
if (currentScopes) {
|
||||
this.reject(reason);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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 { OAuthRequestManager } from './OAuthRequestManager';
|
||||
|
||||
describe('OAuthApi login popup', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should show an auth popup', async () => {
|
||||
const oauth = new OAuthRequestManager();
|
||||
|
||||
const popupMock = { closed: false };
|
||||
const openSpy = jest
|
||||
.spyOn(window, 'open')
|
||||
.mockReturnValue(popupMock as Window);
|
||||
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
const payloadPromise = oauth.showLoginPopup({
|
||||
url:
|
||||
'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb',
|
||||
name: 'test-popup',
|
||||
origin: 'my-origin',
|
||||
});
|
||||
|
||||
expect(openSpy).toBeCalledTimes(1);
|
||||
expect(openSpy.mock.calls[0][0]).toBe(
|
||||
'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb',
|
||||
);
|
||||
expect(openSpy.mock.calls[0][1]).toBe('test-popup');
|
||||
expect(addEventListenerSpy).toBeCalledTimes(1);
|
||||
expect(removeEventListenerSpy).toBeCalledTimes(0);
|
||||
|
||||
const listener = addEventListenerSpy.mock.calls[0][1] as EventListener;
|
||||
|
||||
await expect(Promise.race([payloadPromise, 'waiting'])).resolves.toBe(
|
||||
'waiting',
|
||||
);
|
||||
|
||||
listener({} as MessageEvent);
|
||||
|
||||
await expect(Promise.race([payloadPromise, 'waiting'])).resolves.toBe(
|
||||
'waiting',
|
||||
);
|
||||
|
||||
// None of these should be accepted
|
||||
listener({ source: popupMock } as MessageEvent);
|
||||
listener({ origin: 'my-origin' } as MessageEvent);
|
||||
listener({ data: { type: 'oauth-result' } } as MessageEvent);
|
||||
listener({
|
||||
source: popupMock,
|
||||
origin: 'my-origin',
|
||||
data: {},
|
||||
} as MessageEvent);
|
||||
listener({
|
||||
source: popupMock,
|
||||
origin: 'my-origin',
|
||||
data: { type: 'not-oauth-result', payload: {} },
|
||||
} as MessageEvent);
|
||||
|
||||
await expect(Promise.race([payloadPromise, 'waiting'])).resolves.toBe(
|
||||
'waiting',
|
||||
);
|
||||
|
||||
const myPayload = {};
|
||||
|
||||
// This should be accepted as a valid sessions response
|
||||
listener({
|
||||
source: popupMock,
|
||||
origin: 'my-origin',
|
||||
data: {
|
||||
type: 'oauth-result',
|
||||
payload: myPayload,
|
||||
},
|
||||
} as MessageEvent);
|
||||
|
||||
await expect(payloadPromise).resolves.toBe(myPayload);
|
||||
|
||||
expect(openSpy).toBeCalledTimes(1);
|
||||
expect(addEventListenerSpy).toBeCalledTimes(1);
|
||||
expect(removeEventListenerSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fail if popup returns error', async () => {
|
||||
const oauth = new OAuthRequestManager();
|
||||
|
||||
const popupMock = { closed: false };
|
||||
const openSpy = jest
|
||||
.spyOn(window, 'open')
|
||||
.mockReturnValue(popupMock as Window);
|
||||
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
const payloadPromise = oauth.showLoginPopup({
|
||||
url: 'url',
|
||||
name: 'name',
|
||||
origin: 'my-origin',
|
||||
});
|
||||
|
||||
expect(openSpy).toBeCalledTimes(1);
|
||||
expect(addEventListenerSpy).toBeCalledTimes(1);
|
||||
expect(removeEventListenerSpy).toBeCalledTimes(0);
|
||||
|
||||
const listener = addEventListenerSpy.mock.calls[0][1] as EventListener;
|
||||
|
||||
listener({
|
||||
source: popupMock,
|
||||
origin: 'my-origin',
|
||||
data: {
|
||||
type: 'oauth-result',
|
||||
payload: {
|
||||
error: {
|
||||
message: 'NOPE',
|
||||
name: 'NopeError',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as MessageEvent);
|
||||
|
||||
await expect(payloadPromise).rejects.toThrow({
|
||||
name: 'NopeError',
|
||||
message: 'NOPE',
|
||||
});
|
||||
|
||||
expect(openSpy).toBeCalledTimes(1);
|
||||
expect(addEventListenerSpy).toBeCalledTimes(1);
|
||||
expect(removeEventListenerSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fail if popup is closed', async () => {
|
||||
const oauth = new OAuthRequestManager();
|
||||
|
||||
const openSpy = jest
|
||||
.spyOn(window, 'open')
|
||||
.mockReturnValue({ closed: false } as Window);
|
||||
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
||||
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
|
||||
const popupMock = { closed: false };
|
||||
|
||||
openSpy.mockReturnValue(popupMock as Window);
|
||||
|
||||
const payloadPromise = oauth.showLoginPopup({
|
||||
url: 'url',
|
||||
name: 'name',
|
||||
origin: 'origin',
|
||||
});
|
||||
|
||||
expect(openSpy).toBeCalledTimes(1);
|
||||
expect(addEventListenerSpy).toBeCalledTimes(1);
|
||||
expect(removeEventListenerSpy).toBeCalledTimes(0);
|
||||
|
||||
setTimeout(() => {
|
||||
popupMock.closed = true;
|
||||
}, 150);
|
||||
await expect(payloadPromise).rejects.toThrow(
|
||||
'Login failed, popup was closed',
|
||||
);
|
||||
|
||||
expect(openSpy).toBeCalledTimes(1);
|
||||
expect(addEventListenerSpy).toBeCalledTimes(1);
|
||||
expect(removeEventListenerSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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 {
|
||||
OAuthRequestApi,
|
||||
LoginPopupOptions,
|
||||
PendingAuthRequest,
|
||||
AuthRequester,
|
||||
AuthRequesterOptions,
|
||||
} from '../../definitions';
|
||||
import Observable from 'zen-observable';
|
||||
import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';
|
||||
|
||||
/**
|
||||
* The OAuthRequestManager is an implementation of the OAuthRequestApi.
|
||||
*
|
||||
* The purpose of this class and the API is to read a stream of incoming requests
|
||||
* of OAuth access tokens from different providers with varying scope, and funnel
|
||||
* them all together into a single request for each OAuth provider.
|
||||
*/
|
||||
export class OAuthRequestManager implements OAuthRequestApi {
|
||||
private readonly request$: Observable<PendingAuthRequest[]>;
|
||||
private readonly observers = new Set<
|
||||
ZenObservable.SubscriptionObserver<PendingAuthRequest[]>
|
||||
>();
|
||||
private currentRequests: PendingAuthRequest[] = [];
|
||||
private handlerCount = 0;
|
||||
|
||||
constructor() {
|
||||
this.request$ = new Observable<PendingAuthRequest[]>((observer) => {
|
||||
observer.next(this.currentRequests);
|
||||
|
||||
this.observers.add(observer);
|
||||
return () => {
|
||||
this.observers.delete(observer);
|
||||
};
|
||||
}).map((requests) => requests.filter(Boolean)); // Convert from sparse array to array of present items only
|
||||
}
|
||||
|
||||
createAuthRequester<T>(options: AuthRequesterOptions<T>): AuthRequester<T> {
|
||||
const handler = new OAuthPendingRequests<T>();
|
||||
|
||||
const index = this.handlerCount;
|
||||
this.handlerCount++;
|
||||
|
||||
handler.pending().subscribe({
|
||||
next: (scopeRequest) => {
|
||||
const newRequests = this.currentRequests.slice();
|
||||
const request = this.makeAuthRequest(scopeRequest, options);
|
||||
if (!request) {
|
||||
delete newRequests[index];
|
||||
} else {
|
||||
newRequests[index] = request;
|
||||
}
|
||||
this.currentRequests = newRequests;
|
||||
this.observers.forEach((observer) => observer.next(newRequests));
|
||||
},
|
||||
});
|
||||
|
||||
return (scopes) => {
|
||||
return handler.request(scopes);
|
||||
};
|
||||
}
|
||||
|
||||
// Converts the pending request and popup options into a popup request that we can forward to subscribers.
|
||||
private makeAuthRequest(
|
||||
request: PendingRequest<any>,
|
||||
options: AuthRequesterOptions<any>,
|
||||
): PendingAuthRequest | undefined {
|
||||
const { scopes } = request;
|
||||
if (!scopes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
provider: options.provider,
|
||||
trigger: async () => {
|
||||
const result = await options.onAuthRequest(scopes);
|
||||
request.resolve(result);
|
||||
},
|
||||
reject: () => {
|
||||
const error = new Error('Login failed, rejected by user');
|
||||
error.name = 'RejectedError';
|
||||
request.reject(error);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
authRequest$(): Observable<PendingAuthRequest[]> {
|
||||
return this.request$;
|
||||
}
|
||||
|
||||
async showLoginPopup(options: LoginPopupOptions): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const width = options.width || 500;
|
||||
const height = options.height || 700;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
const popup = window.open(
|
||||
options.url,
|
||||
options.name,
|
||||
`menubar=no,location=no,resizable=no,scrollbars=no,status=no,width=${width},height=${height},top=${top},left=${left}`,
|
||||
);
|
||||
|
||||
if (!popup || typeof popup.closed === 'undefined' || popup.closed) {
|
||||
reject(new Error('Failed to open auth popup.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const messageListener = (event: MessageEvent) => {
|
||||
if (event.source !== popup) {
|
||||
return;
|
||||
}
|
||||
if (event.origin !== options.origin) {
|
||||
return;
|
||||
}
|
||||
const { data } = event;
|
||||
if (data.type !== 'oauth-result') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.payload.error) {
|
||||
const error = new Error(data.payload.error.message);
|
||||
error.name = data.payload.error.name;
|
||||
// TODO: proper error type
|
||||
// error.extra = data.payload.error.extra;
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(data.payload);
|
||||
}
|
||||
done();
|
||||
};
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
const error = new Error('Login failed, popup was closed');
|
||||
error.name = 'PopupClosedError';
|
||||
reject(error);
|
||||
done();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
function done() {
|
||||
window.removeEventListener('message', messageListener);
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
window.addEventListener('message', messageListener);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 { OAuthRequestManager } from './OAuthRequestManager';
|
||||
@@ -20,3 +20,4 @@
|
||||
|
||||
export * from './AlertApiForwarder';
|
||||
export * from './ErrorApiForwarder';
|
||||
export * from './OAuthRequestManager';
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default as ApiProvider, useApi } from './ApiProvider';
|
||||
export { default as ApiRegistry } from './ApiRegistry';
|
||||
export { default as ApiTestRegistry } from './ApiTestRegistry';
|
||||
export { default as ApiRef } from './ApiRef';
|
||||
export { ApiProvider, useApi } from './ApiProvider';
|
||||
export { ApiRegistry } from './ApiRegistry';
|
||||
export { ApiTestRegistry } from './ApiTestRegistry';
|
||||
export * from './ApiRef';
|
||||
export * from './types';
|
||||
export * from './helpers';
|
||||
export * from './definitions';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ApiRef from './ApiRef';
|
||||
import { ApiRef } from './ApiRef';
|
||||
|
||||
export type AnyApiRef = ApiRef<any>;
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ import React, { ComponentType } from 'react';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { AppContextProvider } from './AppContext';
|
||||
import { App } from './types';
|
||||
import BackstagePlugin from '../plugin/Plugin';
|
||||
import { BackstagePlugin } from '../plugin';
|
||||
import { FeatureFlagsRegistryItem } from './FeatureFlags';
|
||||
import { featureFlagsApiRef } from '../apis/definitions/featureFlags';
|
||||
import { featureFlagsApiRef } from '../apis/definitions';
|
||||
import ErrorPage from '../../layout/ErrorPage';
|
||||
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ class AppImpl implements App {
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppBuilder {
|
||||
export class AppBuilder {
|
||||
private apis?: ApiHolder;
|
||||
private systemIcons = { ...defaultSystemIcons };
|
||||
private readonly plugins = new Set<BackstagePlugin>();
|
||||
@@ -84,6 +84,19 @@ export default class AppBuilder {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'nav-target-component': {
|
||||
const { target, component, options = {} } = output;
|
||||
const { exact = true } = options;
|
||||
routes.push(
|
||||
<Route
|
||||
key={`${plugin.getId()}-${target.path}`}
|
||||
path={target.path}
|
||||
component={component}
|
||||
exact={exact}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'redirect-route': {
|
||||
const { path, target, options = {} } = output;
|
||||
const { exact = true } = options;
|
||||
@@ -132,3 +145,7 @@ export default class AppBuilder {
|
||||
return () => <AppContextProvider app={app} children={rendered} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function createApp() {
|
||||
return new AppBuilder();
|
||||
}
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
*/
|
||||
|
||||
import { FeatureFlags as FeatureFlagsImpl } from './FeatureFlags';
|
||||
import {
|
||||
FeatureFlagState,
|
||||
FeatureFlagsApi,
|
||||
} from '../apis/definitions/featureFlags';
|
||||
import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions';
|
||||
|
||||
describe('FeatureFlags', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
*/
|
||||
|
||||
import { FeatureFlagName } from '../plugin/types';
|
||||
import {
|
||||
FeatureFlagState,
|
||||
FeatureFlagsApi,
|
||||
} from '../apis/definitions/featureFlags';
|
||||
import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions';
|
||||
|
||||
/**
|
||||
* Helper method for validating compatibility and flag name.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 { createApp } from './AppBuilder';
|
||||
export { FeatureFlags } from './FeatureFlags';
|
||||
export { useApp } from './AppContext';
|
||||
export * from './types';
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './api';
|
||||
export * from './apis';
|
||||
export { FeatureFlags } from './app/FeatureFlags';
|
||||
export { useApp } from './app/AppContext';
|
||||
export * from './app';
|
||||
export * from './navTargets';
|
||||
export * from './plugin';
|
||||
export * from './types';
|
||||
|
||||
@@ -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 type { NavTargetConfig, NavTargetOverrideConfig } from './types';
|
||||
|
||||
export class MutableNavTarget {
|
||||
private effectiveConfig: NavTargetConfig = this.config;
|
||||
|
||||
constructor(private readonly config: NavTargetConfig) {}
|
||||
|
||||
override(overrideConfig: NavTargetOverrideConfig) {
|
||||
this.effectiveConfig = { ...this.config, ...overrideConfig };
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this.effectiveConfig.icon;
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.effectiveConfig.path;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.effectiveConfig.title;
|
||||
}
|
||||
}
|
||||
|
||||
export function createNavTarget(config: NavTargetConfig): MutableNavTarget {
|
||||
return new MutableNavTarget(config);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 type {
|
||||
NavTarget,
|
||||
NavTargetConfig,
|
||||
NavTargetOverrideConfig,
|
||||
} from './types';
|
||||
export { createNavTarget } from './NavTarget';
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { IconComponent } from '../../icons';
|
||||
|
||||
export type NavTarget = {
|
||||
path: string;
|
||||
icon: IconComponent;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type NavTargetConfig = {
|
||||
path: string;
|
||||
icon: IconComponent;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type NavTargetOverrideConfig = {
|
||||
path?: string;
|
||||
icon?: IconComponent;
|
||||
title?: string;
|
||||
};
|
||||
@@ -20,8 +20,10 @@ import {
|
||||
RoutePath,
|
||||
RouteOptions,
|
||||
FeatureFlagName,
|
||||
BackstagePlugin,
|
||||
} from './types';
|
||||
import { validateBrowserCompat, validateFlagName } from '../app/FeatureFlags';
|
||||
import { NavTarget } from '../navTargets';
|
||||
|
||||
export type PluginConfig = {
|
||||
id: string;
|
||||
@@ -34,6 +36,12 @@ export type PluginHooks = {
|
||||
};
|
||||
|
||||
export type RouterHooks = {
|
||||
addRoute(
|
||||
target: NavTarget,
|
||||
Component: ComponentType<any>,
|
||||
options?: RouteOptions,
|
||||
): void;
|
||||
|
||||
registerRoute(
|
||||
path: RoutePath,
|
||||
Component: ComponentType<any>,
|
||||
@@ -51,10 +59,7 @@ export type FeatureFlagsHooks = {
|
||||
register(name: FeatureFlagName): void;
|
||||
};
|
||||
|
||||
export const registerSymbol = Symbol('plugin-register');
|
||||
export const outputSymbol = Symbol('plugin-output');
|
||||
|
||||
export default class Plugin {
|
||||
export class PluginImpl {
|
||||
private storedOutput?: PluginOutput[];
|
||||
|
||||
constructor(private readonly config: PluginConfig) {}
|
||||
@@ -75,6 +80,14 @@ export default class Plugin {
|
||||
|
||||
this.config.register({
|
||||
router: {
|
||||
addRoute(target, component, options) {
|
||||
outputs.push({
|
||||
type: 'nav-target-component',
|
||||
target,
|
||||
component,
|
||||
options,
|
||||
});
|
||||
},
|
||||
registerRoute(path, component, options) {
|
||||
outputs.push({ type: 'route', path, component, options });
|
||||
},
|
||||
@@ -99,3 +112,7 @@ export default class Plugin {
|
||||
return `plugin{${this.config.id}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createPlugin(config: PluginConfig): BackstagePlugin {
|
||||
return new PluginImpl(config);
|
||||
}
|
||||
|
||||
+1
-2
@@ -14,6 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './AggregatorInventory';
|
||||
export * from './StaticInventory';
|
||||
export { createPlugin } from './Plugin';
|
||||
export * from './types';
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { NavTarget } from '../navTargets';
|
||||
|
||||
export type RouteOptions = {
|
||||
// Whether the route path must match exactly, defaults to true.
|
||||
@@ -30,6 +31,13 @@ export type RouteOutput = {
|
||||
options?: RouteOptions;
|
||||
};
|
||||
|
||||
export type RouteTargetOutput = {
|
||||
type: 'nav-target-component';
|
||||
target: NavTarget;
|
||||
component: ComponentType<{}>;
|
||||
options?: RouteOptions;
|
||||
};
|
||||
|
||||
export type RedirectRouteOutput = {
|
||||
type: 'redirect-route';
|
||||
path: RoutePath;
|
||||
@@ -46,5 +54,11 @@ export type FeatureFlagOutput = {
|
||||
|
||||
export type PluginOutput =
|
||||
| RouteOutput
|
||||
| RouteTargetOutput
|
||||
| RedirectRouteOutput
|
||||
| FeatureFlagOutput;
|
||||
|
||||
export type BackstagePlugin = {
|
||||
getId(): string;
|
||||
output(): PluginOutput[];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file contains non-react related core types used throught Backstage.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Observer interface for consuming an Observer, see TC39.
|
||||
*/
|
||||
export type Observer<T> = {
|
||||
next?(value: T): void;
|
||||
error?(error: Error): void;
|
||||
complete?(): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscription returned when subscribing to an Observable, see TC39.
|
||||
*/
|
||||
export type Subscription = {
|
||||
/**
|
||||
* Cancels the subscription
|
||||
*/
|
||||
unsubscribe(): void;
|
||||
|
||||
/**
|
||||
* Value indicating whether the subscription is closed.
|
||||
*/
|
||||
readonly closed: Boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Observable sequence of values and errors, see TC39.
|
||||
*
|
||||
* https://github.com/tc39/proposal-observable
|
||||
*
|
||||
* This is used as a common return type for observable values and can be created
|
||||
* using many different observable implementations, such as zen-observable or RxJS 5.
|
||||
*/
|
||||
export type Observable<T> = {
|
||||
/**
|
||||
* Subscribes to this observable to start receiving new values.
|
||||
*/
|
||||
subscribe(observer: Observer<T>): Subscription;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: Error) => void,
|
||||
onComplete?: () => void,
|
||||
): Subscription;
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import CodeSnippet from './CodeSnippet';
|
||||
|
||||
export default {
|
||||
title: 'CodeSnippet',
|
||||
component: CodeSnippet,
|
||||
};
|
||||
|
||||
const containerStyle = { width: 300 };
|
||||
|
||||
const JAVASCRIPT = `const greeting = "Hello";
|
||||
const world = "World";
|
||||
|
||||
const greet = person => greeting + " " + person + "!";
|
||||
|
||||
greet(world);
|
||||
`;
|
||||
|
||||
const TYPESCRIPT = `const greeting: string = "Hello";
|
||||
const world: string = "World";
|
||||
|
||||
const greet = (person: string): string => greeting + " " + person + "!";
|
||||
|
||||
greet(world);
|
||||
`;
|
||||
|
||||
const PYTHON = `greeting = "Hello"
|
||||
world = "World"
|
||||
|
||||
def greet(person):
|
||||
return f"{greeting} {person}!"
|
||||
|
||||
greet(world)
|
||||
`;
|
||||
|
||||
export const Default = () => (
|
||||
<CodeSnippet text={"const hello = 'World';"} language="javascript" />
|
||||
);
|
||||
|
||||
export const MultipleLines = () => (
|
||||
<CodeSnippet text={JAVASCRIPT} language="javascript" />
|
||||
);
|
||||
|
||||
export const LineNumbers = () => (
|
||||
<CodeSnippet text={JAVASCRIPT} language="javascript" showLineNumbers />
|
||||
);
|
||||
|
||||
export const Overflow = () => (
|
||||
<>
|
||||
<div style={containerStyle}>
|
||||
<CodeSnippet text={JAVASCRIPT} language="javascript" />
|
||||
</div>
|
||||
<div style={containerStyle}>
|
||||
<CodeSnippet text={JAVASCRIPT} language="javascript" showLineNumbers />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export const Laguages = () => (
|
||||
<>
|
||||
<CodeSnippet text={JAVASCRIPT} language="javascript" showLineNumbers />
|
||||
<CodeSnippet text={TYPESCRIPT} language="typescript" showLineNumbers />
|
||||
<CodeSnippet text={PYTHON} language="python" showLineNumbers />
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { wrapInThemedTestApp } from '@backstage/test-utils';
|
||||
|
||||
import CodeSnippet from './CodeSnippet';
|
||||
|
||||
const JAVASCRIPT = `const greeting = "Hello";
|
||||
const world = "World";
|
||||
|
||||
const greet = person => gretting + " " + person + "!";
|
||||
`;
|
||||
|
||||
const minProps = {
|
||||
text: JAVASCRIPT,
|
||||
language: 'javascript',
|
||||
};
|
||||
|
||||
describe('<CodeSnippet />', () => {
|
||||
it('renders text without exploding', () => {
|
||||
const { getByText } = render(
|
||||
wrapInThemedTestApp(<CodeSnippet {...minProps} />),
|
||||
);
|
||||
expect(getByText(/"Hello"/)).toBeInTheDocument();
|
||||
expect(getByText(/"World"/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without line numbers', () => {
|
||||
const { queryByText } = render(
|
||||
wrapInThemedTestApp(<CodeSnippet {...minProps} />),
|
||||
);
|
||||
expect(queryByText('1')).not.toBeInTheDocument();
|
||||
expect(queryByText('2')).not.toBeInTheDocument();
|
||||
expect(queryByText('3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with line numbers', () => {
|
||||
const { queryByText } = render(
|
||||
wrapInThemedTestApp(<CodeSnippet {...minProps} showLineNumbers />),
|
||||
);
|
||||
expect(queryByText(/1/)).toBeInTheDocument();
|
||||
expect(queryByText(/2/)).toBeInTheDocument();
|
||||
expect(queryByText(/3/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 PropTypes from 'prop-types';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { docco, dark } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
|
||||
import { useTheme } from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
language: string;
|
||||
showLineNumbers?: boolean;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
showLineNumbers: false,
|
||||
};
|
||||
|
||||
const CodeSnippet: FC<Props> = (props) => {
|
||||
const { text, language, showLineNumbers } = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
const mode = theme.palette.type === 'dark' ? dark : docco;
|
||||
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={mode}
|
||||
showLineNumbers={showLineNumbers}
|
||||
>
|
||||
{text}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
};
|
||||
|
||||
// Type check for the JS files using this core component
|
||||
CodeSnippet.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
showLineNumbers: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CodeSnippet;
|
||||
@@ -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 './CodeSnippet';
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import {
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
Typography,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { PendingAuthRequest } from '../../api';
|
||||
|
||||
const useItemStyles = makeStyles<Theme>((theme) => ({
|
||||
root: {
|
||||
paddingLeft: theme.spacing(3),
|
||||
},
|
||||
}));
|
||||
|
||||
type RowProps = {
|
||||
request: PendingAuthRequest;
|
||||
busy: boolean;
|
||||
setBusy: (busy: boolean) => void;
|
||||
};
|
||||
|
||||
const LoginRequestListItem: FC<RowProps> = ({ request, busy, setBusy }) => {
|
||||
const classes = useItemStyles();
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
const handleContinue = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await request.trigger();
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const IconComponent = request.provider.icon;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
button
|
||||
disabled={busy}
|
||||
onClick={handleContinue}
|
||||
classes={{ root: classes.root }}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<IconComponent fontSize="large" />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={request.provider.title}
|
||||
secondary={
|
||||
error && (
|
||||
<Typography color="error">
|
||||
{error.message || 'An unspecified error occurred'}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginRequestListItem;
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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 {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
List,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Button,
|
||||
} from '@material-ui/core';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
import LoginRequestListItem from './LoginRequestListItem';
|
||||
import { useApi, oauthRequestApiRef } from '../../api';
|
||||
|
||||
const useStyles = makeStyles<Theme>((theme) => ({
|
||||
dialog: {
|
||||
paddingTop: theme.spacing(1),
|
||||
},
|
||||
title: {
|
||||
minWidth: 0,
|
||||
},
|
||||
contentList: {
|
||||
padding: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
type OAuthRequestDialogProps = {};
|
||||
|
||||
export const OAuthRequestDialog: FC<OAuthRequestDialogProps> = () => {
|
||||
const classes = useStyles();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const oauthRequestApi = useApi(oauthRequestApiRef);
|
||||
const requests = useObservable(
|
||||
useMemo(() => oauthRequestApi.authRequest$(), [oauthRequestApi]),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRejectAll = () => {
|
||||
requests.forEach((request) => request.reject());
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={Boolean(requests.length)}
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
classes={{ paper: classes.dialog }}
|
||||
>
|
||||
<DialogTitle classes={{ root: classes.title }}>
|
||||
Login Required
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent classes={{ root: classes.contentList }}>
|
||||
<List>
|
||||
{requests.map((request) => (
|
||||
<LoginRequestListItem
|
||||
key={request.provider.title}
|
||||
request={request}
|
||||
busy={busy}
|
||||
setBusy={setBusy}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleRejectAll}>Reject All</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -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 { OAuthRequestDialog } from './OAuthRequestDialog';
|
||||
@@ -17,8 +17,7 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
StatusError,
|
||||
StatusFailed,
|
||||
StatusNA,
|
||||
StatusAborted,
|
||||
StatusOK,
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
@@ -34,45 +33,34 @@ export default {
|
||||
|
||||
const data = [
|
||||
{
|
||||
status: <StatusOK />,
|
||||
status: <StatusOK>OK</StatusOK>,
|
||||
label: 'OK',
|
||||
usage: 'Deployment successful',
|
||||
},
|
||||
{
|
||||
status: <StatusWarning />,
|
||||
label: 'Warning',
|
||||
status: <StatusWarning>Warning</StatusWarning>,
|
||||
usage: 'CPU utilization at 90%',
|
||||
},
|
||||
{
|
||||
status: <StatusError />,
|
||||
label: 'Error',
|
||||
status: <StatusError>Error</StatusError>,
|
||||
usage: 'Service could not be created',
|
||||
},
|
||||
{
|
||||
status: <StatusFailed />,
|
||||
label: 'Failed',
|
||||
usage: 'Build for PR #34 failed',
|
||||
status: <StatusAborted>Aborted</StatusAborted>,
|
||||
usage: 'Build for PR #34 aborted',
|
||||
},
|
||||
{
|
||||
status: <StatusPending />,
|
||||
label: 'Pending',
|
||||
status: <StatusPending>Pending</StatusPending>,
|
||||
usage: 'Job is waiting',
|
||||
},
|
||||
{
|
||||
status: <StatusRunning />,
|
||||
label: 'Running',
|
||||
status: <StatusRunning>Running</StatusRunning>,
|
||||
usage: 'Job is running',
|
||||
},
|
||||
{
|
||||
status: <StatusNA />,
|
||||
label: 'N/A',
|
||||
usage: 'Not sure what to do',
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ field: 'status', title: 'Status' },
|
||||
{ field: 'label', title: 'Label' },
|
||||
{ field: 'usage', title: 'Example usage' },
|
||||
];
|
||||
|
||||
@@ -97,7 +85,6 @@ export const Default = () => (
|
||||
export const statusOK = () => <StatusOK />;
|
||||
export const statusWarning = () => <StatusWarning />;
|
||||
export const statusError = () => <StatusError />;
|
||||
export const statusFailed = () => <StatusFailed />;
|
||||
export const statusAborted = () => <StatusAborted />;
|
||||
export const statusPending = () => <StatusPending />;
|
||||
export const statusRunning = () => <StatusRunning />;
|
||||
export const statusNA = () => <StatusNA />;
|
||||
|
||||
@@ -21,79 +21,67 @@ import React, { FC } from 'react';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
||||
status: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
display: 'inline-block',
|
||||
marginRight: 1,
|
||||
fontWeight: 500,
|
||||
'&::before': {
|
||||
width: '0.7em',
|
||||
height: '0.7em',
|
||||
display: 'inline-block',
|
||||
marginRight: 8,
|
||||
borderRadius: '50%',
|
||||
content: '""',
|
||||
},
|
||||
},
|
||||
ok: {
|
||||
backgroundColor: theme.palette.status.ok,
|
||||
borderRadius: '50%',
|
||||
'&::before': {
|
||||
backgroundColor: theme.palette.status.ok,
|
||||
},
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: theme.palette.status.warning,
|
||||
'&::before': {
|
||||
backgroundColor: theme.palette.status.warning,
|
||||
},
|
||||
},
|
||||
error: {
|
||||
// Use same for Failed status.
|
||||
width: '0',
|
||||
height: '0',
|
||||
borderLeft: '7px solid transparent',
|
||||
borderRight: '7px solid transparent',
|
||||
borderBottom: `14px solid ${theme.palette.status.error}`,
|
||||
'&::before': {
|
||||
backgroundColor: theme.palette.status.error,
|
||||
},
|
||||
},
|
||||
pending: {
|
||||
backgroundColor: theme.palette.status.pending,
|
||||
'&::before': {
|
||||
backgroundColor: theme.palette.status.pending,
|
||||
},
|
||||
},
|
||||
running: {
|
||||
animation: '$blink 0.8s step-start 0s infinite',
|
||||
backgroundColor: theme.palette.status.running,
|
||||
'&::before': {
|
||||
backgroundColor: theme.palette.status.running,
|
||||
},
|
||||
},
|
||||
'@keyframes blink': {
|
||||
'50%': {
|
||||
backgroundColor: theme.palette.status.background,
|
||||
aborted: {
|
||||
'&::before': {
|
||||
backgroundColor: theme.palette.status.aborted,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const StatusOK: FC<{}> = (props) => {
|
||||
const classes = useStyles(props);
|
||||
return (
|
||||
<span
|
||||
className={classNames(classes.status, classes.ok)}
|
||||
aria-label="Status OK"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <span className={classNames(classes.status, classes.ok)} {...props} />;
|
||||
};
|
||||
|
||||
export const StatusWarning: FC<{}> = (props) => {
|
||||
const classes = useStyles(props);
|
||||
return (
|
||||
<span
|
||||
className={classNames(classes.status, classes.warning)}
|
||||
aria-label="Status warning"
|
||||
{...props}
|
||||
/>
|
||||
<span className={classNames(classes.status, classes.warning)} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusError: FC<{}> = (props) => {
|
||||
const classes = useStyles(props);
|
||||
return (
|
||||
<span
|
||||
className={classNames(classes.status, classes.error)}
|
||||
aria-label="Status error"
|
||||
{...props}
|
||||
/>
|
||||
<span className={classNames(classes.status, classes.error)} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusNA: FC<{}> = (props) => (
|
||||
<span aria-label="Status N/A" {...props}>
|
||||
N/A
|
||||
</span>
|
||||
);
|
||||
|
||||
export const StatusPending: FC<{}> = (props) => {
|
||||
const classes = useStyles(props);
|
||||
return (
|
||||
@@ -116,12 +104,12 @@ export const StatusRunning: FC<{}> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusFailed: FC<{}> = (props) => {
|
||||
export const StatusAborted: FC<{}> = (props) => {
|
||||
const classes = useStyles(props);
|
||||
return (
|
||||
<span
|
||||
className={classNames(classes.status, classes.error)}
|
||||
aria-label="Status failed"
|
||||
className={classNames(classes.status, classes.aborted)}
|
||||
aria-label="Status aborted"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
export {
|
||||
StatusError,
|
||||
StatusFailed,
|
||||
StatusNA,
|
||||
StatusAborted,
|
||||
StatusOK,
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
|
||||
@@ -33,6 +33,7 @@ export { default as HorizontalProgress } from './components/ProgressBars/Horizon
|
||||
export { default as CopyTextButton } from './components/CopyTextButton';
|
||||
export { default as Progress } from './components/Progress';
|
||||
export * from './components/SimpleStepper';
|
||||
export { OAuthRequestDialog } from './components/OAuthRequestDialog';
|
||||
export { AlphaLabel, BetaLabel } from './components/Lifecycle';
|
||||
export { default as SupportButton } from './components/SupportButton';
|
||||
export { default as Table, SubvalueCell } from './components/Table';
|
||||
|
||||
@@ -14,19 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Link,
|
||||
makeStyles,
|
||||
styled,
|
||||
SvgIcon,
|
||||
Theme,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { Link, makeStyles, styled, Theme, Typography } from '@material-ui/core';
|
||||
import clsx from 'clsx';
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { sidebarConfig, SidebarContext } from './config';
|
||||
import { IconComponent } from '../../icons';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
const useStyles = makeStyles<Theme>((theme) => ({
|
||||
root: {
|
||||
color: '#b5b5b5',
|
||||
display: 'flex',
|
||||
@@ -59,7 +53,7 @@ const useStyles = makeStyles<Theme>(theme => ({
|
||||
}));
|
||||
|
||||
type SidebarItemProps = {
|
||||
icon: typeof SvgIcon;
|
||||
icon: IconComponent;
|
||||
text: string;
|
||||
to?: string;
|
||||
onClick?: () => void;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/dev-utils",
|
||||
"description": "Utilities for developing Backstage plugins.",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -17,31 +17,33 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@backstage/cli": "^0.1.1-alpha.5",
|
||||
"@backstage/core": "^0.1.1-alpha.5",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.5",
|
||||
"@backstage/theme": "^0.1.1-alpha.5",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2"
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -127,6 +127,18 @@ class DevAppBuilder {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'nav-target-component': {
|
||||
const { target } = output;
|
||||
sidebarItems.push(
|
||||
<SidebarItem
|
||||
key={target.path}
|
||||
to={target.path}
|
||||
text={target.title}
|
||||
icon={target.icon}
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "storybook",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"description": "Storybook build for core package",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -14,7 +14,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/theme": "0.1.1-alpha.4"
|
||||
"@backstage/theme": "^0.1.1-alpha.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^5.3.17",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/test-utils-core",
|
||||
"description": "Utilities to test Backstage core",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -17,22 +17,24 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/test-utils",
|
||||
"description": "Utilities to test Backstage plugins and apps.",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -17,29 +17,31 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@backstage/test-utils-core": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
"@backstage/cli": "^0.1.1-alpha.5",
|
||||
"@backstage/test-utils-core": "^0.1.1-alpha.5",
|
||||
"@backstage/theme": "^0.1.1-alpha.5",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"react": "^16.12.0",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2"
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/theme",
|
||||
"description": "material-ui theme for use with Backstage.",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -17,19 +17,21 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4"
|
||||
"@backstage/cli": "^0.1.1-alpha.5"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist/**/*.{js,d.ts}"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ export const lightTheme = createTheme({
|
||||
default: '#F8F8F8',
|
||||
},
|
||||
status: {
|
||||
ok: '#1db855',
|
||||
warning: '#f49b20',
|
||||
error: '#CA001B',
|
||||
running: '#BEBEBE',
|
||||
pending: '#5BC0DE',
|
||||
background: '#F8F8F8',
|
||||
ok: '#1DB954',
|
||||
warning: '#FF9800',
|
||||
error: '#E22134',
|
||||
running: '#2E77D0',
|
||||
pending: '#FFED51',
|
||||
aborted: '#757575',
|
||||
},
|
||||
bursts: {
|
||||
fontColor: '#FEFEFE',
|
||||
@@ -63,12 +63,12 @@ export const darkTheme = createTheme({
|
||||
default: '#333333',
|
||||
},
|
||||
status: {
|
||||
ok: '#1db855',
|
||||
warning: '#f49b20',
|
||||
error: '#CA001B',
|
||||
running: '#BEBEBE',
|
||||
pending: '#5BC0DE',
|
||||
background: '#282828',
|
||||
ok: '#1DB954',
|
||||
warning: '#FF9800',
|
||||
error: '#E22134',
|
||||
running: '#2E77D0',
|
||||
pending: '#FFED51',
|
||||
aborted: '#757575',
|
||||
},
|
||||
bursts: {
|
||||
fontColor: '#FEFEFE',
|
||||
|
||||
@@ -27,7 +27,7 @@ type PaletteAdditions = {
|
||||
error: string;
|
||||
pending: string;
|
||||
running: string;
|
||||
background: string;
|
||||
aborted: string;
|
||||
};
|
||||
border: string;
|
||||
textContrast: string;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Inventory Backend
|
||||
# Catalog Backend
|
||||
|
||||
WORK IN PROGRESS
|
||||
|
||||
This is the backend part of the default inventory plugin.
|
||||
This is the backend part of the default catalog plugin.
|
||||
|
||||
It responds to requests from the frontend part, and fulfills them by delegating
|
||||
to your existing inventory related services.
|
||||
to your existing catalog related services.
|
||||
|
||||
## Links
|
||||
|
||||
- (Frontend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/inventory]
|
||||
- (Frontend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/catalog]
|
||||
- (The Backstage homepage)[https://backstage.io]
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: catalog.backstage.io/v1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: component1
|
||||
spec:
|
||||
type: service
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-inventory-backend",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"name": "@backstage/plugin-catalog-backend",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -12,17 +12,25 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "0.1.1-alpha.4",
|
||||
"@backstage/backend-common": "^0.1.1-alpha.5",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.0",
|
||||
"helmet": "^3.22.0",
|
||||
"knex": "^0.21.1",
|
||||
"morgan": "^1.10.0",
|
||||
"winston": "^3.2.1"
|
||||
"uuid": "^8.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"yaml": "^1.9.2",
|
||||
"yn": "^4.0.0",
|
||||
"yup": "^0.28.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@backstage/cli": "^0.1.1-alpha.5",
|
||||
"@types/uuid": "^7.0.3",
|
||||
"@types/yup": "^0.28.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"tsc-watch": "^4.2.3"
|
||||
},
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 { PassThrough } from 'stream';
|
||||
import winston from 'winston';
|
||||
import { CatalogLogic } from './CatalogLogic';
|
||||
import { Catalog } from './types';
|
||||
|
||||
describe('CatalogLogic', () => {
|
||||
const logger = winston.createLogger({
|
||||
transports: [new winston.transports.Stream({ stream: new PassThrough() })],
|
||||
});
|
||||
|
||||
describe('refreshLocations', () => {
|
||||
it('works with no locations added', async () => {
|
||||
const catalog = ({
|
||||
addOrUpdateComponent: jest.fn(),
|
||||
locations: jest.fn().mockResolvedValue([]),
|
||||
} as unknown) as Catalog;
|
||||
const locationReader = jest.fn();
|
||||
|
||||
await expect(
|
||||
CatalogLogic.refreshLocations(catalog, locationReader, logger),
|
||||
).resolves.toBeUndefined();
|
||||
expect(locationReader).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can update a single location', async () => {
|
||||
const catalog = ({
|
||||
addOrUpdateComponent: jest.fn(),
|
||||
locations: jest.fn().mockResolvedValue([
|
||||
{
|
||||
id: '123',
|
||||
type: 'some',
|
||||
target: 'thing',
|
||||
},
|
||||
]),
|
||||
} as unknown) as Catalog;
|
||||
|
||||
const locationReader = jest.fn().mockResolvedValue([
|
||||
{
|
||||
name: 'c1',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
CatalogLogic.refreshLocations(catalog, locationReader, logger),
|
||||
).resolves.toBeUndefined();
|
||||
expect(locationReader).toHaveBeenCalledTimes(1);
|
||||
expect(locationReader).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ type: 'some' }),
|
||||
);
|
||||
expect(catalog.addOrUpdateComponent).toHaveBeenCalledTimes(1);
|
||||
expect(catalog.addOrUpdateComponent).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'123',
|
||||
expect.objectContaining({ name: 'c1' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 { Logger } from 'winston';
|
||||
import { LocationReader } from '../ingestion/types';
|
||||
import { Catalog } from './types';
|
||||
|
||||
export class CatalogLogic {
|
||||
public static startRefreshLoop(
|
||||
catalog: Catalog,
|
||||
reader: LocationReader,
|
||||
logger: Logger,
|
||||
): () => void {
|
||||
let cancel: () => void;
|
||||
let cancelled = false;
|
||||
const cancellationPromise = new Promise((resolve) => {
|
||||
cancel = () => {
|
||||
resolve();
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
const startRefresh = async () => {
|
||||
while (!cancelled) {
|
||||
await CatalogLogic.refreshLocations(catalog, reader, logger);
|
||||
await Promise.race([
|
||||
new Promise((resolve) => setTimeout(resolve, 10000)),
|
||||
cancellationPromise,
|
||||
]);
|
||||
}
|
||||
};
|
||||
startRefresh();
|
||||
|
||||
return cancel!;
|
||||
}
|
||||
|
||||
public static async refreshLocations(
|
||||
catalog: Catalog,
|
||||
reader: LocationReader,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
const locations = await catalog.locations();
|
||||
for (const location of locations) {
|
||||
try {
|
||||
logger.debug(`Attempting refresh of location: ${location.id}`);
|
||||
const componentDescriptors = await reader(location);
|
||||
for (const componentDescriptor of componentDescriptors) {
|
||||
await catalog.addOrUpdateComponent(location.id, componentDescriptor);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug(`Failed to update location "${location.id}", ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 knex from 'knex';
|
||||
import path from 'path';
|
||||
import { PassThrough } from 'stream';
|
||||
import winston from 'winston';
|
||||
import { DatabaseCatalog } from './DatabaseCatalog';
|
||||
|
||||
describe('DatabaseCatalog', () => {
|
||||
const database = knex({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
|
||||
const logger = winston.createLogger({
|
||||
transports: [new winston.transports.Stream({ stream: new PassThrough() })],
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await database.migrate.latest({
|
||||
directory: path.resolve(__dirname, '..', 'migrations'),
|
||||
loadExtensions: ['.ts'],
|
||||
});
|
||||
});
|
||||
|
||||
it('manages locations', async () => {
|
||||
const catalog = new DatabaseCatalog(database, logger);
|
||||
const input = { type: 'a', target: 'b' };
|
||||
const output = { id: expect.anything(), type: 'a', target: 'b' };
|
||||
|
||||
await catalog.addLocation(input);
|
||||
|
||||
const locations = await catalog.locations();
|
||||
expect(locations).toEqual([output]);
|
||||
const location = await catalog.location(locations[0].id);
|
||||
expect(location).toEqual(output);
|
||||
|
||||
await catalog.removeLocation(locations[0].id);
|
||||
|
||||
await expect(catalog.locations()).resolves.toEqual([]);
|
||||
await expect(catalog.location(locations[0].id)).rejects.toThrow(
|
||||
/Found no location/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 { NotFoundError } from '@backstage/backend-common';
|
||||
import Knex from 'knex';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import { ComponentDescriptor } from '../descriptors';
|
||||
import { readLocation } from '../ingestion';
|
||||
import { CatalogLogic } from './CatalogLogic';
|
||||
import { AddLocationRequest, Catalog, Component, Location } from './types';
|
||||
|
||||
export class DatabaseCatalog implements Catalog {
|
||||
static async create(
|
||||
database: Knex,
|
||||
logger: Logger,
|
||||
): Promise<DatabaseCatalog> {
|
||||
await database.migrate.latest({
|
||||
directory: path.resolve(__dirname, '..', 'migrations'),
|
||||
loadExtensions: ['.js'],
|
||||
});
|
||||
|
||||
const databaseCatalog = new DatabaseCatalog(database, logger);
|
||||
CatalogLogic.startRefreshLoop(databaseCatalog, readLocation, logger);
|
||||
|
||||
return databaseCatalog;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly database: Knex,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async addOrUpdateComponent(
|
||||
locationId: string,
|
||||
descriptor: ComponentDescriptor,
|
||||
): Promise<void> {
|
||||
const component: Component = {
|
||||
name: descriptor.metadata.name,
|
||||
};
|
||||
|
||||
await this.database.transaction(async (tx) => {
|
||||
// TODO(freben): Currently, several locations can compete for the same component
|
||||
const count = await tx('components')
|
||||
.where({ name: component.name })
|
||||
.update({ ...component, locationId });
|
||||
if (!count) {
|
||||
await tx('components').insert({
|
||||
...component,
|
||||
id: uuidv4(),
|
||||
locationId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async components(): Promise<Component[]> {
|
||||
return await this.database('components').select();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async component(name: string): Promise<Component> {
|
||||
const items = await this.database('components').where({ name }).select();
|
||||
if (!items.length) {
|
||||
throw new NotFoundError(`Found no component with name ${name}`);
|
||||
}
|
||||
return items[0];
|
||||
}
|
||||
|
||||
async addLocation(location: AddLocationRequest): Promise<Location> {
|
||||
const id = uuidv4();
|
||||
const { type, target } = location;
|
||||
await this.database('locations').insert({ id, type, target });
|
||||
return await this.location(id);
|
||||
}
|
||||
|
||||
async removeLocation(id: string): Promise<void> {
|
||||
const result = await this.database('locations').where({ id }).del();
|
||||
|
||||
if (!result) {
|
||||
throw new NotFoundError(`Found no location with ID ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async location(id: string): Promise<Location> {
|
||||
const items = await this.database('locations').where({ id }).select();
|
||||
if (!items.length) {
|
||||
throw new NotFoundError(`Found no location with ID ${id}`);
|
||||
}
|
||||
return items[0];
|
||||
}
|
||||
|
||||
async locations(): Promise<Location[]> {
|
||||
return await this.database('locations').select();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { NotFoundError } from '@backstage/backend-common';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ComponentDescriptor } from '../descriptors';
|
||||
import { AddLocationRequest, Catalog, Component, Location } from './types';
|
||||
|
||||
export class StaticCatalog implements Catalog {
|
||||
private _components: Component[];
|
||||
private _locations: Location[];
|
||||
|
||||
constructor(components: Component[], locations: Location[]) {
|
||||
this._components = components;
|
||||
this._locations = locations;
|
||||
}
|
||||
|
||||
async addOrUpdateComponent(
|
||||
locationId: string,
|
||||
descriptor: ComponentDescriptor,
|
||||
): Promise<void> {
|
||||
this._components = this._components
|
||||
.filter((c) => c.name !== descriptor.metadata.name)
|
||||
.concat([{ name: descriptor.metadata.name }]);
|
||||
}
|
||||
|
||||
async components(): Promise<Component[]> {
|
||||
return this._components.slice();
|
||||
}
|
||||
|
||||
async component(name: string): Promise<Component> {
|
||||
const item = this._components.find((i) => i.name === name);
|
||||
if (!item) {
|
||||
throw new NotFoundError(`Found no component with name ${name}`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
async addLocation(location: AddLocationRequest): Promise<Location> {
|
||||
const l = { id: uuidv4(), type: location.type, target: location.target };
|
||||
this._locations.push(l);
|
||||
return l;
|
||||
}
|
||||
|
||||
async removeLocation(id: string): Promise<void> {
|
||||
this._locations = this._locations.filter((l) => l.id !== id);
|
||||
}
|
||||
|
||||
async location(id: string): Promise<Location> {
|
||||
const location = this._locations.find((l) => l.id === id);
|
||||
if (!location) {
|
||||
throw new NotFoundError(`Found no location with ID ${id}`);
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
async locations(): Promise<Location[]> {
|
||||
return this._locations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 * from './DatabaseCatalog';
|
||||
export * from './StaticCatalog';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 * as yup from 'yup';
|
||||
import { ComponentDescriptor } from '../descriptors';
|
||||
|
||||
export type Component = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
id: string;
|
||||
type: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export type AddLocationRequest = {
|
||||
type: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export const addLocationRequestShape: yup.Schema<AddLocationRequest> = yup
|
||||
.object({
|
||||
type: yup.string().required(),
|
||||
target: yup.string().required(),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
export type Catalog = {
|
||||
addOrUpdateComponent(
|
||||
locationId: string,
|
||||
descriptor: ComponentDescriptor,
|
||||
): Promise<void>;
|
||||
components(): Promise<Component[]>;
|
||||
component(id: string): Promise<Component>;
|
||||
addLocation(location: AddLocationRequest): Promise<Location>;
|
||||
removeLocation(id: string): Promise<void>;
|
||||
locations(): Promise<Location[]>;
|
||||
location(id: string): Promise<Location>;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 * as yup from 'yup';
|
||||
import { DescriptorEnvelope } from './envelope';
|
||||
|
||||
export type ComponentDescriptor = {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: {
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
|
||||
const componentDescriptorSchema: yup.Schema<ComponentDescriptor> = yup.object({
|
||||
kind: yup
|
||||
.string()
|
||||
.required()
|
||||
.matches(/^Component$/),
|
||||
metadata: yup.object({
|
||||
name: yup.string().required(),
|
||||
}),
|
||||
spec: yup.object({
|
||||
type: yup.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
export async function parseComponentDescriptor(
|
||||
envelope: DescriptorEnvelope,
|
||||
): Promise<ComponentDescriptor[]> {
|
||||
let componentDescriptor;
|
||||
try {
|
||||
componentDescriptor = await componentDescriptorSchema.validate(envelope, {
|
||||
strict: true,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Malformed component, ${e}`);
|
||||
}
|
||||
|
||||
return [componentDescriptor];
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 yaml from 'yaml';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export type DescriptorEnvelope = {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata?: object;
|
||||
spec?: object;
|
||||
};
|
||||
|
||||
const descriptorEnvelopeSchema: yup.Schema<DescriptorEnvelope> = yup
|
||||
.object({
|
||||
apiVersion: yup.string().required(),
|
||||
kind: yup.string().required(),
|
||||
metadata: yup.object(),
|
||||
spec: yup.object(),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
export async function parseDescriptorEnvelope(
|
||||
rawYaml: string,
|
||||
): Promise<DescriptorEnvelope> {
|
||||
let descriptor;
|
||||
try {
|
||||
descriptor = yaml.parse(rawYaml);
|
||||
} catch (e) {
|
||||
throw new Error(`Malformed YAML, ${e}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await descriptorEnvelopeSchema.validate(descriptor, {
|
||||
strict: true,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Malformed envelope, ${e}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 * from './component';
|
||||
export * from './envelope';
|
||||
export * from './util';
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 { ComponentDescriptor, parseComponentDescriptor } from './component';
|
||||
import { parseDescriptorEnvelope } from './envelope';
|
||||
|
||||
// TODO(freben): Temporary helper that ignores the kind
|
||||
export async function parseDescriptor(
|
||||
rawYaml: string,
|
||||
): Promise<ComponentDescriptor[]> {
|
||||
const env = await parseDescriptorEnvelope(rawYaml);
|
||||
return await parseComponentDescriptor(env);
|
||||
}
|
||||
@@ -14,5 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './inventory';
|
||||
export * from './catalog';
|
||||
export * from './service/router';
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { ComponentDescriptor, parseDescriptor } from '../descriptors';
|
||||
|
||||
export async function readFileLocation(
|
||||
target: string,
|
||||
): Promise<ComponentDescriptor[]> {
|
||||
let rawYaml;
|
||||
try {
|
||||
rawYaml = await fs.readFile(target, 'utf8');
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to read "${target}", ${e}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return parseDescriptor(rawYaml);
|
||||
} catch (e) {
|
||||
throw new Error(`Malformed descriptor at "${target}", ${e}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 * from './fileLocation';
|
||||
export * from './types';
|
||||
export * from './util';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user