From 6207cf47c6ffce764f6e07db0e91c59f124fda5b Mon Sep 17 00:00:00 2001 From: Jordi Polo Date: Fri, 5 Jun 2020 17:47:44 -0700 Subject: [PATCH 01/60] Initial version of a json schema for the service specification --- docs/service_specification.schema.json | 117 +++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/service_specification.schema.json diff --git a/docs/service_specification.schema.json b/docs/service_specification.schema.json new file mode 100644 index 0000000000..01216226c5 --- /dev/null +++ b/docs/service_specification.schema.json @@ -0,0 +1,117 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "backstage.io/v1beta1", + "type": "object", + "title": "A JSON Schema for Backstage service definitions.", + "description": "Each Backstage description file has a number of files. This schema matches each of those.", + "examples": [ + { + "apiVersion": "backstage.io/v1beta1", + "kind": "Component", + "spec": { + "type": "service" + }, + "metadata": { + "name": "LoremService", + "description": "Creates Lorems like a pro.", + "labels": { + "product_name": "Random value Generator" + }, + "annnotations": { + "docs": "https://github.com/..../tree/develop/doc" + }, + "teams": { + "name": "Team super great", + "email": "greatTeam@geemel.com" + } + } + } + ], + "required": [ + "apiVersion", + "kind", + "spec", + "metadata" + ], + "additionalProperties": false, + "properties": { + "apiVersion": { + "type": "string", + "description": "Version of the specification format for a particular file is written against.", + "default": "backstage.io/v1beta1", + "enum": [ + "backstage.io/v1beta1" + ] + }, + "kind": { + "type": "string", + "description": "High level entity type being described, from the Backstage system model.", + "enum": [ + "Compontent" + ] + }, + "spec": { + "type": "object", + "description": "Actual specification data that describes the entity. TODO: shape depend on `kind`", + "required": [ + "type" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "The type of component.", + "default": "", + "examples": [ + "service" + ] + } + } + }, + "metadata": { + "type": "object", + "description": "Metadata about the entity, i.e. things that aren't directly part of the entity specification itself.", + "required": [ + "name" + ], + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9A-Z_\\-\\.]{1,63}$", + "description": "The name of the entity. This name is both meant for human eyes to recognize the entity, and for machines and other components to reference the entity" + }, + "description": { + "type": "string", + "description": "A human readable description of the entity, to be shown in Backstage. Should be kept short and informative." + }, + "namespace": { + "type": "string", + "description": "The name of a namespace that the entity belongs to." + }, + "labels": { + "type": "object", + "description": "Labels are optional key/value pairs of that are attached to the entity, and their use is identical to kubernetes object labels.", + "additionalProperties": true, + "patternProperties": { + "^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}\/)?[a-z0-9A-Z_\\-\\.]{1,63}$": { + "type": "string", + "pattern": "^[a-z0-9A-Z_\\-\\.]{1,63}$", + } + } + }, + "annnotations": { + "type": "object", + "description": "Arbitrary non-identifying metadata attached to the entity, identical in use to kubernetes object annotations.", + "additionalProperties": true, + "patternProperties": { + "^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}\/)?[a-z0-9A-Z_\\-\\.]{1,63}$": { + "type": "string", + "pattern": "^[a-z0-9A-Z_\\-\\.]{1,63}$", + } + } + } + } + } + } +} From ad9225ecba087e04fef7293c7d721434c01c92da Mon Sep 17 00:00:00 2001 From: Jordi Polo Date: Tue, 9 Jun 2020 22:46:17 -0700 Subject: [PATCH 02/60] Address review comments --- docs/service_specification.schema.json | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/service_specification.schema.json b/docs/service_specification.schema.json index 01216226c5..15b31127f4 100644 --- a/docs/service_specification.schema.json +++ b/docs/service_specification.schema.json @@ -2,8 +2,8 @@ "$schema": "http://json-schema.org/draft-07/schema", "$id": "backstage.io/v1beta1", "type": "object", - "title": "A JSON Schema for Backstage service definitions.", - "description": "Each Backstage description file has a number of files. This schema matches each of those.", + "title": "A JSON Schema for Backstage catalog entities.", + "description": "Each descriptor file has a number of entities. This schema matches each of those.", "examples": [ { "apiVersion": "backstage.io/v1beta1", @@ -20,17 +20,16 @@ "annnotations": { "docs": "https://github.com/..../tree/develop/doc" }, - "teams": { + "teams": [{ "name": "Team super great", "email": "greatTeam@geemel.com" - } + }] } } ], "required": [ "apiVersion", "kind", - "spec", "metadata" ], "additionalProperties": false, @@ -38,7 +37,6 @@ "apiVersion": { "type": "string", "description": "Version of the specification format for a particular file is written against.", - "default": "backstage.io/v1beta1", "enum": [ "backstage.io/v1beta1" ] @@ -47,7 +45,7 @@ "type": "string", "description": "High level entity type being described, from the Backstage system model.", "enum": [ - "Compontent" + "Component" ] }, "spec": { @@ -78,7 +76,7 @@ "properties": { "name": { "type": "string", - "pattern": "^[a-z0-9A-Z_\\-\\.]{1,63}$", + "pattern": "^[a-z0-9A-Z_.-]{1,63}$", "description": "The name of the entity. This name is both meant for human eyes to recognize the entity, and for machines and other components to reference the entity" }, "description": { @@ -96,7 +94,7 @@ "patternProperties": { "^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}\/)?[a-z0-9A-Z_\\-\\.]{1,63}$": { "type": "string", - "pattern": "^[a-z0-9A-Z_\\-\\.]{1,63}$", + "pattern": "^[a-z0-9A-Z_.-]{1,63}$" } } }, @@ -107,7 +105,7 @@ "patternProperties": { "^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}\/)?[a-z0-9A-Z_\\-\\.]{1,63}$": { "type": "string", - "pattern": "^[a-z0-9A-Z_\\-\\.]{1,63}$", + "pattern": "^[a-z0-9A-Z_.-]{1,63}$" } } } From b943eb065e9cd912cbb901950d0c30d262d080f8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 00:10:15 +0200 Subject: [PATCH 03/60] packages/core-api: move config loading logic in AppProvider into separate hook --- packages/core-api/src/app/App.tsx | 66 ++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 1923adba0d..6da2b5f1b9 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -45,6 +45,41 @@ type FullAppOptions = { configLoader?: AppConfigLoader; }; +function useConfigLoader( + configLoader: AppConfigLoader | undefined, + components: AppComponents, + appThemeApi: AppThemeApi, +): { api: ConfigApi } | { node: JSX.Element } { + // Keeping this synchronous when a config loader isn't set simplifies tests a lot + const hasConfig = Boolean(configLoader); + const config = useAsync(configLoader || (() => Promise.resolve([]))); + + let noConfigNode = undefined; + + if (hasConfig && config.loading) { + const { Progress } = components; + noConfigNode = ; + } else if (config.error) { + const { BootErrorPage } = components; + noConfigNode = ; + } + + // Before the config is loaded we can't use a router, so exit early + if (noConfigNode) { + return { + node: ( + + {noConfigNode} + + ), + }; + } + + const configReader = ConfigReader.fromConfigs(config.value ?? []); + + return { api: configReader }; +} + export class PrivateAppImpl implements BackstageApp { private apis?: ApiHolder = undefined; private readonly icons: SystemIcons; @@ -151,32 +186,17 @@ export class PrivateAppImpl implements BackstageApp { [], ); - // Keeping this synchronous when a config loader isn't set simplifies tests a lot - const hasConfig = Boolean(this.configLoader); - const config = useAsync(this.configLoader || (() => Promise.resolve([]))); + const loadedConfig = useConfigLoader( + this.configLoader, + this.components, + appThemeApi, + ); - let noConfigNode = undefined; - - if (hasConfig && config.loading) { - const { Progress } = this.components; - noConfigNode = ; - } else if (config.error) { - const { BootErrorPage } = this.components; - noConfigNode = ( - - ); + if ('node' in loadedConfig) { + return loadedConfig.node; } + const configReader = loadedConfig.api; - // Before the config is loaded we can't use a router, so exit early - if (noConfigNode) { - return ( - - {noConfigNode} - - ); - } - - const configReader = ConfigReader.fromConfigs(config.value ?? []); const appApis = ApiRegistry.from([ [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], [configApiRef, configReader], From 9a3799515167dbdd3af389d60b1fc7fc4f7c25aa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 00:46:39 +0200 Subject: [PATCH 04/60] packages/core-api: split AppComponent into AppRouter and AppRoutes --- packages/app/src/App.tsx | 11 ++-- .../default-app/packages/app/src/App.tsx | 7 ++- packages/core-api/src/app/App.tsx | 55 ++++++++++++------- packages/core-api/src/app/types.ts | 21 ++++--- packages/dev-utils/src/devApp/render.tsx | 15 +++-- .../test-utils/src/testUtils/appWrappers.tsx | 9 ++- 6 files changed, 76 insertions(+), 42 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ed0518485a..ba7fb4a8a5 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -27,15 +27,18 @@ const app = createApp({ }); const AppProvider = app.getProvider(); -const AppComponent = app.getRootComponent(); +const AppRouter = app.getRouter(); +const AppRoutes = app.getRoutes(); const App: FC<{}> = () => ( - - - + + + + + ); diff --git a/packages/cli/templates/default-app/packages/app/src/App.tsx b/packages/cli/templates/default-app/packages/app/src/App.tsx index 140d1c1660..1b58a3e5df 100644 --- a/packages/cli/templates/default-app/packages/app/src/App.tsx +++ b/packages/cli/templates/default-app/packages/app/src/App.tsx @@ -26,13 +26,16 @@ const app = createApp({ }); const AppProvider = app.getProvider(); -const AppComponent = app.getRootComponent(); +const AppRouter = app.getRouter(); +const AppRoutes = app.getRoutes(); const App: FC<{}> = () => { useStyles(); return ( - + + + ); }; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 6da2b5f1b9..8d625a3a0d 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -19,7 +19,11 @@ import { AppContextProvider } from './AppContext'; import { BackstageApp, AppComponents, AppConfigLoader, Apis } from './types'; import { BackstagePlugin } from '../plugin'; import { FeatureFlagsRegistryItem } from './FeatureFlags'; -import { featureFlagsApiRef } from '../apis/definitions'; +import { + featureFlagsApiRef, + AppThemeApi, + ConfigApi, +} from '../apis/definitions'; import { AppThemeProvider } from './AppThemeProvider'; import { IconComponent, SystemIcons, SystemIconKey } from '../icons'; @@ -32,6 +36,7 @@ import { appThemeApiRef, configApiRef, ConfigReader, + useApi, } from '../apis'; import { ApiAggregator } from '../apis/ApiAggregator'; import { useAsync } from 'react-use'; @@ -114,7 +119,7 @@ export class PrivateAppImpl implements BackstageApp { return this.icons[key]; } - getRootComponent(): ComponentType<{}> { + getRoutes(): ComponentType<{}> { const routes = new Array(); const registeredFeatureFlags = new Array(); @@ -195,26 +200,42 @@ export class PrivateAppImpl implements BackstageApp { if ('node' in loadedConfig) { return loadedConfig.node; } - const configReader = loadedConfig.api; + const configApi = loadedConfig.api; const appApis = ApiRegistry.from([ - [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], - [configApiRef, configReader], + [appThemeApiRef, appThemeApi], + [configApiRef, configApi], ]); if (!this.apis) { if ('get' in this.apisOrFactory) { this.apis = this.apisOrFactory; } else { - this.apis = this.apisOrFactory(configReader); + this.apis = this.apisOrFactory(configApi); } } const apis = new ApiAggregator(this.apis, appApis); - const { Router } = this.components; + return ( + + + {children} + + + ); + }; + return Provider; + } + + getRouter(): ComponentType<{}> { + const { Router: RouterComponent } = this.components; + + const AppRouter: FC<{}> = ({ children }) => { + const configApi = useApi(configApiRef); + let { pathname } = new URL( - configReader.getString('app.baseUrl') ?? '/', + configApi.getString('app.baseUrl') ?? '/', 'http://dummy.dev', // baseUrl can be specified as just a path ); if (pathname.endsWith('/')) { @@ -222,20 +243,14 @@ export class PrivateAppImpl implements BackstageApp { } return ( - - - - - - {children}} /> - - - - - + + + {children}} /> + + ); }; - return Provider; + return AppRouter; } verify() { diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index e30c79dd73..97809ca899 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -117,14 +117,19 @@ export type BackstageApp = { getSystemIcon(key: SystemIconKey): IconComponent; /** - * Creates a root component for this app, including the App chrome - * and routes to all plugins. - */ - getRootComponent(): ComponentType<{}>; - - /** - * Provider component that should wrap the App's RootComponent and - * any other components that need to be within the app context. + * Provider component that should wrap the Router created with getRouter() + * and any other components that need to be within the app context. */ getProvider(): ComponentType<{}>; + + /** + * Router component that should wrap the App Routes create with getRoutes() + * and any other components that should only be available while signed in. + */ + getRouter(): ComponentType<{}>; + + /** + * Routes component that contains all routes for plugin pages in the app. + */ + getRoutes(): ComponentType<{}>; }; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 8900ecd54b..0e28a5bf8b 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -82,8 +82,10 @@ class DevAppBuilder { apis: this.setupApiRegistry(this.factories), plugins: this.plugins, }); + const AppProvider = app.getProvider(); - const AppComponent = app.getRootComponent(); + const AppRouter = app.getRouter(); + const AppRoutes = app.getRoutes(); const sidebar = this.setupSidebar(this.plugins); @@ -93,10 +95,13 @@ class DevAppBuilder { {this.rootChildren} - - {sidebar} - - + + + + {sidebar} + + + ); }; diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index c474678fd1..15814d918d 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -89,12 +89,15 @@ export function wrapInTestApp( } const AppProvider = app.getProvider(); + const AppRouter = app.getRouter(); return ( - {/* The path of * here is needed to be set as a catch all, so it will render the wrapper element - * and work with nested routes if they exist too */} - } /> + + {/* The path of * here is needed to be set as a catch all, so it will render the wrapper element + * and work with nested routes if they exist too */} + } /> + ); } From 2ed4db0f00f263747db6a8c5cd7a1af390213efa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 11:21:32 +0200 Subject: [PATCH 05/60] packages/core-api: add IdentityApi to app and hook up to optional sign-in page component --- .../src/apis/definitions/IdentityApi.ts | 2 +- packages/core-api/src/app/App.tsx | 79 +++++++++++++++++-- packages/core-api/src/app/AppIdentity.ts | 61 ++++++++++++++ packages/core-api/src/app/types.ts | 34 ++++++++ 4 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 packages/core-api/src/app/AppIdentity.ts diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index bbfab0ef35..4c9ccfdbf6 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -40,7 +40,7 @@ export type IdentityApi = { // TODO: getProfile(): Promise - We want this to be async when added, but needs more work. }; -export const identifyApiRef = createApiRef({ +export const identityApiRef = createApiRef({ id: 'core.identity', description: 'Provides access to the identity of the signed in user', }); diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 8d625a3a0d..163a546943 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -13,16 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ComponentType, FC, useMemo } from 'react'; +import React, { + ComponentType, + FC, + useMemo, + useCallback, + useState, + ReactElement, +} from 'react'; import { Route, Routes, Navigate } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; -import { BackstageApp, AppComponents, AppConfigLoader, Apis } from './types'; +import { + BackstageApp, + AppComponents, + AppConfigLoader, + Apis, + SignInResult, + SignInPageProps, +} from './types'; import { BackstagePlugin } from '../plugin'; import { FeatureFlagsRegistryItem } from './FeatureFlags'; import { featureFlagsApiRef, AppThemeApi, ConfigApi, + identityApiRef, } from '../apis/definitions'; import { AppThemeProvider } from './AppThemeProvider'; @@ -40,6 +55,7 @@ import { } from '../apis'; import { ApiAggregator } from '../apis/ApiAggregator'; import { useAsync } from 'react-use'; +import { AppIdentity } from './AppIdentity'; type FullAppOptions = { apis: Apis; @@ -93,6 +109,8 @@ export class PrivateAppImpl implements BackstageApp { private readonly themes: AppTheme[]; private readonly configLoader?: AppConfigLoader; + private readonly identityApi = new AppIdentity(); + private apisOrFactory: Apis; constructor(options: FullAppOptions) { @@ -205,6 +223,7 @@ export class PrivateAppImpl implements BackstageApp { const appApis = ApiRegistry.from([ [appThemeApiRef, appThemeApi], [configApiRef, configApi], + [identityApiRef, this.identityApi], ]); if (!this.apis) { @@ -229,7 +248,35 @@ export class PrivateAppImpl implements BackstageApp { } getRouter(): ComponentType<{}> { - const { Router: RouterComponent } = this.components; + const { + Router: RouterComponent, + SignInPage: SignInPageComponent, + } = this.components; + + // This wraps the sign-in page and waits for sign-in to be completed before rendering the app + const SignInPageWrapper: FC<{ + component: ComponentType; + children: ReactElement; + }> = ({ component: Component, children }) => { + const [done, setDone] = useState(false); + + const onResult = useCallback( + (result: SignInResult) => { + if (done) { + throw new Error('Identity result callback was called twice'); + } + setDone(true); + this.identityApi.setSignInResult(result); + }, + [done], + ); + + if (done) { + return children; + } + + return ; + }; const AppRouter: FC<{}> = ({ children }) => { const configApi = useApi(configApiRef); @@ -242,14 +289,34 @@ export class PrivateAppImpl implements BackstageApp { pathname = pathname.replace(/\/$/, ''); } + // If the app hasn't configured a sign-in page, we just continue as guest. + if (!SignInPageComponent) { + this.identityApi.setSignInResult({ + userId: 'guest', + idToken: undefined, + logout: async () => {}, + }); + + return ( + + + {children}} /> + + + ); + } + return ( - - {children}} /> - + + + {children}} /> + + ); }; + return AppRouter; } diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts new file mode 100644 index 0000000000..a0f933710b --- /dev/null +++ b/packages/core-api/src/app/AppIdentity.ts @@ -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 { IdentityApi } from '../apis'; +import { SignInResult } from './types'; + +/** + * Implementation of the connection between the App-wide IdentityApi + * and sign-in page. + */ +export class AppIdentity implements IdentityApi { + private userId?: string; + private idToken?: string; + private logoutFunc?: () => Promise; + + getUserId(): string { + if (!this.userId) { + throw new Error( + 'Tried to access IdentityApi userId before app was loaded', + ); + } + return this.userId; + } + + getIdToken(): string { + if (!this.idToken) { + throw new Error( + 'Tried to access IdentityApi idToken before app was loaded', + ); + } + return this.idToken; + } + + async logout(): Promise { + if (!this.logoutFunc) { + throw new Error( + 'Tried to access IdentityApi logoutFunc before app was loaded', + ); + } + await this.logoutFunc; + } + + setSignInResult(result: SignInResult) { + this.userId = result.userId; + this.idToken = result.idToken; + this.logoutFunc = result.logout; + } +} diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 97809ca899..099438a58c 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -25,11 +25,45 @@ export type BootErrorPageProps = { step: 'load-config'; error: Error; }; + +export type SignInResult = { + /** + * User ID that will be returned by the IdentityApi + */ + userId: string; + /** + * ID token that will be returned by the IdentityApi + */ + idToken: string | undefined; + /** + * Logout handler that will be called if the user requests a logout. + */ + logout: () => Promise; +}; + +export type SignInPageProps = { + /** + * Set the sign-in result for the app. This should only be called once. + */ + onResult(result: SignInResult): void; +}; + export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; BootErrorPage: ComponentType; Progress: ComponentType<{}>; Router: ComponentType<{}>; + + /** + * An optional sign-in page that will be rendered instead of the AppRouter at startup. + * + * If a sign-in page is set, it will always be shown before the app, and it is up + * to the sign-in page to handle e.g. saving of login methods for subsequent visits. + * + * The sign-in page will be displayed until it has passed up a result to the parent, + * and which point the AppRouter and all of its children will be rendered instead. + */ + SignInPage?: ComponentType; }; /** From 09c16fb5542b86593b5ee6e8c27c911e383de881 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 14:14:05 +0200 Subject: [PATCH 06/60] packages/core-api: make idToken and logout optional in SignInResult --- packages/core-api/src/app/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 099438a58c..a152ef9dfb 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -34,11 +34,11 @@ export type SignInResult = { /** * ID token that will be returned by the IdentityApi */ - idToken: string | undefined; + idToken?: string; /** * Logout handler that will be called if the user requests a logout. */ - logout: () => Promise; + logout?: () => Promise; }; export type SignInPageProps = { From ee7f6f2b2e3afeecdf6f4a1d49ec9b99776b78aa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 15:06:26 +0200 Subject: [PATCH 07/60] packages/core-api: add logout to IdentityApi and properly handle multiple sign-in attempts --- .../src/apis/definitions/IdentityApi.ts | 5 +++++ packages/core-api/src/app/AppIdentity.ts | 21 +++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index 4c9ccfdbf6..5da9ed4814 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -38,6 +38,11 @@ export type IdentityApi = { getIdToken(): string | undefined; // TODO: getProfile(): Promise - We want this to be async when added, but needs more work. + + /** + * Log out the current user + */ + logout(): Promise; }; export const identityApiRef = createApiRef({ diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts index a0f933710b..77de445a20 100644 --- a/packages/core-api/src/app/AppIdentity.ts +++ b/packages/core-api/src/app/AppIdentity.ts @@ -22,21 +22,22 @@ import { SignInResult } from './types'; * and sign-in page. */ export class AppIdentity implements IdentityApi { + private hasIdentity = false; private userId?: string; private idToken?: string; private logoutFunc?: () => Promise; getUserId(): string { - if (!this.userId) { + if (!this.hasIdentity) { throw new Error( 'Tried to access IdentityApi userId before app was loaded', ); } - return this.userId; + return this.userId!; } - getIdToken(): string { - if (!this.idToken) { + getIdToken(): string | undefined { + if (!this.hasIdentity) { throw new Error( 'Tried to access IdentityApi idToken before app was loaded', ); @@ -45,15 +46,23 @@ export class AppIdentity implements IdentityApi { } async logout(): Promise { - if (!this.logoutFunc) { + if (!this.hasIdentity) { throw new Error( 'Tried to access IdentityApi logoutFunc before app was loaded', ); } - await this.logoutFunc; + await this.logoutFunc?.(); + location.reload(); } setSignInResult(result: SignInResult) { + if (this.hasIdentity) { + return; + } + if (!result.userId) { + throw new Error('Invalid sign-in result, userId not set'); + } + this.hasIdentity = true; this.userId = result.userId; this.idToken = result.idToken; this.logoutFunc = result.logout; From b2c54fd4baed2aa0a1f0f16ec4ca704456c88ed8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 14:14:41 +0200 Subject: [PATCH 08/60] packages/core: add initial simple SignInPage with guest provider --- .../core/src/layout/SignInPage/SignInPage.tsx | 69 +++++++++++++++++++ packages/core/src/layout/SignInPage/index.ts | 17 +++++ packages/core/src/layout/index.ts | 1 + 3 files changed, 87 insertions(+) create mode 100644 packages/core/src/layout/SignInPage/SignInPage.tsx create mode 100644 packages/core/src/layout/SignInPage/index.ts diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx new file mode 100644 index 0000000000..7e24607eee --- /dev/null +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { Page } from '../Page'; +import { Header } from '../Header'; +import { Content } from '../Content/Content'; +import { ContentHeader } from '../ContentHeader/ContentHeader'; +import { Grid, Typography, Button } from '@material-ui/core'; +import { InfoCard } from '../InfoCard/InfoCard'; +import { SignInPageProps } from '@backstage/core-api'; + +const GuestProvider: FC = ({ onResult }) => ( + + onResult({ userId: 'guest' })} + > + Enter + + } + > + + Enter as a Guest User. +
+ You will not have a verified identity, +
+ so some features might be unavailable. +
+
+
+); + +export type SignInProviders = 'guest'; + +export type Props = SignInPageProps & { + providers: SignInProviders[]; +}; + +export const SignInPage: FC = ({ onResult, providers }) => { + return ( + +
+ + + + {providers.includes('guest') && } + + + + ); +}; diff --git a/packages/core/src/layout/SignInPage/index.ts b/packages/core/src/layout/SignInPage/index.ts new file mode 100644 index 0000000000..49f55aefc5 --- /dev/null +++ b/packages/core/src/layout/SignInPage/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 { SignInPage } from './SignInPage'; diff --git a/packages/core/src/layout/index.ts b/packages/core/src/layout/index.ts index 9e1298f3d6..e2de159ec6 100644 --- a/packages/core/src/layout/index.ts +++ b/packages/core/src/layout/index.ts @@ -23,5 +23,6 @@ export * from './HomepageTimer'; export * from './InfoCard'; export * from './Page'; export * from './Sidebar'; +export * from './SignInPage'; export * from './TabbedCard'; export * from './HeaderTabs'; From e7165fd887ca014da620d622528bcdef2157e192 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 15:13:46 +0200 Subject: [PATCH 09/60] packages/core: add logout item to user settings in sidebar --- packages/core/src/layout/Sidebar/UserSettings.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 8ca2580df9..36e8e1088b 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -17,17 +17,25 @@ import React, { useContext, useEffect } from 'react'; import Collapse from '@material-ui/core/Collapse'; import Star from '@material-ui/icons/Star'; +import SignOutIcon from '@material-ui/icons/MeetingRoom'; import { SidebarContext } from './config'; -import { googleAuthApiRef, githubAuthApiRef } from '@backstage/core-api'; +import { + googleAuthApiRef, + githubAuthApiRef, + identityApiRef, + useApi, +} from '@backstage/core-api'; import { OAuthProviderSettings, OIDCProviderSettings, UserProfile as SidebarUserProfile, } from './Settings'; +import { SidebarItem } from './Items'; export function SidebarUserSettings() { const { isOpen: sidebarOpen } = useContext(SidebarContext); const [open, setOpen] = React.useState(false); + const identityApi = useApi(identityApiRef); // Close the provider list when sidebar collapse useEffect(() => { @@ -48,6 +56,11 @@ export function SidebarUserSettings() { apiRef={githubAuthApiRef} icon={Star} /> + identityApi.logout()} + /> ); From d221bc0daf53818d3938f0864c957a22eec06a2e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 15:18:46 +0200 Subject: [PATCH 10/60] packages/core: add logout and provider storage to SignInPage --- .../core/src/layout/SignInPage/SignInPage.tsx | 94 +++++++++++++------ 1 file changed, 65 insertions(+), 29 deletions(-) diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 7e24607eee..ff7bbd4ef3 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -14,54 +14,90 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, useLayoutEffect } from 'react'; import { Page } from '../Page'; import { Header } from '../Header'; import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; import { Grid, Typography, Button } from '@material-ui/core'; import { InfoCard } from '../InfoCard/InfoCard'; -import { SignInPageProps } from '@backstage/core-api'; +import { SignInPageProps, SignInResult } from '@backstage/core-api'; -const GuestProvider: FC = ({ onResult }) => ( - - onResult({ userId: 'guest' })} - > - Enter - - } - > - - Enter as a Guest User. -
- You will not have a verified identity, -
- so some features might be unavailable. -
-
-
-); +const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; -export type SignInProviders = 'guest'; +type ProviderProps = SignInPageProps & { + selected: boolean; +}; + +const GuestProvider: FC = ({ selected, onResult }) => { + useLayoutEffect(() => { + if (selected) { + onResult({ userId: 'guest' }); + } + }, [selected, onResult]); + + return ( + + onResult({ userId: 'guest' })} + > + Enter + + } + > + + Enter as a Guest User. +
+ You will not have a verified identity, +
+ so some features might be unavailable. +
+
+
+ ); +}; + +export type SignInProvider = 'guest'; export type Props = SignInPageProps & { - providers: SignInProviders[]; + providers: SignInProvider[]; }; export const SignInPage: FC = ({ onResult, providers }) => { + // We can't use storageApi here, as it might have a dependency on the IdentityApi + const selectedProvider = localStorage.getItem(PROVIDER_STORAGE_KEY); + + const makeResultHandler = (provider: SignInProvider) => ( + result: SignInResult, + ) => { + localStorage.setItem(PROVIDER_STORAGE_KEY, provider); + + onResult({ + ...result, + logout: async () => { + localStorage.removeItem(PROVIDER_STORAGE_KEY); + await result.logout?.(); + }, + }); + }; + return (
- {providers.includes('guest') && } + {providers.includes('guest') && ( + + )} From 70042fb378a964bd43b94541c860d3f20404ef59 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 15:20:06 +0200 Subject: [PATCH 11/60] packages/core: use app title as SignInPage header --- packages/core/src/layout/SignInPage/SignInPage.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index ff7bbd4ef3..f8879aa55b 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -21,7 +21,12 @@ import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; import { Grid, Typography, Button } from '@material-ui/core'; import { InfoCard } from '../InfoCard/InfoCard'; -import { SignInPageProps, SignInResult } from '@backstage/core-api'; +import { + SignInPageProps, + SignInResult, + useApi, + configApiRef, +} from '@backstage/core-api'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; @@ -69,6 +74,8 @@ export type Props = SignInPageProps & { }; export const SignInPage: FC = ({ onResult, providers }) => { + const configApi = useApi(configApiRef); + // We can't use storageApi here, as it might have a dependency on the IdentityApi const selectedProvider = localStorage.getItem(PROVIDER_STORAGE_KEY); @@ -88,7 +95,7 @@ export const SignInPage: FC = ({ onResult, providers }) => { return ( -
+
From b365d0ee6b6ed00d1f6dc58ea1a118320d1219f1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 16:45:05 +0200 Subject: [PATCH 12/60] packages/core-api: add useApiHolder hook for when you want defer api access outside of react tree --- packages/core-api/src/apis/ApiProvider.tsx | 8 +++++++- packages/core-api/src/apis/index.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core-api/src/apis/ApiProvider.tsx b/packages/core-api/src/apis/ApiProvider.tsx index e157782024..24610a1372 100644 --- a/packages/core-api/src/apis/ApiProvider.tsx +++ b/packages/core-api/src/apis/ApiProvider.tsx @@ -39,13 +39,19 @@ ApiProvider.propTypes = { children: PropTypes.node, }; -export function useApi(apiRef: ApiRef): T { +export function useApiHolder(): ApiHolder { const apiHolder = useContext(Context); if (!apiHolder) { throw new Error('No ApiProvider available in react context'); } + return apiHolder; +} + +export function useApi(apiRef: ApiRef): T { + const apiHolder = useApiHolder(); + const api = apiHolder.get(apiRef); if (!api) { throw new Error(`No implementation available for ${apiRef}`); diff --git a/packages/core-api/src/apis/index.ts b/packages/core-api/src/apis/index.ts index 332636580c..c3689b6703 100644 --- a/packages/core-api/src/apis/index.ts +++ b/packages/core-api/src/apis/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { ApiProvider, useApi } from './ApiProvider'; +export { ApiProvider, useApi, useApiHolder } from './ApiProvider'; export { ApiRegistry } from './ApiRegistry'; export { ApiTestRegistry } from './ApiTestRegistry'; export * from './ApiRef'; From 9b5fa0da86faa63672403f824485cc378ed9e2c0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 16:52:05 +0200 Subject: [PATCH 13/60] packages/core: pluggable sign-in providers and re-loading of session --- .../core/src/layout/SignInPage/SignInPage.tsx | 182 ++++++++++++------ 1 file changed, 126 insertions(+), 56 deletions(-) diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index f8879aa55b..2feb7be3bc 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -14,7 +14,13 @@ * limitations under the License. */ -import React, { FC, useLayoutEffect } from 'react'; +import React, { + FC, + useLayoutEffect, + useState, + ComponentType, + useMemo, +} from 'react'; import { Page } from '../Page'; import { Header } from '../Header'; import { Content } from '../Content/Content'; @@ -26,86 +32,150 @@ import { SignInResult, useApi, configApiRef, + useApiHolder, + ApiHolder, + errorApiRef, } from '@backstage/core-api'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; -type ProviderProps = SignInPageProps & { - selected: boolean; +type ProviderComponent = ComponentType; + +type ProviderLoader = (apis: ApiHolder) => Promise; + +type SignInProvider = { + component: ProviderComponent; + loader: ProviderLoader; }; -const GuestProvider: FC = ({ selected, onResult }) => { - useLayoutEffect(() => { - if (selected) { - onResult({ userId: 'guest' }); - } - }, [selected, onResult]); +const GuestProvider: ProviderComponent = ({ onResult }) => ( + + onResult({ userId: 'guest' })} + > + Enter + + } + > + + Enter as a Guest User. +
+ You will not have a verified identity, +
+ meaning some features might be unavailable. +
+
+
+); - return ( - - onResult({ userId: 'guest' })} - > - Enter - - } - > - - Enter as a Guest User. -
- You will not have a verified identity, -
- so some features might be unavailable. -
-
-
- ); +const guestLoader: ProviderLoader = async () => { + return { userId: 'guest' }; }; -export type SignInProvider = 'guest'; +const guestProvider: SignInProvider = { + component: GuestProvider, + loader: guestLoader, +}; + +const signInProviders = { + guest: guestProvider, +}; + +export type SignInProviderId = keyof typeof signInProviders; export type Props = SignInPageProps & { - providers: SignInProvider[]; + providers: SignInProviderId[]; }; export const SignInPage: FC = ({ onResult, providers }) => { const configApi = useApi(configApiRef); + const errorApi = useApi(errorApiRef); + const apiHolder = useApiHolder(); // We can't use storageApi here, as it might have a dependency on the IdentityApi - const selectedProvider = localStorage.getItem(PROVIDER_STORAGE_KEY); + const selectedProvider = localStorage.getItem( + PROVIDER_STORAGE_KEY, + ) as SignInProviderId; - const makeResultHandler = (provider: SignInProvider) => ( - result: SignInResult, - ) => { - localStorage.setItem(PROVIDER_STORAGE_KEY, provider); + const [attempting, setAttempting] = useState(Boolean(selectedProvider)); - onResult({ - ...result, - logout: async () => { - localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.logout?.(); - }, - }); - }; + useLayoutEffect(() => { + if (!attempting || selectedProvider === null) { + return undefined; + } + + const provider = signInProviders[selectedProvider]; + if (!provider) { + setAttempting(false); + return undefined; + } + + let didCancel = false; + provider + .loader(apiHolder) + .then(result => { + if (didCancel) { + return; + } + setAttempting(false); + if (result) { + onResult({ + ...result, + logout: async () => { + localStorage.removeItem(PROVIDER_STORAGE_KEY); + await result.logout?.(); + }, + }); + } + }) + .catch(error => { + if (!didCancel) { + errorApi.post(error); + } + }); + + return () => { + didCancel = true; + }; + }, [attempting, errorApi, onResult, apiHolder, providers, selectedProvider]); + + const providerElements = useMemo( + () => + providers.map(providerId => { + const provider = signInProviders[providerId]; + if (!provider) { + throw new Error(`Unknown sign-in provider: ${providerId}`); + } + const { component: Component } = provider; + + const handleResult = (result: SignInResult) => { + localStorage.setItem(PROVIDER_STORAGE_KEY, providerId); + + onResult({ + ...result, + logout: async () => { + localStorage.removeItem(PROVIDER_STORAGE_KEY); + await result.logout?.(); + }, + }); + }; + + return ; + }), + [providers, onResult], + ); return (
- - {providers.includes('guest') && ( - - )} - + {providerElements} ); From ecbfa5f093d8e1b314006fcc2bf6156f48ee334f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 23:56:44 +0200 Subject: [PATCH 14/60] packages/auth-backend: fix for errors not being forwarded properly in EnvironmentHandler --- plugins/auth-backend/src/lib/EnvironmentHandler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/EnvironmentHandler.ts index 4fb80ca3e8..c4d8d2b6e7 100644 --- a/plugins/auth-backend/src/lib/EnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/EnvironmentHandler.ts @@ -37,7 +37,7 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers { async start(req: express.Request, res: express.Response): Promise { const provider = this.getProviderForEnv(req); - provider.start(req, res); + await provider.start(req, res); } async frameHandler( @@ -45,18 +45,18 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers { res: express.Response, ): Promise { const provider = this.getProviderForEnv(req); - provider.frameHandler(req, res); + await provider.frameHandler(req, res); } async refresh(req: express.Request, res: express.Response): Promise { const provider = this.getProviderForEnv(req); if (provider.refresh) { - provider.refresh(req, res); + await provider.refresh(req, res); } } async logout(req: express.Request, res: express.Response): Promise { const provider = this.getProviderForEnv(req); - provider.logout(req, res); + await provider.logout(req, res); } } From 506f20974236ac15294f836c90a0332c245e8b54 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 17:15:31 +0200 Subject: [PATCH 15/60] packages/core: split up SignInPage and make it display progress --- .../core/src/layout/SignInPage/SignInPage.tsx | 151 ++---------------- .../src/layout/SignInPage/guestProvider.tsx | 51 ++++++ .../core/src/layout/SignInPage/providers.tsx | 128 +++++++++++++++ packages/core/src/layout/SignInPage/types.ts | 29 ++++ 4 files changed, 217 insertions(+), 142 deletions(-) create mode 100644 packages/core/src/layout/SignInPage/guestProvider.tsx create mode 100644 packages/core/src/layout/SignInPage/providers.tsx create mode 100644 packages/core/src/layout/SignInPage/types.ts diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 2feb7be3bc..91bc251f90 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -14,79 +14,15 @@ * limitations under the License. */ -import React, { - FC, - useLayoutEffect, - useState, - ComponentType, - useMemo, -} from 'react'; +import React, { FC } from 'react'; import { Page } from '../Page'; import { Header } from '../Header'; import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; -import { Grid, Typography, Button } from '@material-ui/core'; -import { InfoCard } from '../InfoCard/InfoCard'; -import { - SignInPageProps, - SignInResult, - useApi, - configApiRef, - useApiHolder, - ApiHolder, - errorApiRef, -} from '@backstage/core-api'; - -const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; - -type ProviderComponent = ComponentType; - -type ProviderLoader = (apis: ApiHolder) => Promise; - -type SignInProvider = { - component: ProviderComponent; - loader: ProviderLoader; -}; - -const GuestProvider: ProviderComponent = ({ onResult }) => ( - - onResult({ userId: 'guest' })} - > - Enter - - } - > - - Enter as a Guest User. -
- You will not have a verified identity, -
- meaning some features might be unavailable. -
-
-
-); - -const guestLoader: ProviderLoader = async () => { - return { userId: 'guest' }; -}; - -const guestProvider: SignInProvider = { - component: GuestProvider, - loader: guestLoader, -}; - -const signInProviders = { - guest: guestProvider, -}; - -export type SignInProviderId = keyof typeof signInProviders; +import { Grid } from '@material-ui/core'; +import { SignInPageProps, useApi, configApiRef } from '@backstage/core-api'; +import { useSignInProviders, SignInProviderId } from './providers'; +import Progress from '../../components/Progress'; export type Props = SignInPageProps & { providers: SignInProviderId[]; @@ -94,81 +30,12 @@ export type Props = SignInPageProps & { export const SignInPage: FC = ({ onResult, providers }) => { const configApi = useApi(configApiRef); - const errorApi = useApi(errorApiRef); - const apiHolder = useApiHolder(); - // We can't use storageApi here, as it might have a dependency on the IdentityApi - const selectedProvider = localStorage.getItem( - PROVIDER_STORAGE_KEY, - ) as SignInProviderId; + const [loading, providerElements] = useSignInProviders(providers, onResult); - const [attempting, setAttempting] = useState(Boolean(selectedProvider)); - - useLayoutEffect(() => { - if (!attempting || selectedProvider === null) { - return undefined; - } - - const provider = signInProviders[selectedProvider]; - if (!provider) { - setAttempting(false); - return undefined; - } - - let didCancel = false; - provider - .loader(apiHolder) - .then(result => { - if (didCancel) { - return; - } - setAttempting(false); - if (result) { - onResult({ - ...result, - logout: async () => { - localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.logout?.(); - }, - }); - } - }) - .catch(error => { - if (!didCancel) { - errorApi.post(error); - } - }); - - return () => { - didCancel = true; - }; - }, [attempting, errorApi, onResult, apiHolder, providers, selectedProvider]); - - const providerElements = useMemo( - () => - providers.map(providerId => { - const provider = signInProviders[providerId]; - if (!provider) { - throw new Error(`Unknown sign-in provider: ${providerId}`); - } - const { component: Component } = provider; - - const handleResult = (result: SignInResult) => { - localStorage.setItem(PROVIDER_STORAGE_KEY, providerId); - - onResult({ - ...result, - logout: async () => { - localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.logout?.(); - }, - }); - }; - - return ; - }), - [providers, onResult], - ); + if (loading) { + return ; + } return ( diff --git a/packages/core/src/layout/SignInPage/guestProvider.tsx b/packages/core/src/layout/SignInPage/guestProvider.tsx new file mode 100644 index 0000000000..78d0191ba9 --- /dev/null +++ b/packages/core/src/layout/SignInPage/guestProvider.tsx @@ -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 React from 'react'; +import { Grid, Typography, Button } from '@material-ui/core'; +import { InfoCard } from '../InfoCard/InfoCard'; +import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; + +const Component: ProviderComponent = ({ onResult }) => ( + + onResult({ userId: 'guest' })} + > + Enter + + } + > + + Enter as a Guest User. +
+ You will not have a verified identity, +
+ meaning some features might be unavailable. +
+
+
+); + +const loader: ProviderLoader = async () => { + return { userId: 'guest' }; +}; + +export const guestProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx new file mode 100644 index 0000000000..ae9c3cc010 --- /dev/null +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -0,0 +1,128 @@ +/* + * 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, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; +import { guestProvider } from './guestProvider'; +import { + SignInPageProps, + SignInResult, + useApi, + useApiHolder, + errorApiRef, +} from '@backstage/core-api'; + +const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; + +const signInProviders = { + guest: guestProvider, +}; + +export type SignInProviderId = keyof typeof signInProviders; + +export const useSignInProviders = ( + providers: SignInProviderId[], + onResult: SignInPageProps['onResult'], +) => { + const errorApi = useApi(errorApiRef); + const apiHolder = useApiHolder(); + const [loading, setLoading] = useState(true); + + // This decorates the result with logout logic from this hook + const handleWrappedResult = useCallback( + (result: SignInResult) => { + onResult({ + ...result, + logout: async () => { + localStorage.removeItem(PROVIDER_STORAGE_KEY); + await result.logout?.(); + }, + }); + }, + [onResult], + ); + + // In this effect we check if the user has already selected an existing login + // provider, and in that case try to load an existing session for the provider. + useLayoutEffect(() => { + if (!loading) { + return undefined; + } + + // We can't use storageApi here, as it might have a dependency on the IdentityApi + const selectedProvider = localStorage.getItem( + PROVIDER_STORAGE_KEY, + ) as SignInProviderId; + + // No provider selected, let the user pick one + if (selectedProvider === null) { + setLoading(false); + return undefined; + } + + const provider = signInProviders[selectedProvider]; + if (!provider) { + setLoading(false); + return undefined; + } + + let didCancel = false; + provider + .loader(apiHolder) + .then(result => { + if (didCancel) { + return; + } + if (result) { + handleWrappedResult(result); + } + setLoading(false); + }) + .catch(error => { + if (didCancel) { + return; + } + errorApi.post(error); + setLoading(false); + }); + + return () => { + didCancel = true; + }; + }, [loading, errorApi, onResult, apiHolder, providers, handleWrappedResult]); + + // This renders all available sign-in providers + const elements = useMemo( + () => + providers.map(providerId => { + const provider = signInProviders[providerId]; + if (!provider) { + throw new Error(`Unknown sign-in provider: ${providerId}`); + } + const { Component } = provider; + + const handleResult = (result: SignInResult) => { + localStorage.setItem(PROVIDER_STORAGE_KEY, providerId); + + handleWrappedResult(result); + }; + + return ; + }), + [providers, handleWrappedResult], + ); + + return [loading, elements]; +}; diff --git a/packages/core/src/layout/SignInPage/types.ts b/packages/core/src/layout/SignInPage/types.ts new file mode 100644 index 0000000000..e13cda5ddd --- /dev/null +++ b/packages/core/src/layout/SignInPage/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentType } from 'react'; +import { SignInPageProps, SignInResult, ApiHolder } from '@backstage/core-api'; + +export type ProviderComponent = ComponentType; + +export type ProviderLoader = ( + apis: ApiHolder, +) => Promise; + +export type SignInProvider = { + Component: ProviderComponent; + loader: ProviderLoader; +}; From 730b73aff129ff8059917893deb1bf7737c242f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 17:18:40 +0200 Subject: [PATCH 16/60] packages/core: separate list of sign-in provider IDs to avoid exporting internal types --- packages/core/src/layout/SignInPage/providers.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index ae9c3cc010..10fb09d082 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -23,15 +23,17 @@ import { useApiHolder, errorApiRef, } from '@backstage/core-api'; +import { SignInProvider } from './types'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; -const signInProviders = { +// Separate list here to avoid exporting internal types +export type SignInProviderId = 'guest'; + +const signInProviders: { [id in SignInProviderId]: SignInProvider } = { guest: guestProvider, }; -export type SignInProviderId = keyof typeof signInProviders; - export const useSignInProviders = ( providers: SignInProviderId[], onResult: SignInPageProps['onResult'], From f65f982b1ce760a9a180c4ab07055c45cb37b5c2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 17:26:49 +0200 Subject: [PATCH 17/60] packages/core: added google sign-in provider --- .../src/layout/SignInPage/googleProvider.tsx | 86 +++++++++++++++++++ .../core/src/layout/SignInPage/providers.tsx | 4 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/layout/SignInPage/googleProvider.tsx diff --git a/packages/core/src/layout/SignInPage/googleProvider.tsx b/packages/core/src/layout/SignInPage/googleProvider.tsx new file mode 100644 index 0000000000..2db7e2fd53 --- /dev/null +++ b/packages/core/src/layout/SignInPage/googleProvider.tsx @@ -0,0 +1,86 @@ +/* + * 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 { Grid, Typography, Button } from '@material-ui/core'; +import { InfoCard } from '../InfoCard/InfoCard'; +import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { + useApi, + googleAuthApiRef, + errorApiRef, + ProfileInfo, +} from '@backstage/core-api'; + +function parseUserId(profile: ProfileInfo) { + return profile!.email.replace(/@.*/, ''); +} + +const Component: ProviderComponent = ({ onResult }) => { + const googleAuthApi = useApi(googleAuthApiRef); + const errorApi = useApi(errorApiRef); + + const handleLogin = async () => { + try { + const idToken = await googleAuthApi.getIdToken({ instantPopup: true }); + const profile = await googleAuthApi.getProfile(); + + onResult({ + userId: parseUserId(profile!), + idToken, + logout: async () => { + await googleAuthApi.logout(); + }, + }); + } catch (error) { + errorApi.post(error); + } + }; + + return ( + + + Sign In + + } + > + Sign In using Google + + + ); +}; + +const loader: ProviderLoader = async apis => { + const googleAuthApi = apis.get(googleAuthApiRef)!; + + const [idToken, profile] = await Promise.all([ + googleAuthApi.getIdToken({ optional: true }), + googleAuthApi.getProfile({ optional: true }), + ]); + + return { + userId: parseUserId(profile!), + idToken, + logout: async () => { + await googleAuthApi.logout(); + }, + }; +}; + +export const googleProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index 10fb09d082..8e5cc7fc54 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -16,6 +16,7 @@ import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; import { guestProvider } from './guestProvider'; +import { googleProvider } from './googleProvider'; import { SignInPageProps, SignInResult, @@ -28,10 +29,11 @@ import { SignInProvider } from './types'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; // Separate list here to avoid exporting internal types -export type SignInProviderId = 'guest'; +export type SignInProviderId = 'guest' | 'google'; const signInProviders: { [id in SignInProviderId]: SignInProvider } = { guest: guestProvider, + google: googleProvider, }; export const useSignInProviders = ( From afc4ba85c8285f21b097ca6ee9488c69e2373f2a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 17:42:09 +0200 Subject: [PATCH 18/60] packages/core: added custom sign-in provider --- packages/core/package.json | 1 + .../src/layout/SignInPage/customProvider.tsx | 111 ++++++++++++++++++ .../core/src/layout/SignInPage/providers.tsx | 4 +- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/layout/SignInPage/customProvider.tsx diff --git a/packages/core/package.json b/packages/core/package.json index a412af3fa1..ccf2bce628 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,6 +47,7 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-helmet": "6.0.0", + "react-hook-form": "^5.7.2", "react-router": "6.0.0-alpha.5", "react-router-dom": "6.0.0-alpha.5", "react-sparklines": "^1.7.0", diff --git a/packages/core/src/layout/SignInPage/customProvider.tsx b/packages/core/src/layout/SignInPage/customProvider.tsx new file mode 100644 index 0000000000..8141175d8f --- /dev/null +++ b/packages/core/src/layout/SignInPage/customProvider.tsx @@ -0,0 +1,111 @@ +/* + * 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 { useForm } from 'react-hook-form'; +import { + Grid, + Typography, + Button, + FormControl, + TextField, + FormHelperText, + makeStyles, +} from '@material-ui/core'; +import isEmpty from 'lodash/isEmpty'; +import { InfoCard } from '../InfoCard/InfoCard'; +import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { SignInResult } from '@backstage/core-api'; + +const ID_TOKEN_REGEX = /^[a-z0-9+/]+\.[a-z0-9+/]+\.[a-z0-9+/]+$/i; + +const useFormStyles = makeStyles(theme => ({ + form: { + display: 'flex', + flexFlow: 'column nowrap', + }, + button: { + alignSelf: 'center', + marginTop: theme.spacing(2), + }, +})); + +const Component: ProviderComponent = ({ onResult }) => { + const classes = useFormStyles(); + const { register, handleSubmit, errors, formState } = useForm({ + mode: 'onChange', + }); + + return ( + + + + Enter your own User ID and credentials. +
+ This selection will not be stored. +
+ +
+ + + {errors.userId && ( + {errors.userId.message} + )} + + + + !token || + ID_TOKEN_REGEX.test(token) || + 'Token is not a valid OpenID Connect JWT Token', + })} + /> + {errors.idToken && ( + {errors.idToken.message} + )} + + +
+
+
+ ); +}; + +// Custom provider doesn't store credentials +const loader: ProviderLoader = async () => undefined; + +export const customProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index 8e5cc7fc54..d60c8ff281 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -17,6 +17,7 @@ import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; import { guestProvider } from './guestProvider'; import { googleProvider } from './googleProvider'; +import { customProvider } from './customProvider'; import { SignInPageProps, SignInResult, @@ -29,11 +30,12 @@ import { SignInProvider } from './types'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; // Separate list here to avoid exporting internal types -export type SignInProviderId = 'guest' | 'google'; +export type SignInProviderId = 'guest' | 'google' | 'custom'; const signInProviders: { [id in SignInProviderId]: SignInProvider } = { guest: guestProvider, google: googleProvider, + custom: customProvider, }; export const useSignInProviders = ( From b4284e7b3e4fc1c7c19218cb60a61416ccd54614 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 17:42:51 +0200 Subject: [PATCH 19/60] packages/app: add sign-in page --- packages/app/src/App.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ba7fb4a8a5..f4515cbf01 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import { createApp, AlertDisplay, OAuthRequestDialog } from '@backstage/core'; +import { + createApp, + AlertDisplay, + OAuthRequestDialog, + SignInPage, +} from '@backstage/core'; import React, { FC } from 'react'; import Root from './components/Root'; import * as plugins from './plugins'; @@ -24,6 +29,11 @@ import { hot } from 'react-hot-loader/root'; const app = createApp({ apis, plugins: Object.values(plugins), + components: { + SignInPage: props => ( + + ), + }, }); const AppProvider = app.getProvider(); From af25113b584e99e31762af3f7481650dd5f4f9ae Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 16 Jun 2020 18:05:49 +0200 Subject: [PATCH 20/60] packages/storybook: added mock identity api --- packages/storybook/.storybook/apis.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index d3150400cf..0450f3969d 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -2,6 +2,7 @@ import { ApiRegistry, alertApiRef, errorApiRef, + identityApiRef, oauthRequestApiRef, OAuthRequestManager, googleAuthApiRef, @@ -19,6 +20,12 @@ const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); +builder.add(identityApiRef, { + getUserId: () => 'guest', + getIdToken: () => undefined, + logout: async () => {}, +}); + const oauthRequestApi = builder.add( oauthRequestApiRef, new OAuthRequestManager(), From f47782834d13d7e022f2b9f77b16d7bb1236ef55 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Wed, 17 Jun 2020 09:54:28 +0200 Subject: [PATCH 21/60] fix(backend): server standalone backend plugins --- packages/cli/src/lib/bundler/config.ts | 6 +++++- packages/cli/src/lib/bundler/paths.ts | 5 +++++ plugins/auth-backend/package.json | 2 +- plugins/catalog-backend/package.json | 2 +- plugins/identity-backend/package.json | 2 +- plugins/scaffolder-backend/package.json | 1 + plugins/sentry-backend/package.json | 2 +- 7 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index e6d47573ef..50d174368a 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -182,7 +182,11 @@ export function createBackendConfig( }, devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map', context: paths.targetPath, - entry: ['webpack/hot/poll?100', paths.targetEntry], + entry: [ + 'webpack/hot/poll?100', + paths.targetEntry, + ...(paths.targetRunFile ? [paths.targetRunFile] : []), + ], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], mainFields: ['main:src', 'browser', 'module', 'main'], diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/lib/bundler/paths.ts index 1e1cd1298a..70e82d8d48 100644 --- a/packages/cli/src/lib/bundler/paths.ts +++ b/packages/cli/src/lib/bundler/paths.ts @@ -48,10 +48,15 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) { } } + // Backend plugin dev run file + const targetRunFile = paths.resolveTarget('src/run.ts'); + const runFileExists = fs.pathExistsSync(targetRunFile); + return { targetHtml, targetPublic, targetPath: paths.resolveTarget('.'), + targetRunFile: runFileExists ? targetRunFile : undefined, targetDist: paths.resolveTarget('dist'), targetAssets: paths.resolveTarget('assets'), targetSrc: paths.resolveTarget('src'), diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 5eff22126d..82a0a525ba 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"", + "start": "backstage-cli backend:dev", "build": "tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index e8b43de86a..ed0ce4c3a4 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"cross-env NODE_ENV=development nodemon -r esm dist/run.js\\\"", + "start": "backstage-cli backend:dev", "build": "tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index ae7526daa2..74881b1cd1 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"", + "start": "backstage-cli backend:dev", "build": "tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 2408471229..f6c6509916 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -7,6 +7,7 @@ "private": true, "scripts": { "build": "tsc", + "start": "backstage-cli backend:dev", "lint": "backstage-cli lint", "test": "backstage-cli test", "prepack": "backstage-cli prepack", diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index 03846ea5a9..696df5aaa9 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "private": true, "scripts": { - "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"", + "start": "backstage-cli backend:dev", "build": "tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", From b073b610f4df17d6a9d9d3e5cd2c241858703777 Mon Sep 17 00:00:00 2001 From: Nikki Beesetti <12538017+nikkibeesetti@users.noreply.github.com> Date: Wed, 17 Jun 2020 03:30:39 -0500 Subject: [PATCH 22/60] Updated FAQ.md with the correct links (#1308) * Updated FAQ.md with the correct links Two links regarding plugins were broken and now they have been fixed. * Updated FAQ.md with correct links --- docs/FAQ.md | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 5fb131528a..3bd526f94b 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -11,15 +11,11 @@ brand. ### Is Backstage a monitoring platform? -No, but it can be! Backstage is designed to be a developer portal for all your -infrastructure tooling, services, and documentation. So, it's not a monitoring -platform — but that doesn't mean you can't integrate a monitoring tool into -Backstage by writing -[a plugin](https://github.com/spotify/faq#what-is-a-plugin-in-backstage). +No, but it can be! Backstage is designed to be a developer portal for all your infrastructure tooling, services, and documentation. So, it's not a monitoring platform — but that doesn't mean you can't integrate a monitoring tool into Backstage by writing [a plugin](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage). ### How is Backstage licensed? -Backstage was released as free and open software by Spotify and is licensed +Backstage was released as open sourced software by Spotify and is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). ### Why did we open source Backstage? @@ -99,15 +95,7 @@ plugins. ​ ### What is a "plugin" in Backstage? -​ Plugins are what provide the feature functionality in Backstage. They are used -to integrate different systems into Backstage's frontend, so that the developer -gets a consistent UX, no matter what tool or service is being accessed on the -other side. ​ Each plugin is treated as a self-contained web app and can include -almost any type of content. Plugins all use a common set of platform APIs and -reusable UI components. Plugins can fetch data either from the backend or an API -exposed through the proxy. ​ Learn more about -[the different components](https://github.com/spotify/backstage#overview) that -make up Backstage. ​ +By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage)" above) ### Do I have to write plugins in TypeScript? From 83f331caf51e658e37bb4c33a867886f4376bf95 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 17 Jun 2020 10:48:12 +0200 Subject: [PATCH 23/60] Add static json adapter to serve user groups --- .../src/adapters/StaticJsonAdapter.ts | 87 +++++++++++++++++++ .../src/adapters/data/userGroups.json | 20 +++++ .../identity-backend/src/adapters/index.ts | 17 ++++ .../identity-backend/src/adapters/types.ts | 43 +++++++++ .../identity-backend/src/service/router.ts | 18 ++-- 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 plugins/identity-backend/src/adapters/StaticJsonAdapter.ts create mode 100644 plugins/identity-backend/src/adapters/data/userGroups.json create mode 100644 plugins/identity-backend/src/adapters/index.ts create mode 100644 plugins/identity-backend/src/adapters/types.ts diff --git a/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts new file mode 100644 index 0000000000..a88e561ee6 --- /dev/null +++ b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts @@ -0,0 +1,87 @@ +/* + * 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 { Group, GroupsJson, GroupsResponse, IdentityApiAdapter } from './types'; +import fs from 'fs-extra'; +import express from 'express'; +import path from 'path'; + +const GROUPS_JSON_FILE = path.join(__dirname, 'data', 'userGroups.json'); + +export class StaticJsonAdapter implements IdentityApiAdapter { + private readonly groups: Group[]; + + constructor() { + const groupsJson: GroupsJson = fs.readJsonSync(GROUPS_JSON_FILE, { + encoding: 'utf8', + }); + this.groups = groupsJson.groups; + } + + getUserGroups( + req: express.Request, + res: express.Response, + ): express.Response { + const user = req.params.user; + const type = req.query.type?.toString() ?? ''; + + const userGroups = this._getUserGroups(this.groups, user); + const groups = this.filterGroupsByType(userGroups, type); + return res.json({ groups }); + } + + _getUserGroups(groups: Group[], user: string) { + const userGroups: Set = new Set(); + groups.forEach(group => { + if (this.isUserInGroup(group, user)) { + userGroups.add(group); + } + + if (group.children) { + const userSubGroups = this._getUserGroups(group.children, user) ?? []; + const isUserInSubGroup = Boolean(userSubGroups.length); + if (isUserInSubGroup) { + userGroups.add(group); + } + userSubGroups.forEach(subGroup => userGroups.add(subGroup)); + } + }); + return Array.from(userGroups); + } + + private filterGroupsByType = (userGroups: Group[], type: string) => { + const groups = type + ? userGroups + .filter((group: Group) => group.type === type) + .map(group => ({ name: group.name, type: group.type })) + : userGroups.map(group => ({ + name: group.name, + type: group.type, + })); + return groups; + }; + + private isUserInGroup = (group: Group, user: string): boolean => { + if (group.members) { + const groupMembers = group.members; + const groupsWithUser = groupMembers.filter( + member => member.name === user, + ); + return Boolean(groupsWithUser.length); + } + return false; + }; +} diff --git a/plugins/identity-backend/src/adapters/data/userGroups.json b/plugins/identity-backend/src/adapters/data/userGroups.json new file mode 100644 index 0000000000..9ab83480df --- /dev/null +++ b/plugins/identity-backend/src/adapters/data/userGroups.json @@ -0,0 +1,20 @@ +{ + "groups": [ + { + "name": "engineering", + "type": "org", + "children": [ + { + "name": "authentication", + "type": "team", + "members": [{ "name": "kent" }, { "name": "dobbs" }] + }, + { + "name": "checkout", + "type": "team", + "members": [{ "name": "don" }, { "name": "abramev" }] + } + ] + } + ] +} diff --git a/plugins/identity-backend/src/adapters/index.ts b/plugins/identity-backend/src/adapters/index.ts new file mode 100644 index 0000000000..357391a25b --- /dev/null +++ b/plugins/identity-backend/src/adapters/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 * from './StaticJsonAdapter'; diff --git a/plugins/identity-backend/src/adapters/types.ts b/plugins/identity-backend/src/adapters/types.ts new file mode 100644 index 0000000000..85719205f8 --- /dev/null +++ b/plugins/identity-backend/src/adapters/types.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 express from 'express'; + +export type User = { + name: string; +}; + +export type Group = { + name: string; + type: string; + members?: User[]; + children?: Group[]; +}; + +export type GroupsJson = { + groups: Group[]; +}; + +export type GroupsResponse = { + groups: Group[]; +}; + +export interface IdentityApiAdapter { + getUserGroups( + req: express.Request, + res: express.Response, + ): express.Response; +} diff --git a/plugins/identity-backend/src/service/router.ts b/plugins/identity-backend/src/service/router.ts index 52e73db0fb..ac6267ba75 100644 --- a/plugins/identity-backend/src/service/router.ts +++ b/plugins/identity-backend/src/service/router.ts @@ -17,21 +17,25 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import { StaticJsonAdapter } from '../adapters'; +import { IdentityApiAdapter } from '../adapters/types'; export interface RouterOptions { logger: Logger; } +const makeRouter = (adapter: IdentityApiAdapter): express.Router => { + const router = Router(); + router.get('/users/:user/groups', adapter.getUserGroups.bind(adapter)); + return router; +}; + export async function createRouter( options: RouterOptions, ): Promise { - const router = Router(); const logger = options.logger; - router.use('/ping', (_, res) => { - logger.info('heartbeat for identity service'); - res.send('pong'); - }); - - return router; + logger.info('Initializing identity provider'); + const adapter = new StaticJsonAdapter(); + return makeRouter(adapter); } From 8658ea3f27fd90663933176f2a252c11620bcbbe Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Wed, 17 Jun 2020 12:03:23 +0200 Subject: [PATCH 24/60] Working catalog type filter (#1267) * WIP Working type filter * Fixed type error * Some interactivity between type filters and subfilters * Revert "Some interactivity between type filters and subfilters" This reverts commit 91a99f6c5759c33f1d35100f365af397d4ee9810. * Rename services to entities in catalog filter menu --- packages/core/src/layout/HeaderTabs/index.tsx | 16 +++- .../components/CatalogPage/CatalogPage.tsx | 83 +++++++++++-------- .../utils/locales/goodEvening.locales.json | 2 +- plugins/catalog/src/data/filters.ts | 9 +- 4 files changed, 68 insertions(+), 42 deletions(-) diff --git a/packages/core/src/layout/HeaderTabs/index.tsx b/packages/core/src/layout/HeaderTabs/index.tsx index 340b39f862..2617a27aa9 100644 --- a/packages/core/src/layout/HeaderTabs/index.tsx +++ b/packages/core/src/layout/HeaderTabs/index.tsx @@ -17,7 +17,7 @@ // TODO(blam): Remove this implementation when the Tabs are ready // This is just a temporary solution to implementing tabs for now -import React from 'react'; +import React, { useState } from 'react'; import { makeStyles, Tabs, Tab } from '@material-ui/core'; const useStyles = makeStyles(theme => ({ @@ -42,9 +42,18 @@ export type Tab = { id: string; label: string; }; -export const HeaderTabs: React.FC<{ tabs: Tab[] }> = ({ tabs }) => { +export const HeaderTabs: React.FC<{ + tabs: Tab[]; + onChange?: (index: Number) => void; +}> = ({ tabs, onChange }) => { + const [selectedTab, setSelectedTab] = useState(0); const styles = useStyles(); + const handleChange = (_: React.ChangeEvent<{}>, index: Number) => { + setSelectedTab(index); + if (onChange) onChange(index); + }; + return (
= ({ tabs }) => { variant="scrollable" scrollButtons="auto" aria-label="scrollable auto tabs example" - value={0} + onChange={handleChange} + value={selectedTab} > {tabs.map((tab, index) => ( ({ contentWrapper: { display: 'grid', @@ -58,8 +87,8 @@ const useStyles = makeStyles(theme => ({ export const CatalogPage: FC<{}> = () => { const catalogApi = useApi(catalogApiRef); + const [selectedTab, setSelectedTab] = useState(tabs[0].id); const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); - const [selectedFilter, setSelectedFilter] = useState( defaultFilter, ); @@ -69,16 +98,19 @@ export const CatalogPage: FC<{}> = () => { async () => catalogApi.getEntities(), ); - const data = - entities?.filter(e => - entityFilters[selectedFilter.id](e, { isStarred: isStarredEntity(e) }), - ) ?? []; - const onFilterSelected = useCallback( selected => setSelectedFilter(selected), [], ); + const filteredEntities = useMemo(() => { + const typeFilter = entityFilters[EntityFilterType.TYPE]; + const leftMenuFilter = entityFilters[selectedFilter.id]; + return entities + ?.filter(e => leftMenuFilter(e, { isStarred: isStarredEntity(e) })) + .filter(e => typeFilter(e, { type: selectedTab })); + }, [selectedFilter.id, selectedTab, isStarredEntity, entities?.filter]); + const styles = useStyles(); const actions = [ @@ -127,33 +159,14 @@ export const CatalogPage: FC<{}> = () => { }, ]; - // TODO: replace me with the proper tabs implemntation - const tabs = [ - { - id: 'services', - label: 'Services', - }, - { - id: 'websites', - label: 'Websites', - }, - { - id: 'libs', - label: 'Libraries', - }, - { - id: 'documentation', - label: 'Documentation', - }, - { - id: 'other', - label: 'Other', - }, - ]; - return ( - + { + setSelectedTab(tabs[index as number].id); + }} + /> = () => {
diff --git a/plugins/catalog/src/components/CatalogPage/utils/locales/goodEvening.locales.json b/plugins/catalog/src/components/CatalogPage/utils/locales/goodEvening.locales.json index a70aa122b6..a5506f123d 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/locales/goodEvening.locales.json +++ b/plugins/catalog/src/components/CatalogPage/utils/locales/goodEvening.locales.json @@ -88,7 +88,7 @@ "Swedish": "God afton", "Tagalog": "Magandang gabi", "Tatar": "Xäyerle kiç", - "Telugu" : "శుభ సాయంత్రం", + "Telugu": "శుభ సాయంత్రం", "Thai": "Sawat-dii torn khum", "Turkish": "İyi akşamlar", "Ukrainian": "Dobry vechir", diff --git a/plugins/catalog/src/data/filters.ts b/plugins/catalog/src/data/filters.ts index 71efa923c6..bbe959caa3 100644 --- a/plugins/catalog/src/data/filters.ts +++ b/plugins/catalog/src/data/filters.ts @@ -28,6 +28,7 @@ export enum EntityFilterType { ALL = 'ALL', STARRED = 'STARRED', OWNED = 'OWNED', + TYPE = 'TYPE', } export const filterGroups: CatalogFilterGroup[] = [ @@ -54,7 +55,7 @@ export const filterGroups: CatalogFilterGroup[] = [ items: [ { id: EntityFilterType.ALL, - label: 'All Services', + label: 'All Entities', count: AllServicesCount, }, ], @@ -64,13 +65,15 @@ export const filterGroups: CatalogFilterGroup[] = [ type EntityFilter = (entity: Entity, options: EntityFilterOptions) => boolean; type EntityFilterOptions = { - isStarred: boolean; + isStarred?: boolean; + type?: string; }; export const entityFilters: Record = { [EntityFilterType.OWNED]: () => false, [EntityFilterType.ALL]: () => true, - [EntityFilterType.STARRED]: (_, { isStarred }) => isStarred, + [EntityFilterType.STARRED]: (_, { isStarred }) => !!isStarred, + [EntityFilterType.TYPE]: (e, { type }) => (e.spec as any)?.type === type, }; export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0]; From 242827b36356f8ed34b09f6b50adf398746c868e Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 17 Jun 2020 13:31:47 +0200 Subject: [PATCH 25/60] set disable refresh in options passed to OAuthProviders --- plugins/auth-backend/src/providers/github/provider.ts | 1 + plugins/auth-backend/src/providers/google/provider.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 0f8f185015..0bcd80cf2c 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -96,6 +96,7 @@ export function createGithubProvider( } envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), { + disableRefresh: true, providerId: 'github', secure, baseUrl, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 971f588dce..9675d4eef2 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -143,6 +143,7 @@ export function createGoogleProvider( } envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), { + disableRefresh: false, providerId: 'google', secure, baseUrl, From 3ab7a241726593c5e7fc0a0cc58303929518fc55 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 14:14:00 +0200 Subject: [PATCH 26/60] packages/cli: remove watch-deps and build-cache --- .github/workflows/frontend.yml | 9 - .github/workflows/master.yml | 9 - .../cli/src/commands/build-cache/index.ts | 32 --- packages/cli/src/commands/clean/clean.ts | 13 +- packages/cli/src/commands/watch-deps/index.ts | 49 ---- packages/cli/src/index.ts | 23 -- packages/cli/src/lib/buildCache/archive.ts | 39 ---- packages/cli/src/lib/buildCache/cache.ts | 214 ------------------ packages/cli/src/lib/buildCache/index.ts | 18 -- packages/cli/src/lib/buildCache/options.ts | 59 ----- packages/cli/src/lib/buildCache/withCache.ts | 56 ----- packages/cli/src/lib/watchDeps/compiler.ts | 64 ------ packages/cli/src/lib/watchDeps/index.ts | 17 -- packages/cli/src/lib/watchDeps/logger.ts | 33 --- packages/cli/src/lib/watchDeps/packages.ts | 62 ----- packages/cli/src/lib/watchDeps/watchDeps.ts | 79 ------- packages/cli/src/lib/watchDeps/watcher.ts | 125 ---------- 17 files changed, 1 insertion(+), 900 deletions(-) delete mode 100644 packages/cli/src/commands/build-cache/index.ts delete mode 100644 packages/cli/src/commands/watch-deps/index.ts delete mode 100644 packages/cli/src/lib/buildCache/archive.ts delete mode 100644 packages/cli/src/lib/buildCache/cache.ts delete mode 100644 packages/cli/src/lib/buildCache/index.ts delete mode 100644 packages/cli/src/lib/buildCache/options.ts delete mode 100644 packages/cli/src/lib/buildCache/withCache.ts delete mode 100644 packages/cli/src/lib/watchDeps/compiler.ts delete mode 100644 packages/cli/src/lib/watchDeps/index.ts delete mode 100644 packages/cli/src/lib/watchDeps/logger.ts delete mode 100644 packages/cli/src/lib/watchDeps/packages.ts delete mode 100644 packages/cli/src/lib/watchDeps/watchDeps.ts delete mode 100644 packages/cli/src/lib/watchDeps/watcher.ts diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index c3fa6e4e23..f154c586fa 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -14,8 +14,6 @@ jobs: env: CI: true NODE_OPTIONS: --max-old-space-size=4096 - BACKSTAGE_CACHE_DIR: /.backstage-build-cache - BACKSTAGE_CACHE_MAX_ENTRIES: 2 steps: - uses: actions/checkout@v2 @@ -36,13 +34,6 @@ jobs: with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - name: cache build cache - uses: actions/cache@v2 - with: - path: .backstage-build-cache - key: build-cache-${{ github.sha }} - restore-keys: | - build-cache- - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 52f2760469..3ef0c1fee3 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -15,8 +15,6 @@ jobs: env: CI: true NODE_OPTIONS: --max-old-space-size=4096 - BACKSTAGE_CACHE_DIR: /.backstage-build-cache - BACKSTAGE_CACHE_MAX_ENTRIES: 2 steps: - uses: actions/checkout@v2 @@ -35,13 +33,6 @@ jobs: with: path: node_modules key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - name: cache build cache - uses: actions/cache@v2 - with: - path: .backstage-build-cache - key: build-cache-${{ github.sha }} - restore-keys: | - build-cache- - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: diff --git a/packages/cli/src/commands/build-cache/index.ts b/packages/cli/src/commands/build-cache/index.ts deleted file mode 100644 index 72199ec42e..0000000000 --- a/packages/cli/src/commands/build-cache/index.ts +++ /dev/null @@ -1,32 +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 { Command } from 'commander'; -import { run } from '../../lib/run'; -import { withCache, parseOptions } from '../../lib/buildCache'; - -/* - * The build-cache command is used to make builds a no-op if there are no changes to the package. - * It supports both local development where the output directory remains intact, as well as CI - * where the output directory is stored in a separate cache dir. - */ -export default async (cmd: Command, args: string[]) => { - const options = await parseOptions(cmd); - - await withCache(options, async () => { - await run(args[0], args.slice(1)); - }); -}; diff --git a/packages/cli/src/commands/clean/clean.ts b/packages/cli/src/commands/clean/clean.ts index 91531a53ed..2fa3e4a18f 100644 --- a/packages/cli/src/commands/clean/clean.ts +++ b/packages/cli/src/commands/clean/clean.ts @@ -15,20 +15,9 @@ */ import fs from 'fs-extra'; -import { resolve as resolvePath, relative as relativePath } from 'path'; import { paths } from '../../lib/paths'; -import { getDefaultCacheOptions } from '../../lib/buildCache'; export default async function clean() { - const cacheOptions = getDefaultCacheOptions(); - const packagePath = getPackagePath(cacheOptions.cacheDir); - await fs.remove(cacheOptions.output); - await fs.remove(packagePath); + await fs.remove(paths.resolveTarget('dist')); await fs.remove(paths.resolveTarget('coverage')); } - -function getPackagePath(cacheDir: string) { - const relativePackagePath = relativePath(paths.targetRoot, paths.targetDir); - const packagePath = resolvePath(cacheDir, relativePackagePath); - return packagePath; -} diff --git a/packages/cli/src/commands/watch-deps/index.ts b/packages/cli/src/commands/watch-deps/index.ts deleted file mode 100644 index 61f403883c..0000000000 --- a/packages/cli/src/commands/watch-deps/index.ts +++ /dev/null @@ -1,49 +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 { Command } from 'commander'; -import { run } from '../../lib/run'; -import { createLogPipe } from '../../lib/logging'; -import { watchDeps, Options } from '../../lib/watchDeps'; - -/* - * The watch-deps command is meant to improve iteration speed while working in a large monorepo - * with packages that are built independently, meaning packages depends on each other's build output. - * - * The command traverses all dependencies of the current package within the monorepo, and starts - * watching for updates in all those packages. If a change is detected, we stop listening for changes, - * and instead start up watch mode for that package. Starting watch mode means running the first - * available yarn script out of "build:watch", "watch", or "build" --watch. - */ -export default async (cmd: Command, args: string[]) => { - const options: Options = {}; - - if (cmd.build) { - options.build = true; - } - - await watchDeps(options); - - if (args?.length) { - // Use log pipe to avoid clearing the terminal - const logPipe = createLogPipe(); - - await run(args[0], args.slice(1), { - stdoutLogFunc: logPipe(process.stdout), - stderrLogFunc: logPipe(process.stderr), - }); - } -}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a8b6250a20..e67a52c795 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -126,29 +126,6 @@ const main = (argv: string[]) => { .description('Restores the changes made by the prepack command') .action(lazyAction(() => import('./commands/pack'), 'post')); - program - .command('watch-deps') - .option('--build', 'Build all dependencies on startup') - .description('Watch all dependencies while running another command') - .action(lazyAction(() => import('./commands/watch-deps'), 'default')); - - program - .command('build-cache') - .description('Wrap build command with a cache') - .option( - '--input ', - 'List of input directories that invalidate the cache [.]', - (value, acc) => acc.concat(value), - [], - ) - .option('--output ', 'Output directory to cache', 'dist') - .option( - '--cache-dir ', - 'Cache dir', - '/node_modules/.cache/backstage-builds', - ) - .action(lazyAction(() => import('./commands/build-cache'), 'default')); - program .command('clean') .description('Delete cache directories') diff --git a/packages/cli/src/lib/buildCache/archive.ts b/packages/cli/src/lib/buildCache/archive.ts deleted file mode 100644 index b4a736240d..0000000000 --- a/packages/cli/src/lib/buildCache/archive.ts +++ /dev/null @@ -1,39 +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 fs from 'fs-extra'; -import tar from 'tar'; -import { dirname } from 'path'; - -// packages all files in inputDir into an archive at archivePath, deleting any existing archive -export async function createArchive( - archivePath: string, - inputDir: string, -): Promise { - await fs.remove(archivePath); - await fs.ensureDir(dirname(archivePath)); - await tar.create({ gzip: true, file: archivePath, cwd: inputDir }, ['.']); -} - -// extracts archive at archive path into outputDir, deleting any existing files at outputDir -export async function extractArchive( - archivePath: string, - outputDir: string, -): Promise { - await fs.remove(outputDir); - await fs.ensureDir(outputDir); - await tar.extract({ file: archivePath, cwd: outputDir }); -} diff --git a/packages/cli/src/lib/buildCache/cache.ts b/packages/cli/src/lib/buildCache/cache.ts deleted file mode 100644 index d49eedaba9..0000000000 --- a/packages/cli/src/lib/buildCache/cache.ts +++ /dev/null @@ -1,214 +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 fs from 'fs-extra'; -import { resolve as resolvePath, relative as relativePath } from 'path'; -import { runPlain, runCheck } from '../run'; -import { Options } from './options'; -import { extractArchive, createArchive } from './archive'; -import { paths } from '../paths'; -import { version, isDev } from '../version'; - -const INFO_FILE = '.backstage-build-cache'; - -// Result from a cache query -export type CacheQueryResult = { - // True if there was a cache hit - hit: boolean; - // If there is a cache hit and this method is defined, it needs to be called to restore the - // output contents before continuing. - copy?: (outputDir: string) => Promise; - // Call after a successful build to archive the output content. - // The content will be archived using the same key as was used to query the cache. - archive: (outputDir: string, maxEntries: number) => Promise; -}; - -// Key that determines whether cached output can be reused -export type CacheKey = string[]; - -type CacheEntry = { - // Key for the input of this cache entry - key: CacheKey; - // Path to the archive of this cache entry - path: string; -}; - -// Struct containing information about cache entries on the filesystem. -// Stored inside the INFO_FILE as JSON. -type CacheInfo = { - // Optional key entry for the contents of the current directory. Used to key the - // output present in the outputs folder, where the info file resides inside the output folder. - key?: CacheKey; - // Optional list of cache archives present in the same directory. Resides in the external - // cache location inside one info file for each package. - entries?: CacheEntry[]; -}; - -export class Cache { - // Read the current cache state form the filesystem. - static async read(options: Options) { - const relativePackagePath = relativePath(paths.targetRoot, paths.targetDir); - const location = resolvePath(options.cacheDir, relativePackagePath); - - const outputInfo = await readCacheInfo(options.output); - const localKey = outputInfo?.key; - - const { entries = [] } = (await readCacheInfo(location)) ?? {}; - return new Cache(location, entries, localKey); - } - - // Generates a key based on the contents of the input paths. - // Returns undefined if it's not possible to generate a stable key. - static async readInputKey( - inputPaths: string[], - ): Promise { - const allInputPaths = inputPaths.slice(); - - // If we're executing the cli inside the backstage repo, we add the cli src as cache key as well - if (isDev) { - allInputPaths.unshift(paths.ownDir); - } - - // Make sure we don't have any uncommitted changes to the input, in that case we skip caching. - const noChanges = await runCheck( - 'git', - 'diff', - '--quiet', - 'HEAD', - '--', - ...allInputPaths, - ); - if (!noChanges) { - return undefined; - } - - const trees = []; - for (const inputPath of allInputPaths) { - const output = await runPlain('git', 'ls-tree', 'HEAD', inputPath); - const [, , sha] = output.split(/\s+/, 3); - // If we can't get a tree sha it means we're outside of tracked files, so treat as dirty - if (!sha) { - return undefined; - } - trees.push(sha); - } - - if (isDev) { - return trees; - } - // If we're executing as a dependency, use the version as a key - return [version, ...trees]; - } - - constructor( - private readonly location: string, - private readonly entries: CacheEntry[] = [], - private readonly localKey?: CacheKey, - ) {} - - // Query for the presense of cached output for a given key - query(key: CacheKey): CacheQueryResult { - const { location } = this; - - const archive = async (outputDir: string, maxEntries: number) => { - await writeCacheInfo(outputDir, { key }); - - const timestamp = new Date().toISOString().replace(/-|:|\..*/g, ''); - const rand = Math.random().toString(36).slice(2, 6); - const archiveName = `cache-${timestamp}-${rand}.tgz`; - const archivePath = resolvePath(location, archiveName); - - // Read existing entries and prepend the new one - const { entries = [] } = (await readCacheInfo(location)) ?? {}; - - // Check if there's already aan entry for this key, in that case we just wanna bump it - const entryIndex = entries.findIndex((e) => compareKeys(e.key, key)); - if (entryIndex !== -1) { - const [existingEntry] = entries.splice(entryIndex, 1); - entries.unshift(existingEntry); - - await writeCacheInfo(location, { entries }); - return; - } - - // Create and add new archive to entries - await createArchive(archivePath, outputDir); - entries.unshift({ key, path: archiveName }); - - // Remove old cache entries - const removedEntries = entries.splice(maxEntries); - for (const entry of removedEntries) { - try { - await fs.remove(resolvePath(location, entry.path)); - } catch (error) { - process.stderr.write(`failed to remove old cache entry, ${error}\n`); - } - } - - await writeCacheInfo(location, { entries }); - }; - - if (compareKeys(this.localKey, key)) { - return { hit: true, archive }; - } - - const matchingEntry = this.entries.find((e) => compareKeys(e.key, key)); - if (!matchingEntry) { - return { hit: false, archive }; - } - - return { - hit: true, - archive, - copy: async (outputDir: string) => { - const archivePath = resolvePath(location, matchingEntry.path); - await extractArchive(archivePath, outputDir); - }, - }; - } -} - -// Compares to cache keys, returning true if they are both defined and equal -function compareKeys(a?: CacheKey, b?: CacheKey): boolean { - if (!a || !b) { - return false; - } - return a.join(',') === b.join(','); -} - -// Read and parse a cache info file in the given directory -async function readCacheInfo( - parentDir: string, -): Promise { - const infoFile = resolvePath(parentDir, INFO_FILE); - const exists = await fs.pathExists(infoFile); - if (!exists) { - return undefined; - } - - const infoData = await fs.readFile(infoFile); - const cacheInfo = JSON.parse(infoData.toString('utf8')) as CacheInfo; - return cacheInfo; -} - -// Write a cache info file to the given directory -async function writeCacheInfo( - parentDir: string, - cacheInfo: CacheInfo, -): Promise { - const infoData = Buffer.from(JSON.stringify(cacheInfo, null, 2), 'utf8'); - await fs.writeFile(resolvePath(parentDir, INFO_FILE), infoData); -} diff --git a/packages/cli/src/lib/buildCache/index.ts b/packages/cli/src/lib/buildCache/index.ts deleted file mode 100644 index 70bcbd36b8..0000000000 --- a/packages/cli/src/lib/buildCache/index.ts +++ /dev/null @@ -1,18 +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. - */ - -export * from './withCache'; -export * from './options'; diff --git a/packages/cli/src/lib/buildCache/options.ts b/packages/cli/src/lib/buildCache/options.ts deleted file mode 100644 index 9a6a379fce..0000000000 --- a/packages/cli/src/lib/buildCache/options.ts +++ /dev/null @@ -1,59 +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 { resolve as resolvePath } from 'path'; -import { Command } from 'commander'; -import { paths } from '../paths'; - -const DEFAULT_CACHE_DIR = '/node_modules/.cache/backstage-builds'; -const DEFAULT_MAX_ENTRIES = 10; - -export type Options = { - inputs: string[]; - output: string; - cacheDir: string; - maxCacheEntries: number; -}; - -function transformPath(path: string): string { - return resolvePath(path.replace(//g, paths.targetRoot)); -} - -export async function parseOptions(cmd: Command): Promise { - const inputs = cmd.input.map(transformPath) as string[]; - if (inputs.length === 0) { - inputs.push(transformPath('.')); - } - const output = transformPath(cmd.output); - const cacheDir = transformPath( - process.env.BACKSTAGE_CACHE_DIR || cmd.cacheDir, - ); - const maxCacheEntries = - Number(process.env.BACKSTAGE_CACHE_MAX_ENTRIES) || DEFAULT_MAX_ENTRIES; - return { inputs, output, cacheDir, maxCacheEntries }; -} - -export function getDefaultCacheOptions(): Options { - return { - inputs: [transformPath('.')], - output: transformPath('dist'), - cacheDir: transformPath( - process.env.BACKSTAGE_CACHE_DIR || DEFAULT_CACHE_DIR, - ), - maxCacheEntries: - Number(process.env.BACKSTAGE_CACHE_MAX_ENTRIES) || DEFAULT_MAX_ENTRIES, - }; -} diff --git a/packages/cli/src/lib/buildCache/withCache.ts b/packages/cli/src/lib/buildCache/withCache.ts deleted file mode 100644 index d21de94865..0000000000 --- a/packages/cli/src/lib/buildCache/withCache.ts +++ /dev/null @@ -1,56 +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 fs from 'fs-extra'; -import { Cache } from './cache'; -import { Options } from './options'; - -function print(msg: string) { - process.stdout.write(`[build-cache] ${msg}\n`); -} - -// Wrap a build function with a cache, which won't call the build function on cache hit -export async function withCache( - options: Options, - buildFunc: () => Promise, -): Promise { - const key = await Cache.readInputKey(options.inputs); - if (!key) { - print('input directory is dirty, skipping cache'); - await fs.remove(options.output); - await buildFunc(); - return; - } - - const cache = await Cache.read(options); - - const cacheResult = cache.query(key); - if (cacheResult.hit) { - if (cacheResult.copy) { - print('external cache hit, copying archive to output folder'); - await cacheResult.copy(options.output); - } else { - print('cache hit, nothing to be done'); - } - return; - } - - print('cache miss, need to build'); - await fs.remove(options.output); - await buildFunc(); - - await cacheResult.archive(options.output, options.maxCacheEntries); -} diff --git a/packages/cli/src/lib/watchDeps/compiler.ts b/packages/cli/src/lib/watchDeps/compiler.ts deleted file mode 100644 index 5ea8efc3e6..0000000000 --- a/packages/cli/src/lib/watchDeps/compiler.ts +++ /dev/null @@ -1,64 +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 { spawn } from 'child_process'; -import { LogPipe } from '../logging'; -import chalk from 'chalk'; -import { Package } from './packages'; - -export function startCompiler(pkg: Package, logPipe: LogPipe) { - // First we figure out which yarn script is a available, falling back to "build --watch" - const scriptName = ['build:watch', 'watch'].find( - (script) => script in pkg.scripts, - ); - const args = scriptName ? [scriptName] : ['build', '--watch']; - - // Start the watch script inside the dependency - const watch = spawn('yarn', ['run', ...args], { - cwd: pkg.location, - env: { FORCE_COLOR: 'true', ...process.env }, - stdio: 'pipe', - shell: true, - }); - - watch.stdin.end(); - watch.stdout.on('data', logPipe(process.stdout)); - const logErr = logPipe(process.stderr); - watch.stderr.on('data', logErr); - - const promise = new Promise((resolve, reject) => { - watch.on('error', (error) => { - reject(error); - }); - - watch.on('close', (code: number) => { - if (code !== 0) { - const msg = `Compiler exited with code ${code}`; - logErr(chalk.red(msg)); - reject(new Error(msg)); - } else { - resolve(); - } - }); - }); - - return { - promise, - close() { - watch.kill('SIGINT'); - }, - }; -} diff --git a/packages/cli/src/lib/watchDeps/index.ts b/packages/cli/src/lib/watchDeps/index.ts deleted file mode 100644 index a6fc92ca91..0000000000 --- a/packages/cli/src/lib/watchDeps/index.ts +++ /dev/null @@ -1,17 +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. - */ - -export * from './watchDeps'; diff --git a/packages/cli/src/lib/watchDeps/logger.ts b/packages/cli/src/lib/watchDeps/logger.ts deleted file mode 100644 index 59eb400301..0000000000 --- a/packages/cli/src/lib/watchDeps/logger.ts +++ /dev/null @@ -1,33 +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 { createLogPipe } from '../logging'; - -export type ColorFunc = (msg: string) => string; - -// A factory for creating log pipes that rotate between different coloring functions -export function createLogPipeFactory(colorFuncs: ColorFunc[]) { - let colorIndex = 0; - - return (name: string) => { - const colorFunc = colorFuncs[colorIndex]; - - colorIndex = (colorIndex + 1) % colorFuncs.length; - - const prefix = `${colorFunc(name)}: `; - return createLogPipe({ prefix }); - }; -} diff --git a/packages/cli/src/lib/watchDeps/packages.ts b/packages/cli/src/lib/watchDeps/packages.ts deleted file mode 100644 index 7a45524b45..0000000000 --- a/packages/cli/src/lib/watchDeps/packages.ts +++ /dev/null @@ -1,62 +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 { paths } from '../paths'; - -const LernaProject = require('@lerna/project'); -const PackageGraph = require('@lerna/package-graph'); - -export type Package = { - name: string; - location: string; - scripts: { [name in string]: string }; -}; - -// Uses lerna to find all local deps of the root package, excluding itself or any package in the blacklist -export async function findAllDeps( - rootPackageName: string, - blacklist: string[], -): Promise { - const project = new LernaProject(paths.targetDir); - const packages = await project.getPackages(); - const graph = new PackageGraph(packages); - - const deps = new Map(); - const searchNames = [rootPackageName]; - - while (searchNames.length) { - const name = searchNames.pop()!; - - if (deps.has(name)) { - continue; - } - - const node = graph.get(name); - if (!node) { - throw new Error(`Package '${name}' not found`); - } - - searchNames.push(...node.localDependencies.keys()); - deps.set(name, node.pkg); - } - - deps.delete(rootPackageName); - for (const name of blacklist) { - deps.delete(name); - } - - return [...deps.values()]; -} diff --git a/packages/cli/src/lib/watchDeps/watchDeps.ts b/packages/cli/src/lib/watchDeps/watchDeps.ts deleted file mode 100644 index c986749d3b..0000000000 --- a/packages/cli/src/lib/watchDeps/watchDeps.ts +++ /dev/null @@ -1,79 +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 chalk from 'chalk'; -import fs from 'fs-extra'; -import { createLogPipeFactory } from './logger'; -import { findAllDeps } from './packages'; -import { startWatcher, startPackageWatcher } from './watcher'; -import { startCompiler } from './compiler'; -import { run } from '../run'; -import { paths } from '../paths'; - -const PACKAGE_BLACKLIST = [ - // We never want to watch for changes in the cli, but all packages will depend on it. - '@backstage/cli', -]; - -const WATCH_LOCATIONS = ['package.json', 'src', 'assets']; - -export type Options = { - build?: boolean; -}; - -// Start watching for dependency changes. -// The returned promise resolves when watchers have started for all current dependencies. -export async function watchDeps(options: Options = {}) { - const localPackagePath = paths.resolveTarget('package.json'); - - // Rotate through different prefix colors to make it easier to differenciate between different deps - const createLogPipe = createLogPipeFactory([ - chalk.yellow, - chalk.blue, - chalk.magenta, - chalk.green, - chalk.cyan, - ]); - - // Find all direct and transitive local dependencies of the current package. - const packageData = await fs.readFile(localPackagePath, 'utf8'); - const packageJson = JSON.parse(packageData); - const packageName = packageJson.name; - - const deps = await findAllDeps(packageName, PACKAGE_BLACKLIST); - - if (options.build) { - await run('lerna', [ - 'run', - '--scope', - packageName, - '--include-dependencies', - 'build', - ]); - } - - // We lazily watch all our deps, as in we don't start the actual watch compiler until a change is detected - const watcher = await startWatcher(deps, WATCH_LOCATIONS, (pkg) => { - startCompiler(pkg, createLogPipe(pkg.name)).promise.catch((error) => { - process.stderr.write(`${error}\n`); - }); - }); - - await startPackageWatcher(localPackagePath, async () => { - const newDeps = await findAllDeps(packageName, PACKAGE_BLACKLIST); - await watcher.update(newDeps); - }); -} diff --git a/packages/cli/src/lib/watchDeps/watcher.ts b/packages/cli/src/lib/watchDeps/watcher.ts deleted file mode 100644 index bcfe766b63..0000000000 --- a/packages/cli/src/lib/watchDeps/watcher.ts +++ /dev/null @@ -1,125 +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 { resolve as resolvePath } from 'path'; -import chalk from 'chalk'; -import chokidar from 'chokidar'; -import { Package } from './packages'; -import { createLogFunc } from '../logging'; - -export type Watcher = { - update(newPackages: Package[]): Promise; -}; - -/* - * Watch for changes inside a collection of packages. When a change is detected, stop - * watching and call the callback with the package the change occured in. - * - * The returned promise is resolved once all watchers are ready. - */ -export async function startWatcher( - packages: Package[], - paths: string[], - callback: (pkg: Package) => void, -): Promise { - const watchedPackageLocations = new Set(); - const log = createLogFunc(process.stdout); - - const watchPackage = async (pkg: Package) => { - let signalled = false; - watchedPackageLocations.add(pkg.location); - - const watchLocations = paths.map((path) => resolvePath(pkg.location, path)); - const watcher = chokidar - .watch(watchLocations, { - cwd: pkg.location, - ignoreInitial: true, - disableGlobbing: true, - }) - .on('all', () => { - if (!signalled) { - signalled = true; - callback(pkg); - } - watcher.close(); - }); - - return new Promise((resolve, reject) => { - watcher.on('ready', resolve); - watcher.on('error', reject); - }); - }; - - const update = async (newPackages: Package[]) => { - const promises = new Array>(); - - for (const pkg of newPackages) { - if (watchedPackageLocations.has(pkg.location)) { - continue; - } - - log(chalk.green(`Starting watch of new dependency ${pkg.name}`)); - promises.push(watchPackage(pkg)); - } - - await Promise.all(promises); - }; - - await Promise.all(packages.map(watchPackage)); - - return { update }; -} - -/** - * Watch a package.json for updates - */ -export function startPackageWatcher(packagePath: string, callback: () => void) { - let changed = false; - let working = false; - - const notifyDeps = async () => { - changed = true; - if (working) { - return; - } - working = true; - changed = false; - - try { - await callback(); - } finally { - working = false; - // Keep going if a change was emitted while working - if (changed) { - notifyDeps(); - } - } - }; - - const watcher = chokidar - .watch(packagePath, { - ignoreInitial: true, - disableGlobbing: true, - }) - .on('all', () => { - notifyDeps(); - }); - - return new Promise((resolve, reject) => { - watcher.on('ready', resolve); - watcher.on('error', reject); - }); -} From e69a4bcdd4d41510b8dbdca4e1f5fc38c2d9b543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 17 Jun 2020 14:15:58 +0200 Subject: [PATCH 27/60] chore(docs): just ran prettier on the entity schema --- docs/service_specification.schema.json | 208 ++++++++++++------------- 1 file changed, 98 insertions(+), 110 deletions(-) diff --git a/docs/service_specification.schema.json b/docs/service_specification.schema.json index 15b31127f4..6031dc3e8d 100644 --- a/docs/service_specification.schema.json +++ b/docs/service_specification.schema.json @@ -1,115 +1,103 @@ { - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "backstage.io/v1beta1", - "type": "object", - "title": "A JSON Schema for Backstage catalog entities.", - "description": "Each descriptor file has a number of entities. This schema matches each of those.", - "examples": [ - { - "apiVersion": "backstage.io/v1beta1", - "kind": "Component", - "spec": { - "type": "service" - }, - "metadata": { - "name": "LoremService", - "description": "Creates Lorems like a pro.", - "labels": { - "product_name": "Random value Generator" - }, - "annnotations": { - "docs": "https://github.com/..../tree/develop/doc" - }, - "teams": [{ - "name": "Team super great", - "email": "greatTeam@geemel.com" - }] - } - } - ], - "required": [ - "apiVersion", - "kind", - "metadata" - ], - "additionalProperties": false, - "properties": { - "apiVersion": { - "type": "string", - "description": "Version of the specification format for a particular file is written against.", - "enum": [ - "backstage.io/v1beta1" - ] + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "backstage.io/v1beta1", + "type": "object", + "title": "A JSON Schema for Backstage catalog entities.", + "description": "Each descriptor file has a number of entities. This schema matches each of those.", + "examples": [ + { + "apiVersion": "backstage.io/v1beta1", + "kind": "Component", + "spec": { + "type": "service" + }, + "metadata": { + "name": "LoremService", + "description": "Creates Lorems like a pro.", + "labels": { + "product_name": "Random value Generator" }, - "kind": { - "type": "string", - "description": "High level entity type being described, from the Backstage system model.", - "enum": [ - "Component" - ] + "annnotations": { + "docs": "https://github.com/..../tree/develop/doc" }, - "spec": { - "type": "object", - "description": "Actual specification data that describes the entity. TODO: shape depend on `kind`", - "required": [ - "type" - ], - "additionalProperties": false, - "properties": { - "type": { - "type": "string", - "description": "The type of component.", - "default": "", - "examples": [ - "service" - ] - } - } - }, - "metadata": { - "type": "object", - "description": "Metadata about the entity, i.e. things that aren't directly part of the entity specification itself.", - "required": [ - "name" - ], - "additionalProperties": true, - "properties": { - "name": { - "type": "string", - "pattern": "^[a-z0-9A-Z_.-]{1,63}$", - "description": "The name of the entity. This name is both meant for human eyes to recognize the entity, and for machines and other components to reference the entity" - }, - "description": { - "type": "string", - "description": "A human readable description of the entity, to be shown in Backstage. Should be kept short and informative." - }, - "namespace": { - "type": "string", - "description": "The name of a namespace that the entity belongs to." - }, - "labels": { - "type": "object", - "description": "Labels are optional key/value pairs of that are attached to the entity, and their use is identical to kubernetes object labels.", - "additionalProperties": true, - "patternProperties": { - "^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}\/)?[a-z0-9A-Z_\\-\\.]{1,63}$": { - "type": "string", - "pattern": "^[a-z0-9A-Z_.-]{1,63}$" - } - } - }, - "annnotations": { - "type": "object", - "description": "Arbitrary non-identifying metadata attached to the entity, identical in use to kubernetes object annotations.", - "additionalProperties": true, - "patternProperties": { - "^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}\/)?[a-z0-9A-Z_\\-\\.]{1,63}$": { - "type": "string", - "pattern": "^[a-z0-9A-Z_.-]{1,63}$" - } - } - } - } - } + "teams": [ + { + "name": "Team super great", + "email": "greatTeam@geemel.com" + } + ] + } } + ], + "required": ["apiVersion", "kind", "metadata"], + "additionalProperties": false, + "properties": { + "apiVersion": { + "type": "string", + "description": "Version of the specification format for a particular file is written against.", + "enum": ["backstage.io/v1beta1"] + }, + "kind": { + "type": "string", + "description": "High level entity type being described, from the Backstage system model.", + "enum": ["Component"] + }, + "spec": { + "type": "object", + "description": "Actual specification data that describes the entity. TODO: shape depend on `kind`", + "required": ["type"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "The type of component.", + "default": "", + "examples": ["service"] + } + } + }, + "metadata": { + "type": "object", + "description": "Metadata about the entity, i.e. things that aren't directly part of the entity specification itself.", + "required": ["name"], + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9A-Z_.-]{1,63}$", + "description": "The name of the entity. This name is both meant for human eyes to recognize the entity, and for machines and other components to reference the entity" + }, + "description": { + "type": "string", + "description": "A human readable description of the entity, to be shown in Backstage. Should be kept short and informative." + }, + "namespace": { + "type": "string", + "description": "The name of a namespace that the entity belongs to." + }, + "labels": { + "type": "object", + "description": "Labels are optional key/value pairs of that are attached to the entity, and their use is identical to kubernetes object labels.", + "additionalProperties": true, + "patternProperties": { + "^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}/)?[a-z0-9A-Z_\\-\\.]{1,63}$": { + "type": "string", + "pattern": "^[a-z0-9A-Z_.-]{1,63}$" + } + } + }, + "annnotations": { + "type": "object", + "description": "Arbitrary non-identifying metadata attached to the entity, identical in use to kubernetes object annotations.", + "additionalProperties": true, + "patternProperties": { + "^([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.[a-zA-Z]{2,}/)?[a-z0-9A-Z_\\-\\.]{1,63}$": { + "type": "string", + "pattern": "^[a-z0-9A-Z_.-]{1,63}$" + } + } + } + } + } + } } From 42ec00922801b7033d4fa2007fb07d0ef4e12711 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 17 Jun 2020 14:18:40 +0200 Subject: [PATCH 28/60] refactor IdentityApi --- .../src/adapters/StaticJsonAdapter.ts | 28 ++++++++++--------- .../identity-backend/src/adapters/types.ts | 13 ++++----- .../identity-backend/src/service/router.ts | 14 +++++++--- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts index a88e561ee6..cd2a40ea78 100644 --- a/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts +++ b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts @@ -14,14 +14,19 @@ * limitations under the License. */ -import { Group, GroupsJson, GroupsResponse, IdentityApiAdapter } from './types'; +import { + Group, + GroupsJson, + GroupsResponse, + IdentityApi, + GroupsRequest, +} from './types'; import fs from 'fs-extra'; -import express from 'express'; import path from 'path'; const GROUPS_JSON_FILE = path.join(__dirname, 'data', 'userGroups.json'); -export class StaticJsonAdapter implements IdentityApiAdapter { +export class StaticJsonAdapter implements IdentityApi { private readonly groups: Group[]; constructor() { @@ -31,16 +36,13 @@ export class StaticJsonAdapter implements IdentityApiAdapter { this.groups = groupsJson.groups; } - getUserGroups( - req: express.Request, - res: express.Response, - ): express.Response { - const user = req.params.user; - const type = req.query.type?.toString() ?? ''; - - const userGroups = this._getUserGroups(this.groups, user); - const groups = this.filterGroupsByType(userGroups, type); - return res.json({ groups }); + getUserGroups(req: GroupsRequest): Promise { + return new Promise(resolve => { + const { user, type } = req; + const userGroups = this._getUserGroups(this.groups, user); + const groups = this.filterGroupsByType(userGroups, type); + resolve({ groups }); + }); } _getUserGroups(groups: Group[], user: string) { diff --git a/plugins/identity-backend/src/adapters/types.ts b/plugins/identity-backend/src/adapters/types.ts index 85719205f8..59fd1324d1 100644 --- a/plugins/identity-backend/src/adapters/types.ts +++ b/plugins/identity-backend/src/adapters/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import express from 'express'; - export type User = { name: string; }; @@ -35,9 +33,10 @@ export type GroupsResponse = { groups: Group[]; }; -export interface IdentityApiAdapter { - getUserGroups( - req: express.Request, - res: express.Response, - ): express.Response; +export type GroupsRequest = { + user: string; + type: string; +}; +export interface IdentityApi { + getUserGroups(req: GroupsRequest): Promise; } diff --git a/plugins/identity-backend/src/service/router.ts b/plugins/identity-backend/src/service/router.ts index ac6267ba75..256ef6ce3c 100644 --- a/plugins/identity-backend/src/service/router.ts +++ b/plugins/identity-backend/src/service/router.ts @@ -18,15 +18,21 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { StaticJsonAdapter } from '../adapters'; -import { IdentityApiAdapter } from '../adapters/types'; +import { IdentityApi } from '../adapters/types'; export interface RouterOptions { logger: Logger; } -const makeRouter = (adapter: IdentityApiAdapter): express.Router => { +const makeRouter = (adapter: IdentityApi): express.Router => { const router = Router(); - router.get('/users/:user/groups', adapter.getUserGroups.bind(adapter)); + router.get('/users/:user/groups', async (req, res) => { + const user = req.params.user; + const type = req.query.type?.toString() ?? ''; + + const response = await adapter.getUserGroups({ user, type }); + res.send(response); + }); return router; }; @@ -35,7 +41,7 @@ export async function createRouter( ): Promise { const logger = options.logger; - logger.info('Initializing identity provider'); + logger.info('Initializing identity API backend'); const adapter = new StaticJsonAdapter(); return makeRouter(adapter); } From fedfbfae58ee4c8708e4b70f1e082efcc0d44bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 17 Jun 2020 14:41:10 +0200 Subject: [PATCH 29/60] chore(docs): more tweaks to the schema --- docs/service_specification.schema.json | 57 +++++++++++++++++--------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/docs/service_specification.schema.json b/docs/service_specification.schema.json index 6031dc3e8d..a17f67d30a 100644 --- a/docs/service_specification.schema.json +++ b/docs/service_specification.schema.json @@ -1,16 +1,13 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "$id": "backstage.io/v1beta1", + "$id": "backstage.io/v1alpha1", "type": "object", "title": "A JSON Schema for Backstage catalog entities.", "description": "Each descriptor file has a number of entities. This schema matches each of those.", "examples": [ { - "apiVersion": "backstage.io/v1beta1", + "apiVersion": "backstage.io/v1alpha1", "kind": "Component", - "spec": { - "type": "service" - }, "metadata": { "name": "LoremService", "description": "Creates Lorems like a pro.", @@ -26,6 +23,11 @@ "email": "greatTeam@geemel.com" } ] + }, + "spec": { + "type": "service", + "lifecycle": "production", + "owner": "tools@example.com" } } ], @@ -35,27 +37,21 @@ "apiVersion": { "type": "string", "description": "Version of the specification format for a particular file is written against.", - "enum": ["backstage.io/v1beta1"] + "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] }, "kind": { "type": "string", "description": "High level entity type being described, from the Backstage system model.", "enum": ["Component"] }, - "spec": { - "type": "object", - "description": "Actual specification data that describes the entity. TODO: shape depend on `kind`", - "required": ["type"], - "additionalProperties": false, - "properties": { - "type": { - "type": "string", - "description": "The type of component.", - "default": "", - "examples": ["service"] - } - } + "metadata": { + "$ref": "#/definitions/metadata" }, + "spec": { + "$ref": "#/definitions/spec" + } + }, + "definitions": { "metadata": { "type": "object", "description": "Metadata about the entity, i.e. things that aren't directly part of the entity specification itself.", @@ -98,6 +94,29 @@ } } } + }, + "spec": { + "type": "object", + "description": "Actual specification data that describes the entity. TODO: shape depend on `kind`", + "required": ["type", "lifecycle", "owner"], + "additionalProperties": true, + "properties": { + "type": { + "type": "string", + "description": "The type of component.", + "examples": ["service"] + }, + "lifecycle": { + "type": "string", + "description": "The lifecycle step that this component is in.", + "examples": ["production"] + }, + "owner": { + "type": "string", + "description": "The owner of the component.", + "examples": ["tools@example.com"] + } + } } } } From aa2916e5b8447e723adc66f55c476f9a884dd7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 17 Jun 2020 15:23:00 +0200 Subject: [PATCH 30/60] release: make almost all packages public --- packages/catalog-model/package.json | 2 +- plugins/auth-backend/package.json | 4 ++-- plugins/catalog-backend/package.json | 6 +++--- plugins/catalog/package.json | 2 +- plugins/circleci/package.json | 2 +- plugins/explore/package.json | 2 +- plugins/gitops-profiles/package.json | 2 +- plugins/identity-backend/package.json | 4 ++-- plugins/register-component/package.json | 2 +- plugins/scaffolder-backend/package.json | 7 +++++-- plugins/scaffolder/package.json | 2 +- plugins/sentry-backend/package.json | 4 ++-- plugins/sentry/package.json | 2 +- plugins/welcome/package.json | 2 +- 14 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 170fb36281..8ee796992f 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -6,7 +6,7 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 69e4a99acc..c1d6064188 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -50,6 +50,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist" + "dist/**/*.{js,d.ts}" ] } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index a350ab4454..895be5a2f7 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -48,7 +48,7 @@ "supertest": "^4.0.2" }, "files": [ - "dist", - "migrations" + "dist/**/*.{js,d.ts}", + "migrations/**/*.{js,d.ts}" ] } diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index d7329e4b2f..5584e664f1 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -5,7 +5,7 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 60787b146e..e1cbc7f31f 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -5,7 +5,7 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index f0f631eb09..318e8dd1b5 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -5,7 +5,7 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 441f2dd77c..ae45cbfbe0 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -5,7 +5,7 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index 862fb273d2..aebdd26083 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -36,6 +36,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist" + "dist/**/*.{js,d.ts}" ] } diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index d5fc49405d..ec31edb58d 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -5,7 +5,7 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 3d8d95164e..38eb7f0b09 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -38,5 +38,8 @@ "@types/fs-extra": "^9.0.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2" - } + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] } diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 66738acea6..8ff5d586b4 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -5,7 +5,7 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index 8652ecda7c..3f159a62cd 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", @@ -37,6 +37,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist" + "dist/**/*.{js,d.ts}" ] } diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index dcec4eceb0..bc99bdfbbb 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -5,7 +5,7 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 388f0335a7..5c9f83000e 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -4,7 +4,7 @@ "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", - "private": true, + "private": false, "license": "Apache-2.0", "publishConfig": { "access": "public", From 38ba8978577af8928fe76579f8980daa660c7f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 17 Jun 2020 15:24:54 +0200 Subject: [PATCH 31/60] v0.1.1-alpha.9 --- lerna.json | 2 +- packages/app/package.json | 28 ++++++++++++------------- packages/backend-common/package.json | 4 ++-- packages/backend/package.json | 18 ++++++++-------- packages/catalog-model/package.json | 4 ++-- packages/cli/package.json | 6 +++--- packages/config-loader/package.json | 2 +- packages/core-api/package.json | 6 +++--- packages/core/package.json | 10 ++++----- packages/dev-utils/package.json | 10 ++++----- packages/storybook/package.json | 4 ++-- packages/test-utils/package.json | 8 +++---- packages/theme/package.json | 4 ++-- plugins/auth-backend/package.json | 6 +++--- plugins/catalog-backend/package.json | 8 +++---- plugins/catalog/package.json | 18 ++++++++-------- plugins/circleci/package.json | 10 ++++----- plugins/explore/package.json | 12 +++++------ plugins/gitops-profiles/package.json | 10 ++++----- plugins/graphiql/package.json | 12 +++++------ plugins/identity-backend/package.json | 6 +++--- plugins/lighthouse/package.json | 12 +++++------ plugins/register-component/package.json | 14 ++++++------- plugins/scaffolder-backend/package.json | 6 +++--- plugins/scaffolder/package.json | 10 ++++----- plugins/sentry-backend/package.json | 6 +++--- plugins/sentry/package.json | 10 ++++----- plugins/tech-radar/package.json | 10 ++++----- plugins/welcome/package.json | 10 ++++----- 29 files changed, 133 insertions(+), 133 deletions(-) diff --git a/lerna.json b/lerna.json index aa535f7aeb..5d63f88c11 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.8" + "version": "0.1.1-alpha.9" } diff --git a/packages/app/package.json b/packages/app/package.json index 9a296a913c..6aa82a0b36 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,21 +1,21 @@ { "name": "example-app", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "private": true, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/plugin-catalog": "^0.1.1-alpha.8", - "@backstage/plugin-circleci": "^0.1.1-alpha.8", - "@backstage/plugin-explore": "^0.1.1-alpha.8", - "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.8", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.8", - "@backstage/plugin-register-component": "^0.1.1-alpha.8", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.8", - "@backstage/plugin-sentry": "^0.1.1-alpha.8", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.8", - "@backstage/plugin-welcome": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/plugin-catalog": "^0.1.1-alpha.9", + "@backstage/plugin-circleci": "^0.1.1-alpha.9", + "@backstage/plugin-explore": "^0.1.1-alpha.9", + "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.9", + "@backstage/plugin-lighthouse": "^0.1.1-alpha.9", + "@backstage/plugin-register-component": "^0.1.1-alpha.9", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.9", + "@backstage/plugin-sentry": "^0.1.1-alpha.9", + "@backstage/plugin-tech-radar": "^0.1.1-alpha.9", + "@backstage/plugin-welcome": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "prop-types": "^15.7.2", diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c265a7f489..1a52e06013 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -40,7 +40,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "@types/compression": "^1.7.0", "@types/http-errors": "^1.6.3", "@types/morgan": "^1.9.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 7e9dbb71a7..7a0ad95e67 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -17,20 +17,20 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.8", - "@backstage/catalog-model": "^0.1.1-alpha.8", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.8", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.8", - "@backstage/plugin-identity-backend": "^0.1.1-alpha.8", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.8", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.8", + "@backstage/backend-common": "^0.1.1-alpha.9", + "@backstage/catalog-model": "^0.1.1-alpha.9", + "@backstage/plugin-auth-backend": "^0.1.1-alpha.9", + "@backstage/plugin-catalog-backend": "^0.1.1-alpha.9", + "@backstage/plugin-identity-backend": "^0.1.1-alpha.9", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.9", + "@backstage/plugin-sentry-backend": "^0.1.1-alpha.9", "express": "^4.17.1", "knex": "^0.21.1", "sqlite3": "^4.2.0", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", "@types/helmet": "^0.0.47" diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 8ee796992f..0810479f0c 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", "main:src": "src/index.ts", @@ -26,7 +26,7 @@ "yup": "^0.28.5" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "@types/jest": "^25.2.2", "@types/lodash": "^4.14.151", "@types/yup": "^0.28.2", diff --git a/packages/cli/package.json b/packages/cli/package.json index a4c862351e..c686e3feb8 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.8", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public" @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.1-alpha.7", - "@backstage/config-loader": "^0.1.1-alpha.7", + "@backstage/config-loader": "^0.1.1-alpha.9", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", @@ -40,8 +40,8 @@ "@spotify/eslint-config": "^7.0.1", "@sucrase/webpack-loader": "^2.0.0", "@types/start-server-webpack-plugin": "^2.2.0", - "@types/webpack-node-externals": "^1.7.1", "@types/webpack-env": "^1.15.2", + "@types/webpack-node-externals": "^1.7.1", "bfj": "^7.0.2", "chalk": "^4.0.0", "chokidar": "^3.3.1", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index de6b26363a..df198a5039 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.1.1-alpha.7", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 80a94b2e34..0cc50143a5 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/config": "^0.1.1-alpha.7", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@types/react": "^16.9", @@ -42,7 +42,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "@backstage/test-utils-core": "^0.1.1-alpha.7", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", diff --git a/packages/core/package.json b/packages/core/package.json index ccf2bce628..7f11230405 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.8", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/config": "^0.1.1-alpha.7", - "@backstage/core-api": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core-api": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -55,8 +55,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/test-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/test-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 5e6d26111d..af1f813148 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.8", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/test-utils": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/test-utils": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.7.0", diff --git a/packages/storybook/package.json b/packages/storybook/package.json index c02557910b..21222cf412 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "description": "Storybook build for core package", "private": true, "scripts": { @@ -14,7 +14,7 @@ ] }, "dependencies": { - "@backstage/theme": "^0.1.1-alpha.8" + "@backstage/theme": "^0.1.1-alpha.9" }, "devDependencies": { "@storybook/addon-actions": "^5.3.17", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index a82ec779b8..a116738984 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.8", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/core-api": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/core-api": "^0.1.1-alpha.9", "@backstage/test-utils-core": "^0.1.1-alpha.7", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", diff --git a/packages/theme/package.json b/packages/theme/package.json index 95a7605c05..72a10eb1f3 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.8", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", @@ -32,7 +32,7 @@ "@material-ui/core": "^4.9.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8" + "@backstage/cli": "^0.1.1-alpha.9" }, "files": [ "dist/**/*.{js,d.ts}" diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c1d6064188..5055e09a2c 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.8", + "@backstage/backend-common": "^0.1.1-alpha.9", "@types/cookie-parser": "^1.4.2", "@types/jwt-decode": "2.2.1", "@types/passport": "^1.0.3", @@ -44,7 +44,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "@types/body-parser": "^1.19.0", "@types/passport-saml": "^1.1.2", "jest-fetch-mock": "^3.0.3" diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 895be5a2f7..58e1006e24 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "mock-data": "./scripts/mock-data" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.8", - "@backstage/catalog-model": "^0.1.1-alpha.8", + "@backstage/backend-common": "^0.1.1-alpha.9", + "@backstage/catalog-model": "^0.1.1-alpha.9", "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", @@ -38,7 +38,7 @@ "yup": "^0.28.5" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "@types/lodash": "^4.14.151", "@types/node-fetch": "^2.5.7", "@types/supertest": "^2.0.8", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 5584e664f1..54d5737adb 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,11 +22,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.8", - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.8", - "@backstage/plugin-sentry": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/catalog-model": "^0.1.1-alpha.9", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.9", + "@backstage/plugin-sentry": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -39,9 +39,9 @@ "swr": "^0.2.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", - "@backstage/test-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", + "@backstage/test-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index e1cbc7f31f..45a21da798 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -31,8 +31,8 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,8 +47,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 318e8dd1b5..ad3205f514 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,9 +33,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", - "@backstage/test-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", + "@backstage/test-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index ae45cbfbe0..c9dca36e08 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,8 +33,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 0a6f25e616..49293acbfb 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.8", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", @@ -32,8 +32,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -44,9 +44,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", - "@backstage/test-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", + "@backstage/test-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index aebdd26083..7f3ad341a3 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-identity-backend", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.8", + "@backstage/backend-common": "^0.1.1-alpha.9", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -32,7 +32,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index f53ae0930d..a3c65d05ea 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,9 +34,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", - "@backstage/test-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", + "@backstage/test-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index ec31edb58d..d999ab1dcc 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,10 +22,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.8", - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/plugin-catalog": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/catalog-model": "^0.1.1-alpha.9", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/plugin-catalog": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -37,8 +37,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 38eb7f0b09..a4003db28b 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ "mock-data": "./scripts/mock-data" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.8", + "@backstage/backend-common": "^0.1.1-alpha.9", "compression": "^1.7.4", "cors": "^2.8.5", "dockerode": "^3.2.0", @@ -34,7 +34,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "@types/fs-extra": "^9.0.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2" diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 8ff5d586b4..84132a42f9 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,8 +33,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index 3f159a62cd..a1a93c6710 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry-backend", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.8", + "@backstage/backend-common": "^0.1.1-alpha.9", "axios": "^0.19.2", "compression": "^1.7.4", "cors": "^2.8.5", @@ -33,7 +33,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index bc99bdfbbb..cea86234b0 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,8 +34,8 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 00d008bbd1..a0c5cf52e8 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,9 +22,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", "@backstage/test-utils-core": "^0.1.1-alpha.7", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -37,8 +37,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 5c9f83000e..6780f7df98 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.1.1-alpha.8", + "version": "0.1.1-alpha.9", "main": "dist/index.esm.js", "main:src": "src/index.ts", "types": "src/index.ts", @@ -22,8 +22,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.8", - "@backstage/theme": "^0.1.1-alpha.8", + "@backstage/core": "^0.1.1-alpha.9", + "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,8 +33,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.8", - "@backstage/dev-utils": "^0.1.1-alpha.8", + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", From 03bbb41f2cf1a4767e12932b9c47fdffdf41b330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 17 Jun 2020 15:47:08 +0200 Subject: [PATCH 32/60] fix: move deps to where they need to be --- packages/catalog-model/package.json | 3 ++- plugins/auth-backend/package.json | 11 ++++++----- plugins/catalog-backend/package.json | 1 + plugins/circleci/package.json | 2 +- plugins/gitops-profiles/src/api.ts | 2 +- .../components/ProfileCatalog/ProfileCatalog.test.tsx | 2 +- plugins/identity-backend/package.json | 1 + plugins/scaffolder-backend/package.json | 1 + plugins/sentry-backend/package.json | 1 + plugins/sentry/package.json | 1 + 10 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 0810479f0c..60208aeebb 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -22,14 +22,15 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@types/yup": "^0.28.2", "lodash": "^4.17.15", "yup": "^0.28.5" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.9", + "@types/express": "^4.17.6", "@types/jest": "^25.2.2", "@types/lodash": "^4.14.151", - "@types/yup": "^0.28.2", "yaml": "^1.9.2" }, "files": [ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 5055e09a2c..e0f9252773 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -21,11 +21,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", - "@types/cookie-parser": "^1.4.2", - "@types/jwt-decode": "2.2.1", - "@types/passport": "^1.0.3", - "@types/passport-github2": "^1.2.4", - "@types/passport-google-oauth20": "^2.0.3", + "@types/express": "^4.17.6", "body-parser": "^1.19.0", "compression": "^1.7.4", "cookie-parser": "^1.4.5", @@ -46,7 +42,12 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.9", "@types/body-parser": "^1.19.0", + "@types/cookie-parser": "^1.4.2", + "@types/jwt-decode": "2.2.1", + "@types/passport-github2": "^1.2.4", + "@types/passport-google-oauth20": "^2.0.3", "@types/passport-saml": "^1.1.2", + "@types/passport": "^1.0.3", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 58e1006e24..a12aa4e21d 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -23,6 +23,7 @@ "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", "@backstage/catalog-model": "^0.1.1-alpha.9", + "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 45a21da798..0104c5cc7b 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -36,7 +36,6 @@ "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "@types/react-lazylog": "^4.5.0", "circleci-api": "^4.0.0", "moment": "^2.25.3", "react": "^16.13.1", @@ -54,6 +53,7 @@ "@testing-library/user-event": "^10.2.4", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", + "@types/react-lazylog": "^4.5.0", "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/gitops-profiles/src/api.ts b/plugins/gitops-profiles/src/api.ts index ad95b1cd52..20e4dc4307 100644 --- a/plugins/gitops-profiles/src/api.ts +++ b/plugins/gitops-profiles/src/api.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '@backstage/core-api'; +import { createApiRef } from '@backstage/core'; export interface CloneFromTemplateRequest { templateRepository: string; diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx index d7e65c2a26..463e493afc 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx @@ -20,7 +20,7 @@ import mockFetch from 'jest-fetch-mock'; import ProfileCatalog from './ProfileCatalog'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { ApiProvider, ApiRegistry } from '@backstage/core-api'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; import { gitOpsApiRef, GitOpsRestApi } from '../../api'; describe('ProfileCatalog', () => { diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index 7f3ad341a3..9c2ca5341f 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", + "@types/express": "^4.17.6", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index a4003db28b..d74c8ffdb6 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", + "@types/express": "^4.17.6", "compression": "^1.7.4", "cors": "^2.8.5", "dockerode": "^3.2.0", diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index a1a93c6710..a23cc1ab09 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", + "@types/express": "^4.17.6", "axios": "^0.19.2", "compression": "^1.7.4", "cors": "^2.8.5", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index cea86234b0..7a8197ab87 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -27,6 +27,7 @@ "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "@types/react": "^16.9", "react": "^16.13.1", "react-dom": "^16.13.1", "react-sparklines": "^1.7.0", From 4a1fa314af2b825dabff845207fa2fae9cbefc64 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 17 Jun 2020 16:31:22 +0200 Subject: [PATCH 33/60] Consume identity data from ts instead of Json --- .../src/adapters/StaticJsonAdapter.ts | 13 ++----- .../src/adapters/data/userGroups.json | 20 ----------- .../src/adapters/userGroups.ts | 35 +++++++++++++++++++ .../identity-backend/src/service/router.ts | 3 +- 4 files changed, 40 insertions(+), 31 deletions(-) delete mode 100644 plugins/identity-backend/src/adapters/data/userGroups.json create mode 100644 plugins/identity-backend/src/adapters/userGroups.ts diff --git a/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts index cd2a40ea78..9a58da2e85 100644 --- a/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts +++ b/plugins/identity-backend/src/adapters/StaticJsonAdapter.ts @@ -16,24 +16,17 @@ import { Group, - GroupsJson, GroupsResponse, IdentityApi, GroupsRequest, + GroupsJson, } from './types'; -import fs from 'fs-extra'; -import path from 'path'; - -const GROUPS_JSON_FILE = path.join(__dirname, 'data', 'userGroups.json'); export class StaticJsonAdapter implements IdentityApi { private readonly groups: Group[]; - constructor() { - const groupsJson: GroupsJson = fs.readJsonSync(GROUPS_JSON_FILE, { - encoding: 'utf8', - }); - this.groups = groupsJson.groups; + constructor(userGroups: GroupsJson) { + this.groups = userGroups.groups; } getUserGroups(req: GroupsRequest): Promise { diff --git a/plugins/identity-backend/src/adapters/data/userGroups.json b/plugins/identity-backend/src/adapters/data/userGroups.json deleted file mode 100644 index 9ab83480df..0000000000 --- a/plugins/identity-backend/src/adapters/data/userGroups.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "groups": [ - { - "name": "engineering", - "type": "org", - "children": [ - { - "name": "authentication", - "type": "team", - "members": [{ "name": "kent" }, { "name": "dobbs" }] - }, - { - "name": "checkout", - "type": "team", - "members": [{ "name": "don" }, { "name": "abramev" }] - } - ] - } - ] -} diff --git a/plugins/identity-backend/src/adapters/userGroups.ts b/plugins/identity-backend/src/adapters/userGroups.ts new file mode 100644 index 0000000000..806ca1f43f --- /dev/null +++ b/plugins/identity-backend/src/adapters/userGroups.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. + */ +export const userGroups = { + groups: [ + { + name: 'engineering', + type: 'org', + children: [ + { + name: 'authentication', + type: 'team', + members: [{ name: 'kent' }, { name: 'dobbs' }], + }, + { + name: 'checkout', + type: 'team', + members: [{ name: 'don' }, { name: 'abramev' }], + }, + ], + }, + ], +}; diff --git a/plugins/identity-backend/src/service/router.ts b/plugins/identity-backend/src/service/router.ts index 256ef6ce3c..9c6515ac04 100644 --- a/plugins/identity-backend/src/service/router.ts +++ b/plugins/identity-backend/src/service/router.ts @@ -19,6 +19,7 @@ import Router from 'express-promise-router'; import { Logger } from 'winston'; import { StaticJsonAdapter } from '../adapters'; import { IdentityApi } from '../adapters/types'; +import { userGroups } from '../adapters/userGroups'; export interface RouterOptions { logger: Logger; @@ -42,6 +43,6 @@ export async function createRouter( const logger = options.logger; logger.info('Initializing identity API backend'); - const adapter = new StaticJsonAdapter(); + const adapter = new StaticJsonAdapter(userGroups); return makeRouter(adapter); } From 455362285d426c5007ed0d04aca698564f79ad1f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2020 16:35:33 +0200 Subject: [PATCH 34/60] build(deps): bump swr from 0.2.2 to 0.2.3 (#1326) Bumps [swr](https://github.com/zeit/swr) from 0.2.2 to 0.2.3. - [Release notes](https://github.com/zeit/swr/releases) - [Commits](https://github.com/zeit/swr/compare/0.2.2...0.2.3) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ebcd9a20a2..b3b0af2615 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17767,9 +17767,9 @@ svgo@^1.0.0, svgo@^1.2.2: util.promisify "~1.0.0" swr@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/swr/-/swr-0.2.2.tgz#6e1b3e5c0e545c4fdb36ae3aa38cd94d0f9a88b7" - integrity sha512-D/z+PTUchZhoUA0tNC8TNJivf7Hc61WPxbUdXPi+VxRloddWYNP1ZicaEgyAph42ZnKl1L7twcZr4q6d0UMXcg== + version "0.2.3" + resolved "https://registry.npmjs.org/swr/-/swr-0.2.3.tgz#e0fb260d27f12fafa2388312083368f45127480d" + integrity sha512-JhuuD5ojqgjAQpZAhoPBd8Di0Mr1+ykByVKuRJdtKaxkUX/y8kMACWKkLgLQc8pcDOKEAnbIreNjU7HfqI9nHQ== dependencies: fast-deep-equal "2.0.1" From aae6f1503a300d57cf93b9072027081b8cb43295 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 16:35:33 +0200 Subject: [PATCH 35/60] packages: bump lagging package versions --- packages/cli/package.json | 2 +- packages/config-loader/package.json | 2 +- packages/config/package.json | 2 +- packages/core-api/package.json | 4 ++-- packages/core/package.json | 2 +- packages/test-utils-core/package.json | 2 +- packages/test-utils/package.json | 2 +- plugins/tech-radar/package.json | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index c686e3feb8..96ca8e4a4f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,7 +29,7 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.7", + "@backstage/config": "0.1.1-alpha.9", "@backstage/config-loader": "^0.1.1-alpha.9", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index df198a5039..01aa0ed735 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.7", + "@backstage/config": "0.1.1-alpha.9", "fs-extra": "^9.0.0", "yaml": "^1.9.2", "yup": "^0.28.5" diff --git a/packages/config/package.json b/packages/config/package.json index c67264d4c9..8bf4001e62 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.1-alpha.7", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 0cc50143a5..2659a23ef6 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.7", + "@backstage/config": "0.1.1-alpha.9", "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", @@ -43,7 +43,7 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.9", - "@backstage/test-utils-core": "^0.1.1-alpha.7", + "@backstage/test-utils-core": "0.1.1-alpha.9", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/packages/core/package.json b/packages/core/package.json index 7f11230405..69b0590802 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.7", + "@backstage/config": "0.1.1-alpha.9", "@backstage/core-api": "^0.1.1-alpha.9", "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index f0b98add94..a71a23c844 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.7", + "version": "0.1.1-alpha.9", "private": false, "publishConfig": { "access": "public", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index a116738984..7c5a229aba 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -32,7 +32,7 @@ "dependencies": { "@backstage/cli": "^0.1.1-alpha.9", "@backstage/core-api": "^0.1.1-alpha.9", - "@backstage/test-utils-core": "^0.1.1-alpha.7", + "@backstage/test-utils-core": "0.1.1-alpha.9", "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@testing-library/jest-dom": "^5.7.0", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index a0c5cf52e8..e0146e8a72 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@backstage/core": "^0.1.1-alpha.9", - "@backstage/test-utils-core": "^0.1.1-alpha.7", + "@backstage/test-utils-core": "0.1.1-alpha.9", "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", From 64a8bd21877330c7241da6dc63051511a46aaf5c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 16:52:57 +0200 Subject: [PATCH 36/60] packages/backend: load config and make it part of plugin env --- packages/backend/package.json | 2 ++ packages/backend/src/index.ts | 30 +++++++++++++++++++----------- packages/backend/src/types.ts | 2 ++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 7a0ad95e67..516208bf3b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -19,6 +19,8 @@ "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", "@backstage/catalog-model": "^0.1.1-alpha.9", + "@backstage/config": "^0.1.1-alpha.9", + "@backstage/config-loader": "^0.1.1-alpha.9", "@backstage/plugin-auth-backend": "^0.1.1-alpha.9", "@backstage/plugin-catalog-backend": "^0.1.1-alpha.9", "@backstage/plugin-identity-backend": "^0.1.1-alpha.9", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 0a9d852222..df0d08fa7d 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -27,6 +27,8 @@ import { getRootLogger, useHotMemoize, } from '@backstage/backend-common'; +import { ConfigReader, AppConfig } from '@backstage/config'; +import { loadConfig } from '@backstage/config-loader'; import knex from 'knex'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; @@ -35,20 +37,26 @@ import scaffolder from './plugins/scaffolder'; import sentry from './plugins/sentry'; import { PluginEnvironment } from './types'; -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 }; +function makeCreateEnv(loadedConfigs: AppConfig[]) { + const config = ConfigReader.fromConfigs(loadedConfigs); + + return (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, config }; + }; } async function main() { + const createEnv = makeCreateEnv(await loadConfig()); + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); const authEnv = useHotMemoize(module, () => createEnv('auth')); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index ce681a6bdd..f7df3d05c6 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -16,8 +16,10 @@ import Knex from 'knex'; import { Logger } from 'winston'; +import { Config } from '@backstage/config'; export type PluginEnvironment = { logger: Logger; database: Knex; + config: Config; }; From e923d751a754d2e8391ff2453851aebea10baa1f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 16:54:02 +0200 Subject: [PATCH 37/60] plugins/auth-backend: read backend.baseUrl from config --- packages/backend/src/plugins/auth.ts | 7 +++++-- plugins/auth-backend/package.json | 1 + plugins/auth-backend/src/service/router.ts | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 7cf9610dc2..a9c687cbc4 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -17,6 +17,9 @@ import { createRouter } from '@backstage/plugin-auth-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin({ logger }: PluginEnvironment) { - return await createRouter({ logger }); +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); } diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index e0f9252773..7b448df126 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", + "@backstage/config": "^0.1.1-alpha.9", "@types/express": "^4.17.6", "body-parser": "^1.19.0", "compression": "^1.7.4", diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 20c35a4084..2e7b4623ff 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -20,9 +20,11 @@ import cookieParser from 'cookie-parser'; import bodyParser from 'body-parser'; import { Logger } from 'winston'; import { createAuthProviderRouter } from '../providers'; +import { Config } from '@backstage/config'; export interface RouterOptions { logger: Logger; + config: Config; } export async function createRouter( @@ -38,7 +40,7 @@ export async function createRouter( // TODO: read from app config const config = { backend: { - baseUrl: 'http://localhost:7000', + baseUrl: options.config.getString('backend.baseUrl'), }, auth: { providers: { @@ -77,7 +79,7 @@ export async function createRouter( const providerConfigs = config.auth.providers; for (const [providerId, providerConfig] of Object.entries(providerConfigs)) { - const baseUrl = `${config.backend.baseUrl}/auth`; + const baseUrl = `${options.config.getString('backend.baseUrl')}/auth`; logger.info(`Configuring provider, ${providerId}`); try { const providerRouter = createAuthProviderRouter( From 034c04c25934171750dff00549003eef482d553a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 17:06:03 +0200 Subject: [PATCH 38/60] plugins/auth-backend: load config in standalone environment --- plugins/auth-backend/package.json | 1 + plugins/auth-backend/src/service/standaloneApplication.ts | 6 ++++-- plugins/auth-backend/src/service/standaloneServer.ts | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 7b448df126..d22a5ef6d6 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -22,6 +22,7 @@ "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.9", "@backstage/config": "^0.1.1-alpha.9", + "@backstage/config-loader": "^0.1.1-alpha.9", "@types/express": "^4.17.6", "body-parser": "^1.19.0", "compression": "^1.7.4", diff --git a/plugins/auth-backend/src/service/standaloneApplication.ts b/plugins/auth-backend/src/service/standaloneApplication.ts index 5eea4fb8f5..cd81e32ccb 100644 --- a/plugins/auth-backend/src/service/standaloneApplication.ts +++ b/plugins/auth-backend/src/service/standaloneApplication.ts @@ -19,6 +19,7 @@ import { notFoundHandler, requestLoggingHandler, } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; import express from 'express'; @@ -29,12 +30,13 @@ import { createRouter } from './router'; export interface ApplicationOptions { enableCors: boolean; logger: Logger; + config: Config; } export async function createStandaloneApplication( options: ApplicationOptions, ): Promise { - const { enableCors, logger } = options; + const { enableCors, logger, config } = options; const app = express(); app.use(helmet()); @@ -44,7 +46,7 @@ export async function createStandaloneApplication( app.use(compression()); app.use(express.json()); app.use(requestLoggingHandler()); - app.use('/', await createRouter({ logger })); + app.use('/', await createRouter({ logger, config })); app.use(notFoundHandler()); app.use(errorHandler()); diff --git a/plugins/auth-backend/src/service/standaloneServer.ts b/plugins/auth-backend/src/service/standaloneServer.ts index be1021d604..c03fe44b40 100644 --- a/plugins/auth-backend/src/service/standaloneServer.ts +++ b/plugins/auth-backend/src/service/standaloneServer.ts @@ -17,6 +17,8 @@ import { Server } from 'http'; import { Logger } from 'winston'; import { createStandaloneApplication } from './standaloneApplication'; +import { ConfigReader } from '@backstage/config'; +import { loadConfig } from '@backstage/config-loader'; export interface ServerOptions { port: number; @@ -28,11 +30,13 @@ export async function startStandaloneServer( options: ServerOptions, ): Promise { const logger = options.logger.child({ service: 'auth-backend' }); + const config = ConfigReader.fromConfigs(await loadConfig()); logger.debug('Creating application...'); const app = await createStandaloneApplication({ enableCors: options.enableCors, logger, + config, }); logger.debug('Starting application server...'); From 9793bebce4ac675e9cf377723d951baf58328a3c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 16:58:09 +0200 Subject: [PATCH 39/60] packages/core-api: re-use Config type from config package --- .../core-api/src/apis/definitions/ConfigApi.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts index 20676df899..63a588fc10 100644 --- a/packages/core-api/src/apis/definitions/ConfigApi.ts +++ b/packages/core-api/src/apis/definitions/ConfigApi.ts @@ -14,20 +14,7 @@ * limitations under the License. */ import { createApiRef } from '../ApiRef'; - -export type Config = { - getConfig(key: string): Config; - - getConfigArray(key: string): Config[]; - - getNumber(key: string): number | undefined; - - getBoolean(key: string): boolean | undefined; - - getString(key: string): string | undefined; - - getStringArray(key: string): string[] | undefined; -}; +import { Config } from '@backstage/config'; // Using interface to make the ConfigApi name show up in docs export interface ConfigApi extends Config {} From 31585800a4f320a76731f92259b3112e9518be1f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 17:15:31 +0200 Subject: [PATCH 40/60] packages/config: added must* variant for reading required primitive values --- packages/config/src/reader.test.ts | 25 +++++++++++++++++++++++ packages/config/src/reader.ts | 32 ++++++++++++++++++++++++++++++ packages/config/src/types.ts | 4 ++++ 3 files changed, 61 insertions(+) diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 3d3287dfe7..e33939bb4b 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -49,6 +49,10 @@ function expectValidValues(config: ConfigReader) { 'string1', 'string2', ]); + expect(config.mustNumber('zero')).toBe(0); + expect(config.mustBoolean('true')).toBe(true); + expect(config.mustString('string')).toBe('string'); + expect(config.mustStringArray('strings')).toEqual(['string1', 'string2']); const [config1, config2, config3] = config.getConfigArray('nestlings'); expect(config1.getBoolean('boolean')).toBe(true); @@ -57,6 +61,9 @@ function expectValidValues(config: ConfigReader) { } function expectInvalidValues(config: ConfigReader) { + expect(() => config.getBoolean('string')).toThrow( + 'Invalid type in config for key string, got string, wanted boolean', + ); expect(() => config.getNumber('string')).toThrow( 'Invalid type in config for key string, got string, wanted number', ); @@ -87,6 +94,18 @@ function expectInvalidValues(config: ConfigReader) { expect(() => config.getConfigArray('one')).toThrow( 'Invalid type in config for key one, got number, wanted object-array', ); + expect(() => config.mustBoolean('missing')).toThrow( + "Missing required config value at 'missing'", + ); + expect(() => config.mustNumber('missing')).toThrow( + "Missing required config value at 'missing'", + ); + expect(() => config.mustString('missing')).toThrow( + "Missing required config value at 'missing'", + ); + expect(() => config.mustStringArray('missing')).toThrow( + "Missing required config value at 'missing'", + ); } describe('ConfigReader', () => { @@ -210,6 +229,12 @@ describe('ConfigReader with fallback', () => { // Config arrays aren't merged either expect(config.getConfigArray('merged.configs').length).toBe(1); expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a'); + expect(config.getConfigArray('merged.configs')[0].mustString('a')).toBe( + 'a', + ); + expect(() => + config.getConfigArray('merged.configs')[0].mustString('missing'), + ).toThrow("Missing required config value at 'missing'"); expect( config.getConfigArray('merged.configs')[0].getString('b'), ).toBeUndefined(); diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 95c4f47383..7878a392ff 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -92,6 +92,14 @@ export class ConfigReader implements Config { return (configs ?? []).map(obj => new ConfigReader(obj)); } + mustNumber(key: string): number { + const value = this.getNumber(key); + if (value === undefined) { + throw new Error(`Missing required config value at '${key}'`); + } + return value; + } + getNumber(key: string): number | undefined { return this.readConfigValue( key, @@ -99,6 +107,14 @@ export class ConfigReader implements Config { ); } + mustBoolean(key: string): boolean { + const value = this.getBoolean(key); + if (value === undefined) { + throw new Error(`Missing required config value at '${key}'`); + } + return value; + } + getBoolean(key: string): boolean | undefined { return this.readConfigValue( key, @@ -106,6 +122,14 @@ export class ConfigReader implements Config { ); } + mustString(key: string): string { + const value = this.getString(key); + if (value === undefined) { + throw new Error(`Missing required config value at '${key}'`); + } + return value; + } + getString(key: string): string | undefined { return this.readConfigValue( key, @@ -114,6 +138,14 @@ export class ConfigReader implements Config { ); } + mustStringArray(key: string): string[] { + const value = this.getStringArray(key); + if (value === undefined) { + throw new Error(`Missing required config value at '${key}'`); + } + return value; + } + getStringArray(key: string): string[] | undefined { return this.readConfigValue(key, values => { if (!Array.isArray(values)) { diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 9eda6c8b32..73b8e42130 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -32,10 +32,14 @@ export type Config = { getConfigArray(key: string): Config[]; getNumber(key: string): number | undefined; + mustNumber(key: string): number; getBoolean(key: string): boolean | undefined; + mustBoolean(key: string): boolean; getString(key: string): string | undefined; + mustString(key: string): string; getStringArray(key: string): string[] | undefined; + mustStringArray(key: string): string[]; }; From 32971c811d80534fe452aa8fc2561c362eee3e0c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 17:45:18 +0200 Subject: [PATCH 41/60] packages/core-api: fix invocation order when continuing to app after sign-in --- packages/core-api/src/app/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 163a546943..849db24648 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -265,8 +265,8 @@ export class PrivateAppImpl implements BackstageApp { if (done) { throw new Error('Identity result callback was called twice'); } - setDone(true); this.identityApi.setSignInResult(result); + setDone(true); }, [done], ); From 3dcef70e3c100c059afe1de4c774c32564b8f938 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 20:10:44 +0200 Subject: [PATCH 42/60] packages/config: flip around must* and get* to get* and getOptional* --- packages/cli/src/lib/bundler/config.ts | 6 --- packages/config/src/reader.test.ts | 45 ++++++++++--------- packages/config/src/reader.ts | 24 +++++----- packages/config/src/types.ts | 16 +++---- packages/core-api/src/app/App.tsx | 2 +- .../core/src/layout/SignInPage/SignInPage.tsx | 2 +- .../components/WelcomePage/WelcomePage.tsx | 3 +- 7 files changed, 47 insertions(+), 51 deletions(-) diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 50d174368a..d5fb674ec1 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -33,9 +33,6 @@ import { BundlingOptions, BackendBundlingOptions } from './types'; export function resolveBaseUrl(config: Config): URL { const baseUrl = config.getString('app.baseUrl'); - if (!baseUrl) { - throw new Error('app.baseUrl must be set in config'); - } try { return new URL(baseUrl, 'http://localhost:3000'); } catch (error) { @@ -52,9 +49,6 @@ export function createConfig( const { plugins, loaders } = transforms(options); const baseUrl = options.config.getString('app.baseUrl'); - if (!baseUrl) { - throw new Error('app.baseUrl must be set in config'); - } const validBaseUrl = new URL(baseUrl, 'https://backstage-app.dev'); if (checksEnabled) { diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index e33939bb4b..f7804d5d49 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -49,10 +49,10 @@ function expectValidValues(config: ConfigReader) { 'string1', 'string2', ]); - expect(config.mustNumber('zero')).toBe(0); - expect(config.mustBoolean('true')).toBe(true); - expect(config.mustString('string')).toBe('string'); - expect(config.mustStringArray('strings')).toEqual(['string1', 'string2']); + expect(config.getNumber('zero')).toBe(0); + expect(config.getBoolean('true')).toBe(true); + expect(config.getString('string')).toBe('string'); + expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); const [config1, config2, config3] = config.getConfigArray('nestlings'); expect(config1.getBoolean('boolean')).toBe(true); @@ -94,16 +94,16 @@ function expectInvalidValues(config: ConfigReader) { expect(() => config.getConfigArray('one')).toThrow( 'Invalid type in config for key one, got number, wanted object-array', ); - expect(() => config.mustBoolean('missing')).toThrow( + expect(() => config.getBoolean('missing')).toThrow( "Missing required config value at 'missing'", ); - expect(() => config.mustNumber('missing')).toThrow( + expect(() => config.getNumber('missing')).toThrow( "Missing required config value at 'missing'", ); - expect(() => config.mustString('missing')).toThrow( + expect(() => config.getString('missing')).toThrow( "Missing required config value at 'missing'", ); - expect(() => config.mustStringArray('missing')).toThrow( + expect(() => config.getStringArray('missing')).toThrow( "Missing required config value at 'missing'", ); } @@ -111,13 +111,13 @@ function expectInvalidValues(config: ConfigReader) { describe('ConfigReader', () => { it('should read empty config with valid keys', () => { const config = new ConfigReader({}); - expect(config.getString('x')).toBeUndefined(); - expect(config.getString('x_x')).toBeUndefined(); - expect(config.getString('x-X')).toBeUndefined(); - expect(config.getString('x0')).toBeUndefined(); - expect(config.getString('X-x2')).toBeUndefined(); - expect(config.getString('x0_x0')).toBeUndefined(); - expect(config.getString('x_x-x_x')).toBeUndefined(); + expect(config.getOptionalString('x')).toBeUndefined(); + expect(config.getOptionalString('x_x')).toBeUndefined(); + expect(config.getOptionalString('x-X')).toBeUndefined(); + expect(config.getOptionalString('x0')).toBeUndefined(); + expect(config.getOptionalString('X-x2')).toBeUndefined(); + expect(config.getOptionalString('x0_x0')).toBeUndefined(); + expect(config.getOptionalString('x_x-x_x')).toBeUndefined(); }); it('should throw on invalid keys', () => { @@ -154,7 +154,7 @@ describe('ConfigReader', () => { describe('ConfigReader with fallback', () => { it('should behave as if without fallback', () => { const config = new ConfigReader({}, new ConfigReader(DATA)); - expect(config.getString('x')).toBeUndefined(); + expect(config.getOptionalString('x')).toBeUndefined(); expect(() => config.getString('.')).toThrow(/^Invalid config key/); expect(() => config.getString('a.')).toThrow(/^Invalid config key/); }); @@ -229,14 +229,12 @@ describe('ConfigReader with fallback', () => { // Config arrays aren't merged either expect(config.getConfigArray('merged.configs').length).toBe(1); expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a'); - expect(config.getConfigArray('merged.configs')[0].mustString('a')).toBe( - 'a', - ); + expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a'); expect(() => - config.getConfigArray('merged.configs')[0].mustString('missing'), + config.getConfigArray('merged.configs')[0].getString('missing'), ).toThrow("Missing required config value at 'missing'"); expect( - config.getConfigArray('merged.configs')[0].getString('b'), + config.getConfigArray('merged.configs')[0].getOptionalString('b'), ).toBeUndefined(); // Config arrays aren't merged either @@ -245,7 +243,10 @@ describe('ConfigReader with fallback', () => { config.getConfig('merged').getConfigArray('configs')[0].getString('a'), ).toBe('a'); expect( - config.getConfig('merged').getConfigArray('configs')[0].getString('b'), + config + .getConfig('merged') + .getConfigArray('configs')[0] + .getOptionalString('b'), ).toBeUndefined(); }); }); diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 7878a392ff..ff7c8be6e5 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -92,45 +92,45 @@ export class ConfigReader implements Config { return (configs ?? []).map(obj => new ConfigReader(obj)); } - mustNumber(key: string): number { - const value = this.getNumber(key); + getNumber(key: string): number { + const value = this.getOptionalNumber(key); if (value === undefined) { throw new Error(`Missing required config value at '${key}'`); } return value; } - getNumber(key: string): number | undefined { + getOptionalNumber(key: string): number | undefined { return this.readConfigValue( key, value => typeof value === 'number' || { expected: 'number' }, ); } - mustBoolean(key: string): boolean { - const value = this.getBoolean(key); + getBoolean(key: string): boolean { + const value = this.getOptionalBoolean(key); if (value === undefined) { throw new Error(`Missing required config value at '${key}'`); } return value; } - getBoolean(key: string): boolean | undefined { + getOptionalBoolean(key: string): boolean | undefined { return this.readConfigValue( key, value => typeof value === 'boolean' || { expected: 'boolean' }, ); } - mustString(key: string): string { - const value = this.getString(key); + getString(key: string): string { + const value = this.getOptionalString(key); if (value === undefined) { throw new Error(`Missing required config value at '${key}'`); } return value; } - getString(key: string): string | undefined { + getOptionalString(key: string): string | undefined { return this.readConfigValue( key, value => @@ -138,15 +138,15 @@ export class ConfigReader implements Config { ); } - mustStringArray(key: string): string[] { - const value = this.getStringArray(key); + getStringArray(key: string): string[] { + const value = this.getOptionalStringArray(key); if (value === undefined) { throw new Error(`Missing required config value at '${key}'`); } return value; } - getStringArray(key: string): string[] | undefined { + getOptionalStringArray(key: string): string[] | undefined { return this.readConfigValue(key, values => { if (!Array.isArray(values)) { return { expected: 'string-array' }; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 73b8e42130..03c9dc4383 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -31,15 +31,15 @@ export type Config = { getConfigArray(key: string): Config[]; - getNumber(key: string): number | undefined; - mustNumber(key: string): number; + getNumber(key: string): number; + getOptionalNumber(key: string): number | undefined; - getBoolean(key: string): boolean | undefined; - mustBoolean(key: string): boolean; + getBoolean(key: string): boolean; + getOptionalBoolean(key: string): boolean | undefined; - getString(key: string): string | undefined; - mustString(key: string): string; + getString(key: string): string; + getOptionalString(key: string): string | undefined; - getStringArray(key: string): string[] | undefined; - mustStringArray(key: string): string[]; + getStringArray(key: string): string[]; + getOptionalStringArray(key: string): string[] | undefined; }; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 163a546943..12f36a6e8b 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -282,7 +282,7 @@ export class PrivateAppImpl implements BackstageApp { const configApi = useApi(configApiRef); let { pathname } = new URL( - configApi.getString('app.baseUrl') ?? '/', + configApi.getOptionalString('app.baseUrl') ?? '/', 'http://dummy.dev', // baseUrl can be specified as just a path ); if (pathname.endsWith('/')) { diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 91bc251f90..5911fe86b2 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -39,7 +39,7 @@ export const SignInPage: FC = ({ onResult, providers }) => { return ( -
+
{providerElements} diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx index 6a1b0e0008..ce151737b3 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx @@ -39,7 +39,8 @@ import { } from '@backstage/core'; const WelcomePage: FC<{}> = () => { - const appTitle = useApi(configApiRef).getString('app.title') ?? 'Backstage'; + const appTitle = + useApi(configApiRef).getOptionalString('app.title') ?? 'Backstage'; const profile = { givenName: '' }; return ( From c9ee24b8bb296a6df075a3f56d7de03a739d1aca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 17:28:26 +0200 Subject: [PATCH 43/60] packages/config: allow config readers to be backed by undefined data --- packages/config/src/reader.test.ts | 6 ++++++ packages/config/src/reader.ts | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index f7804d5d49..2e61a34512 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -118,6 +118,8 @@ describe('ConfigReader', () => { expect(config.getOptionalString('X-x2')).toBeUndefined(); expect(config.getOptionalString('x0_x0')).toBeUndefined(); expect(config.getOptionalString('x_x-x_x')).toBeUndefined(); + + expect(new ConfigReader(undefined).getOptionalString('x')).toBeUndefined(); }); it('should throw on invalid keys', () => { @@ -138,6 +140,10 @@ describe('ConfigReader', () => { expect(() => config.getString('a.a.a.a.')).toThrow(/^Invalid config key/); expect(() => config.getString('a._')).toThrow(/^Invalid config key/); expect(() => config.getString('a.-.a')).toThrow(/^Invalid config key/); + + expect(() => new ConfigReader(undefined).getString('.')).toThrow( + /^Invalid config key/, + ); }); it('should read valid values', () => { diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index ff7c8be6e5..f1ed8a4f3d 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -40,11 +40,9 @@ function typeOf(value: JsonValue | undefined): string { } export class ConfigReader implements Config { - private static readonly nullReader = new ConfigReader({}); - static fromConfigs(configs: AppConfig[]): ConfigReader { if (configs.length === 0) { - return new ConfigReader({}); + return new ConfigReader(undefined); } // Merge together all configs info a single config with recursive fallback @@ -55,13 +53,14 @@ export class ConfigReader implements Config { } constructor( - private readonly data: JsonObject, + private readonly data: JsonObject | undefined, private readonly fallback?: ConfigReader, ) {} getConfig(key: string): ConfigReader { const value = this.readValue(key); const fallbackConfig = this.fallback?.getConfig(key); + if (isObject(value)) { return new ConfigReader(value, fallbackConfig); } @@ -72,7 +71,7 @@ export class ConfigReader implements Config { )}, wanted object`, ); } - return fallbackConfig ?? ConfigReader.nullReader; + return fallbackConfig ?? new ConfigReader(undefined, undefined); } getConfigArray(key: string): ConfigReader[] { @@ -191,12 +190,18 @@ export class ConfigReader implements Config { private readValue(key: string): JsonValue | undefined { const parts = key.split('.'); - - let value: JsonValue | undefined = this.data; for (const part of parts) { if (!CONFIG_KEY_PART_PATTERN.test(part)) { throw new TypeError(`Invalid config key '${key}'`); } + } + + if (this.data === undefined) { + return undefined; + } + + let value: JsonValue | undefined = this.data; + for (const part of parts) { if (isObject(value)) { value = value[part]; } else { From 5294d71fe914d5f7f70dd9ab9281c5a44bb3ec21 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 17:35:16 +0200 Subject: [PATCH 44/60] packages/config: optimize some error message handling --- packages/config/src/reader.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index f1ed8a4f3d..2c2b560536 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -39,6 +39,16 @@ function typeOf(value: JsonValue | undefined): string { return type; } +// Separate out a couple of common error messages to reduce bundle size. +const errors = { + type(key: string, typeName: string, expected: string) { + return `Invalid type in config for key ${key}, got ${typeName}, wanted ${expected}`; + }, + missing(key: string) { + return `Missing required config value at '${key}'`; + }, +}; + export class ConfigReader implements Config { static fromConfigs(configs: AppConfig[]): ConfigReader { if (configs.length === 0) { @@ -65,11 +75,7 @@ export class ConfigReader implements Config { return new ConfigReader(value, fallbackConfig); } if (value !== undefined) { - throw new TypeError( - `Invalid type in config for key ${key}, got ${typeOf( - value, - )}, wanted object`, - ); + throw new TypeError(errors.type(key, typeOf(value), 'object')); } return fallbackConfig ?? new ConfigReader(undefined, undefined); } @@ -94,7 +100,7 @@ export class ConfigReader implements Config { getNumber(key: string): number { const value = this.getOptionalNumber(key); if (value === undefined) { - throw new Error(`Missing required config value at '${key}'`); + throw new Error(errors.missing(key)); } return value; } @@ -109,7 +115,7 @@ export class ConfigReader implements Config { getBoolean(key: string): boolean { const value = this.getOptionalBoolean(key); if (value === undefined) { - throw new Error(`Missing required config value at '${key}'`); + throw new Error(errors.missing(key)); } return value; } @@ -124,7 +130,7 @@ export class ConfigReader implements Config { getString(key: string): string { const value = this.getOptionalString(key); if (value === undefined) { - throw new Error(`Missing required config value at '${key}'`); + throw new Error(errors.missing(key)); } return value; } @@ -140,7 +146,7 @@ export class ConfigReader implements Config { getStringArray(key: string): string[] { const value = this.getOptionalStringArray(key); if (value === undefined) { - throw new Error(`Missing required config value at '${key}'`); + throw new Error(errors.missing(key)); } return value; } @@ -178,10 +184,7 @@ export class ConfigReader implements Config { value: theValue = value, expected, } = result; - const typeName = typeOf(theValue); - throw new TypeError( - `Invalid type in config for key ${keyName}, got ${typeName}, wanted ${expected}`, - ); + throw new TypeError(errors.type(keyName, typeOf(theValue), expected)); } } From 625a50989d253eeb9417efac6082d59482281560 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 17:54:11 +0200 Subject: [PATCH 45/60] packages/config: keep track of key prefix to display better error messages --- packages/config/src/reader.test.ts | 5 ++++- packages/config/src/reader.ts | 31 +++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 2e61a34512..f23fad8d3b 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -231,6 +231,9 @@ describe('ConfigReader with fallback', () => { 'a', 'b', ]); + expect(() => config.getConfig('merged').getStringArray('x')).toThrow( + 'Invalid type in config for key merged.x, got string, wanted string-array', + ); // Config arrays aren't merged either expect(config.getConfigArray('merged.configs').length).toBe(1); @@ -238,7 +241,7 @@ describe('ConfigReader with fallback', () => { expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a'); expect(() => config.getConfigArray('merged.configs')[0].getString('missing'), - ).toThrow("Missing required config value at 'missing'"); + ).toThrow("Missing required config value at 'merged.configs[0].missing'"); expect( config.getConfigArray('merged.configs')[0].getOptionalString('b'), ).toBeUndefined(); diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 2c2b560536..e3c349768b 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -65,19 +65,23 @@ export class ConfigReader implements Config { constructor( private readonly data: JsonObject | undefined, private readonly fallback?: ConfigReader, + private readonly prefix: string = '', ) {} getConfig(key: string): ConfigReader { const value = this.readValue(key); const fallbackConfig = this.fallback?.getConfig(key); + const prefix = this.fullKey(key); if (isObject(value)) { - return new ConfigReader(value, fallbackConfig); + return new ConfigReader(value, fallbackConfig, prefix); } if (value !== undefined) { - throw new TypeError(errors.type(key, typeOf(value), 'object')); + throw new TypeError( + errors.type(this.fullKey(key), typeOf(value), 'object'), + ); } - return fallbackConfig ?? new ConfigReader(undefined, undefined); + return fallbackConfig ?? new ConfigReader(undefined, undefined, prefix); } getConfigArray(key: string): ConfigReader[] { @@ -94,13 +98,16 @@ export class ConfigReader implements Config { return true; }); - return (configs ?? []).map(obj => new ConfigReader(obj)); + return (configs ?? []).map( + (obj, index) => + new ConfigReader(obj, undefined, this.fullKey(`${key}[${index}]`)), + ); } getNumber(key: string): number { const value = this.getOptionalNumber(key); if (value === undefined) { - throw new Error(errors.missing(key)); + throw new Error(errors.missing(this.fullKey(key))); } return value; } @@ -115,7 +122,7 @@ export class ConfigReader implements Config { getBoolean(key: string): boolean { const value = this.getOptionalBoolean(key); if (value === undefined) { - throw new Error(errors.missing(key)); + throw new Error(errors.missing(this.fullKey(key))); } return value; } @@ -130,7 +137,7 @@ export class ConfigReader implements Config { getString(key: string): string { const value = this.getOptionalString(key); if (value === undefined) { - throw new Error(errors.missing(key)); + throw new Error(errors.missing(this.fullKey(key))); } return value; } @@ -146,7 +153,7 @@ export class ConfigReader implements Config { getStringArray(key: string): string[] { const value = this.getOptionalStringArray(key); if (value === undefined) { - throw new Error(errors.missing(key)); + throw new Error(errors.missing(this.fullKey(key))); } return value; } @@ -165,6 +172,10 @@ export class ConfigReader implements Config { }); } + private fullKey(key: string): string { + return `${this.prefix}${this.prefix ? '.' : ''}${key}`; + } + private readConfigValue( key: string, validate: ( @@ -184,7 +195,9 @@ export class ConfigReader implements Config { value: theValue = value, expected, } = result; - throw new TypeError(errors.type(keyName, typeOf(theValue), expected)); + throw new TypeError( + errors.type(this.fullKey(keyName), typeOf(theValue), expected), + ); } } From 8cda915c70eb68872de454d201324a47682174cb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 18:43:58 +0200 Subject: [PATCH 46/60] packages/config: added .keys() --- packages/config/src/reader.test.ts | 13 +++++++++++++ packages/config/src/reader.ts | 6 ++++++ packages/config/src/types.ts | 2 ++ 3 files changed, 21 insertions(+) diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index f23fad8d3b..27dd1b9e3b 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -37,6 +37,7 @@ const DATA = { }; function expectValidValues(config: ConfigReader) { + expect(config.keys()).toEqual(Object.keys(DATA)); expect(config.getNumber('zero')).toBe(0); expect(config.getNumber('one')).toBe(1); expect(config.getBoolean('true')).toBe(true); @@ -111,6 +112,7 @@ function expectInvalidValues(config: ConfigReader) { describe('ConfigReader', () => { it('should read empty config with valid keys', () => { const config = new ConfigReader({}); + expect(config.keys()).toEqual([]); expect(config.getOptionalString('x')).toBeUndefined(); expect(config.getOptionalString('x_x')).toBeUndefined(); expect(config.getOptionalString('x-X')).toBeUndefined(); @@ -208,6 +210,17 @@ describe('ConfigReader with fallback', () => { const config = new ConfigReader(a, new ConfigReader(b)); + expect(config.keys()).toEqual(['merged']); + expect(config.getConfig('merged').keys()).toEqual([ + 'x', + 'z', + 'arr', + 'config', + 'configs', + 'y', + ]); + expect(config.getConfig('merged.config').keys()).toEqual(['d', 'e']); + expect(config.getString('merged.x')).toBe('x'); expect(config.getString('merged.y')).toBe('y'); expect(config.getString('merged.z')).toBe('z1'); diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index e3c349768b..e992b7caac 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -68,6 +68,12 @@ export class ConfigReader implements Config { private readonly prefix: string = '', ) {} + keys(): string[] { + const localKeys = this.data ? Object.keys(this.data) : []; + const fallbackKeys = this.fallback?.keys() ?? []; + return [...new Set([...localKeys, ...fallbackKeys])]; + } + getConfig(key: string): ConfigReader { const value = this.readValue(key); const fallbackConfig = this.fallback?.getConfig(key); diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 03c9dc4383..4f8a12b5bd 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -27,6 +27,8 @@ export type JsonValue = export type AppConfig = JsonObject; export type Config = { + keys(): string[]; + getConfig(key: string): Config; getConfigArray(key: string): Config[]; From ef4991fc2b1436f7e639b5ebc30015dbfd3d30b6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Jun 2020 20:04:49 +0200 Subject: [PATCH 47/60] packages/config: added context to keep track of where config is from for error messages --- packages/config/src/reader.test.ts | 130 ++++++++++++++++++++++++----- packages/config/src/reader.ts | 46 +++++++--- packages/config/src/types.ts | 5 +- 3 files changed, 144 insertions(+), 37 deletions(-) diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 27dd1b9e3b..365fddd4cf 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -63,37 +63,37 @@ function expectValidValues(config: ConfigReader) { function expectInvalidValues(config: ConfigReader) { expect(() => config.getBoolean('string')).toThrow( - 'Invalid type in config for key string, got string, wanted boolean', + "Invalid type in config for key 'string' in 'ctx', got string, wanted boolean", ); expect(() => config.getNumber('string')).toThrow( - 'Invalid type in config for key string, got string, wanted number', + "Invalid type in config for key 'string' in 'ctx', got string, wanted number", ); expect(() => config.getString('one')).toThrow( - 'Invalid type in config for key one, got number, wanted string', + "Invalid type in config for key 'one' in 'ctx', got number, wanted string", ); expect(() => config.getNumber('true')).toThrow( - 'Invalid type in config for key true, got boolean, wanted number', + "Invalid type in config for key 'true' in 'ctx', got boolean, wanted number", ); expect(() => config.getStringArray('null')).toThrow( - 'Invalid type in config for key null, got null, wanted string-array', + "Invalid type in config for key 'null' in 'ctx', got null, wanted string-array", ); expect(() => config.getString('emptyString')).toThrow( - 'Invalid type in config for key emptyString, got empty-string, wanted string', + "Invalid type in config for key 'emptyString' in 'ctx', got empty-string, wanted string", ); expect(() => config.getStringArray('badStrings')).toThrow( - 'Invalid type in config for key badStrings[1], got empty-string, wanted string', + "Invalid type in config for key 'badStrings[1]' in 'ctx', got empty-string, wanted string", ); expect(() => config.getStringArray('worseStrings')).toThrow( - 'Invalid type in config for key worseStrings[1], got number, wanted string', + "Invalid type in config for key 'worseStrings[1]' in 'ctx', got number, wanted string", ); expect(() => config.getStringArray('worstStrings')).toThrow( - 'Invalid type in config for key worstStrings[2], got object, wanted string', + "Invalid type in config for key 'worstStrings[2]' in 'ctx', got object, wanted string", ); expect(() => config.getConfig('one')).toThrow( - 'Invalid type in config for key one, got number, wanted object', + "Invalid type in config for key 'one' in 'ctx', got number, wanted object", ); expect(() => config.getConfigArray('one')).toThrow( - 'Invalid type in config for key one, got number, wanted object-array', + "Invalid type in config for key 'one' in 'ctx', got number, wanted object-array", ); expect(() => config.getBoolean('missing')).toThrow( "Missing required config value at 'missing'", @@ -109,9 +109,11 @@ function expectInvalidValues(config: ConfigReader) { ); } +const CTX = 'ctx'; + describe('ConfigReader', () => { it('should read empty config with valid keys', () => { - const config = new ConfigReader({}); + const config = new ConfigReader({}, CTX); expect(config.keys()).toEqual([]); expect(config.getOptionalString('x')).toBeUndefined(); expect(config.getOptionalString('x_x')).toBeUndefined(); @@ -121,11 +123,13 @@ describe('ConfigReader', () => { expect(config.getOptionalString('x0_x0')).toBeUndefined(); expect(config.getOptionalString('x_x-x_x')).toBeUndefined(); - expect(new ConfigReader(undefined).getOptionalString('x')).toBeUndefined(); + expect( + new ConfigReader(undefined, CTX).getOptionalString('x'), + ).toBeUndefined(); }); it('should throw on invalid keys', () => { - const config = new ConfigReader({}); + const config = new ConfigReader({}, CTX); expect(() => config.getString('.')).toThrow(/^Invalid config key/); expect(() => config.getString('0')).toThrow(/^Invalid config key/); @@ -143,38 +147,38 @@ describe('ConfigReader', () => { expect(() => config.getString('a._')).toThrow(/^Invalid config key/); expect(() => config.getString('a.-.a')).toThrow(/^Invalid config key/); - expect(() => new ConfigReader(undefined).getString('.')).toThrow( + expect(() => new ConfigReader(undefined, CTX).getString('.')).toThrow( /^Invalid config key/, ); }); it('should read valid values', () => { - const config = new ConfigReader(DATA); + const config = new ConfigReader(DATA, CTX); expectValidValues(config); }); it('should fail to read invalid values', () => { - const config = new ConfigReader(DATA); + const config = new ConfigReader(DATA, CTX); expectInvalidValues(config); }); }); describe('ConfigReader with fallback', () => { it('should behave as if without fallback', () => { - const config = new ConfigReader({}, new ConfigReader(DATA)); + const config = new ConfigReader({}, CTX, new ConfigReader(DATA, CTX)); expect(config.getOptionalString('x')).toBeUndefined(); expect(() => config.getString('.')).toThrow(/^Invalid config key/); expect(() => config.getString('a.')).toThrow(/^Invalid config key/); }); it('should read values from itself', () => { - const config = new ConfigReader(DATA, new ConfigReader({})); + const config = new ConfigReader(DATA, CTX, new ConfigReader({}, CTX)); expectValidValues(config); expectInvalidValues(config); }); it('should read values from a fallback', () => { - const config = new ConfigReader({}, new ConfigReader(DATA)); + const config = new ConfigReader({}, CTX, new ConfigReader(DATA, CTX)); expectValidValues(config); expectInvalidValues(config); }); @@ -182,12 +186,92 @@ describe('ConfigReader with fallback', () => { it('should read values from multiple levels of fallbacks', () => { const config = new ConfigReader( {}, - new ConfigReader({}, new ConfigReader({}, new ConfigReader(DATA))), + CTX, + new ConfigReader( + {}, + CTX, + new ConfigReader({}, CTX, new ConfigReader(DATA, CTX)), + ), ); expectValidValues(config); expectInvalidValues(config); }); + it('should show error with correct context', () => { + const config = ConfigReader.fromConfigs([ + { + data: { + c: true, + }, + context: 'x', + }, + { + data: { + b: true, + c: true, + nested1: { + a: true, + }, + badBefore: true, + badAfter: { + a: true, + }, + }, + context: 'y', + }, + { + data: { + a: true, + b: true, + c: true, + nested1: { + a: true, + b: true, + }, + badBefore: { + a: true, + }, + badAfter: true, + }, + context: 'z', + }, + ]); + + expect(() => config.getNumber('a')).toThrow( + "Invalid type in config for key 'a' in 'z', got boolean, wanted number", + ); + expect(() => config.getNumber('b')).toThrow( + "Invalid type in config for key 'b' in 'y', got boolean, wanted number", + ); + expect(() => config.getNumber('c')).toThrow( + "Invalid type in config for key 'c' in 'x', got boolean, wanted number", + ); + expect(() => config.getNumber('nested1.a')).toThrow( + "Invalid type in config for key 'nested1.a' in 'y', got boolean, wanted number", + ); + expect(() => config.getNumber('nested1.b')).toThrow( + "Invalid type in config for key 'nested1.b' in 'z', got boolean, wanted number", + ); + expect(() => config.getConfig('nested1').getNumber('a')).toThrow( + "Invalid type in config for key 'nested1.a' in 'y', got boolean, wanted number", + ); + expect(() => config.getConfig('nested1').getNumber('b')).toThrow( + "Invalid type in config for key 'nested1.b' in 'z', got boolean, wanted number", + ); + expect(() => config.getNumber('badBefore.a')).toThrow( + "Invalid type in config for key 'badBefore' in 'y', got boolean, wanted object", + ); + expect(() => config.getNumber('badBefore.b')).toThrow( + "Invalid type in config for key 'badBefore' in 'y', got boolean, wanted object", + ); + expect(() => config.getNumber('badAfter.a')).toThrow( + "Invalid type in config for key 'badAfter.a' in 'y', got boolean, wanted number", + ); + expect(() => config.getNumber('badAfter.b')).toThrow( + "Invalid type in config for key 'badAfter' in 'z', got boolean, wanted object", + ); + }); + it('should read merged objects', () => { const a = { merged: { @@ -208,7 +292,7 @@ describe('ConfigReader with fallback', () => { }, }; - const config = new ConfigReader(a, new ConfigReader(b)); + const config = new ConfigReader(a, CTX, new ConfigReader(b, CTX)); expect(config.keys()).toEqual(['merged']); expect(config.getConfig('merged').keys()).toEqual([ @@ -245,7 +329,7 @@ describe('ConfigReader with fallback', () => { 'b', ]); expect(() => config.getConfig('merged').getStringArray('x')).toThrow( - 'Invalid type in config for key merged.x, got string, wanted string-array', + "Invalid type in config for key 'merged.x' in 'ctx', got string, wanted string-array", ); // Config arrays aren't merged either diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index e992b7caac..e4c4c4d304 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -41,8 +41,8 @@ function typeOf(value: JsonValue | undefined): string { // Separate out a couple of common error messages to reduce bundle size. const errors = { - type(key: string, typeName: string, expected: string) { - return `Invalid type in config for key ${key}, got ${typeName}, wanted ${expected}`; + type(key: string, context: string, typeName: string, expected: string) { + return `Invalid type in config for key '${key}' in '${context}', got ${typeName}, wanted ${expected}`; }, missing(key: string) { return `Missing required config value at '${key}'`; @@ -57,13 +57,17 @@ export class ConfigReader implements Config { // Merge together all configs info a single config with recursive fallback // readers, giving the first config object in the array the highest priority. - return configs.reduceRight((previousReader, nextConfig) => { - return new ConfigReader(nextConfig, previousReader); - }, undefined!); + return configs.reduceRight( + (previousReader, { data, context }) => { + return new ConfigReader(data, context, previousReader); + }, + undefined!, + ); } constructor( private readonly data: JsonObject | undefined, + private readonly context: string = 'empty-config', private readonly fallback?: ConfigReader, private readonly prefix: string = '', ) {} @@ -80,14 +84,17 @@ export class ConfigReader implements Config { const prefix = this.fullKey(key); if (isObject(value)) { - return new ConfigReader(value, fallbackConfig, prefix); + return new ConfigReader(value, this.context, fallbackConfig, prefix); } if (value !== undefined) { throw new TypeError( - errors.type(this.fullKey(key), typeOf(value), 'object'), + errors.type(this.fullKey(key), this.context, typeOf(value), 'object'), ); } - return fallbackConfig ?? new ConfigReader(undefined, undefined, prefix); + return ( + fallbackConfig ?? + new ConfigReader(undefined, undefined, undefined, prefix) + ); } getConfigArray(key: string): ConfigReader[] { @@ -106,7 +113,12 @@ export class ConfigReader implements Config { return (configs ?? []).map( (obj, index) => - new ConfigReader(obj, undefined, this.fullKey(`${key}[${index}]`)), + new ConfigReader( + obj, + this.context, + undefined, + this.fullKey(`${key}[${index}]`), + ), ); } @@ -202,7 +214,12 @@ export class ConfigReader implements Config { expected, } = result; throw new TypeError( - errors.type(this.fullKey(keyName), typeOf(theValue), expected), + errors.type( + this.fullKey(keyName), + this.context, + typeOf(theValue), + expected, + ), ); } } @@ -223,11 +240,14 @@ export class ConfigReader implements Config { } let value: JsonValue | undefined = this.data; - for (const part of parts) { + for (const [index, part] of parts.entries()) { if (isObject(value)) { value = value[part]; - } else { - value = undefined; + } else if (value !== undefined) { + const badKey = this.fullKey(parts.slice(0, index).join('.')); + throw new TypeError( + errors.type(badKey, this.context, typeOf(value), 'object'), + ); } } diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 4f8a12b5bd..9c49a45af4 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -24,7 +24,10 @@ export type JsonValue = | boolean | null; -export type AppConfig = JsonObject; +export type AppConfig = { + context: string; + data: JsonObject; +}; export type Config = { keys(): string[]; From d4653864fb4512ad791d197811ed0e889168e330 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2020 21:18:38 +0200 Subject: [PATCH 48/60] build(deps): bump react-helmet from 6.0.0 to 6.1.0 (#1327) Bumps [react-helmet](https://github.com/nfl/react-helmet) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/nfl/react-helmet/releases) - [Changelog](https://github.com/nfl/react-helmet/blob/master/CHANGELOG.md) - [Commits](https://github.com/nfl/react-helmet/compare/6.0.0...6.1.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/core/package.json | 2 +- yarn.lock | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 69b0590802..01b7ed124d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,7 +46,7 @@ "rc-progress": "^3.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-helmet": "6.0.0", + "react-helmet": "6.1.0", "react-hook-form": "^5.7.2", "react-router": "6.0.0-alpha.5", "react-router-dom": "6.0.0-alpha.5", diff --git a/yarn.lock b/yarn.lock index b3b0af2615..37247d71be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15485,6 +15485,11 @@ react-fast-compare@^2.0.4: resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== +react-fast-compare@^3.1.1: + version "3.2.0" + resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + react-focus-lock@^2.1.0: version "2.2.1" resolved "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.2.1.tgz#1d12887416925dc53481914b7cedd39494a3b24a" @@ -15508,14 +15513,14 @@ react-helmet-async@^1.0.2: react-fast-compare "^2.0.4" shallowequal "^1.1.0" -react-helmet@6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-6.0.0.tgz#fcb93ebaca3ba562a686eb2f1f9d46093d83b5f8" - integrity sha512-My6S4sa0uHN/IuVUn0HFmasW5xj9clTkB9qmMngscVycQ5vVG51Qp44BEvLJ4lixupTwDlU9qX1/sCrMN4AEPg== +react-helmet@6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" + integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== dependencies: object-assign "^4.1.1" prop-types "^15.7.2" - react-fast-compare "^2.0.4" + react-fast-compare "^3.1.1" react-side-effect "^2.1.0" react-hook-form@^5.7.2: From 19f4e434db888efcc58991c446622f4241ee1cb2 Mon Sep 17 00:00:00 2001 From: Nikki Beesetti <12538017+nikkibeesetti@users.noreply.github.com> Date: Wed, 17 Jun 2020 14:21:35 -0500 Subject: [PATCH 49/60] Updated FAQ with Gitlab link (#1352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated FAQ with Gitlab link added gitlab link * Update FAQ.md Co-authored-by: Stefan Ålund --- docs/FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 3bd526f94b..feb1667717 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -144,7 +144,7 @@ Integrators also configure closed source plugins locally from the monorepo. ​ ​ We chose GitHub because it is the tool that we are most familiar with, so that will naturally lead to integrations for GitHub being developed at an early stage. ​ Hosting this project on GitHub does not exclude integrations with -alternatives, such as GitLab or Bitbucket. We believe that in time there will be +alternatives, such as [GitLab](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+GitLab) or Bitbucket. We believe that in time there will be plugins that will provide functionality for these tools as well. Hopefully, contributed by the community! ​ Also note, implementations of Backstage can be hosted wherever you feel suits your needs best. ​ From 0f1ba02bc7e8ea9eb568b917fa9659b825d07aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Wed, 17 Jun 2020 22:38:45 +0200 Subject: [PATCH 50/60] Starred icon is yellow (#1351) --- .../src/components/CatalogPage/CatalogPage.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 2a3746b707..8c9313ca66 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -25,7 +25,13 @@ import { } from '@backstage/core'; import CatalogLayout from './CatalogLayout'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; -import { Button, Link, makeStyles, Typography } from '@material-ui/core'; +import { + Button, + Link, + makeStyles, + Typography, + withStyles, +} from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; import Star from '@material-ui/icons/Star'; @@ -113,6 +119,12 @@ export const CatalogPage: FC<{}> = () => { const styles = useStyles(); + const YellowStar = withStyles({ + root: { + color: '#f3ba37', + }, + })(Star); + const actions = [ (rowData: Entity) => { const location = findLocationForEntityMeta(rowData.metadata); @@ -152,7 +164,7 @@ export const CatalogPage: FC<{}> = () => { (rowData: Entity) => { const isStarred = isStarredEntity(rowData); return { - icon: isStarred ? Star : StarOutline, + icon: isStarred ? YellowStar : StarOutline, tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites', onClick: () => toggleStarredEntity(rowData), }; From f763729120d0e6ab976f77e008d639af949a65f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Wed, 17 Jun 2020 22:44:21 +0200 Subject: [PATCH 51/60] Add some air between sidebar sections (#1355) --- packages/core/src/layout/Sidebar/Items.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index a7f40f56ba..da0b44d273 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -233,5 +233,5 @@ export const SidebarDivider = styled('hr')({ width: '100%', background: '#383838', border: 'none', - margin: 0, + margin: '12px 0px', }); From 1150e0a125183d53f02c02d7db23dd6e8d7e3edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Wed, 17 Jun 2020 22:57:25 +0200 Subject: [PATCH 52/60] Polishing the Create page (#1353) * Polishing the Create page * Review comments --- .../src/components/ScaffolderPage/index.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index aa1cf36082..d675b59f52 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -20,23 +20,33 @@ import { Content, ContentHeader, Header, + SupportButton, Page, pageTheme, } from '@backstage/core'; -import { Typography, Link, Button } from '@material-ui/core'; +import { Button, Grid, Link, Typography } from '@material-ui/core'; import { Link as RouterLink } from 'react-router-dom'; import TemplateCard from '../TemplateCard'; // TODO(blam): Connect to backend const STATIC_DATA = [ + { + id: 'springboot-template', + type: 'service', + name: 'Spring Boot Service', + tags: ['Recommended', 'Java'], + description: + 'Standard Spring Boot (Java) microservice with recommended configuration.', + ownerId: 'spotify', + }, { id: 'react-ssr-template', - type: 'web-infra', + type: 'website', name: 'SSR React Website', - tags: ['Experimental'], + tags: ['Recommended', 'React'], description: 'Next.js application skeleton for creating isomorphic web applications.', - ownerId: 'something', + ownerId: 'spotify', }, ]; const ScaffolderPage: React.FC<{}> = () => { @@ -46,7 +56,7 @@ const ScaffolderPage: React.FC<{}> = () => { pageTitleOverride="Create a new component" title={ <> - Create a new component {' '} + Create a new component } subtitle="Create new software components using standard templates" @@ -61,6 +71,11 @@ const ScaffolderPage: React.FC<{}> = () => { > Register existing component + + Create new software components using standard templates. Different + templates create different kinds of components (services, websites, + documentation, ...). + NOTE! This feature is WIP. You can follow progress{' '} @@ -69,7 +84,7 @@ const ScaffolderPage: React.FC<{}> = () => { . -
+ {STATIC_DATA.map(item => { return ( = () => { /> ); })} -
+
); From 66a0bab7f981d3f61110d1fd86e491067db6b5e9 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2020 09:16:59 +0200 Subject: [PATCH 53/60] build(deps-dev): bump lint-staged from 10.2.9 to 10.2.11 (#1359) Bumps [lint-staged](https://github.com/okonet/lint-staged) from 10.2.9 to 10.2.11. - [Release notes](https://github.com/okonet/lint-staged/releases) - [Commits](https://github.com/okonet/lint-staged/compare/v10.2.9...v10.2.11) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 37247d71be..58ea0f0e06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12177,9 +12177,9 @@ linkify-it@^2.0.0: uc.micro "^1.0.1" lint-staged@^10.1.0: - version "10.2.9" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.9.tgz#6013ecfa80829cd422446b545fd30a96bca3098c" - integrity sha512-ziRAuXEqvJLSXg43ezBpHxRW8FOJCXISaXU//BWrxRrp5cBdRkIx7g5IsB3OI45xYGE0S6cOacfekSjDyDKF2g== + version "10.2.11" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.11.tgz#713c80877f2dc8b609b05bc59020234e766c9720" + integrity sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA== dependencies: chalk "^4.0.0" cli-truncate "2.1.0" From 15b840acb676dbaca8d2eba4e2f374433bd263f3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2020 09:17:22 +0200 Subject: [PATCH 54/60] build(deps): bump rollup-plugin-esbuild from 2.0.0 to 2.1.0 (#1361) Bumps rollup-plugin-esbuild from 2.0.0 to 2.1.0. Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/yarn.lock b/yarn.lock index 58ea0f0e06..8a2314898c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2452,16 +2452,7 @@ is-module "^1.0.0" resolve "^1.14.2" -"@rollup/pluginutils@^3.0.8": - version "3.0.10" - resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.10.tgz#a659b9025920378494cd8f8c59fbf9b3a50d5f12" - integrity sha512-d44M7t+PjmMrASHbhgpSbVgtL6EFyX7J4mYxwQ/c5eoaE6N2VgCgEcWVzNnwycIloti+/MpwFr8qfw+nRw00sw== - dependencies: - "@types/estree" "0.0.39" - estree-walker "^1.0.1" - picomatch "^2.2.2" - -"@rollup/pluginutils@^3.1.0": +"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== @@ -8121,10 +8112,10 @@ es6-shim@^0.35.5: resolved "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.5.tgz#46f59dc0a84a1c5029e8ff1166ca0a902077a9ab" integrity sha512-E9kK/bjtCQRpN1K28Xh4BlmP8egvZBGJJ+9GtnzOwt7mdqtrjHFuVGr7QJfdjBIKqrlU5duPf3pCBoDrkjVYFg== -esbuild@^0.4.11: - version "0.4.14" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.4.14.tgz#19c3ec44fe3bb6c637dd5287d870a87d36352dcc" - integrity sha512-8lx+KpHMQM6t3JFutzCWLckcaVQyv5qvdCzWHQdXGGh16SXdv5nfo/+izWcF367PlMkCMfV7iWW7J5I8Skx7ZQ== +esbuild@^0.5.3: + version "0.5.3" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.5.3.tgz#18f5bdb618220c6f14bcb1cf5af528d02d4734c9" + integrity sha512-RVzTK62svYjnh+agJRh+NWfZX74iKwFNUX52cF7Mo4QPS6bKxP1o+8GacPUMND2QnodVp2D3nKJs8gLspSfZzA== escape-goat@^2.0.0: version "2.1.1" @@ -16413,12 +16404,12 @@ rollup-plugin-dts@^1.4.6: "@babel/code-frame" "^7.8.3" rollup-plugin-esbuild@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-2.0.0.tgz#ec6a779f5680410801ad47be9fa9447d02cef5f0" - integrity sha512-a5jeHL9Ay1xc8RUULcqkHQ6poMMYCbcTYmfFlYavLg3ALXeqhlmueWAZMIMvr8YFit0Ru75/uKKpBRmN395gEA== + version "2.1.0" + resolved "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-2.1.0.tgz#8e12337c63a5b1144e0c5e8adf2f1568ad4d7d69" + integrity sha512-XYqmwk4X0SPEExgilARbre/PplhLtE3q6wiZtfgIbwxJOVGXWec1Bkcux7TFTHGX3TQozzqEASTsRJCt7py/5Q== dependencies: "@rollup/pluginutils" "^3.1.0" - esbuild "^0.4.11" + esbuild "^0.5.3" rollup-plugin-image-files@^1.4.2: version "1.4.2" From 44f717deeece580b2b576fb0e887069021366dad Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2020 09:17:48 +0200 Subject: [PATCH 55/60] build(deps): bump @types/react from 16.9.25 to 16.9.37 (#1342) Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 16.9.25 to 16.9.37. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8a2314898c..b1b521b438 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3940,9 +3940,9 @@ "@types/react" "*" "@types/react@*", "@types/react@^16.9": - version "16.9.25" - resolved "https://registry.npmjs.org/@types/react/-/react-16.9.25.tgz#6ae2159b40138c792058a23c3c04fd3db49e929e" - integrity sha512-Dlj2V72cfYLPNscIG3/SMUOzhzj7GK3bpSrfefwt2YT9GLynvLCCZjbhyF6VsT0q0+aRACRX03TDJGb7cA0cqg== + version "16.9.37" + resolved "https://registry.npmjs.org/@types/react/-/react-16.9.37.tgz#8fb93e7dbd5b1d3796f69aa979a7fe0439bc7bea" + integrity sha512-ZqnAXallQiZ08LTSqMfWMNvAfJEzRLOxdlbbbCIJlYGjU98BEU6bE2uBpKPGeWn+v3hIgCraHKtqUcKZBzMP/Q== dependencies: "@types/prop-types" "*" csstype "^2.2.0" From b010319400ed10a32e09d8ce58420025433f3fc5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jun 2020 09:45:33 +0200 Subject: [PATCH 56/60] packages/config-loader,core: update config usage to include context --- packages/config-loader/src/lib/env.test.ts | 9 ++++--- packages/config-loader/src/lib/env.ts | 6 ++--- packages/config-loader/src/lib/reader.test.ts | 16 +++++++++---- packages/config-loader/src/lib/reader.ts | 10 +++++--- .../core/src/api-wrappers/createApp.test.tsx | 24 +++++++++++++------ packages/core/src/api-wrappers/createApp.tsx | 5 ++-- 6 files changed, 47 insertions(+), 23 deletions(-) diff --git a/packages/config-loader/src/lib/env.test.ts b/packages/config-loader/src/lib/env.test.ts index 6a0a115b24..9ecc80d29a 100644 --- a/packages/config-loader/src/lib/env.test.ts +++ b/packages/config-loader/src/lib/env.test.ts @@ -45,9 +45,12 @@ describe('readEnv', () => { }), ).toEqual([ { - foo: 'bar', - numbers: { a: 1, b: 2, c: false }, - very: { deep: { nested: { config: { object: {} } } } }, + data: { + foo: 'bar', + numbers: { a: 1, b: 2, c: false }, + very: { deep: { nested: { config: { object: {} } } } }, + }, + context: 'env', }, ]); }); diff --git a/packages/config-loader/src/lib/env.ts b/packages/config-loader/src/lib/env.ts index 83f86d4212..aa986931ff 100644 --- a/packages/config-loader/src/lib/env.ts +++ b/packages/config-loader/src/lib/env.ts @@ -42,7 +42,7 @@ const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; export function readEnv(env: { [name: string]: string | undefined; }): AppConfig[] { - let config: JsonObject | undefined = undefined; + let data: JsonObject | undefined = undefined; for (const [name, value] of Object.entries(env)) { if (!value) { @@ -52,7 +52,7 @@ export function readEnv(env: { const key = name.replace(ENV_PREFIX, ''); const keyParts = key.split('_'); - let obj = (config = config ?? {}); + let obj = (data = data ?? {}); for (const [index, part] of keyParts.entries()) { if (!CONFIG_KEY_PART_PATTERN.test(part)) { throw new TypeError(`Invalid env config key '${key}'`); @@ -87,5 +87,5 @@ export function readEnv(env: { } } - return config ? [config] : []; + return data ? [{ data, context: 'env' }] : []; } diff --git a/packages/config-loader/src/lib/reader.test.ts b/packages/config-loader/src/lib/reader.test.ts index b90c53ee6b..719939ad7f 100644 --- a/packages/config-loader/src/lib/reader.test.ts +++ b/packages/config-loader/src/lib/reader.test.ts @@ -38,11 +38,14 @@ describe('readConfigFile', () => { } as ReaderContext); await expect(config).resolves.toEqual({ - app: { - title: 'Test', - x: 1, - y: [true], + data: { + app: { + title: 'Test', + x: 1, + y: [true], + }, }, + context: 'app-config.yaml', }); }); @@ -83,7 +86,10 @@ describe('readConfigFile', () => { }); await expect(config).resolves.toEqual({ - app: 'secret', + data: { + app: 'secret', + }, + context: 'app-config.yaml', }); expect(readSecret).toHaveBeenCalledWith({ file: './my-secret' }); }); diff --git a/packages/config-loader/src/lib/reader.ts b/packages/config-loader/src/lib/reader.ts index 044bf68288..4efaf1986a 100644 --- a/packages/config-loader/src/lib/reader.ts +++ b/packages/config-loader/src/lib/reader.ts @@ -15,15 +15,19 @@ */ import yaml from 'yaml'; +import { basename } from 'path'; import { isObject } from './utils'; -import { JsonValue, JsonObject } from '@backstage/config'; +import { JsonValue, JsonObject, AppConfig } from '@backstage/config'; import { ReaderContext } from './types'; /** * Reads and parses, and validates, and transforms a single config file. * The transformation rewrites any special values, like the $secret key. */ -export async function readConfigFile(filePath: string, ctx: ReaderContext) { +export async function readConfigFile( + filePath: string, + ctx: ReaderContext, +): Promise { const configYaml = await ctx.readFile(filePath); const config = yaml.parse(configYaml); @@ -76,5 +80,5 @@ export async function readConfigFile(filePath: string, ctx: ReaderContext) { if (!isObject(finalConfig)) { throw new TypeError('Expected object at config root'); } - return finalConfig; + return { data: finalConfig, context: basename(filePath) }; } diff --git a/packages/core/src/api-wrappers/createApp.test.tsx b/packages/core/src/api-wrappers/createApp.test.tsx index 30553d84a7..0a3756ad92 100644 --- a/packages/core/src/api-wrappers/createApp.test.tsx +++ b/packages/core/src/api-wrappers/createApp.test.tsx @@ -15,6 +15,7 @@ */ import { defaultConfigLoader } from './createApp'; +import { AppConfig } from '@backstage/config'; describe('defaultConfigLoader', () => { afterEach(() => { @@ -24,24 +25,33 @@ describe('defaultConfigLoader', () => { it('loads static config', async () => { Object.defineProperty(process.env, 'APP_CONFIG', { configurable: true, - value: [{ my: 'config' }, { my: 'override-config' }] as any, + value: [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ] as AppConfig[], }); const configs = await defaultConfigLoader(); - expect(configs).toEqual([{ my: 'config' }, { my: 'override-config' }]); + expect(configs).toEqual([ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]); }); it('loads runtime config', async () => { Object.defineProperty(process.env, 'APP_CONFIG', { configurable: true, - value: [{ my: 'override-config' }, { my: 'config' }] as any, + value: [ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + ] as AppConfig[], }); const configs = await (defaultConfigLoader as any)( '{"my":"runtime-config"}', ); expect(configs).toEqual([ - { my: 'runtime-config' }, - { my: 'override-config' }, - { my: 'config' }, + { data: { my: 'runtime-config' }, context: 'env' }, + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, ]); }); @@ -64,7 +74,7 @@ describe('defaultConfigLoader', () => { it('fails to load bad runtime config', async () => { Object.defineProperty(process.env, 'APP_CONFIG', { configurable: true, - value: [{ my: 'config' }] as any, + value: [{ data: { my: 'config' }, context: 'a' }] as AppConfig[], }); await expect((defaultConfigLoader as any)('}')).rejects.toThrow( diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index f69451249b..c13758a0b9 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -27,7 +27,7 @@ import { BrowserRouter, MemoryRouter } from 'react-router-dom'; import { ErrorPage } from '../layout/ErrorPage'; import Progress from '../components/Progress'; import { lightTheme, darkTheme } from '@backstage/theme'; -import { AppConfig } from '@backstage/config'; +import { AppConfig, JsonObject } from '@backstage/config'; const { PrivateAppImpl } = privateExports; @@ -59,7 +59,8 @@ export const defaultConfigLoader: AppConfigLoader = async ( // Avoiding this string also being replaced at runtime if (runtimeConfigJson !== '__app_injected_runtime_config__'.toUpperCase()) { try { - configs.unshift(JSON.parse(runtimeConfigJson)); + const data = JSON.parse(runtimeConfigJson) as JsonObject; + configs.unshift({ data, context: 'env' }); } catch (error) { throw new Error(`Failed to load runtime configuration, ${error}`); } From 32a4da583c3fecf9acae9085de161e5f26e8a74e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jun 2020 11:03:29 +0200 Subject: [PATCH 57/60] packages/config: add getOptionalConfig and getOptionalConfigArray to mirror other accessors --- packages/config/src/reader.test.ts | 12 ++++++++---- packages/config/src/reader.ts | 29 +++++++++++++++++++++++------ packages/config/src/types.ts | 2 ++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 365fddd4cf..64e4b3ed69 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -46,10 +46,11 @@ function expectValidValues(config: ConfigReader) { expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); expect(config.getConfig('nested').getNumber('one')).toBe(1); expect(config.getConfig('nested').getString('string')).toBe('string'); - expect(config.getConfig('nested').getStringArray('strings')).toEqual([ - 'string1', - 'string2', - ]); + expect( + config.getOptionalConfig('nested')!.getStringArray('strings'), + ).toEqual(['string1', 'string2']); + expect(config.getOptionalConfig('missing')).toBe(undefined); + expect(config.getOptionalConfigArray('missing')).toBe(undefined); expect(config.getNumber('zero')).toBe(0); expect(config.getBoolean('true')).toBe(true); expect(config.getString('string')).toBe('string'); @@ -59,6 +60,9 @@ function expectValidValues(config: ConfigReader) { expect(config1.getBoolean('boolean')).toBe(true); expect(config2.getString('string')).toBe('string'); expect(config3.getNumber('number')).toBe(42); + expect( + config.getOptionalConfigArray('nestlings')![0].getBoolean('boolean'), + ).toBe(true); } function expectInvalidValues(config: ConfigReader) { diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index e4c4c4d304..4e302a73a4 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -79,8 +79,16 @@ export class ConfigReader implements Config { } getConfig(key: string): ConfigReader { + const value = this.getOptionalConfig(key); + if (value === undefined) { + throw new Error(errors.missing(this.fullKey(key))); + } + return value; + } + + getOptionalConfig(key: string): ConfigReader | undefined { const value = this.readValue(key); - const fallbackConfig = this.fallback?.getConfig(key); + const fallbackConfig = this.fallback?.getOptionalConfig(key); const prefix = this.fullKey(key); if (isObject(value)) { @@ -91,13 +99,18 @@ export class ConfigReader implements Config { errors.type(this.fullKey(key), this.context, typeOf(value), 'object'), ); } - return ( - fallbackConfig ?? - new ConfigReader(undefined, undefined, undefined, prefix) - ); + return fallbackConfig; } getConfigArray(key: string): ConfigReader[] { + const value = this.getOptionalConfigArray(key); + if (value === undefined) { + throw new Error(errors.missing(this.fullKey(key))); + } + return value; + } + + getOptionalConfigArray(key: string): ConfigReader[] | undefined { const configs = this.readConfigValue(key, values => { if (!Array.isArray(values)) { return { expected: 'object-array' }; @@ -111,7 +124,11 @@ export class ConfigReader implements Config { return true; }); - return (configs ?? []).map( + if (!configs) { + return undefined; + } + + return configs.map( (obj, index) => new ConfigReader( obj, diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 9c49a45af4..180d526b40 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -33,8 +33,10 @@ export type Config = { keys(): string[]; getConfig(key: string): Config; + getOptionalConfig(key: string): Config | undefined; getConfigArray(key: string): Config[]; + getOptionalConfigArray(key: string): Config[] | undefined; getNumber(key: string): number; getOptionalNumber(key: string): number | undefined; From a23c276bb3a22ddc10f0b0459f2cdf77964ab201 Mon Sep 17 00:00:00 2001 From: Lee Mills Date: Thu, 18 Jun 2020 11:12:46 +0200 Subject: [PATCH 58/60] changed down caret to be up caret on user profile in the sidebar --- packages/core/src/layout/Sidebar/Settings/UserProfile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx index 9cc122e2fa..06c36bf9ce 100644 --- a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx +++ b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx @@ -104,7 +104,7 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({ onClick={handleClick} icon={avatar || AccountCircleIcon} > - {open ? : } + {open ? : } ); From 02b74c376affbbf7e42b7202b1a3cadcb8b79725 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jun 2020 11:58:53 +0200 Subject: [PATCH 59/60] packages/config: added get and getOptional --- packages/config/package.json | 3 + packages/config/src/reader.test.ts | 172 +++++++++++++++++++++++++++++ packages/config/src/reader.ts | 30 +++++ packages/config/src/types.ts | 3 + 4 files changed, 208 insertions(+) diff --git a/packages/config/package.json b/packages/config/package.json index 8bf4001e62..95a5852ae5 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -29,6 +29,9 @@ "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" }, + "dependencies": { + "lodash": "^4.17.15" + }, "devDependencies": { "@types/jest": "^25.2.2", "@types/node": "^12.0.0" diff --git a/packages/config/src/reader.test.ts b/packages/config/src/reader.test.ts index 64e4b3ed69..9d06e0b025 100644 --- a/packages/config/src/reader.test.ts +++ b/packages/config/src/reader.test.ts @@ -38,17 +38,26 @@ const DATA = { function expectValidValues(config: ConfigReader) { expect(config.keys()).toEqual(Object.keys(DATA)); + expect(config.get('zero')).toBe(0); expect(config.getNumber('zero')).toBe(0); expect(config.getNumber('one')).toBe(1); + expect(config.getOptional('true')).toBe(true); expect(config.getBoolean('true')).toBe(true); expect(config.getBoolean('false')).toBe(false); expect(config.getString('string')).toBe('string'); + expect(config.get('strings')).toEqual(['string1', 'string2']); expect(config.getStringArray('strings')).toEqual(['string1', 'string2']); expect(config.getConfig('nested').getNumber('one')).toBe(1); + expect(config.get('nested')).toEqual({ + one: 1, + string: 'string', + strings: ['string1', 'string2'], + }); expect(config.getConfig('nested').getString('string')).toBe('string'); expect( config.getOptionalConfig('nested')!.getStringArray('strings'), ).toEqual(['string1', 'string2']); + expect(config.getOptional('missing')).toBe(undefined); expect(config.getOptionalConfig('missing')).toBe(undefined); expect(config.getOptionalConfigArray('missing')).toBe(undefined); expect(config.getNumber('zero')).toBe(0); @@ -360,3 +369,166 @@ describe('ConfigReader with fallback', () => { ).toBeUndefined(); }); }); + +describe('ConfigReader.get()', () => { + const config1 = { + a: { + x: 'x1', + y: ['y11', 'y12', 'y13'], + z: false, + }, + b: { + x: 'x1', + y: ['y11'], + }, + }; + const config2 = { + b: { + x: 'x2', + y: ['y21', 'y22'], + z: 'z2', + }, + c: { + c1: { + c2: 'c2', + }, + }, + }; + const config3 = { + c: { + c1: 'c1', + }, + }; + const configs = [ + { + data: config1, + context: '1', + }, + { + data: config2, + context: '2', + }, + { + data: config3, + context: '3', + }, + ]; + + it('should be able to select sub-configs', () => { + expect(new ConfigReader(config1).get('a')).toEqual(config1.a); + expect(new ConfigReader(config1).get('b')).toEqual(config1.b); + expect(new ConfigReader(config2).get('b')).toEqual(config2.b); + expect(new ConfigReader(config2).get('c')).toEqual(config2.c); + expect(new ConfigReader(config3).get('c')).toEqual(config3.c); + expect(new ConfigReader(config2).get('c.c1')).toEqual(config2.c.c1); + expect(new ConfigReader(config2).getConfig('c').get('c1')).toEqual( + config2.c.c1, + ); + }); + + it('should merge in fallback configs', () => { + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('a')).toEqual( + { + x: 'x1', + y: ['y11', 'y12', 'y13'], + z: false, + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('b')).toEqual( + { + x: 'x1', + y: ['y11'], + z: 'z2', + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('c')).toEqual( + { + c1: { + c2: 'c2', + }, + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('a')).toEqual( + { + x: 'x1', + y: ['y11', 'y12', 'y13'], + z: false, + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('b')).toEqual( + { + x: 'x1', + y: ['y11'], + z: 'z2', + }, + ); + expect(ConfigReader.fromConfigs([configs[0], configs[1]]).get('c')).toEqual( + { + c1: { + c2: 'c2', + }, + }, + ); + + expect( + ConfigReader.fromConfigs([configs[2], configs[1]]).getOptional('b'), + ).toEqual({ + x: 'x2', + y: ['y21', 'y22'], + z: 'z2', + }); + expect( + ConfigReader.fromConfigs([configs[2], configs[1]]).getOptional('c'), + ).toEqual({ + c1: 'c1', + }); + }); + + it('should not merge non-objects', () => { + const config = ConfigReader.fromConfigs([ + { + data: { + a: ['1', '2'], + c: [], + d: { + x: 'x', + }, + e: ['3'], + f: 'foo', + g: { z: 'z' }, + h: { + a: 'a1', + c: 'c1', + }, + }, + context: '1', + }, + { + data: { + a: ['x', 'y', 'z'], + b: ['1'], + c: ['1'], + d: ['2'], + e: { + y: 'y', + }, + f: { x: 'x' }, + g: 'bar', + h: { + a: 'a2', + b: 'b2', + }, + }, + context: '2', + }, + ]); + expect(config.get('a')).toEqual(['1', '2']); + expect(config.get('b')).toEqual(['1']); + expect(config.get('c')).toEqual([]); + expect(config.get('d')).toEqual({ x: 'x' }); + expect(config.get('e')).toEqual(['3']); + expect(config.get('f')).toEqual('foo'); + expect(config.get('g')).toEqual({ z: 'z' }); + expect(config.get('h')).toEqual({ a: 'a1', b: 'b2', c: 'c1' }); + }); +}); diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 4e302a73a4..0669bcb6d8 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -15,6 +15,8 @@ */ import { AppConfig, Config, JsonValue, JsonObject } from './types'; +import cloneDeep from 'lodash/cloneDeep'; +import mergeWith from 'lodash/mergeWith'; // Update the same pattern in config-loader package if this is changed const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; @@ -78,6 +80,34 @@ export class ConfigReader implements Config { return [...new Set([...localKeys, ...fallbackKeys])]; } + get(key: string): JsonValue { + const value = this.getOptional(key); + if (value === undefined) { + throw new Error(errors.missing(this.fullKey(key))); + } + return value; + } + + getOptional(key: string): JsonValue | undefined { + const value = this.readValue(key); + const fallbackValue = this.fallback?.getOptional(key); + + if (value === undefined) { + return fallbackValue; + } else if (fallbackValue === undefined) { + return value; + } + + // Avoid merging arrays and primitive values, since that's how merging works for other + // methods for reading config. + return mergeWith( + {}, + { value: cloneDeep(fallbackValue) }, + { value }, + (into, from) => (!isObject(from) || !isObject(into) ? from : undefined), + ).value; + } + getConfig(key: string): ConfigReader { const value = this.getOptionalConfig(key); if (value === undefined) { diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 180d526b40..aaadcd70dc 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -32,6 +32,9 @@ export type AppConfig = { export type Config = { keys(): string[]; + get(key: string): JsonValue; + getOptional(key: string): JsonValue | undefined; + getConfig(key: string): Config; getOptionalConfig(key: string): Config | undefined; From a098ebca27c1e5ab18cc7d2ecddb0beaf2e3e1f7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jun 2020 12:20:53 +0200 Subject: [PATCH 60/60] packages,plugins: remove main:src and fix some main fields --- packages/catalog-model/package.json | 4 +-- packages/cli/config/jest.js | 25 +++---------------- packages/cli/src/lib/bundler/config.ts | 4 +-- packages/cli/src/lib/tasks.ts | 5 +--- .../plugins/welcome/package.json.hbs | 3 +-- .../templates/default-plugin/package.json.hbs | 3 +-- packages/core-api/package.json | 3 +-- packages/core/package.json | 3 +-- packages/dev-utils/package.json | 3 +-- packages/storybook/.storybook/main.js | 2 +- packages/test-utils-core/package.json | 3 +-- packages/test-utils/package.json | 3 +-- packages/theme/package.json | 3 +-- plugins/catalog/package.json | 3 +-- plugins/circleci/package.json | 3 +-- plugins/explore/package.json | 3 +-- plugins/gitops-profiles/package.json | 3 +-- plugins/graphiql/package.json | 3 +-- plugins/lighthouse/package.json | 3 +-- plugins/register-component/package.json | 3 +-- plugins/scaffolder/package.json | 3 +-- plugins/sentry/package.json | 3 +-- plugins/tech-radar/package.json | 3 +-- plugins/welcome/package.json | 3 +-- 24 files changed, 27 insertions(+), 70 deletions(-) diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 60208aeebb..64f49e34e6 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,9 +1,7 @@ { "name": "@backstage/catalog-model", "version": "0.1.1-alpha.9", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 4567d8c593..ad24976ad0 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -25,32 +25,13 @@ async function getConfig() { return require(path.resolve('jest.config.ts')); } - const moduleNameMapper = { - '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), - }; - - // Only point to src/ if we're not in CI, there we just build packages first anyway - if (!process.env.CI) { - const LernaProject = require('@lerna/project'); - const project = new LernaProject(path.resolve('.')); - const packages = await project.getPackages(); - - // To avoid having to build all deps inside the monorepo before running tests, - // we point directory to src/ where applicable. - // For example, @backstage/core = /packages/core/src/index.ts is added to moduleNameMapper - for (const pkg of packages) { - const mainSrc = pkg.get('main:src'); - if (mainSrc) { - moduleNameMapper[`^${pkg.name}$`] = path.resolve(pkg.location, mainSrc); - } - } - } - const options = { rootDir: path.resolve('src'), coverageDirectory: path.resolve('coverage'), collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'], - moduleNameMapper, + moduleNameMapper: { + '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), + }, // We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed // TODO: jest is working on module support, it's possible that we can remove this in the future diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index d5fb674ec1..6fe8092e50 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -109,7 +109,7 @@ export function createConfig( entry: [require.resolve('react-hot-loader/patch'), paths.targetEntry], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], - mainFields: ['main:src', 'browser', 'module', 'main'], + mainFields: ['browser', 'module', 'main'], plugins: [ new ModuleScopePlugin( [paths.targetSrc, paths.targetDev], @@ -183,7 +183,7 @@ export function createBackendConfig( ], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], - mainFields: ['main:src', 'browser', 'module', 'main'], + mainFields: ['browser', 'module', 'main'], modules: [paths.targetNodeModules, paths.rootNodeModules], plugins: [ new ModuleScopePlugin( diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 481241a869..b6e2bebb41 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -171,9 +171,7 @@ export async function installWithLocalDeps(dir: string) { }); // This takes care of pointing all the installed packages from this repo to - // dist instead of the local src. - // For example node_modules/@backstage/core/packages.json is rewritten to point - // types to dist/index.d.ts and the main:src field is removed. + // dist instead of the local src, using the field overrides in publishConfig. // Without this we get type checking errors in the e2e test if (process.env.BACKSTAGE_E2E_CLI_TEST) { Task.section('Patching local dependencies for e2e tests'); @@ -192,7 +190,6 @@ export async function installWithLocalDeps(dir: string) { const depJson = await fs.readJson(depJsonPath); // We want dist to be used for e2e tests - delete depJson['main:src']; for (const key of Object.keys(depJson.publishConfig)) { if (key !== 'access') { depJson[key] = depJson.publishConfig[key]; 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 c590b68324..d916677d33 100644 --- a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs +++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs @@ -1,8 +1,7 @@ { "name": "plugin-welcome", "version": "0.0.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "private": true, "publishConfig": { diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index c92795cc04..b04da6b793 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-{{id}}", "version": "{{version}}", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": true, diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 2659a23ef6..4e3c8ef139 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -18,8 +18,7 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", diff --git a/packages/core/package.json b/packages/core/package.json index 01b7ed124d..f84181f626 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,8 +18,7 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index af1f813148..425d507b17 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -18,8 +18,7 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index b8f78f5d2c..2ee58a8c4f 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -16,7 +16,7 @@ module.exports = { const coreSrc = path.resolve(__dirname, '../../core/src'); // Mirror config in packages/cli/src/lib/bundler - config.resolve.mainFields = ['main:src', 'browser', 'module', 'main']; + config.resolve.mainFields = ['browser', 'module', 'main']; // Remove the default babel-loader for js files, we're using sucrase instead const [jsLoader] = config.module.rules.splice(0, 1); diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index a71a23c844..3b18ff4c1c 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -18,8 +18,7 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 7c5a229aba..c0752f87ac 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -18,8 +18,7 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", diff --git a/packages/theme/package.json b/packages/theme/package.json index 72a10eb1f3..1c6c096a8f 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -18,8 +18,7 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { "build": "backstage-cli build --outputs types,esm", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 54d5737adb..ddffb1f932 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-catalog", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 0104c5cc7b..f59d93106a 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-circleci", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/explore/package.json b/plugins/explore/package.json index ad3205f514..74076e9fbb 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-explore", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index c9dca36e08..ee4406be4c 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 49293acbfb..9d4a797c82 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -18,8 +18,7 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { "build": "backstage-cli plugin:build", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index a3c65d05ea..c96013a814 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index d999ab1dcc..e4c84e448d 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-register-component", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 84132a42f9..5c6859cf57 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 7a8197ab87..80435cf38a 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-sentry", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index e0146e8a72..16a36dba5d 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "private": false, diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 6780f7df98..deed9012da 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,8 +1,7 @@ { "name": "@backstage/plugin-welcome", "version": "0.1.1-alpha.9", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "private": false, "license": "Apache-2.0",