feat: move some implementations from the app setup to extensions instead

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Fredrik Adelöw <freben@users.noreply.github.com>
Co-authored-by: Johan Haals <johan@haals.se>
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-08-21 14:38:54 +02:00
parent 8d4b5b3dfd
commit 66736f9e7d
11 changed files with 208 additions and 115 deletions
@@ -16,7 +16,6 @@
import { ComponentType } from 'react';
import {
AppTree,
ComponentRef,
ComponentsApi,
createComponentExtension,
@@ -30,19 +29,12 @@ import {
export class DefaultComponentsApi implements ComponentsApi {
#components: Map<string, ComponentType<any>>;
static fromTree(tree: AppTree) {
const componentEntries = tree.root.edges.attachments
.get('components')
?.reduce((map, e) => {
const data = e.instance?.getData(
createComponentExtension.componentDataRef,
);
if (data) {
map.set(data.ref.id, data.impl);
}
return map;
}, new Map<string, ComponentType>());
return new DefaultComponentsApi(componentEntries ?? new Map());
static fromComponents(
components: Array<typeof createComponentExtension.componentDataRef.T>,
) {
return new DefaultComponentsApi(
new Map(components.map(entry => [entry.ref.id, entry.impl])),
);
}
constructor(components: Map<string, any>) {
@@ -16,15 +16,11 @@
import React from 'react';
import {
ApiBlueprint,
ExtensionBoundary,
coreExtensionData,
createComponentExtension,
createExtension,
createExtensionInput,
IconBundleBlueprint,
ThemeBlueprint,
ApiBlueprint,
TranslationBlueprint,
} from '@backstage/frontend-plugin-api';
export const App = createExtension({
@@ -32,14 +28,6 @@ export const App = createExtension({
attachTo: { id: 'root', input: 'default' }, // ignored
inputs: {
apis: createExtensionInput([ApiBlueprint.dataRefs.factory]),
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme]),
components: createExtensionInput([
createComponentExtension.componentDataRef,
]),
translations: createExtensionInput([
TranslationBlueprint.dataRefs.translation,
]),
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons]),
root: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
}),
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Backstage Authors
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,36 @@ import {
} from '@backstage/theme';
import DarkIcon from '@material-ui/icons/Brightness2';
import LightIcon from '@material-ui/icons/WbSunny';
import { ThemeBlueprint } from '@backstage/frontend-plugin-api';
import {
createExtensionInput,
ThemeBlueprint,
ApiBlueprint,
createApiFactory,
appThemeApiRef,
} from '@backstage/frontend-plugin-api';
import { AppThemeSelector } from '@backstage/core-app-api';
/**
* Contains the themes installed into the app.
*
* @public
*/
export const AppThemeApi = ApiBlueprint.makeWithOverrides({
name: 'app-theme',
inputs: {
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme]),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
factory: createApiFactory(
appThemeApiRef,
AppThemeSelector.createWithStorage(
inputs.themes.map(i => i.get(ThemeBlueprint.dataRefs.theme)),
),
),
});
},
});
export const LightTheme = ThemeBlueprint.make({
name: 'light',
@@ -0,0 +1,50 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createComponentExtension,
createExtensionInput,
ApiBlueprint,
createApiFactory,
componentsApiRef,
} from '@backstage/frontend-plugin-api';
import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
/**
* Contains the shareable components installed into the app.
*
* @public
*/
export const ComponentsApi = ApiBlueprint.makeWithOverrides({
name: 'components',
inputs: {
components: createExtensionInput([
createComponentExtension.componentDataRef,
]),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
factory: createApiFactory(
componentsApiRef,
DefaultComponentsApi.fromComponents(
inputs.components.map(i =>
i.get(createComponentExtension.componentDataRef),
),
),
),
});
},
});
@@ -0,0 +1,49 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createExtensionInput,
IconBundleBlueprint,
ApiBlueprint,
createApiFactory,
iconsApiRef,
} from '@backstage/frontend-plugin-api';
import { DefaultIconsApi } from '../apis/implementations/IconsApi';
/**
* Contains the shareable icons installed into the app.
*
* @public
*/
export const IconsApi = ApiBlueprint.makeWithOverrides({
name: 'icons',
inputs: {
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons]),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
factory: createApiFactory(
iconsApiRef,
new DefaultIconsApi(
inputs.icons
.map(i => i.get(IconBundleBlueprint.dataRefs.icons))
.reduce((acc, bundle) => ({ ...acc, ...bundle }), {}),
),
),
});
},
});
@@ -0,0 +1,57 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ApiBlueprint,
TranslationBlueprint,
createApiFactory,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import {
appLanguageApiRef,
translationApiRef,
} from '@backstage/core-plugin-api/alpha';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
/**
* Contains translations that are installed in the app.
*
* @public
*/
export const TranslationsApi = ApiBlueprint.makeWithOverrides({
name: 'translations',
inputs: {
translations: createExtensionInput([
TranslationBlueprint.dataRefs.translation,
]),
},
factory: (originalFactory, { inputs }) => {
return originalFactory({
factory: createApiFactory({
api: translationApiRef,
deps: { languageApi: appLanguageApiRef },
factory: ({ languageApi }) =>
I18nextTranslationApi.create({
languageApi,
resources: inputs.translations.map(i =>
i.get(TranslationBlueprint.dataRefs.translation),
),
}),
}),
});
},
});
@@ -20,15 +20,10 @@ import {
ApiBlueprint,
AppTree,
appTreeApiRef,
componentsApiRef,
coreExtensionData,
FrontendFeature,
IconBundleBlueprint,
iconsApiRef,
RouteResolutionApi,
routeResolutionApiRef,
ThemeBlueprint,
TranslationBlueprint,
} from '@backstage/frontend-plugin-api';
import { App } from '../extensions/App';
import { AppRoutes } from '../extensions/AppRoutes';
@@ -37,13 +32,10 @@ import { AppNav } from '../extensions/AppNav';
import {
AnyApiFactory,
ApiHolder,
appThemeApiRef,
ConfigApi,
configApiRef,
IconComponent,
featureFlagsApiRef,
identityApiRef,
AppTheme,
errorApiRef,
discoveryApiRef,
fetchApiRef,
@@ -53,7 +45,6 @@ import {
ApiFactoryRegistry,
ApiProvider,
ApiResolver,
AppThemeSelector,
} from '@backstage/core-app-api';
// TODO: Get rid of all of these
@@ -72,21 +63,16 @@ import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBa
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { apis as defaultApis } from '../../../app-defaults/src/defaults';
import { DarkTheme, LightTheme } from '../extensions/themes';
import {
oauthRequestDialogAppRootElement,
alertDisplayAppRootElement,
} from '../extensions/elements';
import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode';
import {
appLanguageApiRef,
translationApiRef,
} from '@backstage/core-plugin-api/alpha';
import { appLanguageApiRef } from '@backstage/core-plugin-api/alpha';
import { CreateAppRouteBinder } from '../routing';
import { RouteResolver } from '../routing/RouteResolver';
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
@@ -103,12 +89,12 @@ import { AppRoot } from '../extensions/AppRoot';
import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
import { DefaultIconsApi } from '../apis/implementations/IconsApi';
import { stringifyError } from '@backstage/errors';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { icons as defaultIcons } from '../../../app-defaults/src/defaults';
import { getBasePath } from '../routing/getBasePath';
import { AppThemeApi, DarkTheme, LightTheme } from '../extensions/AppThemeApi';
import { IconsApi } from '../extensions/IconsApi';
import { TranslationsApi } from '../extensions/TranslationsApi';
import { ComponentsApi } from '../extensions/ComponentsApi';
const DefaultApis = defaultApis.map(factory =>
ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }),
@@ -127,6 +113,10 @@ export const builtinExtensions = [
DarkTheme,
oauthRequestDialogAppRootElement,
alertDisplayAppRootElement,
AppThemeApi,
IconsApi,
TranslationsApi,
ComponentsApi,
...DefaultApis,
].map(def => resolveExtensionDefinition(def));
@@ -174,8 +164,6 @@ export interface CreateAppFeatureLoader {
/** @public */
export function createApp(options?: {
/** @deprecated - Please use {@link @backstage/frontend-plugin-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */
icons?: { [key in string]: IconComponent };
features?: (FrontendFeature | CreateAppFeatureLoader)[];
configLoader?: () => Promise<{ config: ConfigApi }>;
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
@@ -222,7 +210,6 @@ export function createApp(options?: {
}
const app = createSpecializedApp({
icons: options?.icons,
config,
features: [...discoveredFeatures, ...providedFeatures],
bindRoutes: options?.bindRoutes,
@@ -250,8 +237,6 @@ export function createApp(options?: {
* @public
*/
export function createSpecializedApp(options?: {
/** @deprecated - Please use {@link @backstage/frontend-plugin-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */
icons?: { [key in string]: IconComponent };
features?: FrontendFeature[];
config?: ConfigApi;
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
@@ -288,7 +273,6 @@ export function createSpecializedApp(options?: {
routeBindings,
getBasePath(config),
),
options?.icons,
);
if (isProtectedApp()) {
@@ -352,7 +336,6 @@ function createApiHolder(
configApi: ConfigApi,
appIdentityProxy: AppIdentityProxy,
routeResolutionApi: RouteResolutionApi,
icons?: { [key in string]: IconComponent },
): ApiHolder {
const factoryRegistry = new ApiFactoryRegistry();
@@ -362,25 +345,6 @@ function createApiHolder(
?.map(e => e.instance?.getData(ApiBlueprint.dataRefs.factory))
.filter((x): x is AnyApiFactory => !!x) ?? [];
const themeExtensions =
tree.root.edges.attachments
.get('themes')
?.map(e => e.instance?.getData(ThemeBlueprint.dataRefs.theme))
.filter((x): x is AppTheme => !!x) ?? [];
const translationResources =
tree.root.edges.attachments
.get('translations')
?.map(e => e.instance?.getData(TranslationBlueprint.dataRefs.translation))
.filter(
(x): x is typeof TranslationBlueprint.dataRefs.translation.T => !!x,
) ?? [];
const extensionIcons = tree.root.edges.attachments
.get('icons')
?.map(e => e.instance?.getData(IconBundleBlueprint.dataRefs.icons))
.reduce((acc, bundle) => ({ ...acc, ...bundle }), {});
for (const factory of pluginApis) {
factoryRegistry.register('default', factory);
}
@@ -412,26 +376,6 @@ function createApiHolder(
factory: () => routeResolutionApi,
});
factoryRegistry.register('static', {
api: componentsApiRef,
deps: {},
factory: () => DefaultComponentsApi.fromTree(tree),
});
factoryRegistry.register('static', {
api: iconsApiRef,
deps: {},
factory: () =>
new DefaultIconsApi({ ...defaultIcons, ...extensionIcons, ...icons }),
});
factoryRegistry.register('static', {
api: appThemeApiRef,
deps: {},
// TODO: add extension for registering themes
factory: () => AppThemeSelector.createWithStorage(themeExtensions),
});
factoryRegistry.register('static', {
api: appLanguageApiRef,
deps: {},
@@ -444,22 +388,6 @@ function createApiHolder(
factory: () => configApi,
});
factoryRegistry.register('static', {
api: appLanguageApiRef,
deps: {},
factory: () => AppLanguageSelector.createWithStorage(),
});
factoryRegistry.register('static', {
api: translationApiRef,
deps: { languageApi: appLanguageApiRef },
factory: ({ languageApi }) =>
I18nextTranslationApi.create({
languageApi,
resources: translationResources,
}),
});
ApiResolver.validateFactories(factoryRegistry, factoryRegistry.getAllApis());
return new ApiResolver(factoryRegistry);
@@ -25,7 +25,7 @@ const iconsDataRef = createExtensionDataRef<{
export const IconBundleBlueprint = createExtensionBlueprint({
kind: 'icon-bundle',
namespace: 'app',
attachTo: { id: 'app', input: 'icons' },
attachTo: { id: 'api:icons', input: 'icons' },
output: [iconsDataRef],
config: {
schema: {
@@ -29,7 +29,7 @@ const themeDataRef = createExtensionDataRef<AppTheme>().with({
export const ThemeBlueprint = createExtensionBlueprint({
kind: 'theme',
namespace: 'app',
attachTo: { id: 'app', input: 'themes' },
attachTo: { id: 'api:app-theme', input: 'themes' },
output: [themeDataRef],
dataRefs: {
theme: themeDataRef,
@@ -28,7 +28,7 @@ const translationDataRef = createExtensionDataRef<
*/
export const TranslationBlueprint = createExtensionBlueprint({
kind: 'translation',
attachTo: { id: 'app', input: 'translations' },
attachTo: { id: 'api:translations', input: 'translations' },
output: [translationDataRef],
dataRefs: {
translation: translationDataRef,
@@ -35,7 +35,7 @@ export function createComponentExtension<TProps extends {}>(options: {
kind: 'component',
namespace: options.ref.id,
name: options.name,
attachTo: { id: 'app', input: 'components' },
attachTo: { id: 'api:components', input: 'components' },
disabled: options.disabled,
output: [createComponentExtension.componentDataRef],
factory() {