core-api,app: switch to auto DI with ApiResolver and ApiFactories
This commit is contained in:
+160
-133
@@ -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();
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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<AnyApiFactory>;
|
||||
icons: SystemIcons;
|
||||
plugins: BackstagePlugin[];
|
||||
components: AppComponents;
|
||||
themes: AppTheme[];
|
||||
configLoader?: AppConfigLoader;
|
||||
defaultApiFactories: Iterable<AnyApiFactory>;
|
||||
};
|
||||
|
||||
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<AnyApiFactory>;
|
||||
private readonly icons: SystemIcons;
|
||||
private readonly plugins: BackstagePlugin[];
|
||||
private readonly components: AppComponents;
|
||||
private readonly themes: AppTheme[];
|
||||
private readonly configLoader?: AppConfigLoader;
|
||||
private readonly defaultApiFactories: Iterable<AnyApiFactory>;
|
||||
|
||||
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(<Route path="/*" element={<NotFoundErrorPage />} />);
|
||||
@@ -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 (
|
||||
<ApiProvider apis={apis}>
|
||||
<ApiProvider apis={this.getApiHolder()}>
|
||||
<AppContextProvider app={this}>
|
||||
<AppThemeProvider>{children}</AppThemeProvider>
|
||||
</AppContextProvider>
|
||||
@@ -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<string>();
|
||||
|
||||
|
||||
@@ -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<AppConfig[]>;
|
||||
|
||||
// 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<AnyApiFactory>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user