From e0088d9d1a4617295cbbaa94a36630318a3e7194 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 11:17:42 +0200 Subject: [PATCH 01/16] frontend-app-api: implement api factory discovery Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/package.json | 1 + packages/frontend-app-api/src/createApp.tsx | 66 +++++++++++++++---- .../frontend-app-api/src/extensions/Core.tsx | 34 ++++++++++ yarn.lock | 1 + 4 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 packages/frontend-app-api/src/extensions/Core.tsx diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 33b6067858..17f4b1bef3 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -34,6 +34,7 @@ ], "dependencies": { "@backstage/config": "workspace:^", + "@backstage/core-app-api": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-graphiql": "workspace:^", diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index c50b507642..77f0d1948a 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -21,6 +21,7 @@ import { coreExtensionData, } from '@backstage/frontend-plugin-api'; import { CoreRouter } from './extensions/CoreRouter'; +import { Core } from './extensions/Core'; import { createExtensionInstance, ExtensionInstance, @@ -31,8 +32,13 @@ import { readAppExtensionParameters, } from './wiring/parameters'; import { RoutingProvider } from './routing/RoutingContext'; -import { RouteRef } from '@backstage/core-plugin-api'; +import { ApiHolder, RouteRef } from '@backstage/core-plugin-api'; import { getAvailablePlugins } from './wiring/discovery'; +import { + ApiFactoryRegistry, + ApiProvider, + ApiResolver, +} from '@backstage/core-app-api'; /** @public */ export function createApp(options: { plugins: BackstagePlugin[] }): { @@ -40,7 +46,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { } { const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any); - const builtinExtensions = [CoreRouter]; + const builtinExtensions = [CoreRouter, Core]; const discoveredPlugins = getAvailablePlugins(); // pull in default extension instance from discovered packages @@ -115,25 +121,59 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { const routePaths = extractRouteInfoFromInstanceTree(rootInstances); + const coreInstance = rootInstances.find(({ id }) => id === 'core'); + if (!coreInstance) { + throw Error('Unable to find core extension instance'); + } + + const apiHolder = createApiHolder(coreInstance); + return { createRoot() { - const rootComponents = rootInstances.map( - e => - e.data.get( - coreExtensionData.reactComponent.id, - ) as typeof coreExtensionData.reactComponent.T, - ); + const rootComponents = rootInstances + .map( + e => + e.data.get( + coreExtensionData.reactComponent.id, + ) as typeof coreExtensionData.reactComponent.T, + ) + .filter(Boolean); return ( - - {rootComponents.map((Component, i) => ( - - ))} - + + + {rootComponents.map((Component, i) => ( + + ))} + + ); }, }; } +function createApiHolder(coreExtension: ExtensionInstance): ApiHolder { + const factoryRegistry = new ApiFactoryRegistry(); + + const apiFactories = + coreExtension.attachments + .get('apis') + ?.map( + e => + e.data.get( + coreExtensionData.apiFactory.id, + ) as typeof coreExtensionData.apiFactory.T, + ) + .filter(Boolean) ?? []; + + for (const factory of apiFactories) { + factoryRegistry.register('default', factory); + } + + ApiResolver.validateFactories(factoryRegistry, factoryRegistry.getAllApis()); + + return new ApiResolver(factoryRegistry); +} + /** @internal */ export function extractRouteInfoFromInstanceTree( roots: ExtensionInstance[], diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx new file mode 100644 index 0000000000..cb93b2888f --- /dev/null +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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 { + coreExtensionData, + createExtension, +} from '@backstage/frontend-plugin-api'; + +export const Core = createExtension({ + id: 'core', + at: 'root', + inputs: { + apis: { + extensionData: { + api: coreExtensionData.apiFactory, + }, + }, + }, + output: {}, + factory() {}, +}); diff --git a/yarn.lock b/yarn.lock index 7075f315dc..4a9b30d532 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4317,6 +4317,7 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/core-app-api": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-graphiql": "workspace:^" From b628d30a2b0303c66ba96fd46c0d6fc8a6d82505 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 11:43:30 +0200 Subject: [PATCH 02/16] frontend-app-api: add support for default app themes Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/createApp.tsx | 30 ++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index 77f0d1948a..41d16a7a9a 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -32,13 +32,22 @@ import { readAppExtensionParameters, } from './wiring/parameters'; import { RoutingProvider } from './routing/RoutingContext'; -import { ApiHolder, RouteRef } from '@backstage/core-plugin-api'; +import { + ApiHolder, + appThemeApiRef, + RouteRef, +} from '@backstage/core-plugin-api'; import { getAvailablePlugins } from './wiring/discovery'; import { ApiFactoryRegistry, ApiProvider, ApiResolver, + AppThemeSelector, } from '@backstage/core-app-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppThemeProvider } from '../../core-app-api/src/app/AppThemeProvider'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { themes } from '../../app-defaults/src/defaults/themes'; /** @public */ export function createApp(options: { plugins: BackstagePlugin[] }): { @@ -140,11 +149,13 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { .filter(Boolean); return ( - - {rootComponents.map((Component, i) => ( - - ))} - + + + {rootComponents.map((Component, i) => ( + + ))} + + ); }, @@ -169,6 +180,13 @@ function createApiHolder(coreExtension: ExtensionInstance): ApiHolder { factoryRegistry.register('default', factory); } + factoryRegistry.register('static', { + api: appThemeApiRef, + deps: {}, + // TODO: add extension for registering themes + factory: () => AppThemeSelector.createWithStorage(themes), + }); + ApiResolver.validateFactories(factoryRegistry, factoryRegistry.getAllApis()); return new ApiResolver(factoryRegistry); From e30df514e13f0254778207f038737fc32a6fc083 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 13:45:27 +0200 Subject: [PATCH 03/16] frontend-app-api: add config support Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../src/app/defaultConfigLoader.ts | 12 ++++++--- packages/frontend-app-api/src/createApp.tsx | 27 ++++++++++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/core-app-api/src/app/defaultConfigLoader.ts b/packages/core-app-api/src/app/defaultConfigLoader.ts index 00aee2d367..2ea8673f39 100644 --- a/packages/core-app-api/src/app/defaultConfigLoader.ts +++ b/packages/core-app-api/src/app/defaultConfigLoader.ts @@ -16,7 +16,6 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { AppConfigLoader } from './types'; /** * The default config loader, which expects that config is available at compile-time @@ -30,12 +29,17 @@ import { AppConfigLoader } from './types'; * * @public */ -export const defaultConfigLoader: AppConfigLoader = async ( +export async function defaultConfigLoader(): Promise { + return defaultConfigLoaderSync(); +} + +/** @internal */ +export function defaultConfigLoaderSync( // This string may be replaced at runtime to provide additional config. // It should be replaced by a JSON-serialized config object. // It's a param so we can test it, but at runtime this will always fall back to default. runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', -) => { +) { const appConfig = process.env.APP_CONFIG; if (!appConfig) { throw new Error('No static configuration provided'); @@ -70,4 +74,4 @@ export const defaultConfigLoader: AppConfigLoader = async ( }); } return configs; -}; +} diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index 41d16a7a9a..7393740863 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -35,6 +35,8 @@ import { RoutingProvider } from './routing/RoutingContext'; import { ApiHolder, appThemeApiRef, + ConfigApi, + configApiRef, RouteRef, } from '@backstage/core-plugin-api'; import { getAvailablePlugins } from './wiring/discovery'; @@ -44,16 +46,24 @@ import { ApiResolver, AppThemeSelector, } from '@backstage/core-app-api'; + +// TODO: Get rid of all of these // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppThemeProvider } from '../../core-app-api/src/app/AppThemeProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { themes } from '../../app-defaults/src/defaults/themes'; /** @public */ -export function createApp(options: { plugins: BackstagePlugin[] }): { +export function createApp(options: { + plugins: BackstagePlugin[]; + config?: ConfigApi; +}): { createRoot(): JSX.Element; } { - const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any); + const appConfig = + options?.config ?? ConfigReader.fromConfigs(defaultConfigLoaderSync()); const builtinExtensions = [CoreRouter, Core]; const discoveredPlugins = getAvailablePlugins(); @@ -135,7 +145,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { throw Error('Unable to find core extension instance'); } - const apiHolder = createApiHolder(coreInstance); + const apiHolder = createApiHolder(coreInstance, appConfig); return { createRoot() { @@ -162,7 +172,10 @@ export function createApp(options: { plugins: BackstagePlugin[] }): { }; } -function createApiHolder(coreExtension: ExtensionInstance): ApiHolder { +function createApiHolder( + coreExtension: ExtensionInstance, + configApi: ConfigApi, +): ApiHolder { const factoryRegistry = new ApiFactoryRegistry(); const apiFactories = @@ -187,6 +200,12 @@ function createApiHolder(coreExtension: ExtensionInstance): ApiHolder { factory: () => AppThemeSelector.createWithStorage(themes), }); + factoryRegistry.register('static', { + api: configApiRef, + deps: {}, + factory: () => configApi, + }); + ApiResolver.validateFactories(factoryRegistry, factoryRegistry.getAllApis()); return new ApiResolver(factoryRegistry); From 46b4845a7b46dda9aacce5ee51d0a87b15b3a27e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 13:46:03 +0200 Subject: [PATCH 04/16] graphiql: stop disabling endpoint extensions by default + configure in app-next Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/app-next/app-config.yaml | 2 ++ plugins/graphiql/src/alpha.tsx | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index c94d900b21..5a110b06e4 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -2,6 +2,8 @@ app: experimental: packages: 'all' # ✨ + extensions: + - apis.plugin.graphiql.browse.gitlab: true # scmAuthExtension: >- # createScmAuthExtension({ diff --git a/plugins/graphiql/src/alpha.tsx b/plugins/graphiql/src/alpha.tsx index ded517e3c7..f0333c8c6c 100644 --- a/plugins/graphiql/src/alpha.tsx +++ b/plugins/graphiql/src/alpha.tsx @@ -65,13 +65,14 @@ export const graphiqlBrowseApi = createApiExtension({ export function createEndpointExtension(options: { id: string; configSchema?: PortableSchema; + disabled?: boolean; factory: (options: { config: TConfig }) => { endpoint: GraphQLEndpoint }; }) { return createExtension({ id: `apis.plugin.graphiql.browse.${options.id}`, at: 'apis.plugin.graphiql.browse/endpoints', configSchema: options.configSchema, - disabled: true, + disabled: options.disabled ?? false, output: { endpoint: endpointDataRef, }, @@ -86,6 +87,7 @@ export function createEndpointExtension(options: { /** @alpha */ const gitlabGraphiQLBrowseExtension = createEndpointExtension({ id: 'gitlab', + disabled: true, configSchema: createSchemaFromZod(z => z .object({ From 089291c7e53008d55f7b387a02fae464d2797ff6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 14:29:45 +0200 Subject: [PATCH 05/16] core-app-api: fix defaultConfigLoader tests Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../src/app/defaultConfigLoader.test.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/core-app-api/src/app/defaultConfigLoader.test.ts b/packages/core-app-api/src/app/defaultConfigLoader.test.ts index aebba92c43..7579e53dd2 100644 --- a/packages/core-app-api/src/app/defaultConfigLoader.test.ts +++ b/packages/core-app-api/src/app/defaultConfigLoader.test.ts @@ -14,40 +14,38 @@ * limitations under the License. */ -import { defaultConfigLoader } from './defaultConfigLoader'; +import { defaultConfigLoaderSync } from './defaultConfigLoader'; (process as any).env = { NODE_ENV: 'test' }; const anyEnv = process.env as any; const anyWindow = window as any; -describe('defaultConfigLoader', () => { +describe('defaultConfigLoaderSync', () => { afterEach(() => { delete anyEnv.APP_CONFIG; delete anyWindow.__APP_CONFIG__; }); - it('loads static config', async () => { + it('loads static config', () => { anyEnv.APP_CONFIG = [ { data: { my: 'config' }, context: 'a' }, { data: { my: 'override-config' }, context: 'b' }, ]; - const configs = await defaultConfigLoader(); + const configs = defaultConfigLoaderSync(); expect(configs).toEqual([ { data: { my: 'config' }, context: 'a' }, { data: { my: 'override-config' }, context: 'b' }, ]); }); - it('loads runtime config', async () => { + it('loads runtime config', () => { anyEnv.APP_CONFIG = [ { data: { my: 'override-config' }, context: 'a' }, { data: { my: 'config' }, context: 'b' }, ]; - const configs = await (defaultConfigLoader as any)( - '{"my":"runtime-config"}', - ); + const configs = (defaultConfigLoaderSync as any)('{"my":"runtime-config"}'); expect(configs).toEqual([ { data: { my: 'override-config' }, context: 'a' }, { data: { my: 'config' }, context: 'b' }, @@ -55,28 +53,28 @@ describe('defaultConfigLoader', () => { ]); }); - it('fails to load invalid missing config', async () => { - await expect(defaultConfigLoader()).rejects.toThrow( + it('fails to load invalid missing config', () => { + expect(() => defaultConfigLoaderSync()).toThrow( 'No static configuration provided', ); }); - it('fails to load invalid static config', async () => { + it('fails to load invalid static config', () => { anyEnv.APP_CONFIG = { my: 'invalid-config' }; - await expect(defaultConfigLoader()).rejects.toThrow( + expect(() => defaultConfigLoaderSync()).toThrow( 'Static configuration has invalid format', ); }); - it('fails to load bad runtime config', async () => { + it('fails to load bad runtime config', () => { anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; - await expect((defaultConfigLoader as any)('}')).rejects.toThrow( + expect(() => defaultConfigLoaderSync('}')).toThrow( 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', ); }); - it('loads config from window.__APP_CONFIG__', async () => { + it('loads config from window.__APP_CONFIG__', () => { anyEnv.APP_CONFIG = [ { data: { my: 'config' }, context: 'a' }, { data: { my: 'override-config' }, context: 'b' }, @@ -84,7 +82,7 @@ describe('defaultConfigLoader', () => { const windowConfig = { app: { configKey: 'config-value' } }; anyWindow.__APP_CONFIG__ = windowConfig; - const configs = await defaultConfigLoader(); + const configs = defaultConfigLoaderSync(); expect(configs).toEqual([ ...anyEnv.APP_CONFIG, From 528b28ffc554b638f33f297f952ada85a1663f5f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 14:31:47 +0200 Subject: [PATCH 06/16] core-app-api: extract out overrideBaseUrlConfigs Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/AppManager.tsx | 63 ++------------ .../src/app/overrideBaseUrlConfigs.ts | 84 +++++++++++++++++++ 2 files changed, 89 insertions(+), 58 deletions(-) create mode 100644 packages/core-app-api/src/app/overrideBaseUrlConfigs.ts diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index de078c0ffc..0bd495c03b 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AppConfig, Config } from '@backstage/config'; +import { Config } from '@backstage/config'; import React, { ComponentType, PropsWithChildren, @@ -81,6 +81,7 @@ import { InternalAppContext } from './InternalAppContext'; import { AppRouter, getBasePath } from './AppRouter'; import { AppTranslationProvider } from './AppTranslationProvider'; import { AppTranslationApiImpl } from '../apis/implementations/AppTranslationApi'; +import { overrideBaseUrlConfigs } from './overrideBaseUrlConfigs'; type CompatiblePlugin = | BackstagePlugin @@ -88,17 +89,6 @@ type CompatiblePlugin = output(): Array<{ type: 'feature-flag'; name: string }>; }); -/** - * Creates a base URL that uses to the current document origin. - */ -function createLocalBaseUrl(fullUrl: string): string { - const url = new URL(fullUrl); - url.protocol = document.location.protocol; - url.hostname = document.location.hostname; - url.port = document.location.port; - return url.toString().replace(/\/$/, ''); -} - function useConfigLoader( configLoader: AppConfigLoader | undefined, components: AppComponents, @@ -131,52 +121,9 @@ function useConfigLoader( }; } - let configReader; - /** - * config.value can be undefined or empty. If it's either, don't bother overriding anything. - */ - if (config.value?.length) { - const urlConfigReader = ConfigReader.fromConfigs(config.value); - - /** - * Test configs may not define `app.baseUrl` or `backend.baseUrl` and we - * don't want to enforce here. - */ - const appBaseUrl = urlConfigReader.getOptionalString('app.baseUrl'); - const backendBaseUrl = urlConfigReader.getOptionalString('backend.baseUrl'); - - let configs = config.value; - const relativeResolverConfig: AppConfig = { - data: {}, - context: 'relative-resolver', - }; - if (appBaseUrl && backendBaseUrl) { - const appOrigin = new URL(appBaseUrl).origin; - const backendOrigin = new URL(backendBaseUrl).origin; - - if (appOrigin === backendOrigin) { - const newBackendBaseUrl = createLocalBaseUrl(backendBaseUrl); - if (backendBaseUrl !== newBackendBaseUrl) { - relativeResolverConfig.data.backend = { baseUrl: newBackendBaseUrl }; - } - } - } - if (appBaseUrl) { - const newAppBaseUrl = createLocalBaseUrl(appBaseUrl); - if (appBaseUrl !== newAppBaseUrl) { - relativeResolverConfig.data.app = { baseUrl: newAppBaseUrl }; - } - } - /** - * Only add the relative config if there is actually data to add. - */ - if (Object.keys(relativeResolverConfig.data).length) { - configs = configs.concat([relativeResolverConfig]); - } - configReader = ConfigReader.fromConfigs(configs); - } else { - configReader = ConfigReader.fromConfigs([]); - } + const configReader = ConfigReader.fromConfigs( + config.value?.length ? overrideBaseUrlConfigs(config.value) : [], + ); return { api: configReader }; } diff --git a/packages/core-app-api/src/app/overrideBaseUrlConfigs.ts b/packages/core-app-api/src/app/overrideBaseUrlConfigs.ts new file mode 100644 index 0000000000..5dab6db127 --- /dev/null +++ b/packages/core-app-api/src/app/overrideBaseUrlConfigs.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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 { AppConfig, ConfigReader } from '@backstage/config'; + +/** + * Creates a base URL that uses to the current document origin. + */ +function createLocalBaseUrl(fullUrl: string): string { + const url = new URL(fullUrl); + url.protocol = document.location.protocol; + url.hostname = document.location.hostname; + url.port = document.location.port; + return url.toString().replace(/\/$/, ''); +} + +/** + * If we are able to override the app and backend base URLs to values that + * match the origin of the current location, then this function returns a + * new array of app configs that contain the overrides. + * + * @internal + */ +export function overrideBaseUrlConfigs(inputConfigs: AppConfig[]): AppConfig[] { + const urlConfigReader = ConfigReader.fromConfigs(inputConfigs); + + // In tests we may not have `app.baseUrl` or `backend.baseUrl`, to keep them optional + const appBaseUrl = urlConfigReader.getOptionalString('app.baseUrl'); + const backendBaseUrl = urlConfigReader.getOptionalString('backend.baseUrl'); + + let configs = inputConfigs; + + let newBackendBaseUrl: string | undefined = undefined; + let newAppBaseUrl: string | undefined = undefined; + + if (appBaseUrl && backendBaseUrl) { + const appOrigin = new URL(appBaseUrl).origin; + const backendOrigin = new URL(backendBaseUrl).origin; + + if (appOrigin === backendOrigin) { + const maybeNewBackendBaseUrl = createLocalBaseUrl(backendBaseUrl); + if (backendBaseUrl !== maybeNewBackendBaseUrl) { + newBackendBaseUrl = maybeNewBackendBaseUrl; + } + } + } + + if (appBaseUrl) { + const maybeNewAppBaseUrl = createLocalBaseUrl(appBaseUrl); + if (appBaseUrl !== maybeNewAppBaseUrl) { + newAppBaseUrl = maybeNewAppBaseUrl; + } + } + + // Only add the relative config if there is actually data to add. + if (newAppBaseUrl || newBackendBaseUrl) { + configs = configs.concat({ + data: { + app: newAppBaseUrl && { + baseUrl: newAppBaseUrl, + }, + backend: newBackendBaseUrl && { + baseUrl: newBackendBaseUrl, + }, + }, + context: 'relative-resolver', + }); + } + + return configs; +} From 7a77415a91f88e652dfffc9faf725baa41ff92f3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 14:34:26 +0200 Subject: [PATCH 07/16] frontend-app-api: use overrideBaseUrlConfigs Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/createApp.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index 7393740863..588ebc7f5e 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -53,6 +53,8 @@ import { AppThemeProvider } from '../../core-app-api/src/app/AppThemeProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseUrlConfigs'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { themes } from '../../app-defaults/src/defaults/themes'; /** @public */ @@ -63,7 +65,8 @@ export function createApp(options: { createRoot(): JSX.Element; } { const appConfig = - options?.config ?? ConfigReader.fromConfigs(defaultConfigLoaderSync()); + options?.config ?? + ConfigReader.fromConfigs(overrideBaseUrlConfigs(defaultConfigLoaderSync())); const builtinExtensions = [CoreRouter, Core]; const discoveredPlugins = getAvailablePlugins(); From f0e3c10f4096095062fb1fb5c30471aa5f472db0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 15:32:22 +0200 Subject: [PATCH 08/16] frontend-app-api: add legacy app context and default APIs Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/createApp.tsx | 90 ++++++++++++++++++--- 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index 588ebc7f5e..19ca7b367f 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -33,11 +33,16 @@ import { } from './wiring/parameters'; import { RoutingProvider } from './routing/RoutingContext'; import { + AnyApiFactory, ApiHolder, + AppComponents, + AppContext, appThemeApiRef, ConfigApi, configApiRef, + IconComponent, RouteRef, + BackstagePlugin as LegacyBackstagePlugin, } from '@backstage/core-plugin-api'; import { getAvailablePlugins } from './wiring/discovery'; import { @@ -51,11 +56,18 @@ import { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppThemeProvider } from '../../core-app-api/src/app/AppThemeProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppContextProvider } from '../../core-app-api/src/app/AppContext'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseUrlConfigs'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { themes } from '../../app-defaults/src/defaults/themes'; +import { + apis as defaultApis, + components as defaultComponents, + icons as defaultIcons, + themes as defaultThemes, +} from '../../app-defaults/src/defaults'; /** @public */ export function createApp(options: { @@ -70,11 +82,12 @@ export function createApp(options: { const builtinExtensions = [CoreRouter, Core]; const discoveredPlugins = getAvailablePlugins(); + const allPlugins = [...discoveredPlugins, ...options.plugins]; // pull in default extension instance from discovered packages // apply config to adjust default extension instances and add more const extensionParams = mergeExtensionParameters({ - sources: [...options.plugins, ...discoveredPlugins], + sources: allPlugins, builtinExtensions, parameters: readAppExtensionParameters(appConfig), }); @@ -150,6 +163,8 @@ export function createApp(options: { const apiHolder = createApiHolder(coreInstance, appConfig); + const appContext = createLegacyAppContext(allPlugins); + return { createRoot() { const rootComponents = rootInstances @@ -162,19 +177,65 @@ export function createApp(options: { .filter(Boolean); return ( - - - {rootComponents.map((Component, i) => ( - - ))} - - + + + + {rootComponents.map((Component, i) => ( + + ))} + + + ); }, }; } +function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin { + const errorMsg = 'Not implemented in legacy plugin compatibility layer'; + const notImplemented = () => { + throw new Error(errorMsg); + }; + return { + getId(): string { + return plugin.id; + }, + get routes(): never { + throw new Error(errorMsg); + }, + get externalRoutes(): never { + throw new Error(errorMsg); + }, + getApis: notImplemented, + getFeatureFlags: notImplemented, + provide: notImplemented, + __experimentalReconfigure: notImplemented, + }; +} + +function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext { + return { + getPlugins(): LegacyBackstagePlugin[] { + return plugins.map(toLegacyPlugin); + }, + + getSystemIcon(key: string): IconComponent | undefined { + return key in defaultIcons + ? defaultIcons[key as keyof typeof defaultIcons] + : undefined; + }, + + getSystemIcons(): Record { + return defaultIcons; + }, + + getComponents(): AppComponents { + return defaultComponents; + }, + }; +} + function createApiHolder( coreExtension: ExtensionInstance, configApi: ConfigApi, @@ -200,7 +261,7 @@ function createApiHolder( api: appThemeApiRef, deps: {}, // TODO: add extension for registering themes - factory: () => AppThemeSelector.createWithStorage(themes), + factory: () => AppThemeSelector.createWithStorage(defaultThemes), }); factoryRegistry.register('static', { @@ -209,6 +270,15 @@ function createApiHolder( factory: () => configApi, }); + // TODO: ship these as default extensions instead + for (const factory of defaultApis as AnyApiFactory[]) { + if (!factoryRegistry.register('app', factory)) { + throw new Error( + `Duplicate or forbidden API factory for ${factory.api} in app`, + ); + } + } + ApiResolver.validateFactories(factoryRegistry, factoryRegistry.getAllApis()); return new ApiResolver(factoryRegistry); From 3b1d74770484a4a20e9b1629f880906967b911ed Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 15:42:13 +0200 Subject: [PATCH 09/16] app-next: remove legacy app wrapping Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/app-next/src/App.tsx | 6 ++---- packages/app-next/src/index.tsx | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 0452cc3709..aa9bb985c2 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ -import { graphiqlPlugin as legacyGraphiqlPlugin } from '@backstage/plugin-graphiql'; -import { createApp as createLegacyApp } from '@backstage/app-defaults'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; @@ -61,9 +59,9 @@ const app = createApp({ // }, }); -const legacyApp = createLegacyApp({ plugins: [legacyGraphiqlPlugin] }); +// const legacyApp = createLegacyApp({ plugins: [legacyGraphiqlPlugin] }); -export default legacyApp.createRoot(app.createRoot()); +export default app.createRoot(); // const routes = ( // diff --git a/packages/app-next/src/index.tsx b/packages/app-next/src/index.tsx index b15bc4c102..3c354b06d0 100644 --- a/packages/app-next/src/index.tsx +++ b/packages/app-next/src/index.tsx @@ -15,8 +15,7 @@ */ import '@backstage/cli/asset-types'; -import React from 'react'; import ReactDOM from 'react-dom'; -import App from './App'; +import app from './App'; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render(app, document.getElementById('root')); From 70169897f578f635e3d771edeec7be17b4045718 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 15:42:41 +0200 Subject: [PATCH 10/16] frontend-app-api: note on setting base path correctly Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/extensions/CoreRouter.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx index 05f84aba6d..a148931b0f 100644 --- a/packages/frontend-app-api/src/extensions/CoreRouter.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx @@ -48,6 +48,7 @@ export const CoreRouter = createExtension({ return element; }; bind({ + // TODO: set base path using the logic from AppRouter component: () => ( From c55a1a4a93125a576275daaeebc97b80ea4ff6a3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 8 Sep 2023 16:35:59 +0200 Subject: [PATCH 11/16] frontend-app-api: implement core layout and initial sidebar Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/package.json | 2 + packages/frontend-app-api/src/createApp.tsx | 26 ++++-- .../src/extensions/CoreLayout.tsx | 68 +++++++++++++++ .../src/extensions/CoreNav.tsx | 84 +++++++++++++++++++ .../{CoreRouter.tsx => CoreRoutes.tsx} | 15 ++-- .../extensions/createPageExtension.test.tsx | 4 +- .../src/extensions/createPageExtension.tsx | 2 +- .../src/wiring/createPlugin.test.ts | 4 +- yarn.lock | 2 + 9 files changed, 187 insertions(+), 20 deletions(-) create mode 100644 packages/frontend-app-api/src/extensions/CoreLayout.tsx create mode 100644 packages/frontend-app-api/src/extensions/CoreNav.tsx rename packages/frontend-app-api/src/extensions/{CoreRouter.tsx => CoreRoutes.tsx} (80%) diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 17f4b1bef3..f377be2122 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -35,10 +35,12 @@ "dependencies": { "@backstage/config": "workspace:^", "@backstage/core-app-api": "workspace:^", + "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-graphiql": "workspace:^", "@backstage/types": "workspace:^", + "@material-ui/core": "^4.12.4", "lodash": "^4.17.21" }, "peerDependencies": { diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx index 19ca7b367f..4882191206 100644 --- a/packages/frontend-app-api/src/createApp.tsx +++ b/packages/frontend-app-api/src/createApp.tsx @@ -20,8 +20,10 @@ import { BackstagePlugin, coreExtensionData, } from '@backstage/frontend-plugin-api'; -import { CoreRouter } from './extensions/CoreRouter'; import { Core } from './extensions/Core'; +import { CoreRoutes } from './extensions/CoreRoutes'; +import { CoreLayout } from './extensions/CoreLayout'; +import { CoreNav } from './extensions/CoreNav'; import { createExtensionInstance, ExtensionInstance, @@ -43,6 +45,7 @@ import { IconComponent, RouteRef, BackstagePlugin as LegacyBackstagePlugin, + featureFlagsApiRef, } from '@backstage/core-plugin-api'; import { getAvailablePlugins } from './wiring/discovery'; import { @@ -58,6 +61,8 @@ import { AppThemeProvider } from '../../core-app-api/src/app/AppThemeProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppContextProvider } from '../../core-app-api/src/app/AppContext'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { LocalStorageFeatureFlags } from '../../core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseUrlConfigs'; @@ -68,6 +73,7 @@ import { icons as defaultIcons, themes as defaultThemes, } from '../../app-defaults/src/defaults'; +import { BrowserRouter } from 'react-router-dom'; /** @public */ export function createApp(options: { @@ -80,7 +86,7 @@ export function createApp(options: { options?.config ?? ConfigReader.fromConfigs(overrideBaseUrlConfigs(defaultConfigLoaderSync())); - const builtinExtensions = [CoreRouter, Core]; + const builtinExtensions = [Core, CoreRoutes, CoreNav, CoreLayout]; const discoveredPlugins = getAvailablePlugins(); const allPlugins = [...discoveredPlugins, ...options.plugins]; @@ -180,9 +186,12 @@ export function createApp(options: { - {rootComponents.map((Component, i) => ( - - ))} + {/* TODO: set base path using the logic from AppRouter */} + + {rootComponents.map((Component, i) => ( + + ))} + @@ -257,6 +266,13 @@ function createApiHolder( factoryRegistry.register('default', factory); } + // TODO: properly discovery feature flags, maybe rework the whole thing + factoryRegistry.register('default', { + api: featureFlagsApiRef, + deps: {}, + factory: () => new LocalStorageFeatureFlags(), + }); + factoryRegistry.register('static', { api: appThemeApiRef, deps: {}, diff --git a/packages/frontend-app-api/src/extensions/CoreLayout.tsx b/packages/frontend-app-api/src/extensions/CoreLayout.tsx new file mode 100644 index 0000000000..845bc8cf5b --- /dev/null +++ b/packages/frontend-app-api/src/extensions/CoreLayout.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2023 The Backstage Authors + * + * 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 { + createExtension, + coreExtensionData, +} from '@backstage/frontend-plugin-api'; +import { SidebarPage } from '@backstage/core-components'; + +export const CoreLayout = createExtension({ + id: 'core.layout', + at: 'root', + inputs: { + nav: { + extensionData: { + component: coreExtensionData.reactComponent, + }, + }, + content: { + extensionData: { + component: coreExtensionData.reactComponent, + }, + }, + }, + output: { + component: coreExtensionData.reactComponent, + }, + factory({ bind, inputs }) { + // TODO: Support this as part of the core system + if (inputs.nav.length !== 1) { + throw Error( + `Extension 'core.layout' did not receive exactly one 'nav' input, got ${inputs.nav.length}`, + ); + } + const Nav = inputs.nav[0].component; + + if (inputs.content.length !== 1) { + throw Error( + `Extension 'core.layout' did not receive exactly one 'content' input, got ${inputs.content.length}`, + ); + } + const Content = inputs.content[0].component; + + bind({ + // TODO: set base path using the logic from AppRouter + component: () => ( + +