Merge pull request #2285 from spotify/rugvip/di
Switch to automatic dependency injection of Utility APIs
This commit is contained in:
+34
-178
@@ -15,63 +15,18 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
ApiRegistry,
|
||||
alertApiRef,
|
||||
errorApiRef,
|
||||
AlertApiForwarder,
|
||||
ConfigApi,
|
||||
ErrorApiForwarder,
|
||||
ErrorAlerter,
|
||||
featureFlagsApiRef,
|
||||
FeatureFlags,
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery,
|
||||
GoogleAuth,
|
||||
GithubAuth,
|
||||
OAuth2,
|
||||
OktaAuth,
|
||||
GitlabAuth,
|
||||
Auth0Auth,
|
||||
MicrosoftAuth,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
googleAuthApiRef,
|
||||
githubAuthApiRef,
|
||||
oauth2ApiRef,
|
||||
oktaAuthApiRef,
|
||||
gitlabAuthApiRef,
|
||||
auth0AuthApiRef,
|
||||
microsoftAuthApiRef,
|
||||
storageApiRef,
|
||||
WebStorage,
|
||||
createApiFactory,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
|
||||
import {
|
||||
lighthouseApiRef,
|
||||
LighthouseRestApi,
|
||||
} from '@backstage/plugin-lighthouse';
|
||||
|
||||
import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
|
||||
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
|
||||
|
||||
import { gitOpsApiRef, GitOpsRestApi } from '@backstage/plugin-gitops-profiles';
|
||||
import {
|
||||
graphQlBrowseApiRef,
|
||||
GraphQLEndpoints,
|
||||
} from '@backstage/plugin-graphiql';
|
||||
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
|
||||
import {
|
||||
techdocsStorageApiRef,
|
||||
TechDocsStorageApi,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
|
||||
import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar';
|
||||
import { GCPClient, GCPApiRef } from '@backstage/plugin-gcp-projects';
|
||||
import {
|
||||
GithubActionsClient,
|
||||
githubActionsApiRef,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
import { jenkinsApiRef, JenkinsApi } from '@backstage/plugin-jenkins';
|
||||
|
||||
import {
|
||||
TravisCIApi,
|
||||
@@ -82,135 +37,36 @@ 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')}`);
|
||||
export const apis = [
|
||||
// TODO(Rugvip): migrate to use /api
|
||||
createApiFactory({
|
||||
api: discoveryApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) =>
|
||||
UrlPatternDiscovery.compile(
|
||||
`${configApi.getString('backend.baseUrl')}/{{ pluginId }}`,
|
||||
),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: 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,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
|
||||
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',
|
||||
}),
|
||||
GraphQLEndpoints.github({
|
||||
id: 'github',
|
||||
title: 'GitHub',
|
||||
errorApi,
|
||||
githubAuthApi,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
techdocsStorageApiRef,
|
||||
new TechDocsStorageApi({
|
||||
apiOrigin: techdocsUrl,
|
||||
}),
|
||||
);
|
||||
|
||||
return builder.build();
|
||||
};
|
||||
// TODO: move to plugins
|
||||
createApiFactory(travisCIApiRef, new TravisCIApi()),
|
||||
createApiFactory(githubPullRequestsApiRef, new GithubPullRequestsClient()),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 { ApiFactoryRegistry } from './ApiFactoryRegistry';
|
||||
import { createApiRef } from './ApiRef';
|
||||
|
||||
const aRef = createApiRef<number>({ id: 'a', description: '' });
|
||||
const aFactory1 = { api: aRef, deps: {}, factory: () => 1 };
|
||||
const aFactory2 = { api: aRef, deps: {}, factory: () => 2 };
|
||||
const bRef = createApiRef<string>({ id: 'b', description: '' });
|
||||
const bFactory = { api: bRef, deps: {}, factory: () => 'x' };
|
||||
const cRef = createApiRef<string>({ id: 'c', description: '' });
|
||||
const cFactory = { api: cRef, deps: {}, factory: () => 'y' };
|
||||
|
||||
describe('ApiFactoryRegistry', () => {
|
||||
it('should be empty when created', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.getAllApis()).toEqual(new Set());
|
||||
});
|
||||
|
||||
it('should register a factory', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.register('default', aFactory1)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.getAllApis()).toEqual(new Set([aRef]));
|
||||
});
|
||||
|
||||
it('should prioritize factories based on scope', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.register('default', aFactory1)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.register('default', aFactory2)).toBe(false);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.register('app', aFactory2)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory2);
|
||||
expect(registry.register('default', aFactory1)).toBe(false);
|
||||
expect(registry.get(aRef)).toBe(aFactory2);
|
||||
expect(registry.register('static', aFactory1)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.register('static', aFactory2)).toBe(false);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.register('app', aFactory2)).toBe(false);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.getAllApis()).toEqual(new Set([aRef]));
|
||||
});
|
||||
|
||||
it('should register multiple factories without conflict', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
expect(registry.register('static', aFactory1)).toBe(true);
|
||||
expect(registry.register('default', bFactory)).toBe(true);
|
||||
expect(registry.register('app', cFactory)).toBe(true);
|
||||
expect(registry.get(aRef)).toBe(aFactory1);
|
||||
expect(registry.get(bRef)).toBe(bFactory);
|
||||
expect(registry.get(cRef)).toBe(cFactory);
|
||||
expect(registry.getAllApis()).toEqual(new Set([aRef, bRef, cRef]));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 {
|
||||
ApiFactoryHolder,
|
||||
ApiFactory,
|
||||
AnyApiRef,
|
||||
AnyApiFactory,
|
||||
} from './types';
|
||||
import { ApiRef } from './ApiRef';
|
||||
|
||||
type ApiFactoryScope =
|
||||
| 'default' // Default factories registered by core and plugins
|
||||
| 'app' // Factories registered in the app, overriding default ones
|
||||
| 'static'; // APIs that can't be overridden, e.g. config
|
||||
|
||||
enum ScopePriority {
|
||||
default = 10,
|
||||
app = 50,
|
||||
static = 100,
|
||||
}
|
||||
|
||||
type FactoryTuple = {
|
||||
priority: number;
|
||||
factory: AnyApiFactory;
|
||||
};
|
||||
|
||||
/**
|
||||
* ApiFactoryRegistry is an ApiFactoryHolder implementation that enables
|
||||
* registration of API Factories with different scope.
|
||||
*
|
||||
* Each scope has an assigned priority, where factories registered with
|
||||
* higher priority scopes override ones with lower priority.
|
||||
*/
|
||||
export class ApiFactoryRegistry implements ApiFactoryHolder {
|
||||
private readonly factories = new Map<AnyApiRef, FactoryTuple>();
|
||||
|
||||
/**
|
||||
* Register a new API factory. Returns true if the factory was added
|
||||
* to the registry.
|
||||
*
|
||||
* A factory will not be added to the registry if there is already
|
||||
* an existing factory with the same or higher priority.
|
||||
*/
|
||||
register<Api, Deps extends { [name in string]: unknown }>(
|
||||
scope: ApiFactoryScope,
|
||||
factory: ApiFactory<Api, Deps>,
|
||||
) {
|
||||
const priority = ScopePriority[scope];
|
||||
const existing = this.factories.get(factory.api);
|
||||
if (existing && existing.priority >= priority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.factories.set(factory.api, { priority, factory });
|
||||
return true;
|
||||
}
|
||||
|
||||
get<T>(api: ApiRef<T>): ApiFactory<T, { [x: string]: unknown }> | undefined {
|
||||
const tuple = this.factories.get(api);
|
||||
if (!tuple) {
|
||||
return undefined;
|
||||
}
|
||||
return tuple.factory as ApiFactory<T, { [x: string]: unknown }>;
|
||||
}
|
||||
|
||||
getAllApis(): Set<AnyApiRef> {
|
||||
return new Set(this.factories.keys());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 { ApiResolver } from './ApiResolver';
|
||||
import { createApiRef } from './ApiRef';
|
||||
import { ApiFactoryRegistry } from './ApiFactoryRegistry';
|
||||
|
||||
const aRef = createApiRef<number>({ id: 'a', description: '' });
|
||||
const bRef = createApiRef<string>({ id: 'b', description: '' });
|
||||
const cRef = createApiRef<{ x: string }>({ id: 'c', description: '' });
|
||||
|
||||
function createRegistry() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: {},
|
||||
factory: () => 1,
|
||||
});
|
||||
registry.register('default', {
|
||||
api: bRef,
|
||||
deps: {},
|
||||
factory: () => 'b',
|
||||
});
|
||||
registry.register('default', {
|
||||
api: cRef,
|
||||
deps: { b: bRef },
|
||||
factory: ({ b }) => ({ x: 'x', b }),
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
function createLongCyclicRegistry() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: { b: bRef },
|
||||
factory: () => 1,
|
||||
});
|
||||
registry.register('default', {
|
||||
api: bRef,
|
||||
deps: { c: cRef },
|
||||
factory: () => 'b',
|
||||
});
|
||||
registry.register('default', {
|
||||
api: cRef,
|
||||
deps: { a: aRef },
|
||||
factory: () => ({ x: 'x' }),
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
function createShortCyclicRegistry() {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: { a: aRef },
|
||||
factory: () => 1,
|
||||
});
|
||||
registry.register('default', {
|
||||
api: bRef,
|
||||
deps: { c: cRef },
|
||||
factory: () => 'b',
|
||||
});
|
||||
registry.register('default', {
|
||||
api: cRef,
|
||||
deps: { b: bRef },
|
||||
factory: () => ({ x: 'x' }),
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
describe('ApiResolver', () => {
|
||||
it('should be created empty', () => {
|
||||
const resolver = new ApiResolver(new ApiFactoryRegistry());
|
||||
expect(resolver.get(aRef)).toBe(undefined);
|
||||
expect(resolver.get(bRef)).toBe(undefined);
|
||||
expect(resolver.get(cRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should instantiate APIs', () => {
|
||||
const resolver = new ApiResolver(createRegistry());
|
||||
expect(resolver.get(aRef)).toBe(1);
|
||||
expect(resolver.get(bRef)).toBe('b');
|
||||
expect(resolver.get(cRef)).toEqual({ x: 'x', b: 'b' });
|
||||
expect(resolver.get(cRef)).toBe(resolver.get(cRef));
|
||||
});
|
||||
|
||||
it('should detect long dependency cycles', () => {
|
||||
const resolver = new ApiResolver(createLongCyclicRegistry());
|
||||
expect(() => resolver.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
// Second call for same ref should still throw
|
||||
expect(() => resolver.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => resolver.get(bRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => resolver.get(cRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{c}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect short dependency cycles', () => {
|
||||
const resolver = new ApiResolver(createShortCyclicRegistry());
|
||||
expect(() => resolver.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => resolver.get(bRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => resolver.get(cRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{c}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate a factory holder', () => {
|
||||
expect(() => {
|
||||
ApiResolver.validateFactories(createRegistry(), [aRef, bRef, cRef]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should find dependency cycles with validation', () => {
|
||||
const short = createShortCyclicRegistry();
|
||||
expect(() =>
|
||||
ApiResolver.validateFactories(short, short.getAllApis()),
|
||||
).toThrow('Circular dependency of api factory for apiRef{a}');
|
||||
expect(() => ApiResolver.validateFactories(short, [bRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => ApiResolver.validateFactories(short, [cRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{c}',
|
||||
);
|
||||
|
||||
const long = createLongCyclicRegistry();
|
||||
expect(() =>
|
||||
ApiResolver.validateFactories(long, long.getAllApis()),
|
||||
).toThrow('Circular dependency of api factory for apiRef{a}');
|
||||
expect(() => ApiResolver.validateFactories(long, [bRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => ApiResolver.validateFactories(long, [cRef])).toThrow(
|
||||
'Circular dependency of api factory for apiRef{c}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should only call factory func once', () => {
|
||||
const registry = new ApiFactoryRegistry();
|
||||
const factory = jest.fn().mockReturnValue(2);
|
||||
registry.register('default', {
|
||||
api: aRef,
|
||||
deps: {},
|
||||
factory,
|
||||
});
|
||||
|
||||
const resolver = new ApiResolver(registry);
|
||||
expect(factory).toHaveBeenCalledTimes(0);
|
||||
expect(resolver.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(resolver.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
+44
-33
@@ -15,38 +15,54 @@
|
||||
*/
|
||||
|
||||
import { ApiRef } from './ApiRef';
|
||||
import { TypesToApiRefs, AnyApiRef, ApiHolder, ApiFactory } from './types';
|
||||
import {
|
||||
ApiHolder,
|
||||
ApiFactoryHolder,
|
||||
AnyApiRef,
|
||||
TypesToApiRefs,
|
||||
} from './types';
|
||||
|
||||
export class ApiTestRegistry implements ApiHolder {
|
||||
export class ApiResolver implements ApiHolder {
|
||||
private readonly apis = new Map<AnyApiRef, unknown>();
|
||||
private factories = new Map<
|
||||
AnyApiRef,
|
||||
ApiFactory<unknown, unknown, unknown>
|
||||
>();
|
||||
private savedFactories = new Map<
|
||||
AnyApiRef,
|
||||
ApiFactory<unknown, unknown, unknown>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Validate factories by making sure that each of the apis can be created
|
||||
* without hitting any circular dependencies.
|
||||
*/
|
||||
static validateFactories(
|
||||
factories: ApiFactoryHolder,
|
||||
apis: Iterable<AnyApiRef>,
|
||||
) {
|
||||
for (const api of apis) {
|
||||
const heap = [api];
|
||||
const allDeps = new Set<AnyApiRef>();
|
||||
|
||||
while (heap.length) {
|
||||
const apiRef = heap.shift()!;
|
||||
const factory = factories.get(apiRef);
|
||||
if (!factory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const dep of Object.values(factory.deps)) {
|
||||
if (dep === api) {
|
||||
throw new Error(`Circular dependency of api factory for ${api}`);
|
||||
}
|
||||
if (!allDeps.has(dep)) {
|
||||
allDeps.add(dep);
|
||||
heap.push(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private readonly factories: ApiFactoryHolder) {}
|
||||
|
||||
get<T>(ref: ApiRef<T>): T | undefined {
|
||||
return this.load(ref);
|
||||
}
|
||||
|
||||
register<A, I, D>(factory: ApiFactory<A, I, D>): ApiTestRegistry {
|
||||
this.factories.set(factory.implements, factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.factories = this.savedFactories;
|
||||
this.apis.clear();
|
||||
}
|
||||
|
||||
save(): ApiTestRegistry {
|
||||
this.savedFactories = new Map(this.factories);
|
||||
return this;
|
||||
}
|
||||
|
||||
private load<T>(ref: ApiRef<T>, loading: AnyApiRef[] = []): T | undefined {
|
||||
const impl = this.apis.get(ref);
|
||||
if (impl) {
|
||||
@@ -58,16 +74,11 @@ export class ApiTestRegistry implements ApiHolder {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (loading.includes(factory.implements)) {
|
||||
throw new Error(
|
||||
`Circular dependency of api factory for ${factory.implements}`,
|
||||
);
|
||||
if (loading.includes(factory.api)) {
|
||||
throw new Error(`Circular dependency of api factory for ${factory.api}`);
|
||||
}
|
||||
|
||||
const deps = this.loadDeps(ref, factory.deps, [
|
||||
...loading,
|
||||
factory.implements,
|
||||
]);
|
||||
const deps = this.loadDeps(ref, factory.deps, [...loading, factory.api]);
|
||||
const api = factory.factory(deps);
|
||||
this.apis.set(ref, api);
|
||||
return api as T;
|
||||
@@ -1,154 +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 { ApiTestRegistry } from './ApiTestRegistry';
|
||||
import { createApiRef } from './ApiRef';
|
||||
|
||||
describe('ApiTestRegistry', () => {
|
||||
const aRef = createApiRef<number>({ id: 'a', description: '' });
|
||||
const bRef = createApiRef<string>({ id: 'b', description: '' });
|
||||
const cRef = createApiRef<string>({ id: 'c', description: '' });
|
||||
|
||||
it('should be created', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
expect(registry.get(aRef)).toBe(undefined);
|
||||
expect(registry.get(bRef)).toBe(undefined);
|
||||
expect(registry.get(cRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should register a factory', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
expect(registry.get(bRef)).toBe(undefined);
|
||||
expect(registry.get(cRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should remove factories when resetting', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
registry.reset();
|
||||
expect(registry.get(aRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should keep saved factories when resetting', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
|
||||
registry.save();
|
||||
registry.register({ implements: bRef, deps: {}, factory: () => 'x' });
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
expect(registry.get(bRef)).toBe('x');
|
||||
registry.reset();
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
expect(registry.get(bRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should register factories with dependencies', () => {
|
||||
// 100% coverage + happy typescript = hasOwnProperty + this atrocity
|
||||
const cDeps = Object.create(
|
||||
{ c: cRef },
|
||||
{ a: { enumerable: true, value: aRef } },
|
||||
);
|
||||
cDeps.b = bRef;
|
||||
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
|
||||
registry.register({
|
||||
implements: bRef,
|
||||
deps: { dep: aRef },
|
||||
factory: ({ dep }) => `hello ${dep}`,
|
||||
});
|
||||
registry.register({
|
||||
implements: cRef,
|
||||
deps: cDeps,
|
||||
factory: ({ a, b }) => b.repeat(a),
|
||||
});
|
||||
expect(registry.get(aRef)).toBe(3);
|
||||
expect(registry.get(bRef)).toBe('hello 3');
|
||||
expect(registry.get(cRef)).toBe('hello 3hello 3hello 3');
|
||||
});
|
||||
|
||||
it('should not allow cyclic dependencies', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({
|
||||
implements: aRef,
|
||||
deps: { b: bRef },
|
||||
factory: () => 1,
|
||||
});
|
||||
registry.register({
|
||||
implements: bRef,
|
||||
deps: { c: cRef },
|
||||
factory: () => 'b',
|
||||
});
|
||||
registry.register({
|
||||
implements: cRef,
|
||||
deps: { a: aRef },
|
||||
factory: () => 'c',
|
||||
});
|
||||
expect(() => registry.get(aRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{a}',
|
||||
);
|
||||
expect(() => registry.get(bRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{b}',
|
||||
);
|
||||
expect(() => registry.get(cRef)).toThrow(
|
||||
'Circular dependency of api factory for apiRef{c}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if dependency is not available', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
registry.register({
|
||||
implements: aRef,
|
||||
deps: { b: bRef },
|
||||
factory: () => 1,
|
||||
});
|
||||
expect(() => registry.get(aRef)).toThrow(
|
||||
'No API factory available for dependency apiRef{b} of dependent apiRef{a}',
|
||||
);
|
||||
expect(registry.get(bRef)).toBe(undefined);
|
||||
expect(registry.get(cRef)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should only call factory func once', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
const factory = jest.fn().mockReturnValue(2);
|
||||
registry.register({ implements: aRef, deps: {}, factory });
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(0);
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call factory again after reset', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
const factory = jest.fn().mockReturnValue(2);
|
||||
registry.register({ implements: aRef, deps: {}, factory });
|
||||
registry.save();
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(0);
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
registry.reset();
|
||||
expect(registry.get(aRef)).toBe(2);
|
||||
expect(factory).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -14,15 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiFactory } from './types';
|
||||
import { ApiFactory, TypesToApiRefs } from './types';
|
||||
import { ApiRef } from './ApiRef';
|
||||
|
||||
/**
|
||||
* Used to infer types for a standalone ApiFactory that isn't immediately passed
|
||||
* to another function.
|
||||
* This function doesn't actually do anything, it's only used to infer types.
|
||||
*/
|
||||
export function createApiFactory<Api, Impl, Deps>(
|
||||
factory: ApiFactory<Api, Impl, Deps>,
|
||||
): ApiFactory<Api, Impl, Deps> {
|
||||
export function createApiFactory<
|
||||
Api,
|
||||
Impl extends Api,
|
||||
Deps extends { [name in string]: unknown }
|
||||
>(factory: ApiFactory<Api, Deps>): ApiFactory<Api, Deps>;
|
||||
export function createApiFactory<Api>(
|
||||
api: ApiRef<Api>,
|
||||
instance: Api,
|
||||
): ApiFactory<Api, {}>;
|
||||
export function createApiFactory<
|
||||
Api,
|
||||
Deps extends { [name in string]: unknown }
|
||||
>(
|
||||
factory: ApiFactory<Api, Deps> | ApiRef<Api>,
|
||||
instance?: Api,
|
||||
): ApiFactory<Api, Deps> {
|
||||
if ('id' in factory) {
|
||||
return {
|
||||
api: factory,
|
||||
deps: {} as TypesToApiRefs<Deps>,
|
||||
factory: () => instance!,
|
||||
};
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
export { ApiProvider, useApi, useApiHolder } from './ApiProvider';
|
||||
export { ApiRegistry } from './ApiRegistry';
|
||||
export { ApiTestRegistry } from './ApiTestRegistry';
|
||||
export * from './ApiRef';
|
||||
export * from './types';
|
||||
export * from './helpers';
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
|
||||
import { ApiRef } from './ApiRef';
|
||||
|
||||
export type AnyApiRef = ApiRef<any>;
|
||||
export type AnyApiRef = ApiRef<unknown>;
|
||||
|
||||
export type ApiRefType<T> = T extends ApiRef<infer U> ? U : never;
|
||||
|
||||
export type TypesToApiRefs<T> = { [key in keyof T]: ApiRef<T[key]> };
|
||||
|
||||
export type ApiRefsToTypes<T extends { [key in any]: ApiRef<any> }> = {
|
||||
export type ApiRefsToTypes<T extends { [key in string]: ApiRef<unknown> }> = {
|
||||
[key in keyof T]: ApiRefType<T[key]>;
|
||||
};
|
||||
|
||||
@@ -30,8 +30,16 @@ export type ApiHolder = {
|
||||
get<T>(api: ApiRef<T>): T | undefined;
|
||||
};
|
||||
|
||||
export type ApiFactory<Api, Impl, Deps> = {
|
||||
implements: ApiRef<Api>;
|
||||
export type ApiFactory<Api, Deps extends { [name in string]: unknown }> = {
|
||||
api: ApiRef<Api>;
|
||||
deps: TypesToApiRefs<Deps>;
|
||||
factory(deps: Deps): Impl extends Api ? Impl : never;
|
||||
factory(deps: Deps): Api;
|
||||
};
|
||||
|
||||
export type AnyApiFactory = ApiFactory<unknown, { [key in string]: unknown }>;
|
||||
|
||||
export type ApiFactoryHolder = {
|
||||
get<T>(
|
||||
api: ApiRef<T>,
|
||||
): ApiFactory<T, { [key in string]: unknown }> | undefined;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
defaultApis: 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 defaultApis: 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.defaultApis = options.defaultApis;
|
||||
}
|
||||
|
||||
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,67 @@ export class PrivateAppImpl implements BackstageApp {
|
||||
return AppRouter;
|
||||
}
|
||||
|
||||
private getApiHolder(): ApiHolder {
|
||||
if (this.apiHolder) {
|
||||
return this.apiHolder;
|
||||
}
|
||||
|
||||
const registry = new ApiFactoryRegistry();
|
||||
|
||||
registry.register('static', {
|
||||
api: appThemeApiRef,
|
||||
deps: {},
|
||||
factory: () => AppThemeSelector.createWithStorage(this.themes),
|
||||
});
|
||||
registry.register('static', {
|
||||
api: configApiRef,
|
||||
deps: {},
|
||||
factory: () => {
|
||||
if (!this.configApi) {
|
||||
throw new Error(
|
||||
'Tried to access config API before config was loaded',
|
||||
);
|
||||
}
|
||||
return this.configApi;
|
||||
},
|
||||
});
|
||||
registry.register('static', {
|
||||
api: identityApiRef,
|
||||
deps: {},
|
||||
factory: () => this.identityApi,
|
||||
});
|
||||
|
||||
for (const factory of this.defaultApis) {
|
||||
registry.register('default', factory);
|
||||
}
|
||||
|
||||
for (const plugin of this.plugins) {
|
||||
for (const factory of plugin.getApis()) {
|
||||
if (!registry.register('default', factory)) {
|
||||
throw new Error(
|
||||
`Plugin ${plugin.getId()} tried to register duplicate or forbidden API factory for ${
|
||||
factory.api
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const factory of this.apis) {
|
||||
if (!registry.register('app', factory)) {
|
||||
throw new Error(
|
||||
`Duplicate or forbidden API factory for ${factory.api} 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.
|
||||
*/
|
||||
|
||||
@@ -24,9 +24,11 @@ import {
|
||||
} from './types';
|
||||
import { validateBrowserCompat, validateFlagName } from '../app/FeatureFlags';
|
||||
import { RouteRef } from '../routing';
|
||||
import { AnyApiFactory } from '../apis';
|
||||
|
||||
export type PluginConfig = {
|
||||
id: string;
|
||||
apis?: Iterable<AnyApiFactory>;
|
||||
register?(hooks: PluginHooks): void;
|
||||
};
|
||||
|
||||
@@ -65,6 +67,10 @@ export class PluginImpl {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
getApis(): Iterable<AnyApiFactory> {
|
||||
return this.config.apis ?? [];
|
||||
}
|
||||
|
||||
output(): PluginOutput[] {
|
||||
if (this.storedOutput) {
|
||||
return this.storedOutput;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { RouteRef } from '../routing';
|
||||
import { AnyApiFactory } from '../apis';
|
||||
|
||||
export type RouteOptions = {
|
||||
// Whether the route path must match exactly, defaults to true.
|
||||
@@ -70,4 +71,5 @@ export type PluginOutput =
|
||||
export type BackstagePlugin = {
|
||||
getId(): string;
|
||||
output(): PluginOutput[];
|
||||
getApis(): Iterable<AnyApiFactory>;
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import privateExports, {
|
||||
AppOptions,
|
||||
ApiRegistry,
|
||||
defaultSystemIcons,
|
||||
BootErrorPageProps,
|
||||
AppConfigLoader,
|
||||
@@ -26,6 +25,7 @@ import { BrowserRouter, MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { ErrorPage } from '../layout/ErrorPage';
|
||||
import { Progress } from '../components/Progress';
|
||||
import { defaultApis } from './defaultApis';
|
||||
import { lightTheme, darkTheme } from '@backstage/theme';
|
||||
import { AppConfig, JsonObject } from '@backstage/config';
|
||||
|
||||
@@ -94,7 +94,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 +127,7 @@ export function createApp(options?: AppOptions) {
|
||||
components,
|
||||
themes,
|
||||
configLoader,
|
||||
defaultApis,
|
||||
});
|
||||
|
||||
app.verify();
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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 {
|
||||
alertApiRef,
|
||||
errorApiRef,
|
||||
AlertApiForwarder,
|
||||
ErrorApiForwarder,
|
||||
ErrorAlerter,
|
||||
featureFlagsApiRef,
|
||||
FeatureFlags,
|
||||
discoveryApiRef,
|
||||
GoogleAuth,
|
||||
GithubAuth,
|
||||
OAuth2,
|
||||
OktaAuth,
|
||||
GitlabAuth,
|
||||
Auth0Auth,
|
||||
MicrosoftAuth,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
googleAuthApiRef,
|
||||
githubAuthApiRef,
|
||||
oauth2ApiRef,
|
||||
oktaAuthApiRef,
|
||||
gitlabAuthApiRef,
|
||||
auth0AuthApiRef,
|
||||
microsoftAuthApiRef,
|
||||
storageApiRef,
|
||||
WebStorage,
|
||||
createApiFactory,
|
||||
configApiRef,
|
||||
UrlPatternDiscovery,
|
||||
} from '@backstage/core-api';
|
||||
|
||||
export const defaultApis = [
|
||||
createApiFactory({
|
||||
api: discoveryApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) =>
|
||||
UrlPatternDiscovery.compile(
|
||||
`${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`,
|
||||
),
|
||||
}),
|
||||
createApiFactory(alertApiRef, new AlertApiForwarder()),
|
||||
createApiFactory({
|
||||
api: errorApiRef,
|
||||
deps: { alertApi: alertApiRef },
|
||||
factory: ({ alertApi }) =>
|
||||
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: storageApiRef,
|
||||
deps: { errorApi: errorApiRef },
|
||||
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
|
||||
}),
|
||||
createApiFactory(featureFlagsApiRef, new FeatureFlags()),
|
||||
createApiFactory(oauthRequestApiRef, new OAuthRequestManager()),
|
||||
createApiFactory({
|
||||
api: googleAuthApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GoogleAuth.create({ discoveryApi, oauthRequestApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: microsoftAuthApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
MicrosoftAuth.create({ discoveryApi, oauthRequestApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: githubAuthApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GithubAuth.create({ discoveryApi, oauthRequestApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: oktaAuthApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
OktaAuth.create({ discoveryApi, oauthRequestApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: gitlabAuthApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GitlabAuth.create({ discoveryApi, oauthRequestApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: auth0AuthApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
Auth0Auth.create({ discoveryApi, oauthRequestApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: oauth2ApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
OAuth2.create({ discoveryApi, oauthRequestApi }),
|
||||
}),
|
||||
];
|
||||
@@ -1,79 +1,17 @@
|
||||
import {
|
||||
ApiRegistry,
|
||||
alertApiRef,
|
||||
errorApiRef,
|
||||
AlertApiForwarder,
|
||||
ConfigApi,
|
||||
ErrorApiForwarder,
|
||||
ErrorAlerter,
|
||||
discoveryApiRef,
|
||||
UrlPatternDiscovery,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
storageApiRef,
|
||||
WebStorage,
|
||||
createApiFactory,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
|
||||
import {
|
||||
lighthouseApiRef,
|
||||
LighthouseRestApi,
|
||||
} from '@backstage/plugin-lighthouse';
|
||||
|
||||
import {
|
||||
GithubActionsClient,
|
||||
githubActionsApiRef,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
|
||||
import {
|
||||
techdocsStorageApiRef,
|
||||
TechDocsStorageApi,
|
||||
} from '@backstage/plugin-techdocs';
|
||||
|
||||
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
|
||||
import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
|
||||
|
||||
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
|
||||
|
||||
|
||||
|
||||
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 techdocsStorageUrl = 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(oauthRequestApiRef, new OAuthRequestManager());
|
||||
|
||||
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
|
||||
builder.add(githubActionsApiRef, new GithubActionsClient());
|
||||
|
||||
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
|
||||
|
||||
builder.add(
|
||||
circleCIApiRef,
|
||||
new CircleCIApi(`${backendUrl}/proxy/circleci/api`),
|
||||
);
|
||||
|
||||
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
|
||||
|
||||
builder.add(
|
||||
techdocsStorageApiRef,
|
||||
new TechDocsStorageApi({ apiOrigin: techdocsStorageUrl }),
|
||||
);
|
||||
|
||||
return builder.build();
|
||||
};
|
||||
export const apis = [
|
||||
createApiFactory({
|
||||
api: discoveryApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) =>
|
||||
UrlPatternDiscovery.compile(
|
||||
`${configApi.getString('backend.baseUrl')}/{{ pluginId }}`,
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,36 +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 * as apiFactories from './apiFactories';
|
||||
import { ApiTestRegistry, ApiFactory } from '@backstage/core';
|
||||
|
||||
describe('apiFactories', () => {
|
||||
it('should be possible to get an instance of each API', () => {
|
||||
const registry = new ApiTestRegistry();
|
||||
const factories: ApiFactory<unknown, unknown, unknown>[] = Object.values(
|
||||
apiFactories,
|
||||
);
|
||||
|
||||
for (const factory of factories) {
|
||||
registry.register(factory);
|
||||
}
|
||||
|
||||
for (const factory of factories) {
|
||||
const api = registry.get(factory.implements);
|
||||
expect(api).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,108 +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 {
|
||||
alertApiRef,
|
||||
errorApiRef,
|
||||
ErrorApiForwarder,
|
||||
AlertApi,
|
||||
createApiFactory,
|
||||
ErrorAlerter,
|
||||
AlertApiForwarder,
|
||||
oauthRequestApiRef,
|
||||
OAuthRequestManager,
|
||||
UrlPatternDiscovery,
|
||||
discoveryApiRef,
|
||||
GoogleAuth,
|
||||
googleAuthApiRef,
|
||||
GithubAuth,
|
||||
githubAuthApiRef,
|
||||
GitlabAuth,
|
||||
gitlabAuthApiRef,
|
||||
Auth0Auth,
|
||||
auth0AuthApiRef,
|
||||
} from '@backstage/core';
|
||||
|
||||
// TODO(rugvip): We should likely figure out how to reuse all of these between apps
|
||||
// and plugin serve with minimal boilerplate. For example we might move everything
|
||||
// to DI, and provide factories for the default implementations, so this just becomes
|
||||
// a list of things like `[ErrorApiForwarder.factory, AlertApiDialog.factory]`.
|
||||
|
||||
export const alertApiFactory = createApiFactory({
|
||||
implements: alertApiRef,
|
||||
deps: {},
|
||||
factory: (): AlertApi => new AlertApiForwarder(),
|
||||
});
|
||||
|
||||
export const errorApiFactory = createApiFactory({
|
||||
implements: errorApiRef,
|
||||
deps: { alertApi: alertApiRef },
|
||||
factory: ({ alertApi }) =>
|
||||
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
|
||||
});
|
||||
|
||||
export const oauthRequestApiFactory = createApiFactory({
|
||||
implements: oauthRequestApiRef,
|
||||
deps: {},
|
||||
factory: () => new OAuthRequestManager(),
|
||||
});
|
||||
|
||||
export const discoveryApiFactory = createApiFactory({
|
||||
implements: discoveryApiRef,
|
||||
deps: {},
|
||||
factory: () =>
|
||||
UrlPatternDiscovery.compile(`http://localhost:7000/{{ pluginId }}`),
|
||||
});
|
||||
|
||||
export const googleAuthApiFactory = createApiFactory({
|
||||
implements: googleAuthApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GoogleAuth.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const githubAuthApiFactory = createApiFactory({
|
||||
implements: githubAuthApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GithubAuth.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const gitlabAuthApiFactory = createApiFactory({
|
||||
implements: gitlabAuthApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
GitlabAuth.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
|
||||
export const auth0AuthApiFactory = createApiFactory({
|
||||
implements: auth0AuthApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, oauthRequestApi: oauthRequestApiRef },
|
||||
factory: ({ discoveryApi, oauthRequestApi }) =>
|
||||
Auth0Auth.create({
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
}),
|
||||
});
|
||||
@@ -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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { useApi, configApiRef } from '@backstage/core';
|
||||
import { createDevApp } from './render';
|
||||
|
||||
const anyEnv = (process.env = { ...process.env }) as any;
|
||||
|
||||
describe('DevAppBuilder', () => {
|
||||
it('should be able to render a component in a dev app', async () => {
|
||||
anyEnv.APP_CONFIG = [
|
||||
{ context: 'test', data: { app: { title: 'Test App' } } },
|
||||
];
|
||||
|
||||
const MyComponent = () => {
|
||||
const configApi = useApi(configApiRef);
|
||||
return <div>My App: {configApi.getString('app.title')}</div>;
|
||||
};
|
||||
|
||||
const DevApp = createDevApp()
|
||||
.addRootChild(<MyComponent />)
|
||||
.build();
|
||||
|
||||
const rendered = render(<DevApp />);
|
||||
|
||||
expect(await rendered.findByText('My App: Test App')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -26,12 +26,10 @@ import {
|
||||
SidebarSpacer,
|
||||
ApiFactory,
|
||||
createPlugin,
|
||||
ApiTestRegistry,
|
||||
ApiHolder,
|
||||
AlertDisplay,
|
||||
OAuthRequestDialog,
|
||||
AnyApiFactory,
|
||||
} from '@backstage/core';
|
||||
import * as defaultApiFactories from './apiFactories';
|
||||
import SentimentDissatisfiedIcon from '@material-ui/icons/SentimentDissatisfied';
|
||||
|
||||
// TODO(rugvip): export proper plugin type from core that isn't the plugin class
|
||||
@@ -43,7 +41,7 @@ type BackstagePlugin = ReturnType<typeof createPlugin>;
|
||||
*/
|
||||
class DevAppBuilder {
|
||||
private readonly plugins = new Array<BackstagePlugin>();
|
||||
private readonly factories = new Array<ApiFactory<any, any, any>>();
|
||||
private readonly apis = new Array<AnyApiFactory>();
|
||||
private readonly rootChildren = new Array<ReactNode>();
|
||||
|
||||
/**
|
||||
@@ -57,10 +55,10 @@ class DevAppBuilder {
|
||||
/**
|
||||
* Register an API factory to add to the app
|
||||
*/
|
||||
registerApiFactory<Api, Impl, Deps>(
|
||||
factory: ApiFactory<Api, Impl, Deps>,
|
||||
registerApi<Api, Deps extends { [name in string]: unknown }>(
|
||||
factory: ApiFactory<Api, Deps>,
|
||||
): DevAppBuilder {
|
||||
this.factories.push(factory);
|
||||
this.apis.push(factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -79,7 +77,7 @@ class DevAppBuilder {
|
||||
*/
|
||||
build(): ComponentType<{}> {
|
||||
const app = createApp({
|
||||
apis: this.setupApiRegistry(this.factories),
|
||||
apis: this.apis,
|
||||
plugins: this.plugins,
|
||||
});
|
||||
|
||||
@@ -170,30 +168,6 @@ class DevAppBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
// Set up an API registry that merges together default implementations with ones provided through config.
|
||||
private setupApiRegistry(
|
||||
providedFactories: ApiFactory<any, any, any>[],
|
||||
): ApiHolder {
|
||||
const providedApis = new Set(
|
||||
providedFactories.map(factory => factory.implements),
|
||||
);
|
||||
|
||||
// Exlude any default API factory that we receive a factory for in the config
|
||||
const defaultFactories = Object.values(defaultApiFactories).filter(
|
||||
factory => !providedApis.has(factory.implements),
|
||||
);
|
||||
const allFactories = [...defaultFactories, ...providedFactories];
|
||||
|
||||
// Use a test registry with dependency injection so that the consumer
|
||||
// can override APIs but still depend on the default implementations.
|
||||
const registry = new ApiTestRegistry();
|
||||
for (const factory of allFactories) {
|
||||
registry.register(factory);
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
private findPluginPaths(plugins: BackstagePlugin[]) {
|
||||
const paths = new Array<string>();
|
||||
|
||||
|
||||
@@ -14,12 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorApi,
|
||||
ErrorContext,
|
||||
errorApiRef,
|
||||
Observable,
|
||||
} from '@backstage/core-api';
|
||||
import { ErrorApi, ErrorContext, Observable } from '@backstage/core-api';
|
||||
|
||||
type Options = {
|
||||
collect?: boolean;
|
||||
@@ -40,12 +35,6 @@ const nullObservable = {
|
||||
};
|
||||
|
||||
export class MockErrorApi implements ErrorApi {
|
||||
static factory = {
|
||||
implements: errorApiRef,
|
||||
deps: {},
|
||||
factory: () => new MockErrorApi(),
|
||||
};
|
||||
|
||||
private readonly errors = new Array<ErrorWithContext>();
|
||||
private readonly waiters = new Set<Waiter>();
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import {
|
||||
Observable,
|
||||
StorageApi,
|
||||
storageApiRef,
|
||||
StorageValueChange,
|
||||
} from '@backstage/core-api';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
@@ -25,12 +24,6 @@ import ObservableImpl from 'zen-observable';
|
||||
export type MockStorageBucket = { [key: string]: any };
|
||||
|
||||
export class MockStorageApi implements StorageApi {
|
||||
static factory = {
|
||||
implements: storageApiRef,
|
||||
deps: {},
|
||||
factory: () => MockStorageApi.create(),
|
||||
};
|
||||
|
||||
private readonly namespace: string;
|
||||
private readonly data: MockStorageBucket;
|
||||
|
||||
|
||||
@@ -49,9 +49,6 @@ describe('wrapInTestApp', () => {
|
||||
expect.stringMatching(
|
||||
/^Warning: An update to %s inside a test was not wrapped in act\(...\)/,
|
||||
),
|
||||
expect.stringMatching(
|
||||
/^Warning: An update to %s inside a test was not wrapped in act\(...\)/,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import privateExports, {
|
||||
} from '@backstage/core-api';
|
||||
import { RenderResult } from '@testing-library/react';
|
||||
import { renderWithEffects } from '@backstage/test-utils-core';
|
||||
import { createMockApiRegistry } from './mockApiRegistry';
|
||||
import { mockApis } from './mockApis';
|
||||
|
||||
const { PrivateAppImpl } = privateExports;
|
||||
|
||||
@@ -58,10 +58,9 @@ export function wrapInTestApp(
|
||||
options: TestAppOptions = {},
|
||||
): ReactElement {
|
||||
const { routeEntries = ['/'] } = options;
|
||||
const apis = createMockApiRegistry();
|
||||
|
||||
const app = new PrivateAppImpl({
|
||||
apis,
|
||||
apis: [],
|
||||
components: {
|
||||
NotFoundErrorPage,
|
||||
BootErrorPage,
|
||||
@@ -80,6 +79,7 @@ export function wrapInTestApp(
|
||||
variant: 'light',
|
||||
},
|
||||
],
|
||||
defaultApis: mockApis,
|
||||
});
|
||||
|
||||
let Wrapper: ComponentType;
|
||||
|
||||
+9
-9
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiTestRegistry } from '@backstage/core-api';
|
||||
import {
|
||||
storageApiRef,
|
||||
errorApiRef,
|
||||
createApiFactory,
|
||||
} from '@backstage/core-api';
|
||||
import { MockErrorApi, MockStorageApi } from './apis';
|
||||
|
||||
export function createMockApiRegistry(): ApiTestRegistry {
|
||||
const registry = new ApiTestRegistry();
|
||||
|
||||
registry.register(MockErrorApi.factory);
|
||||
registry.register(MockStorageApi.factory);
|
||||
|
||||
return registry;
|
||||
}
|
||||
export const mockApis = [
|
||||
createApiFactory(errorApiRef, new MockErrorApi()),
|
||||
createApiFactory(storageApiRef, MockStorageApi.create()),
|
||||
];
|
||||
@@ -14,8 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import {
|
||||
createPlugin,
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef } from './api/types';
|
||||
import { CatalogClient } from './api/CatalogClient';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'catalog',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: catalogApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -20,9 +20,9 @@ import { circleCIApiRef, CircleCIApi } from '../src/api';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(plugin)
|
||||
.registerApiFactory({
|
||||
.registerApi({
|
||||
api: circleCIApiRef,
|
||||
deps: {},
|
||||
factory: () => new CircleCIApi(),
|
||||
implements: circleCIApiRef,
|
||||
})
|
||||
.render();
|
||||
|
||||
@@ -13,13 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import { createPlugin, createApiFactory, configApiRef } from '@backstage/core';
|
||||
import { circleCIRouteRef, circleCIBuildRouteRef } from './route-refs';
|
||||
import BuildsPage from './pages/BuildsPage/BuildsPage';
|
||||
import BuildWithStepsPage from './pages/BuildWithStepsPage/BuildWithStepsPage';
|
||||
import { circleCIApiRef, CircleCIApi } from './api';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'circleci',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: circleCIApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) =>
|
||||
new CircleCIApi(
|
||||
`${configApi.getString('backend.baseUrl')}/proxy/circleci/api`,
|
||||
),
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
router.addRoute(circleCIRouteRef, BuildsPage);
|
||||
router.addRoute(circleCIBuildRouteRef, BuildWithStepsPage);
|
||||
|
||||
@@ -14,10 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import {
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
createApiFactory,
|
||||
} from '@backstage/core';
|
||||
import { ProjectListPage } from './components/ProjectListPage';
|
||||
import { ProjectDetailsPage } from './components/ProjectDetailsPage';
|
||||
import { NewProjectPage } from './components/NewProjectPage';
|
||||
import { GCPApiRef, GCPClient } from './api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/gcp-projects',
|
||||
@@ -34,6 +39,7 @@ export const NewProjectRouteRef = createRouteRef({
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'gcp-projects',
|
||||
apis: [createApiFactory(GCPApiRef, new GCPClient())],
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, ProjectListPage);
|
||||
router.addRoute(ProjectRouteRef, ProjectDetailsPage);
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import {
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
createApiFactory,
|
||||
} from '@backstage/core';
|
||||
import { githubActionsApiRef, GithubActionsClient } from './api';
|
||||
|
||||
// TODO(freben): This is just a demo route for now
|
||||
export const rootRouteRef = createRouteRef({
|
||||
@@ -29,4 +34,5 @@ export const buildRouteRef = createRouteRef({
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'github-actions',
|
||||
apis: [createApiFactory(githubActionsApiRef, new GithubActionsClient())],
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import { createPlugin, createApiFactory } from '@backstage/core';
|
||||
import ProfileCatalog from './components/ProfileCatalog';
|
||||
import ClusterPage from './components/ClusterPage';
|
||||
import ClusterList from './components/ClusterList';
|
||||
@@ -23,9 +23,13 @@ import {
|
||||
gitOpsClusterDetailsRoute,
|
||||
gitOpsClusterCreateRoute,
|
||||
} from './routes';
|
||||
import { gitOpsApiRef, GitOpsRestApi } from './api';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'gitops-profiles',
|
||||
apis: [
|
||||
createApiFactory(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')),
|
||||
],
|
||||
register({ router }) {
|
||||
router.addRoute(gitOpsClusterListRoute, ClusterList);
|
||||
router.addRoute(gitOpsClusterDetailsRoute, ClusterPage);
|
||||
|
||||
@@ -20,8 +20,8 @@ import { plugin, GraphQLEndpoints, graphQlBrowseApiRef } from '../src';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(plugin)
|
||||
.registerApiFactory({
|
||||
implements: graphQlBrowseApiRef,
|
||||
.registerApi({
|
||||
api: graphQlBrowseApiRef,
|
||||
deps: {
|
||||
errorApi: errorApiRef,
|
||||
githubAuthApi: githubAuthApiRef,
|
||||
|
||||
@@ -14,8 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import { createPlugin, createApiFactory } from '@backstage/core';
|
||||
import { graphQlBrowseApiRef, GraphQLEndpoints } from './lib/api';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'graphiql',
|
||||
apis: [
|
||||
// GitLab is used as an example endpoint, but most plug
|
||||
createApiFactory(
|
||||
graphQlBrowseApiRef,
|
||||
GraphQLEndpoints.from([
|
||||
GraphQLEndpoints.create({
|
||||
id: 'gitlab',
|
||||
title: 'GitLab',
|
||||
url: 'https://gitlab.com/api/graphql',
|
||||
}),
|
||||
]),
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -14,8 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import {
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
createApiFactory,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
import { DetailedViewPage } from './pages/BuildWithStepsPage';
|
||||
import { jenkinsApiRef, JenkinsApi } from './api';
|
||||
|
||||
export const buildRouteRef = createRouteRef({
|
||||
path: '/jenkins/job',
|
||||
@@ -24,6 +30,16 @@ export const buildRouteRef = createRouteRef({
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'jenkins',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: jenkinsApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) =>
|
||||
new JenkinsApi(
|
||||
`${configApi.getString('backend.baseUrl')}/proxy/jenkins/api`,
|
||||
),
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
router.addRoute(buildRouteRef, DetailedViewPage);
|
||||
},
|
||||
|
||||
@@ -20,8 +20,8 @@ import { lighthouseApiRef, LighthouseRestApi } from '../src';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(plugin)
|
||||
.registerApiFactory({
|
||||
implements: lighthouseApiRef,
|
||||
.registerApi({
|
||||
api: lighthouseApiRef,
|
||||
deps: {},
|
||||
factory: () => new LighthouseRestApi('http://localhost:3003'),
|
||||
})
|
||||
|
||||
@@ -14,13 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import { createPlugin, createApiFactory, configApiRef } from '@backstage/core';
|
||||
import AuditList from './components/AuditList';
|
||||
import AuditView from './components/AuditView';
|
||||
import CreateAudit from './components/CreateAudit';
|
||||
import { lighthouseApiRef, LighthouseRestApi } from './api';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'lighthouse',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: lighthouseApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) => LighthouseRestApi.fromConfig(configApi),
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
router.registerRoute('/lighthouse', AuditList);
|
||||
router.registerRoute('/lighthouse/audit/:id', AuditView);
|
||||
|
||||
@@ -14,13 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import {
|
||||
createPlugin,
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
} from '@backstage/core';
|
||||
import { rootRouteRef, entityRouteRef } from './routes';
|
||||
import { RollbarHome } from './components/RollbarHome/RollbarHome';
|
||||
import { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage';
|
||||
import { rollbarApiRef } from './api/RollbarApi';
|
||||
import { RollbarClient } from './api/RollbarClient';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'rollbar',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: rollbarApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }),
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, RollbarHome);
|
||||
router.addRoute(entityRouteRef, RollbarProjectPage);
|
||||
|
||||
@@ -14,13 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import {
|
||||
createPlugin,
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
} from '@backstage/core';
|
||||
import { ScaffolderPage } from './components/ScaffolderPage';
|
||||
import { TemplatePage } from './components/TemplatePage';
|
||||
import { rootRoute, templateRoute } from './routes';
|
||||
import { scaffolderApiRef, ScaffolderApi } from './api';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'scaffolder',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: scaffolderApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef },
|
||||
factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }),
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
router.addRoute(rootRoute, ScaffolderPage);
|
||||
router.addRoute(templateRoute, TemplatePage);
|
||||
|
||||
@@ -20,13 +20,13 @@ import { TechDocsDevStorageApi } from './api';
|
||||
import { techdocsStorageApiRef } from '../src';
|
||||
|
||||
createDevApp()
|
||||
.registerApiFactory({
|
||||
.registerApi({
|
||||
api: techdocsStorageApiRef,
|
||||
deps: {},
|
||||
factory: () =>
|
||||
new TechDocsDevStorageApi({
|
||||
apiOrigin: 'http://localhost:3000/api',
|
||||
}),
|
||||
implements: techdocsStorageApiRef,
|
||||
})
|
||||
.registerPlugin(plugin)
|
||||
.render();
|
||||
|
||||
@@ -29,7 +29,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import {
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
createApiFactory,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
import { techdocsStorageApiRef, TechDocsStorageApi } from './api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '',
|
||||
@@ -48,4 +54,14 @@ export const rootCatalogDocsRouteRef = createRouteRef({
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'techdocs',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: techdocsStorageApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) =>
|
||||
new TechDocsStorageApi({
|
||||
apiOrigin: configApi.getString('techdocs.storageUrl'),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user