diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml
index 0dd8036e84..10330976d7 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -7,6 +7,7 @@ on:
- 'packages/cli/**'
- 'packages/core/**'
- 'scripts/**'
+ - 'yarn.lock'
jobs:
build:
diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
index 15fb2b1fc7..c19583b653 100644
--- a/.github/workflows/frontend.yml
+++ b/.github/workflows/frontend.yml
@@ -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
diff --git a/NOTICE b/NOTICE
index 306a8b20f1..0967e9bd8d 100644
--- a/NOTICE
+++ b/NOTICE
@@ -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
diff --git a/README.md b/README.md
index f5b863cfa6..d2a0bbd3a9 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/docs/contributorheader.png b/docs/contributorheader.png
new file mode 100644
index 0000000000..53d1027d85
Binary files /dev/null and b/docs/contributorheader.png differ
diff --git a/lerna.json b/lerna.json
index 1d77db92fb..39a7a4aae6 100644
--- a/lerna.json
+++ b/lerna.json
@@ -5,5 +5,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
- "version": "0.1.1-alpha.4"
+ "version": "0.1.1-alpha.5"
}
diff --git a/package.json b/package.json
index 6459abcc5e..b3a02bb388 100644
--- a/package.json
+++ b/package.json
@@ -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 --",
diff --git a/packages/app/package.json b/packages/app/package.json
index 47f553a98b..e0c534ea45 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -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",
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 6854ce015b..55cba439bd 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -85,7 +85,7 @@ const Root: FC<{}> = ({ children }) => (
-
+
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index 463d5dc0d1..021005a97a 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -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';
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index eec21063ce..27ef241f3d 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -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"
diff --git a/packages/backend/knexfile.ts b/packages/backend/knexfile.ts
new file mode 100644
index 0000000000..57ccbe8528
--- /dev/null
+++ b/packages/backend/knexfile.ts
@@ -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',
+ },
+ },
+ */
+};
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 5e398a89d5..3a24067da4 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -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": {
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 31b53f6d30..8a7a4c9b72 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -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());
diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts
new file mode 100644
index 0000000000..fb41e01692
--- /dev/null
+++ b/packages/backend/src/plugins/catalog.ts
@@ -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 });
+}
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 196337aec7..ce681a6bdd 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -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;
};
diff --git a/packages/cli/config/tsconfig.json b/packages/cli/config/tsconfig.json
index a05e809caa..f905dcb771 100644
--- a/packages/cli/config/tsconfig.json
+++ b/packages/cli/config/tsconfig.json
@@ -4,7 +4,6 @@
"compilerOptions": {
"allowJs": true,
"noEmit": false,
- "declarationMap": true,
"incremental": true,
"target": "ES2019",
"module": "ESNext",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index ee46b09862..7183320136 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
- "version": "0.1.1-alpha.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",
diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts
index 2a9f6ff9fd..bfb78762d9 100644
--- a/packages/cli/src/commands/create-app/createApp.ts
+++ b/packages/cli/src/commands/create-app/createApp.ts
@@ -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}`,
+ );
+ });
+ },
+ );
}
}
diff --git a/packages/backend/src/plugins/inventory.ts b/packages/cli/src/commands/pack.ts
similarity index 52%
rename from packages/backend/src/plugins/inventory.ts
rename to packages/cli/src/commands/pack.ts
index d48a62b5fe..ba9c7725ae 100644
--- a/packages/backend/src/plugins/inventory.ts
+++ b/packages/cli/src/commands/pack.ts
@@ -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 });
+};
diff --git a/packages/cli/src/commands/plugin/diff/handlers.ts b/packages/cli/src/commands/plugin/diff/handlers.ts
index 32079415ea..87518eab3e 100644
--- a/packages/cli/src/commands/plugin/diff/handlers.ts
+++ b/packages/cli/src/commands/plugin/diff/handlers.ts
@@ -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] =
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index e20cef2615..cf89a3c47d 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -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(
- actionRequireFunc: () => { default(...args: T): Promise },
+ actionRequireFunc:
+ | (() => { default(...args: T): Promise })
+ | (() => (...args: T) => Promise),
): (...args: T) => Promise {
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) {
diff --git a/packages/cli/templates/default-app/package.json.hbs b/packages/cli/templates/default-app/package.json.hbs
index 8b725c69c1..231194a54e 100644
--- a/packages/cli/templates/default-app/package.json.hbs
+++ b/packages/cli/templates/default-app/package.json.hbs
@@ -26,8 +26,5 @@
"@backstage/cli": "^{{version}}",
"lerna": "^3.20.2",
"prettier": "^1.19.1"
- },
- "resolutions": {
- "**/cypress": "4.2.0"
}
}
diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs
index 6956134a3c..5596eb9151 100644
--- a/packages/cli/templates/default-app/packages/app/package.json.hbs
+++ b/packages/cli/templates/default-app/packages/app/package.json.hbs
@@ -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",
diff --git a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs
index 13fe34f682..c814473826 100644
--- a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs
+++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs
@@ -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}"
]
}
diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs
index c4a72c6b06..0a0ecc7e76 100644
--- a/packages/cli/templates/default-plugin/package.json.hbs
+++ b/packages/cli/templates/default-plugin/package.json.hbs
@@ -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}"
]
}
diff --git a/packages/core/package.json b/packages/core/package.json
index 5918fd6008..295b95050c 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
- "version": "0.1.1-alpha.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}"
]
}
diff --git a/packages/core/src/api/apis/ApiProvider.test.tsx b/packages/core/src/api/apis/ApiProvider.test.tsx
index f572907c2f..e79e83a1ed 100644
--- a/packages/core/src/api/apis/ApiProvider.test.tsx
+++ b/packages/core/src/api/apis/ApiProvider.test.tsx
@@ -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';
diff --git a/packages/core/src/api/apis/ApiProvider.tsx b/packages/core/src/api/apis/ApiProvider.tsx
index db87cc1610..46f89cfa0d 100644
--- a/packages/core/src/api/apis/ApiProvider.tsx
+++ b/packages/core/src/api/apis/ApiProvider.tsx
@@ -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(undefined);
-const ApiProvider: FC = ({ apis, children }) => {
+export const ApiProvider: FC = ({ apis, children }) => {
return ;
};
@@ -53,7 +53,7 @@ export function withApis(apis: TypesToApiRefs) {
return function withApisWrapper
(
WrappedComponent: React.ComponentType
,
) {
- const Hoc: FC> = props => {
+ const Hoc: FC> = (props) => {
const apiHolder = useContext(Context);
if (!apiHolder) {
@@ -84,5 +84,3 @@ export function withApis(apis: TypesToApiRefs) {
return Hoc;
};
}
-
-export default ApiProvider;
diff --git a/packages/core/src/api/apis/ApiRef.test.ts b/packages/core/src/api/apis/ApiRef.test.ts
index b592e327b0..93ca639b46 100644
--- a/packages/core/src/api/apis/ApiRef.test.ts
+++ b/packages/core/src/api/apis/ApiRef.test.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import ApiRef from './ApiRef';
+import { ApiRef } from './ApiRef';
describe('ApiRef', () => {
it('should be created', () => {
diff --git a/packages/core/src/api/apis/ApiRef.ts b/packages/core/src/api/apis/ApiRef.ts
index b3f4d48428..c346f89685 100644
--- a/packages/core/src/api/apis/ApiRef.ts
+++ b/packages/core/src/api/apis/ApiRef.ts
@@ -19,7 +19,7 @@ export type ApiRefConfig = {
description: string;
};
-export default class ApiRef {
+export class ApiRef {
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 {
return `apiRef{${this.config.id}}`;
}
}
+
+export function createApiRef(config: ApiRefConfig) {
+ return new ApiRef(config);
+}
diff --git a/packages/core/src/api/apis/ApiRegistry.test.ts b/packages/core/src/api/apis/ApiRegistry.test.ts
index 6eb78892b2..a4043bbe1e 100644
--- a/packages/core/src/api/apis/ApiRegistry.test.ts
+++ b/packages/core/src/api/apis/ApiRegistry.test.ts
@@ -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({ id: 'x', description: '' });
diff --git a/packages/core/src/api/apis/ApiRegistry.ts b/packages/core/src/api/apis/ApiRegistry.ts
index 4149365806..0a9d14a4e7 100644
--- a/packages/core/src/api/apis/ApiRegistry.ts
+++ b/packages/core/src/api/apis/ApiRegistry.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import ApiRef from './ApiRef';
+import { ApiRef } from './ApiRef';
import { ApiHolder } from './types';
type ApiImpl = readonly [ApiRef, T];
@@ -33,7 +33,7 @@ class ApiRegistryBuilder {
}
}
-export default class ApiRegistry implements ApiHolder {
+export class ApiRegistry implements ApiHolder {
static builder() {
return new ApiRegistryBuilder();
}
diff --git a/packages/core/src/api/apis/ApiTestRegistry.test.ts b/packages/core/src/api/apis/ApiTestRegistry.test.ts
index 6974daa3a7..6ae7a83d7e 100644
--- a/packages/core/src/api/apis/ApiTestRegistry.test.ts
+++ b/packages/core/src/api/apis/ApiTestRegistry.test.ts
@@ -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({ id: 'a', description: '' });
diff --git a/packages/core/src/api/apis/ApiTestRegistry.ts b/packages/core/src/api/apis/ApiTestRegistry.ts
index 2af2058fbc..eacc4962ce 100644
--- a/packages/core/src/api/apis/ApiTestRegistry.ts
+++ b/packages/core/src/api/apis/ApiTestRegistry.ts
@@ -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();
private factories = new Map<
AnyApiRef,
diff --git a/packages/core/src/api/apis/definitions/alert.ts b/packages/core/src/api/apis/definitions/AlertApi.ts
similarity index 96%
rename from packages/core/src/api/apis/definitions/alert.ts
rename to packages/core/src/api/apis/definitions/AlertApi.ts
index 133255c719..e5d7001b56 100644
--- a/packages/core/src/api/apis/definitions/alert.ts
+++ b/packages/core/src/api/apis/definitions/AlertApi.ts
@@ -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;
diff --git a/packages/core/src/api/apis/definitions/error.ts b/packages/core/src/api/apis/definitions/ErrorApi.ts
similarity index 98%
rename from packages/core/src/api/apis/definitions/error.ts
rename to packages/core/src/api/apis/definitions/ErrorApi.ts
index f2a8509558..7d9816426e 100644
--- a/packages/core/src/api/apis/definitions/error.ts
+++ b/packages/core/src/api/apis/definitions/ErrorApi.ts
@@ -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
diff --git a/packages/core/src/api/apis/definitions/featureFlags.ts b/packages/core/src/api/apis/definitions/FeatureFlagsApi.ts
similarity index 98%
rename from packages/core/src/api/apis/definitions/featureFlags.ts
rename to packages/core/src/api/apis/definitions/FeatureFlagsApi.ts
index 926fe091d5..8636c1f5c1 100644
--- a/packages/core/src/api/apis/definitions/featureFlags.ts
+++ b/packages/core/src/api/apis/definitions/FeatureFlagsApi.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import ApiRef from '../ApiRef';
+import { ApiRef } from '../ApiRef';
import {
UserFlags,
FeatureFlagsRegistry,
diff --git a/packages/core/src/api/apis/definitions/OAuthRequestApi.ts b/packages/core/src/api/apis/definitions/OAuthRequestApi.ts
new file mode 100644
index 0000000000..09e5f77718
--- /dev/null
+++ b/packages/core/src/api/apis/definitions/OAuthRequestApi.ts
@@ -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;
+ 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 = {
+ /**
+ * 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;
+};
+
+/**
+ * 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 = (
+ scope: OAuthScopes,
+) => Promise;
+
+/**
+ * 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;
+};
+
+/**
+ * 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;
+
+ /**
+ * 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(
+ options: AuthRequesterOptions,
+ ): AuthRequester;
+
+ /**
+ * 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;
+};
+
+export const oauthRequestApiRef = new ApiRef({
+ id: 'core.oauthrequest',
+ description: 'An API for implementing unified OAuth flows in Backstage',
+});
diff --git a/packages/core/src/api/apis/definitions/index.ts b/packages/core/src/api/apis/definitions/index.ts
index ca1290403f..9e7f7534b9 100644
--- a/packages/core/src/api/apis/definitions/index.ts
+++ b/packages/core/src/api/apis/definitions/index.ts
@@ -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';
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/BasicOAuthScopes.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/BasicOAuthScopes.ts
new file mode 100644
index 0000000000..fb022c5385
--- /dev/null
+++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/BasicOAuthScopes.ts
@@ -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,
+ 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 {
+ 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;
+ }
+}
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts
new file mode 100644
index 0000000000..be762ca936
--- /dev/null
+++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts
@@ -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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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);
+ });
+});
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts
new file mode 100644
index 0000000000..6348262022
--- /dev/null
+++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts
@@ -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 = {
+ scopes: OAuthScopes;
+ resolve: (value?: ResultType | PromiseLike | undefined) => void;
+ reject: (reason: Error) => void;
+};
+
+export type PendingRequest = {
+ scopes: OAuthScopes | undefined;
+ resolve: (value: ResultType) => void;
+ reject: (reason: Error) => void;
+};
+
+export type OAuthPendingRequestsApi = {
+ request(scopes: OAuthScopes): Promise;
+ resolve(scopes: OAuthScopes, result: ResultType): void;
+ reject(error: Error): void;
+ pending(): Observable>;
+};
+
+/**
+ * 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
+ implements OAuthPendingRequestsApi {
+ private requests: RequestQueueEntry[] = [];
+ private listeners: ZenObservable.SubscriptionObserver<
+ PendingRequest
+ >[] = [];
+
+ request(scopes: OAuthScopes): Promise {
+ 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> {
+ return new Observable((subscriber) => {
+ this.listeners.push(subscriber);
+ subscriber.next(this.getCurrentPending());
+ return () => {
+ this.listeners = this.listeners.filter((l) => l !== subscriber);
+ };
+ });
+ }
+
+ private getCurrentPending(): PendingRequest {
+ 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);
+ }
+ },
+ };
+ }
+}
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts
new file mode 100644
index 0000000000..940c0230f9
--- /dev/null
+++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts
@@ -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);
+ });
+});
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts
new file mode 100644
index 0000000000..02d89b9546
--- /dev/null
+++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts
@@ -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;
+ private readonly observers = new Set<
+ ZenObservable.SubscriptionObserver
+ >();
+ private currentRequests: PendingAuthRequest[] = [];
+ private handlerCount = 0;
+
+ constructor() {
+ this.request$ = new Observable((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(options: AuthRequesterOptions): AuthRequester {
+ const handler = new OAuthPendingRequests();
+
+ 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,
+ options: AuthRequesterOptions,
+ ): 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 {
+ return this.request$;
+ }
+
+ async showLoginPopup(options: LoginPopupOptions): Promise {
+ 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);
+ });
+ }
+}
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/index.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/index.ts
new file mode 100644
index 0000000000..0fe9bfae0f
--- /dev/null
+++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { OAuthRequestManager } from './OAuthRequestManager';
diff --git a/packages/core/src/api/apis/implementations/index.ts b/packages/core/src/api/apis/implementations/index.ts
index b5ea6467cb..ebd89b54c6 100644
--- a/packages/core/src/api/apis/implementations/index.ts
+++ b/packages/core/src/api/apis/implementations/index.ts
@@ -20,3 +20,4 @@
export * from './AlertApiForwarder';
export * from './ErrorApiForwarder';
+export * from './OAuthRequestManager';
diff --git a/packages/core/src/api/apis/index.ts b/packages/core/src/api/apis/index.ts
index 9ab9a25e50..332636580c 100644
--- a/packages/core/src/api/apis/index.ts
+++ b/packages/core/src/api/apis/index.ts
@@ -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';
diff --git a/packages/core/src/api/apis/types.ts b/packages/core/src/api/apis/types.ts
index c44fd804a5..4ee9b39ca9 100644
--- a/packages/core/src/api/apis/types.ts
+++ b/packages/core/src/api/apis/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import ApiRef from './ApiRef';
+import { ApiRef } from './ApiRef';
export type AnyApiRef = ApiRef;
diff --git a/packages/core/src/api/app/AppBuilder.tsx b/packages/core/src/api/app/AppBuilder.tsx
index f65482b996..a375593cdb 100644
--- a/packages/core/src/api/app/AppBuilder.tsx
+++ b/packages/core/src/api/app/AppBuilder.tsx
@@ -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();
@@ -84,6 +84,19 @@ export default class AppBuilder {
);
break;
}
+ case 'nav-target-component': {
+ const { target, component, options = {} } = output;
+ const { exact = true } = options;
+ routes.push(
+ ,
+ );
+ break;
+ }
case 'redirect-route': {
const { path, target, options = {} } = output;
const { exact = true } = options;
@@ -132,3 +145,7 @@ export default class AppBuilder {
return () => ;
}
}
+
+export function createApp() {
+ return new AppBuilder();
+}
diff --git a/packages/core/src/api/app/FeatureFlags.test.tsx b/packages/core/src/api/app/FeatureFlags.test.tsx
index 5822d05be8..ca4a819f13 100644
--- a/packages/core/src/api/app/FeatureFlags.test.tsx
+++ b/packages/core/src/api/app/FeatureFlags.test.tsx
@@ -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(() => {
diff --git a/packages/core/src/api/app/FeatureFlags.tsx b/packages/core/src/api/app/FeatureFlags.tsx
index 63c1b19d88..6af10f228e 100644
--- a/packages/core/src/api/app/FeatureFlags.tsx
+++ b/packages/core/src/api/app/FeatureFlags.tsx
@@ -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.
diff --git a/packages/core/src/api/app/index.ts b/packages/core/src/api/app/index.ts
new file mode 100644
index 0000000000..156c9870b8
--- /dev/null
+++ b/packages/core/src/api/app/index.ts
@@ -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';
diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts
index e60a7efa18..2eabc38280 100644
--- a/packages/core/src/api/index.ts
+++ b/packages/core/src/api/index.ts
@@ -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';
diff --git a/packages/core/src/api/navTargets/NavTarget.ts b/packages/core/src/api/navTargets/NavTarget.ts
new file mode 100644
index 0000000000..ee964abb01
--- /dev/null
+++ b/packages/core/src/api/navTargets/NavTarget.ts
@@ -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);
+}
diff --git a/packages/core/src/api/navTargets/index.ts b/packages/core/src/api/navTargets/index.ts
new file mode 100644
index 0000000000..b090af277a
--- /dev/null
+++ b/packages/core/src/api/navTargets/index.ts
@@ -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';
diff --git a/packages/core/src/api/navTargets/types.ts b/packages/core/src/api/navTargets/types.ts
new file mode 100644
index 0000000000..104b8203b0
--- /dev/null
+++ b/packages/core/src/api/navTargets/types.ts
@@ -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;
+};
diff --git a/packages/core/src/api/plugin/Plugin.tsx b/packages/core/src/api/plugin/Plugin.tsx
index 3d067c66ad..74aeaaa523 100644
--- a/packages/core/src/api/plugin/Plugin.tsx
+++ b/packages/core/src/api/plugin/Plugin.tsx
@@ -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,
+ options?: RouteOptions,
+ ): void;
+
registerRoute(
path: RoutePath,
Component: ComponentType,
@@ -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);
+}
diff --git a/plugins/inventory-backend/src/inventory/index.ts b/packages/core/src/api/plugin/index.ts
similarity index 89%
rename from plugins/inventory-backend/src/inventory/index.ts
rename to packages/core/src/api/plugin/index.ts
index 3affa1109a..79b0575755 100644
--- a/plugins/inventory-backend/src/inventory/index.ts
+++ b/packages/core/src/api/plugin/index.ts
@@ -14,6 +14,5 @@
* limitations under the License.
*/
-export * from './AggregatorInventory';
-export * from './StaticInventory';
+export { createPlugin } from './Plugin';
export * from './types';
diff --git a/packages/core/src/api/plugin/types.ts b/packages/core/src/api/plugin/types.ts
index c4ac426e5c..859813356a 100644
--- a/packages/core/src/api/plugin/types.ts
+++ b/packages/core/src/api/plugin/types.ts
@@ -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[];
+};
diff --git a/packages/core/src/api/types.ts b/packages/core/src/api/types.ts
new file mode 100644
index 0000000000..09f0bc11ae
--- /dev/null
+++ b/packages/core/src/api/types.ts
@@ -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 = {
+ 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 = {
+ /**
+ * Subscribes to this observable to start receiving new values.
+ */
+ subscribe(observer: Observer): Subscription;
+ subscribe(
+ onNext: (value: T) => void,
+ onError?: (error: Error) => void,
+ onComplete?: () => void,
+ ): Subscription;
+};
diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx
new file mode 100644
index 0000000000..52db02177e
--- /dev/null
+++ b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx
@@ -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 = () => (
+
+);
+
+export const MultipleLines = () => (
+
+);
+
+export const LineNumbers = () => (
+
+);
+
+export const Overflow = () => (
+ <>
+
+
+
+
+
+
+ >
+);
+
+export const Laguages = () => (
+ <>
+
+
+
+ >
+);
diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx
new file mode 100644
index 0000000000..0d0c34a3e6
--- /dev/null
+++ b/packages/core/src/components/CodeSnippet/CodeSnippet.test.tsx
@@ -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('', () => {
+ it('renders text without exploding', () => {
+ const { getByText } = render(
+ wrapInThemedTestApp(),
+ );
+ expect(getByText(/"Hello"/)).toBeInTheDocument();
+ expect(getByText(/"World"/)).toBeInTheDocument();
+ });
+
+ it('renders without line numbers', () => {
+ const { queryByText } = render(
+ wrapInThemedTestApp(),
+ );
+ expect(queryByText('1')).not.toBeInTheDocument();
+ expect(queryByText('2')).not.toBeInTheDocument();
+ expect(queryByText('3')).not.toBeInTheDocument();
+ });
+
+ it('renders with line numbers', () => {
+ const { queryByText } = render(
+ wrapInThemedTestApp(),
+ );
+ expect(queryByText(/1/)).toBeInTheDocument();
+ expect(queryByText(/2/)).toBeInTheDocument();
+ expect(queryByText(/3/)).toBeInTheDocument();
+ });
+});
diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx
new file mode 100644
index 0000000000..fc7c262882
--- /dev/null
+++ b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx
@@ -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) => {
+ const { text, language, showLineNumbers } = {
+ ...defaultProps,
+ ...props,
+ };
+
+ const theme = useTheme();
+ const mode = theme.palette.type === 'dark' ? dark : docco;
+
+ return (
+
+ {text}
+
+ );
+};
+
+// 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;
diff --git a/packages/core/src/components/CodeSnippet/index.tsx b/packages/core/src/components/CodeSnippet/index.tsx
new file mode 100644
index 0000000000..71587789e5
--- /dev/null
+++ b/packages/core/src/components/CodeSnippet/index.tsx
@@ -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';
diff --git a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx
new file mode 100644
index 0000000000..8e3d98c998
--- /dev/null
+++ b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx
@@ -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) => ({
+ root: {
+ paddingLeft: theme.spacing(3),
+ },
+}));
+
+type RowProps = {
+ request: PendingAuthRequest;
+ busy: boolean;
+ setBusy: (busy: boolean) => void;
+};
+
+const LoginRequestListItem: FC = ({ request, busy, setBusy }) => {
+ const classes = useItemStyles();
+ const [error, setError] = useState();
+
+ const handleContinue = async () => {
+ setBusy(true);
+ try {
+ await request.trigger();
+ } catch (e) {
+ setError(e);
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const IconComponent = request.provider.icon;
+
+ return (
+
+
+
+
+
+ {error.message || 'An unspecified error occurred'}
+
+ )
+ }
+ />
+
+ );
+};
+
+export default LoginRequestListItem;
diff --git a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx
new file mode 100644
index 0000000000..617cce2aee
--- /dev/null
+++ b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx
@@ -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) => ({
+ dialog: {
+ paddingTop: theme.spacing(1),
+ },
+ title: {
+ minWidth: 0,
+ },
+ contentList: {
+ padding: 0,
+ },
+}));
+
+type OAuthRequestDialogProps = {};
+
+export const OAuthRequestDialog: FC = () => {
+ 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 (
+
+ );
+};
diff --git a/packages/core/src/components/OAuthRequestDialog/index.ts b/packages/core/src/components/OAuthRequestDialog/index.ts
new file mode 100644
index 0000000000..45f87ece1d
--- /dev/null
+++ b/packages/core/src/components/OAuthRequestDialog/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { OAuthRequestDialog } from './OAuthRequestDialog';
diff --git a/packages/core/src/components/Status/Status.stories.tsx b/packages/core/src/components/Status/Status.stories.tsx
index 7a6a58caa8..0e3717d082 100644
--- a/packages/core/src/components/Status/Status.stories.tsx
+++ b/packages/core/src/components/Status/Status.stories.tsx
@@ -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: ,
+ status: OK,
label: 'OK',
usage: 'Deployment successful',
},
{
- status: ,
- label: 'Warning',
+ status: Warning,
usage: 'CPU utilization at 90%',
},
{
- status: ,
- label: 'Error',
+ status: Error,
usage: 'Service could not be created',
},
{
- status: ,
- label: 'Failed',
- usage: 'Build for PR #34 failed',
+ status: Aborted,
+ usage: 'Build for PR #34 aborted',
},
{
- status: ,
- label: 'Pending',
+ status: Pending,
usage: 'Job is waiting',
},
{
- status: ,
- label: 'Running',
+ status: Running,
usage: 'Job is running',
},
- {
- status: ,
- 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 = () => ;
export const statusWarning = () => ;
export const statusError = () => ;
-export const statusFailed = () => ;
+export const statusAborted = () => ;
export const statusPending = () => ;
export const statusRunning = () => ;
-export const statusNA = () => ;
diff --git a/packages/core/src/components/Status/Status.tsx b/packages/core/src/components/Status/Status.tsx
index a232aceb62..9d8450293f 100644
--- a/packages/core/src/components/Status/Status.tsx
+++ b/packages/core/src/components/Status/Status.tsx
@@ -21,79 +21,67 @@ import React, { FC } from 'react';
const useStyles = makeStyles((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 (
-
- );
+ return ;
};
export const StatusWarning: FC<{}> = (props) => {
const classes = useStyles(props);
return (
-
+
);
};
export const StatusError: FC<{}> = (props) => {
const classes = useStyles(props);
return (
-
+
);
};
-export const StatusNA: FC<{}> = (props) => (
-
- N/A
-
-);
-
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 (
);
diff --git a/packages/core/src/components/Status/index.ts b/packages/core/src/components/Status/index.ts
index 35d2ca1c48..4c0fd6322b 100644
--- a/packages/core/src/components/Status/index.ts
+++ b/packages/core/src/components/Status/index.ts
@@ -16,8 +16,7 @@
export {
StatusError,
- StatusFailed,
- StatusNA,
+ StatusAborted,
StatusOK,
StatusPending,
StatusRunning,
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index e817ada9c9..1bd35f67c3 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -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';
diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx
index 87a3038a55..62f02ca58b 100644
--- a/packages/core/src/layout/Sidebar/Items.tsx
+++ b/packages/core/src/layout/Sidebar/Items.tsx
@@ -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 => ({
+const useStyles = makeStyles((theme) => ({
root: {
color: '#b5b5b5',
display: 'flex',
@@ -59,7 +53,7 @@ const useStyles = makeStyles(theme => ({
}));
type SidebarItemProps = {
- icon: typeof SvgIcon;
+ icon: IconComponent;
text: string;
to?: string;
onClick?: () => void;
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index bafbab2e32..aea4f9b6ae 100644
--- a/packages/dev-utils/package.json
+++ b/packages/dev-utils/package.json
@@ -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}"
]
}
diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx
index 6649f1055d..e68dac4021 100644
--- a/packages/dev-utils/src/devApp/render.tsx
+++ b/packages/dev-utils/src/devApp/render.tsx
@@ -127,6 +127,18 @@ class DevAppBuilder {
);
break;
}
+ case 'nav-target-component': {
+ const { target } = output;
+ sidebarItems.push(
+ ,
+ );
+ break;
+ }
default:
break;
}
diff --git a/packages/storybook/package.json b/packages/storybook/package.json
index 81c2346d14..64be72bdcf 100644
--- a/packages/storybook/package.json
+++ b/packages/storybook/package.json
@@ -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",
diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json
index 33c27dba62..8509763333 100644
--- a/packages/test-utils-core/package.json
+++ b/packages/test-utils-core/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils-core",
"description": "Utilities to test Backstage core",
- "version": "0.1.1-alpha.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}"
]
}
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index e34a750ae0..0367036610 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
- "version": "0.1.1-alpha.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}"
]
}
diff --git a/packages/theme/package.json b/packages/theme/package.json
index 18b4dd7fa2..be43eb616a 100644
--- a/packages/theme/package.json
+++ b/packages/theme/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
- "version": "0.1.1-alpha.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}"
]
}
diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts
index f2c26e6e4a..33cefe3283 100644
--- a/packages/theme/src/themes.ts
+++ b/packages/theme/src/themes.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',
diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts
index c020fbb4e1..82c717e433 100644
--- a/packages/theme/src/types.ts
+++ b/packages/theme/src/types.ts
@@ -27,7 +27,7 @@ type PaletteAdditions = {
error: string;
pending: string;
running: string;
- background: string;
+ aborted: string;
};
border: string;
textContrast: string;
diff --git a/plugins/inventory-backend/.eslintrc.js b/plugins/catalog-backend/.eslintrc.js
similarity index 100%
rename from plugins/inventory-backend/.eslintrc.js
rename to plugins/catalog-backend/.eslintrc.js
diff --git a/plugins/inventory-backend/README.md b/plugins/catalog-backend/README.md
similarity index 58%
rename from plugins/inventory-backend/README.md
rename to plugins/catalog-backend/README.md
index bc224ee42e..051bc50417 100644
--- a/plugins/inventory-backend/README.md
+++ b/plugins/catalog-backend/README.md
@@ -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]
diff --git a/plugins/catalog-backend/fixtures/component1.yaml b/plugins/catalog-backend/fixtures/component1.yaml
new file mode 100644
index 0000000000..b0b54e0902
--- /dev/null
+++ b/plugins/catalog-backend/fixtures/component1.yaml
@@ -0,0 +1,6 @@
+apiVersion: catalog.backstage.io/v1
+kind: Component
+metadata:
+ name: component1
+spec:
+ type: service
diff --git a/plugins/inventory-backend/package.json b/plugins/catalog-backend/package.json
similarity index 63%
rename from plugins/inventory-backend/package.json
rename to plugins/catalog-backend/package.json
index 9a72daed5e..629f4c6d18 100644
--- a/plugins/inventory-backend/package.json
+++ b/plugins/catalog-backend/package.json
@@ -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"
},
diff --git a/plugins/catalog-backend/src/catalog/CatalogLogic.test.ts b/plugins/catalog-backend/src/catalog/CatalogLogic.test.ts
new file mode 100644
index 0000000000..5234136b55
--- /dev/null
+++ b/plugins/catalog-backend/src/catalog/CatalogLogic.test.ts
@@ -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' }),
+ );
+ });
+ });
+});
diff --git a/plugins/catalog-backend/src/catalog/CatalogLogic.ts b/plugins/catalog-backend/src/catalog/CatalogLogic.ts
new file mode 100644
index 0000000000..686aba0c40
--- /dev/null
+++ b/plugins/catalog-backend/src/catalog/CatalogLogic.ts
@@ -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 {
+ 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}`);
+ }
+ }
+ }
+}
diff --git a/plugins/catalog-backend/src/catalog/DatabaseCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseCatalog.test.ts
new file mode 100644
index 0000000000..98a5e9acd4
--- /dev/null
+++ b/plugins/catalog-backend/src/catalog/DatabaseCatalog.test.ts
@@ -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/,
+ );
+ });
+});
diff --git a/plugins/catalog-backend/src/catalog/DatabaseCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseCatalog.ts
new file mode 100644
index 0000000000..87ff5a503f
--- /dev/null
+++ b/plugins/catalog-backend/src/catalog/DatabaseCatalog.ts
@@ -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 {
+ 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 {
+ 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 {
+ return await this.database('components').select();
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async component(name: string): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ return await this.database('locations').select();
+ }
+}
diff --git a/plugins/catalog-backend/src/catalog/StaticCatalog.ts b/plugins/catalog-backend/src/catalog/StaticCatalog.ts
new file mode 100644
index 0000000000..28d6cff9a3
--- /dev/null
+++ b/plugins/catalog-backend/src/catalog/StaticCatalog.ts
@@ -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 {
+ this._components = this._components
+ .filter((c) => c.name !== descriptor.metadata.name)
+ .concat([{ name: descriptor.metadata.name }]);
+ }
+
+ async components(): Promise {
+ return this._components.slice();
+ }
+
+ async component(name: string): Promise {
+ 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 {
+ const l = { id: uuidv4(), type: location.type, target: location.target };
+ this._locations.push(l);
+ return l;
+ }
+
+ async removeLocation(id: string): Promise {
+ this._locations = this._locations.filter((l) => l.id !== id);
+ }
+
+ async location(id: string): Promise {
+ 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 {
+ return this._locations;
+ }
+}
diff --git a/plugins/catalog-backend/src/catalog/index.ts b/plugins/catalog-backend/src/catalog/index.ts
new file mode 100644
index 0000000000..b4bc07c6bb
--- /dev/null
+++ b/plugins/catalog-backend/src/catalog/index.ts
@@ -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';
diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts
new file mode 100644
index 0000000000..3a7b685003
--- /dev/null
+++ b/plugins/catalog-backend/src/catalog/types.ts
@@ -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 = yup
+ .object({
+ type: yup.string().required(),
+ target: yup.string().required(),
+ })
+ .noUnknown();
+
+export type Catalog = {
+ addOrUpdateComponent(
+ locationId: string,
+ descriptor: ComponentDescriptor,
+ ): Promise;
+ components(): Promise;
+ component(id: string): Promise;
+ addLocation(location: AddLocationRequest): Promise;
+ removeLocation(id: string): Promise;
+ locations(): Promise;
+ location(id: string): Promise;
+};
diff --git a/plugins/catalog-backend/src/descriptors/component.ts b/plugins/catalog-backend/src/descriptors/component.ts
new file mode 100644
index 0000000000..abe75a0050
--- /dev/null
+++ b/plugins/catalog-backend/src/descriptors/component.ts
@@ -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 = 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 {
+ let componentDescriptor;
+ try {
+ componentDescriptor = await componentDescriptorSchema.validate(envelope, {
+ strict: true,
+ });
+ } catch (e) {
+ throw new Error(`Malformed component, ${e}`);
+ }
+
+ return [componentDescriptor];
+}
diff --git a/plugins/catalog-backend/src/descriptors/envelope.ts b/plugins/catalog-backend/src/descriptors/envelope.ts
new file mode 100644
index 0000000000..dd78d860ee
--- /dev/null
+++ b/plugins/catalog-backend/src/descriptors/envelope.ts
@@ -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 = yup
+ .object({
+ apiVersion: yup.string().required(),
+ kind: yup.string().required(),
+ metadata: yup.object(),
+ spec: yup.object(),
+ })
+ .noUnknown();
+
+export async function parseDescriptorEnvelope(
+ rawYaml: string,
+): Promise {
+ 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}`);
+ }
+}
diff --git a/plugins/catalog-backend/src/descriptors/index.ts b/plugins/catalog-backend/src/descriptors/index.ts
new file mode 100644
index 0000000000..db3fd4c938
--- /dev/null
+++ b/plugins/catalog-backend/src/descriptors/index.ts
@@ -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';
diff --git a/plugins/catalog-backend/src/descriptors/util.ts b/plugins/catalog-backend/src/descriptors/util.ts
new file mode 100644
index 0000000000..20118c7d97
--- /dev/null
+++ b/plugins/catalog-backend/src/descriptors/util.ts
@@ -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 {
+ const env = await parseDescriptorEnvelope(rawYaml);
+ return await parseComponentDescriptor(env);
+}
diff --git a/plugins/inventory-backend/src/index.test.ts b/plugins/catalog-backend/src/index.test.ts
similarity index 100%
rename from plugins/inventory-backend/src/index.test.ts
rename to plugins/catalog-backend/src/index.test.ts
diff --git a/plugins/inventory-backend/src/index.ts b/plugins/catalog-backend/src/index.ts
similarity index 95%
rename from plugins/inventory-backend/src/index.ts
rename to plugins/catalog-backend/src/index.ts
index 52b4ed9a53..16f59d2c17 100644
--- a/plugins/inventory-backend/src/index.ts
+++ b/plugins/catalog-backend/src/index.ts
@@ -14,5 +14,5 @@
* limitations under the License.
*/
-export * from './inventory';
+export * from './catalog';
export * from './service/router';
diff --git a/plugins/catalog-backend/src/ingestion/fileLocation.ts b/plugins/catalog-backend/src/ingestion/fileLocation.ts
new file mode 100644
index 0000000000..e856e701d3
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/fileLocation.ts
@@ -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 {
+ 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}`);
+ }
+}
diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts
new file mode 100644
index 0000000000..e96be6822a
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/index.ts
@@ -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';
diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts
new file mode 100644
index 0000000000..426188ed2c
--- /dev/null
+++ b/plugins/catalog-backend/src/ingestion/types.ts
@@ -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.
+ */
+
+import { Location } from '../catalog';
+import { ComponentDescriptor } from '../descriptors';
+
+export type LocationReader = (
+ location: Location,
+) => Promise;
diff --git a/packages/core/src/api/api.ts b/plugins/catalog-backend/src/ingestion/util.ts
similarity index 58%
rename from packages/core/src/api/api.ts
rename to plugins/catalog-backend/src/ingestion/util.ts
index cafd0af126..ede6b1b21a 100644
--- a/packages/core/src/api/api.ts
+++ b/plugins/catalog-backend/src/ingestion/util.ts
@@ -14,18 +14,17 @@
* limitations under the License.
*/
-import ApiRef, { ApiRefConfig } from './apis/ApiRef';
-import AppBuilder from './app/AppBuilder';
-import BackstagePlugin, { PluginConfig } from './plugin/Plugin';
+import { Location } from '../catalog/types';
+import { ComponentDescriptor } from '../descriptors';
+import { readFileLocation } from './fileLocation';
-export function createApp() {
- return new AppBuilder();
-}
-
-export function createApiRef(config: ApiRefConfig) {
- return new ApiRef(config);
-}
-
-export function createPlugin(config: PluginConfig): BackstagePlugin {
- return new BackstagePlugin(config);
+export async function readLocation(
+ location: Location,
+): Promise {
+ switch (location.type) {
+ case 'file':
+ return await readFileLocation(location.target);
+ default:
+ throw new Error(`Unknown type "${location.type}"`);
+ }
}
diff --git a/plugins/catalog-backend/src/migrations/20200511113813_init.ts b/plugins/catalog-backend/src/migrations/20200511113813_init.ts
new file mode 100644
index 0000000000..4c71fefddd
--- /dev/null
+++ b/plugins/catalog-backend/src/migrations/20200511113813_init.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 Knex from 'knex';
+
+export async function up(knex: Knex): Promise {
+ return knex.schema
+ .createTable('locations', (table) => {
+ table.comment(
+ 'Registered locations that shall be contiuously scanned for catalog item updates',
+ );
+ table.uuid('id').primary().comment('Auto-generated ID of the location');
+ table.string('type').notNullable().comment('The type of location');
+ table
+ .string('target')
+ .notNullable()
+ .comment('The actual target of the location');
+ })
+ .createTable('components', (table) => {
+ table.comment('All components currently stored in the catalog');
+ table.uuid('id').primary().comment('Auto-generated ID of the component');
+ table
+ .uuid('locationId')
+ .references('id')
+ .inTable('locations')
+ .nullable()
+ .comment('The location that originated the component');
+ table
+ .string('name')
+ .unique()
+ .notNullable()
+ .comment('The external name of the component, as used in references');
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ return knex.schema.dropTable('components').dropTable('locations');
+}
diff --git a/plugins/inventory-backend/src/run.ts b/plugins/catalog-backend/src/run.ts
similarity index 91%
rename from plugins/inventory-backend/src/run.ts
rename to plugins/catalog-backend/src/run.ts
index 0d5c3a5c12..0a684c379e 100644
--- a/plugins/inventory-backend/src/run.ts
+++ b/plugins/catalog-backend/src/run.ts
@@ -14,13 +14,12 @@
* limitations under the License.
*/
+import yn from 'yn';
import { getRootLogger } from '@backstage/backend-common';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003;
-const enableCors = process.env.PLUGIN_CORS
- ? Boolean(process.env.PLUGIN_CORS)
- : false;
+const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch((err) => {
diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts
new file mode 100644
index 0000000000..3bdbedb766
--- /dev/null
+++ b/plugins/catalog-backend/src/service/router.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { InputError } from '@backstage/backend-common';
+import express, { Request } from 'express';
+import Router from 'express-promise-router';
+import { Logger } from 'winston';
+import yup from 'yup';
+import { addLocationRequestShape, Catalog } from '../catalog';
+
+export interface RouterOptions {
+ catalog: Catalog;
+ logger: Logger;
+}
+
+async function validateRequestBody(
+ req: Request,
+ schema: yup.Schema,
+): Promise {
+ const contentType = req.header('content-type');
+ if (!contentType) {
+ throw new InputError('Content-Type missing');
+ } else if (!contentType.match(/^application\/json($|;)/)) {
+ throw new InputError('Illegal Content-Type');
+ }
+
+ const body = req.body;
+ if (!body) {
+ throw new InputError('Missing request body');
+ }
+
+ try {
+ await schema.validate(body, { strict: true });
+ } catch (e) {
+ throw new InputError(`Malformed request: ${e}`);
+ }
+
+ return body as T;
+}
+
+export async function createRouter(
+ options: RouterOptions,
+): Promise {
+ const catalog = options.catalog;
+ const logger = options.logger.child({ plugin: 'catalog' });
+ const router = Router();
+
+ // Components
+ router
+ .get('/components', async (req, res) => {
+ const components = await catalog.components();
+ res.status(200).send(components);
+ })
+ .get('/components/:id', async (req, res) => {
+ const { id } = req.params;
+ const component = await catalog.component(id);
+ res.status(200).send(component);
+ });
+
+ // Locations
+ router
+ .post('/locations', async (req, res) => {
+ const input = await validateRequestBody(req, addLocationRequestShape);
+ const output = await catalog.addLocation(input);
+ res.status(201).send(output);
+ })
+ .get('/locations', async (req, res) => {
+ const output = await catalog.locations();
+ res.status(200).send(output);
+ })
+ .get('/locations/:id', async (req, res) => {
+ const { id } = req.params;
+ const output = await catalog.location(id);
+ res.status(200).send(output);
+ })
+ .delete('/locations/:id', async (req, res) => {
+ const { id } = req.params;
+ await catalog.removeLocation(id);
+ res.status(200).send();
+ });
+
+ const app = express();
+ app.set('logger', logger);
+ app.use('/', router);
+
+ return app;
+}
diff --git a/plugins/inventory-backend/src/service/standaloneApplication.ts b/plugins/catalog-backend/src/service/standaloneApplication.ts
similarity index 88%
rename from plugins/inventory-backend/src/service/standaloneApplication.ts
rename to plugins/catalog-backend/src/service/standaloneApplication.ts
index 8f0518bb8d..da39be8410 100644
--- a/plugins/inventory-backend/src/service/standaloneApplication.ts
+++ b/plugins/catalog-backend/src/service/standaloneApplication.ts
@@ -24,19 +24,19 @@ import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import { Logger } from 'winston';
-import { Inventory } from '../inventory';
+import { Catalog } from '../catalog';
import { createRouter } from './router';
export interface ApplicationOptions {
enableCors: boolean;
- inventory: Inventory;
+ catalog: Catalog;
logger: Logger;
}
export async function createStandaloneApplication(
options: ApplicationOptions,
): Promise {
- const { enableCors, inventory, logger } = options;
+ const { enableCors, catalog, logger } = options;
const app = express();
app.use(helmet());
@@ -46,7 +46,7 @@ export async function createStandaloneApplication(
app.use(compression());
app.use(express.json());
app.use(requestLoggingHandler());
- app.use('/', await createRouter({ inventory, logger }));
+ app.use('/', await createRouter({ catalog, logger }));
app.use(notFoundHandler());
app.use(errorHandler());
diff --git a/plugins/inventory-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts
similarity index 79%
rename from plugins/inventory-backend/src/service/standaloneServer.ts
rename to plugins/catalog-backend/src/service/standaloneServer.ts
index be339c5ecc..d5c46d7293 100644
--- a/plugins/inventory-backend/src/service/standaloneServer.ts
+++ b/plugins/catalog-backend/src/service/standaloneServer.ts
@@ -16,7 +16,7 @@
import { Server } from 'http';
import { Logger } from 'winston';
-import { AggregatorInventory, StaticInventory } from '../inventory';
+import { StaticCatalog } from '../catalog';
import { createStandaloneApplication } from './standaloneApplication';
export interface ServerOptions {
@@ -28,22 +28,22 @@ export interface ServerOptions {
export async function startStandaloneServer(
options: ServerOptions,
): Promise {
- const logger = options.logger.child({ service: 'inventory-backend' });
+ const logger = options.logger.child({ service: 'catalog-backend' });
- const inventory = new AggregatorInventory();
- inventory.enlist(
- new StaticInventory([
- { id: 'component1' },
- { id: 'component2' },
- { id: 'component3' },
- { id: 'component4' },
- ]),
+ const catalog = new StaticCatalog(
+ [
+ { name: 'component1' },
+ { name: 'component2' },
+ { name: 'component3' },
+ { name: 'component4' },
+ ],
+ [],
);
logger.debug('Creating application...');
const app = await createStandaloneApplication({
enableCors: options.enableCors,
- inventory,
+ catalog,
logger,
});
diff --git a/plugins/inventory-backend/src/setupTests.ts b/plugins/catalog-backend/src/setupTests.ts
similarity index 100%
rename from plugins/inventory-backend/src/setupTests.ts
rename to plugins/catalog-backend/src/setupTests.ts
diff --git a/plugins/inventory-backend/tsconfig.json b/plugins/catalog-backend/tsconfig.json
similarity index 92%
rename from plugins/inventory-backend/tsconfig.json
rename to plugins/catalog-backend/tsconfig.json
index dd13cfe1bc..015a967f76 100644
--- a/plugins/inventory-backend/tsconfig.json
+++ b/plugins/catalog-backend/tsconfig.json
@@ -6,7 +6,7 @@
"sourceMap": true,
"declaration": true,
"strict": true,
- "target": "es5",
+ "target": "es2019",
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
diff --git a/plugins/inventory/.eslintrc.js b/plugins/catalog/.eslintrc.js
similarity index 100%
rename from plugins/inventory/.eslintrc.js
rename to plugins/catalog/.eslintrc.js
diff --git a/plugins/catalog/README.md b/plugins/catalog/README.md
new file mode 100644
index 0000000000..52bbd0d0c3
--- /dev/null
+++ b/plugins/catalog/README.md
@@ -0,0 +1,13 @@
+# Catalog Frontend
+
+WORK IN PROGRESS
+
+This is the frontend part of the default catalog plugin.
+
+It will implement the core API for handling your catalog of software, and
+supply the base views to show and manage them.
+
+## Links
+
+- (Backend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/catalog-backend]
+- (The Backstage homepage)[https://backstage.io]
diff --git a/plugins/inventory-backend/src/inventory/types.ts b/plugins/catalog/dev/index.tsx
similarity index 80%
rename from plugins/inventory-backend/src/inventory/types.ts
rename to plugins/catalog/dev/index.tsx
index 863dccb291..812a5585d4 100644
--- a/plugins/inventory-backend/src/inventory/types.ts
+++ b/plugins/catalog/dev/index.tsx
@@ -14,11 +14,7 @@
* limitations under the License.
*/
-export type Component = {
- id: string;
-};
+import { createDevApp } from '@backstage/dev-utils';
+import { plugin } from '../src/plugin';
-export type Inventory = {
- list(): Promise;
- item(id: string): Promise;
-};
+createDevApp().registerPlugin(plugin).render();
diff --git a/plugins/inventory/package.json b/plugins/catalog/package.json
similarity index 65%
rename from plugins/inventory/package.json
rename to plugins/catalog/package.json
index ae871eb0bf..1987dea1f5 100644
--- a/plugins/inventory/package.json
+++ b/plugins/catalog/package.json
@@ -1,8 +1,8 @@
{
- "name": "@backstage/plugin-inventory",
- "version": "0.1.1-alpha.4",
+ "name": "@backstage/plugin-catalog",
+ "version": "0.1.1-alpha.5",
"main": "dist/index.esm.js",
- "types": "dist/index.d.ts",
+ "types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"scripts": {
@@ -11,29 +11,32 @@
"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": {
- "@backstage/core": "^0.1.1-alpha.4",
- "@backstage/theme": "^0.1.1-alpha.4",
+ "@backstage/core": "^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",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-use": "^13.24.0"
+ "react-use": "^14.2.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.4",
+ "@backstage/cli": "^0.1.1-alpha.5",
+ "@backstage/dev-utils": "^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/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}"
]
}
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx
new file mode 100644
index 0000000000..01d821e175
--- /dev/null
+++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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 CatalogPage from './CatalogPage';
+import { ThemeProvider } from '@material-ui/core';
+import { lightTheme } from '@backstage/theme';
+
+describe('CatalogPage', () => {
+ it('should render', async () => {
+ const rendered = render(
+
+
+ ,
+ );
+ expect(await rendered.findByText('Your components')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
new file mode 100644
index 0000000000..9972b9d71f
--- /dev/null
+++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { FC } from 'react';
+import { Content, Header, Page, pageTheme } from '@backstage/core';
+import { useAsync } from 'react-use';
+import { ComponentFactory } from '../../data/component';
+import { MockComponentFactory } from '../../data/mock-factory';
+import CatalogTable from '../CatalogTable/CatalogTable';
+
+const componentFactory: ComponentFactory = MockComponentFactory;
+
+const CatalogPage: FC<{}> = () => {
+ const { value, error, loading } = useAsync(componentFactory.getAllComponents);
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default CatalogPage;
diff --git a/plugins/inventory/src/components/InventoryPage/index.ts b/plugins/catalog/src/components/CatalogPage/index.ts
similarity index 93%
rename from plugins/inventory/src/components/InventoryPage/index.ts
rename to plugins/catalog/src/components/CatalogPage/index.ts
index 7a11d39f10..61182e316f 100644
--- a/plugins/inventory/src/components/InventoryPage/index.ts
+++ b/plugins/catalog/src/components/CatalogPage/index.ts
@@ -14,4 +14,4 @@
* limitations under the License.
*/
-export { default } from './InventoryPage';
+export { default } from './CatalogPage';
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx
new file mode 100644
index 0000000000..0ab95f4231
--- /dev/null
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 React from 'react';
+import { render } from '@testing-library/react';
+import CatalogTable from './CatalogTable';
+import { Component } from '../../data/component';
+
+const components: Component[] = [
+ { name: 'component1' },
+ { name: 'component2' },
+ { name: 'component3' },
+];
+
+describe('CatalogTable component', () => {
+ it('should render loading when loading prop it set to true', async () => {
+ const rendered = render();
+ const progress = await rendered.findByTestId('progress');
+ expect(progress).toBeInTheDOM();
+ });
+
+ it('should render error message when error is passed in props', async () => {
+ const rendered = render(
+ ,
+ );
+ const errorMessage = await rendered.findByText(
+ 'Error encountered while fetching components.',
+ );
+ expect(errorMessage).toBeInTheDOM();
+ });
+
+ it('should display component names when loading has finished and no error occurred', async () => {
+ const rendered = render(
+ ,
+ );
+ expect(await rendered.findByText('component1')).toBeInTheDOM();
+ expect(await rendered.findByText('component2')).toBeInTheDOM();
+ expect(await rendered.findByText('component3')).toBeInTheDOM();
+ });
+});
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
new file mode 100644
index 0000000000..31f15fe5ca
--- /dev/null
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
@@ -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, { FC } from 'react';
+import { Component } from '../../data/component';
+import { InfoCard, Progress, Table, TableColumn } from '@backstage/core';
+import { Typography } from '@material-ui/core';
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ field: 'name',
+ highlight: true,
+ },
+];
+
+type CatalogTableProps = {
+ components: Component[];
+ loading: boolean;
+ error?: any;
+};
+const CatalogTable: FC = ({
+ components,
+ loading,
+ error,
+}) => {
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return (
+
+
+ Error encountered while fetching components.
+
+
+ );
+ }
+ return (
+
+ );
+};
+export default CatalogTable;
diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts
new file mode 100644
index 0000000000..5b67b647e4
--- /dev/null
+++ b/plugins/catalog/src/data/component.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 Component = {
+ name: string;
+};
+
+export interface ComponentFactory {
+ getAllComponents(): Promise;
+ getComponentByName(name: string): Promise;
+}
diff --git a/plugins/catalog/src/data/mock-factory-data.json b/plugins/catalog/src/data/mock-factory-data.json
new file mode 100644
index 0000000000..8fc61a87a6
--- /dev/null
+++ b/plugins/catalog/src/data/mock-factory-data.json
@@ -0,0 +1,35 @@
+[
+ {
+ "name": "example.com"
+ },
+ {
+ "name": "subdomain.example.com"
+ },
+ {
+ "name": "subdomain2.example.com"
+ },
+ {
+ "name": "User data pipeline 1"
+ },
+ {
+ "name": "User data pipeline 2"
+ },
+ {
+ "name": "User data pipeline 3"
+ },
+ {
+ "name": "Aggregation CRON job"
+ },
+ {
+ "name": "Authentication service"
+ },
+ {
+ "name": "Payments service"
+ },
+ {
+ "name": "Backstage supervisor"
+ },
+ {
+ "name": "Identity service"
+ }
+]
diff --git a/plugins/inventory-backend/src/inventory/StaticInventory.ts b/plugins/catalog/src/data/mock-factory.ts
similarity index 53%
rename from plugins/inventory-backend/src/inventory/StaticInventory.ts
rename to plugins/catalog/src/data/mock-factory.ts
index 4569ae8ed2..010036d732 100644
--- a/plugins/inventory-backend/src/inventory/StaticInventory.ts
+++ b/plugins/catalog/src/data/mock-factory.ts
@@ -13,22 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { Component, ComponentFactory } from './component';
+import mock from './mock-factory-data.json';
-import { NotFoundError } from '@backstage/backend-common';
-import { Component, Inventory } from './types';
-
-export class StaticInventory implements Inventory {
- constructor(private components: Component[]) {}
-
- async list(): Promise {
- return this.components.slice();
- }
-
- async item(id: string): Promise {
- const item = this.components.find((i) => i.id === id);
- if (!item) {
- throw new NotFoundError(`Found no component with ID ${id}`);
- }
- return item;
- }
-}
+export const MockComponentFactory: ComponentFactory = {
+ getAllComponents(): Promise {
+ return new Promise((resolve) => setTimeout(() => resolve(mock), 2000));
+ },
+ getComponentByName(name: string): Promise {
+ return new Promise((resolve) =>
+ setTimeout(
+ () => resolve(mock.find((component) => component.name === name)),
+ 2000,
+ ),
+ );
+ },
+};
diff --git a/plugins/inventory/src/index.ts b/plugins/catalog/src/index.ts
similarity index 100%
rename from plugins/inventory/src/index.ts
rename to plugins/catalog/src/index.ts
diff --git a/plugins/inventory/src/plugin.test.ts b/plugins/catalog/src/plugin.test.ts
similarity index 95%
rename from plugins/inventory/src/plugin.test.ts
rename to plugins/catalog/src/plugin.test.ts
index 4fac72593f..427efc39e1 100644
--- a/plugins/inventory/src/plugin.test.ts
+++ b/plugins/catalog/src/plugin.test.ts
@@ -16,7 +16,7 @@
import { plugin } from './plugin';
-describe('inventory', () => {
+describe('catalog', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
diff --git a/plugins/inventory/src/plugin.ts b/plugins/catalog/src/plugin.ts
similarity index 84%
rename from plugins/inventory/src/plugin.ts
rename to plugins/catalog/src/plugin.ts
index 197dd3cd38..ff3a53ce5b 100644
--- a/plugins/inventory/src/plugin.ts
+++ b/plugins/catalog/src/plugin.ts
@@ -15,11 +15,11 @@
*/
import { createPlugin } from '@backstage/core';
-import InventoryPage from './components/InventoryPage';
+import CatalogPage from './components/CatalogPage';
export const plugin = createPlugin({
- id: 'inventory',
+ id: 'catalog',
register({ router }) {
- router.registerRoute('/inventory', InventoryPage);
+ router.registerRoute('/catalog', CatalogPage);
},
});
diff --git a/plugins/inventory/src/setupTests.ts b/plugins/catalog/src/setupTests.ts
similarity index 100%
rename from plugins/inventory/src/setupTests.ts
rename to plugins/catalog/src/setupTests.ts
diff --git a/plugins/inventory/tsconfig.json b/plugins/catalog/tsconfig.json
similarity index 68%
rename from plugins/inventory/tsconfig.json
rename to plugins/catalog/tsconfig.json
index 5a3931ffce..b663b01fa2 100644
--- a/plugins/inventory/tsconfig.json
+++ b/plugins/catalog/tsconfig.json
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
- "include": ["src"],
+ "include": ["src", "dev"],
"compilerOptions": {}
}
diff --git a/plugins/circleci/.eslintrc.js b/plugins/circleci/.eslintrc.js
index 13573efa9c..f1f18a58c8 100644
--- a/plugins/circleci/.eslintrc.js
+++ b/plugins/circleci/.eslintrc.js
@@ -1,3 +1,6 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
+ rules: {
+ 'no-use-before-define': ['error', { variables: false }],
+ },
};
diff --git a/plugins/circleci/src/state/useBuildWithSteps.ts b/plugins/circleci/src/state/useBuildWithSteps.ts
index cb0a01c8c1..2787143658 100644
--- a/plugins/circleci/src/state/useBuildWithSteps.ts
+++ b/plugins/circleci/src/state/useBuildWithSteps.ts
@@ -21,7 +21,6 @@ import { useSettings } from './useSettings';
import { useAsyncPolling } from './useAsyncPolling';
-
const INTERVAL_AMOUNT = 3000;
export function useBuildWithSteps(buildId: number) {
@@ -30,8 +29,11 @@ export function useBuildWithSteps(buildId: number) {
const api = useApi(circleCIApiRef);
const errorApi = useApi(errorApiRef);
- const {isPolling, startPolling, stopPolling} = useAsyncPolling(() => getBuildWithSteps(), INTERVAL_AMOUNT);
-
+ /* eslint-disable-line no-use-before-define */
+ const { isPolling, startPolling, stopPolling } = useAsyncPolling(
+ () => getBuildWithSteps(),
+ INTERVAL_AMOUNT,
+ );
const getBuildWithSteps = async () => {
try {
diff --git a/plugins/circleci/src/state/useBuilds.tsx b/plugins/circleci/src/state/useBuilds.tsx
index 89f85744c8..9104b134cc 100644
--- a/plugins/circleci/src/state/useBuilds.tsx
+++ b/plugins/circleci/src/state/useBuilds.tsx
@@ -27,7 +27,11 @@ export function useBuilds() {
const api = useApi(circleCIApiRef);
const errorApi = useApi(errorApiRef);
- const {isPolling, startPolling, stopPolling} = useAsyncPolling(() => getBuilds(), INTERVAL_AMOUNT);
+ // eslint-disable-line no-use-before-define
+ const { isPolling, startPolling, stopPolling } = useAsyncPolling(
+ () => getBuilds(),
+ INTERVAL_AMOUNT,
+ );
const getBuilds = async () => {
if (settings.owner === '' || settings.repo === '') return;
@@ -40,10 +44,11 @@ export function useBuilds() {
type: GitType.GITHUB,
},
});
- if (isPolling) dispatch({
- type: 'setBuilds',
- payload: newBuilds,
- });
+ if (isPolling)
+ dispatch({
+ type: 'setBuilds',
+ payload: newBuilds,
+ });
} catch (e) {
errorApi.post(e);
}
@@ -64,8 +69,6 @@ export function useBuilds() {
}
};
-
-
return [
builds,
{
diff --git a/plugins/explore/dev/index.tsx b/plugins/explore/dev/index.tsx
new file mode 100644
index 0000000000..812a5585d4
--- /dev/null
+++ b/plugins/explore/dev/index.tsx
@@ -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.
+ */
+
+import { createDevApp } from '@backstage/dev-utils';
+import { plugin } from '../src/plugin';
+
+createDevApp().registerPlugin(plugin).render();
diff --git a/plugins/explore/package.json b/plugins/explore/package.json
index 9adbee7b0f..50c7e0be84 100644
--- a/plugins/explore/package.json
+++ b/plugins/explore/package.json
@@ -1,8 +1,8 @@
{
"name": "@backstage/plugin-explore",
- "version": "0.1.1-alpha.4",
+ "version": "0.1.1-alpha.5",
"main": "dist/index.esm.js",
- "types": "dist/index.d.ts",
+ "types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"scripts": {
@@ -10,31 +10,35 @@
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
- "clean": "backstage-cli clean"
+ "clean": "backstage-cli clean",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "start": "backstage-cli plugin:serve"
},
"dependencies": {
- "@backstage/core": "^0.1.1-alpha.4",
- "@backstage/theme": "^0.1.1-alpha.4",
+ "@backstage/core": "^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",
"classnames": "^2.2.6",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-use": "^13.24.0"
+ "react-use": "^14.2.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.4",
- "@backstage/test-utils": "^0.1.1-alpha.4",
+ "@backstage/cli": "^0.1.1-alpha.5",
+ "@backstage/dev-utils": "^0.1.1-alpha.5",
+ "@backstage/test-utils": "^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/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}"
]
}
diff --git a/plugins/explore/tsconfig.json b/plugins/explore/tsconfig.json
index 5a3931ffce..b663b01fa2 100644
--- a/plugins/explore/tsconfig.json
+++ b/plugins/explore/tsconfig.json
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
- "include": ["src"],
+ "include": ["src", "dev"],
"compilerOptions": {}
}
diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json
index 3740ac01a2..a90bd27a1f 100644
--- a/plugins/graphiql/package.json
+++ b/plugins/graphiql/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphiql",
"description": "Backstage plugin for browsing GraphQL APIs",
- "version": "0.1.1-alpha.4",
+ "version": "0.1.1-alpha.5",
"private": false,
"publishConfig": {
"access": "public"
@@ -17,18 +17,20 @@
],
"license": "Apache-2.0",
"main": "dist/index.esm.js",
- "types": "dist/index.d.ts",
+ "types": "src/index.ts",
"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": {
- "@backstage/core": "^0.1.1-alpha.4",
- "@backstage/theme": "^0.1.1-alpha.4",
+ "@backstage/core": "^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",
@@ -36,22 +38,22 @@
"graphql": "15.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-use": "^13.24.0"
+ "react-use": "^14.2.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.4",
- "@backstage/dev-utils": "^0.1.1-alpha.4",
- "@backstage/test-utils": "^0.1.1-alpha.4",
+ "@backstage/cli": "^0.1.1-alpha.5",
+ "@backstage/dev-utils": "^0.1.1-alpha.5",
+ "@backstage/test-utils": "^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/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",
- "react-router-dom": "^5.1.2"
+ "react-router-dom": "^5.2.0"
},
"files": [
- "dist"
+ "dist/**/*.{js,d.ts}"
]
}
diff --git a/plugins/graphiql/src/index.ts b/plugins/graphiql/src/index.ts
index a1f5587fe1..5590a0d60f 100644
--- a/plugins/graphiql/src/index.ts
+++ b/plugins/graphiql/src/index.ts
@@ -16,3 +16,4 @@
export { plugin } from './plugin';
export * from './lib/api';
+export * from './navTargets';
diff --git a/plugins/graphiql/src/navTargets.tsx b/plugins/graphiql/src/navTargets.tsx
new file mode 100644
index 0000000000..2351780f2c
--- /dev/null
+++ b/plugins/graphiql/src/navTargets.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 { createNavTarget } from '@backstage/core';
+import { SvgIcon, SvgIconProps } from '@material-ui/core';
+
+const GraphiQLIcon: FC = (props) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const navTargetGraphiQL = createNavTarget({
+ icon: GraphiQLIcon,
+ path: '/graphiql',
+ title: 'GraphiQL',
+});
diff --git a/plugins/graphiql/src/plugin.ts b/plugins/graphiql/src/plugin.ts
index 6436380c27..9c73324b43 100644
--- a/plugins/graphiql/src/plugin.ts
+++ b/plugins/graphiql/src/plugin.ts
@@ -16,10 +16,11 @@
import { createPlugin } from '@backstage/core';
import { GraphiQLPage } from './components';
+import { navTargetGraphiQL } from './navTargets';
export const plugin = createPlugin({
id: 'graphiql',
register({ router }) {
- router.registerRoute('/graphiql', GraphiQLPage);
+ router.addRoute(navTargetGraphiQL, GraphiQLPage);
},
});
diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json
index 121bed61da..bd97a6ec4f 100644
--- a/plugins/home-page/package.json
+++ b/plugins/home-page/package.json
@@ -1,8 +1,8 @@
{
"name": "@backstage/plugin-home-page",
- "version": "0.1.1-alpha.4",
+ "version": "0.1.1-alpha.5",
"main": "dist/index.esm.js",
- "types": "dist/index.d.ts",
+ "types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"scripts": {
@@ -11,31 +11,33 @@
"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": {
- "@backstage/core": "^0.1.1-alpha.4",
- "@backstage/theme": "^0.1.1-alpha.4",
+ "@backstage/core": "^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",
"react": "^16.13.1",
"react-dom": "^16.13.1",
- "react-use": "^13.24.0"
+ "react-use": "^14.2.0"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.4",
- "@backstage/dev-utils": "^0.1.1-alpha.4",
+ "@backstage/cli": "^0.1.1-alpha.5",
+ "@backstage/dev-utils": "^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/jest": "^24.0.0",
+ "@types/jest": "^25.2.1",
"@types/node": "^12.0.0",
"@types/testing-library__jest-dom": "^5.0.4",
- "react-router-dom": "^5.1.2",
- "jest-fetch-mock": "^3.0.3"
+ "jest-fetch-mock": "^3.0.3",
+ "react-router-dom": "^5.2.0"
},
"files": [
- "dist"
+ "dist/**/*.{js,d.ts}"
]
}
diff --git a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts
deleted file mode 100644
index b768b8eb23..0000000000
--- a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { NotFoundError } from '@backstage/backend-common';
-import { Component, Inventory } from './types';
-
-export class AggregatorInventory implements Inventory {
- inventories: Inventory[] = [];
-
- async list(): Promise {
- const lists = await Promise.all(this.inventories.map((i) => i.list()));
- return lists.flat();
- }
-
- item(id: string): Promise {
- return new Promise((resolve, reject) => {
- const promises = this.inventories.map((i) =>
- i.item(id).then(resolve, () => {
- // For now, just swallow errors in individual inventories;
- // should handle partial errors better
- }),
- );
- Promise.all(promises).then(() =>
- reject(new NotFoundError(`Found no component with ID ${id}`)),
- );
- });
- }
-
- enlist(inventory: Inventory) {
- this.inventories.push(inventory);
- }
-}
diff --git a/plugins/inventory-backend/src/service/router.ts b/plugins/inventory-backend/src/service/router.ts
deleted file mode 100644
index a05fac08be..0000000000
--- a/plugins/inventory-backend/src/service/router.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express from 'express';
-import { Logger } from 'winston';
-import Router from 'express-promise-router';
-import { Inventory } from '../inventory';
-
-export interface RouterOptions {
- inventory: Inventory;
- logger: Logger;
-}
-
-export async function createRouter(
- options: RouterOptions,
-): Promise {
- const inventory = options.inventory;
- const logger = options.logger.child({ plugin: 'inventory' });
-
- const router = Router();
- router
- .get('/', async (req, res) => {
- const components = await inventory.list();
- res.status(200).send(components);
- })
- .get('/:id', async (req, res) => {
- const { id } = req.params;
- const component = await inventory.item(id);
- res.status(200).send(component);
- });
-
- const app = express();
- app.set('logger', logger);
- app.use('/', router);
-
- return app;
-}
diff --git a/plugins/inventory/README.md b/plugins/inventory/README.md
deleted file mode 100644
index e77a77148f..0000000000
--- a/plugins/inventory/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Inventory Frontend
-
-WORK IN PROGRESS
-
-This is the frontend part of the default inventory plugin.
-
-It will implement the core API for handling your inventory of software, and
-supply the base views to show and manage them.
-
-## Links
-
-- (Backend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/inventory-backend]
-- (The Backstage homepage)[https://backstage.io]
diff --git a/plugins/inventory/src/components/InventoryPage/InventoryPage.tsx b/plugins/inventory/src/components/InventoryPage/InventoryPage.tsx
deleted file mode 100644
index 647899eb35..0000000000
--- a/plugins/inventory/src/components/InventoryPage/InventoryPage.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React, { FC } from 'react';
-import { Typography } from '@material-ui/core';
-import { Content, InfoCard, Header, Page, pageTheme } from '@backstage/core';
-import Table from '@material-ui/core/Table';
-import TableBody from '@material-ui/core/TableBody';
-import TableCell from '@material-ui/core/TableCell';
-import TableContainer from '@material-ui/core/TableContainer';
-import TableHead from '@material-ui/core/TableHead';
-import TableRow from '@material-ui/core/TableRow';
-
-// TODO(freben): Connect to backend
-const STATIC_DATA = [
- { id: 'backstage-frontend', kind: 'website' },
- { id: 'backstage-backend', kind: 'service' },
- { id: 'backstage-microsite', kind: 'website' },
-];
-
-const InventoryPage: FC<{}> = () => {
- return (
-
-
-
- All of it
-
-
-