diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index b79a6850bd..77252ac458 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -15,11 +15,9 @@ */ import { - ApiRegistry, alertApiRef, errorApiRef, AlertApiForwarder, - ConfigApi, ErrorApiForwarder, ErrorAlerter, featureFlagsApiRef, @@ -44,6 +42,8 @@ import { microsoftAuthApiRef, storageApiRef, WebStorage, + createApiFactory, + configApiRef, } from '@backstage/core'; import { @@ -82,135 +82,162 @@ import { githubPullRequestsApiRef, } from '@roadiehq/backstage-plugin-github-pull-requests'; -export const apis = (config: ConfigApi) => { - // eslint-disable-next-line no-console - console.log(`Creating APIs for ${config.getString('app.title')}`); - - const backendUrl = config.getString('backend.baseUrl'); - const techdocsUrl = config.getString('techdocs.storageUrl'); - - const builder = ApiRegistry.builder(); - - const discoveryApi = builder.add( - discoveryApiRef, - UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`), - ); - const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); - const errorApi = builder.add( - errorApiRef, - new ErrorAlerter(alertApi, new ErrorApiForwarder()), - ); - - builder.add(storageApiRef, WebStorage.create({ errorApi })); - builder.add(GCPApiRef, new GCPClient()); - builder.add( - circleCIApiRef, - new CircleCIApi(`${backendUrl}/proxy/circleci/api`), - ); - - builder.add(jenkinsApiRef, new JenkinsApi(`${backendUrl}/proxy/jenkins/api`)); - - builder.add(githubActionsApiRef, new GithubActionsClient()); - - builder.add(featureFlagsApiRef, new FeatureFlags()); - - builder.add(lighthouseApiRef, LighthouseRestApi.fromConfig(config)); - - builder.add(travisCIApiRef, new TravisCIApi()); - builder.add(githubPullRequestsApiRef, new GithubPullRequestsClient()); - - const oauthRequestApi = builder.add( - oauthRequestApiRef, - new OAuthRequestManager(), - ); - - builder.add( - googleAuthApiRef, - GoogleAuth.create({ - discoveryApi, - oauthRequestApi, - }), - ); - - builder.add( - microsoftAuthApiRef, - MicrosoftAuth.create({ - discoveryApi, - oauthRequestApi, - }), - ); - - const githubAuthApi = builder.add( - githubAuthApiRef, - GithubAuth.create({ - discoveryApi, - oauthRequestApi, - }), - ); - - builder.add( - oktaAuthApiRef, - OktaAuth.create({ - discoveryApi, - oauthRequestApi, - }), - ); - - builder.add( - gitlabAuthApiRef, - GitlabAuth.create({ - discoveryApi, - oauthRequestApi, - }), - ); - - builder.add( - auth0AuthApiRef, - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - }), - ); - - builder.add( - oauth2ApiRef, - OAuth2.create({ - discoveryApi, - oauthRequestApi, - }), - ); - - builder.add(catalogApiRef, new CatalogClient({ discoveryApi })); - - builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi })); - - builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')); - - builder.add( - graphQlBrowseApiRef, - GraphQLEndpoints.from([ - GraphQLEndpoints.create({ - id: 'gitlab', - title: 'GitLab', - url: 'https://gitlab.com/api/graphql', +export const apis = [ + createApiFactory(alertApiRef, new AlertApiForwarder()), + createApiFactory({ + implements: errorApiRef, + deps: { alertApi: alertApiRef }, + factory: ({ alertApi }) => + new ErrorAlerter(alertApi, new ErrorApiForwarder()), + }), + createApiFactory({ + implements: storageApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => WebStorage.create({ errorApi }), + }), + createApiFactory({ + implements: discoveryApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + UrlPatternDiscovery.compile( + `${configApi.getString('backend.baseUrl')}/{{ pluginId }}`, + ), + }), + createApiFactory(GCPApiRef, new GCPClient()), + createApiFactory({ + implements: circleCIApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + new CircleCIApi( + `${configApi.getString('backend.baseUrl')}/proxy/circleci/api`, + ), + }), + createApiFactory({ + implements: jenkinsApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + new JenkinsApi( + `${configApi.getString('backend.baseUrl')}/proxy/jenkins/api`, + ), + }), + createApiFactory(githubActionsApiRef, new GithubActionsClient()), + createApiFactory(featureFlagsApiRef, new FeatureFlags()), + createApiFactory({ + implements: lighthouseApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => LighthouseRestApi.fromConfig(configApi), + }), + createApiFactory(travisCIApiRef, new TravisCIApi()), + createApiFactory(githubPullRequestsApiRef, new GithubPullRequestsClient()), + createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), + createApiFactory({ + implements: techRadarApiRef, + deps: {}, + factory: () => new TechRadar({ width: 1500, height: 800 }), + }), + createApiFactory({ + implements: googleAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + GoogleAuth.create({ discoveryApi, oauthRequestApi }), + }), + createApiFactory({ + implements: microsoftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + MicrosoftAuth.create({ discoveryApi, oauthRequestApi }), + }), + createApiFactory({ + implements: githubAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + GithubAuth.create({ discoveryApi, oauthRequestApi }), + }), + createApiFactory({ + implements: oktaAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + OktaAuth.create({ discoveryApi, oauthRequestApi }), + }), + createApiFactory({ + implements: gitlabAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + GitlabAuth.create({ discoveryApi, oauthRequestApi }), + }), + createApiFactory({ + implements: auth0AuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + Auth0Auth.create({ discoveryApi, oauthRequestApi }), + }), + createApiFactory({ + implements: oauth2ApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + OAuth2.create({ discoveryApi, oauthRequestApi }), + }), + createApiFactory({ + implements: catalogApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }), + }), + createApiFactory({ + implements: scaffolderApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }), + }), + createApiFactory(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')), + createApiFactory({ + implements: graphQlBrowseApiRef, + deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef }, + factory: ({ errorApi, githubAuthApi }) => + GraphQLEndpoints.from([ + GraphQLEndpoints.create({ + id: 'gitlab', + title: 'GitLab', + url: 'https://gitlab.com/api/graphql', + }), + GraphQLEndpoints.github({ + id: 'github', + title: 'GitHub', + errorApi, + githubAuthApi, + }), + ]), + }), + createApiFactory({ + implements: rollbarApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }), + }), + createApiFactory({ + implements: techdocsStorageApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + new TechDocsStorageApi({ + apiOrigin: configApi.getString('techdocs.storageUrl'), }), - GraphQLEndpoints.github({ - id: 'github', - title: 'GitHub', - errorApi, - githubAuthApi, - }), - ]), - ); - - builder.add(rollbarApiRef, new RollbarClient({ discoveryApi })); - - builder.add( - techdocsStorageApiRef, - new TechDocsStorageApi({ - apiOrigin: techdocsUrl, - }), - ); - - return builder.build(); -}; + }), +]; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index ccdfc4fc7f..ca8ab28e8c 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -26,7 +26,6 @@ import { BackstageApp, AppComponents, AppConfigLoader, - Apis, SignInResult, SignInPageProps, } from './types'; @@ -42,7 +41,6 @@ import { AppThemeProvider } from './AppThemeProvider'; import { IconComponent, SystemIcons, SystemIconKey } from '../icons'; import { - ApiHolder, ApiProvider, ApiRegistry, AppTheme, @@ -51,18 +49,22 @@ import { configApiRef, ConfigReader, useApi, + AnyApiFactory, + ApiHolder, } from '../apis'; -import { ApiAggregator } from '../apis/ApiAggregator'; import { useAsync } from 'react-use'; import { AppIdentity } from './AppIdentity'; +import { ApiFactoryRegistry } from '../apis/ApiFactoryRegistry'; +import { ApiResolver } from '../apis/ApiResolver'; type FullAppOptions = { - apis: Apis; + apis: Iterable; icons: SystemIcons; plugins: BackstagePlugin[]; components: AppComponents; themes: AppTheme[]; configLoader?: AppConfigLoader; + defaultApiFactories: Iterable; }; function useConfigLoader( @@ -101,31 +103,27 @@ function useConfigLoader( } export class PrivateAppImpl implements BackstageApp { - private apis?: ApiHolder = undefined; + private apiHolder?: ApiHolder; + private configApi?: ConfigApi; + + private readonly apis: Iterable; private readonly icons: SystemIcons; private readonly plugins: BackstagePlugin[]; private readonly components: AppComponents; private readonly themes: AppTheme[]; private readonly configLoader?: AppConfigLoader; + private readonly defaultApiFactories: Iterable; private readonly identityApi = new AppIdentity(); - private apisOrFactory: Apis; - constructor(options: FullAppOptions) { - this.apisOrFactory = options.apis; + this.apis = options.apis; this.icons = options.icons; this.plugins = options.plugins; this.components = options.components; this.themes = options.themes; this.configLoader = options.configLoader; - } - - getApis(): ApiHolder { - if (!this.apis) { - throw new Error('Tried to access APIs before app was loaded'); - } - return this.apis; + this.defaultApiFactories = options.defaultApiFactories; } getPlugins(): BackstagePlugin[] { @@ -186,9 +184,9 @@ export class PrivateAppImpl implements BackstageApp { } } - const FeatureFlags = this.apis && this.apis.get(featureFlagsApiRef); - if (FeatureFlags) { - FeatureFlags.registeredFeatureFlags = registeredFeatureFlags; + const featureFlags = this.getApiHolder().get(featureFlagsApiRef); + if (featureFlags) { + featureFlags.registeredFeatureFlags = registeredFeatureFlags; } routes.push(} />); @@ -210,28 +208,14 @@ export class PrivateAppImpl implements BackstageApp { ); if ('node' in loadedConfig) { + // Loading or error return loadedConfig.node; } - const configApi = loadedConfig.api; - const appApis = ApiRegistry.from([ - [appThemeApiRef, appThemeApi], - [configApiRef, configApi], - [identityApiRef, this.identityApi], - ]); - - if (!this.apis) { - if ('get' in this.apisOrFactory) { - this.apis = this.apisOrFactory; - } else { - this.apis = this.apisOrFactory(configApi); - } - } - - const apis = new ApiAggregator(this.apis, appApis); + this.configApi = loadedConfig.api; return ( - + {children} @@ -306,6 +290,55 @@ export class PrivateAppImpl implements BackstageApp { return AppRouter; } + private getApiHolder(): ApiHolder { + if (this.apiHolder) { + return this.apiHolder; + } + + const registry = new ApiFactoryRegistry(); + + registry.register('static', { + implements: appThemeApiRef, + deps: {}, + factory: () => AppThemeSelector.createWithStorage(this.themes), + }); + registry.register('static', { + implements: configApiRef, + deps: {}, + factory: () => { + if (!this.configApi) { + throw new Error( + 'Tried to access config API before config was loaded', + ); + } + return this.configApi; + }, + }); + registry.register('static', { + implements: identityApiRef, + deps: {}, + factory: () => this.identityApi, + }); + + for (const factory of this.defaultApiFactories) { + registry.register('default', factory); + } + + for (const factory of this.apis) { + if (!registry.register('app', factory)) { + throw new Error( + `Duplicate or forbidden API factory for ${factory.implements} in app`, + ); + } + } + + ApiResolver.validateFactories(registry, registry.getAllApis()); + + this.apiHolder = new ApiResolver(registry); + + return this.apiHolder; + } + verify() { const pluginIds = new Set(); diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index db7a0897b1..565e073aed 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -17,8 +17,8 @@ import { ComponentType } from 'react'; import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; import { BackstagePlugin } from '../plugin'; -import { ApiHolder } from '../apis'; -import { AppTheme, ConfigApi, ProfileInfo } from '../apis/definitions'; +import { AnyApiFactory } from '../apis'; +import { AppTheme, ProfileInfo } from '../apis/definitions'; import { AppConfig } from '@backstage/config'; export type BootErrorPageProps = { @@ -77,16 +77,12 @@ export type AppComponents = { */ export type AppConfigLoader = () => Promise; -// TODO(Rugvip): Temporary workaround for accessing config when instantiating APIs, we might want to do this differently -export type Apis = ApiHolder | ((config: ConfigApi) => ApiHolder); - export type AppOptions = { /** - * A holder of all APIs available in the app. - * - * Use for example ApiRegistry or ApiTestRegistry. + * A collection of ApiFactories to register in the application to either + * add add new ones, or override factories provided by default or by plugins. */ - apis?: Apis; + apis?: Iterable; /** * Supply icons to override the default ones. @@ -138,11 +134,6 @@ export type AppOptions = { }; export type BackstageApp = { - /** - * Get the holder for all APIs available in the app. - */ - getApis(): ApiHolder; - /** * Returns all plugins registered for the app. */ diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index 9d2096cfff..6073b3cef6 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -17,7 +17,6 @@ import React, { FC } from 'react'; import privateExports, { AppOptions, - ApiRegistry, defaultSystemIcons, BootErrorPageProps, AppConfigLoader, @@ -94,7 +93,7 @@ export function createApp(options?: AppOptions) { ); }; - const apis = options?.apis ?? ApiRegistry.from([]); + const apis = options?.apis ?? []; const icons = { ...defaultSystemIcons, ...options?.icons }; const plugins = options?.plugins ?? []; const components = { @@ -127,6 +126,7 @@ export function createApp(options?: AppOptions) { components, themes, configLoader, + defaultApiFactories: [], }); app.verify();