From 66736f9e7dcabcb4af5f090313b38952075bd172 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 14:38:54 +0200 Subject: [PATCH 1/9] feat: move some implementations from the app setup to extensions instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Signed-off-by: blam --- .../ComponentsApi/DefaultComponentsApi.ts | 20 ++-- .../frontend-app-api/src/extensions/App.tsx | 14 +-- .../{themes.tsx => AppThemeApi.tsx} | 33 ++++++- .../src/extensions/ComponentsApi.tsx | 50 ++++++++++ .../src/extensions/IconsApi.ts | 49 ++++++++++ .../src/extensions/TranslationsApi.ts | 57 ++++++++++++ .../frontend-app-api/src/wiring/createApp.tsx | 92 ++----------------- .../src/blueprints/IconBundleBlueprint.ts | 2 +- .../src/blueprints/ThemeBlueprint.ts | 2 +- .../src/blueprints/TranslationBlueprint.ts | 2 +- .../extensions/createComponentExtension.tsx | 2 +- 11 files changed, 208 insertions(+), 115 deletions(-) rename packages/frontend-app-api/src/extensions/{themes.tsx => AppThemeApi.tsx} (65%) create mode 100644 packages/frontend-app-api/src/extensions/ComponentsApi.tsx create mode 100644 packages/frontend-app-api/src/extensions/IconsApi.ts create mode 100644 packages/frontend-app-api/src/extensions/TranslationsApi.ts diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.ts b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.ts index 572b6fc3c4..317233d797 100644 --- a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.ts @@ -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>; - 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()); - return new DefaultComponentsApi(componentEntries ?? new Map()); + static fromComponents( + components: Array, + ) { + return new DefaultComponentsApi( + new Map(components.map(entry => [entry.ref.id, entry.impl])), + ); } constructor(components: Map) { diff --git a/packages/frontend-app-api/src/extensions/App.tsx b/packages/frontend-app-api/src/extensions/App.tsx index ba6c5f65f1..e99b83eb5a 100644 --- a/packages/frontend-app-api/src/extensions/App.tsx +++ b/packages/frontend-app-api/src/extensions/App.tsx @@ -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, }), diff --git a/packages/frontend-app-api/src/extensions/themes.tsx b/packages/frontend-app-api/src/extensions/AppThemeApi.tsx similarity index 65% rename from packages/frontend-app-api/src/extensions/themes.tsx rename to packages/frontend-app-api/src/extensions/AppThemeApi.tsx index a4fd0b253e..375b9e666b 100644 --- a/packages/frontend-app-api/src/extensions/themes.tsx +++ b/packages/frontend-app-api/src/extensions/AppThemeApi.tsx @@ -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', diff --git a/packages/frontend-app-api/src/extensions/ComponentsApi.tsx b/packages/frontend-app-api/src/extensions/ComponentsApi.tsx new file mode 100644 index 0000000000..2949e686f8 --- /dev/null +++ b/packages/frontend-app-api/src/extensions/ComponentsApi.tsx @@ -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), + ), + ), + ), + }); + }, +}); diff --git a/packages/frontend-app-api/src/extensions/IconsApi.ts b/packages/frontend-app-api/src/extensions/IconsApi.ts new file mode 100644 index 0000000000..f7cb25de96 --- /dev/null +++ b/packages/frontend-app-api/src/extensions/IconsApi.ts @@ -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 }), {}), + ), + ), + }); + }, +}); diff --git a/packages/frontend-app-api/src/extensions/TranslationsApi.ts b/packages/frontend-app-api/src/extensions/TranslationsApi.ts new file mode 100644 index 0000000000..16defdde10 --- /dev/null +++ b/packages/frontend-app-api/src/extensions/TranslationsApi.ts @@ -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), + ), + }), + }), + }); + }, +}); diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index ca42dce3e4..13374e2e17 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -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); diff --git a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts index 52d6e2482d..de2df761ff 100644 --- a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts @@ -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: { diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts index a4f550595f..c06078b122 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -29,7 +29,7 @@ const themeDataRef = createExtensionDataRef().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, diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts index 7b01d0b28f..ea7aa64713 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -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, diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index 9561636d22..35d003e99b 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -35,7 +35,7 @@ export function createComponentExtension(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() { From 3d015d4c04778f3b5be12e87ad3e7a3ba3b3e35f Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 14:49:21 +0200 Subject: [PATCH 2/9] chore: move over some more apis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Signed-off-by: blam --- .../DefaultComponentsApi.test.tsx | 73 ++++--------------- .../src/extensions/AppLanguageApi.ts | 30 ++++++++ .../src/extensions/FeatureFlagsApi.ts | 40 ++++++++++ ...TranslationsApi.ts => TranslationsApi.tsx} | 0 .../frontend-app-api/src/wiring/createApp.tsx | 23 ++---- 5 files changed, 88 insertions(+), 78 deletions(-) create mode 100644 packages/frontend-app-api/src/extensions/AppLanguageApi.ts create mode 100644 packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts rename packages/frontend-app-api/src/extensions/{TranslationsApi.ts => TranslationsApi.tsx} (100%) diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx index e2c7581e63..3d19fc413f 100644 --- a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx +++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx @@ -15,57 +15,22 @@ */ import React from 'react'; -import { - coreExtensionData, - createComponentExtension, - createComponentRef, - createExtension, - createExtensionOverrides, -} from '@backstage/frontend-plugin-api'; -import { resolveAppNodeSpecs } from '../../../tree/resolveAppNodeSpecs'; -import { resolveAppTree } from '../../../tree/resolveAppTree'; -import { App } from '../../../extensions/App'; +import { createComponentRef } from '@backstage/frontend-plugin-api'; import { DefaultComponentsApi } from './DefaultComponentsApi'; import { render, screen } from '@testing-library/react'; -import { instantiateAppNodeTree } from '../../../tree/instantiateAppNodeTree'; const testRefA = createComponentRef({ id: 'test.a' }); const testRefB1 = createComponentRef({ id: 'test.b' }); const testRefB2 = createComponentRef({ id: 'test.b' }); -const baseOverrides = createExtensionOverrides({ - extensions: [ - App, - createExtension({ - namespace: 'app', - name: 'root', - attachTo: { id: 'app', input: 'root' }, - output: [coreExtensionData.reactElement], - factory: () => [coreExtensionData.reactElement(
root
)], - }), - ], -}); - describe('DefaultComponentsApi', () => { it('should provide components', () => { - const tree = resolveAppTree( - 'app', - resolveAppNodeSpecs({ - features: [ - baseOverrides, - createExtensionOverrides({ - extensions: [ - createComponentExtension({ - ref: testRefA, - loader: { sync: () => () =>
test.a
}, - }), - ], - }), - ], - }), - ); - instantiateAppNodeTree(tree.root); - const api = DefaultComponentsApi.fromTree(tree); + const api = DefaultComponentsApi.fromComponents([ + { + ref: testRefA, + impl: () =>
test.a
, + }, + ]); const ComponentA = api.getComponent(testRefA); render(); @@ -74,24 +39,12 @@ describe('DefaultComponentsApi', () => { }); it('should key extension refs by ID', () => { - const tree = resolveAppTree( - 'app', - resolveAppNodeSpecs({ - features: [ - baseOverrides, - createExtensionOverrides({ - extensions: [ - createComponentExtension({ - ref: testRefB1, - loader: { sync: () => () =>
test.b
}, - }), - ], - }), - ], - }), - ); - instantiateAppNodeTree(tree.root); - const api = DefaultComponentsApi.fromTree(tree); + const api = DefaultComponentsApi.fromComponents([ + { + ref: testRefB1, + impl: () =>
test.b
, + }, + ]); const ComponentB1 = api.getComponent(testRefB1); const ComponentB2 = api.getComponent(testRefB2); diff --git a/packages/frontend-app-api/src/extensions/AppLanguageApi.ts b/packages/frontend-app-api/src/extensions/AppLanguageApi.ts new file mode 100644 index 0000000000..ff12848d44 --- /dev/null +++ b/packages/frontend-app-api/src/extensions/AppLanguageApi.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementations/AppLanguageApi'; +import { appLanguageApiRef } from '@backstage/core-plugin-api/alpha'; +import { ApiBlueprint, createApiFactory } from '@backstage/frontend-plugin-api'; + +export const AppLanguageApi = ApiBlueprint.make({ + name: 'app-language', + params: { + factory: createApiFactory( + appLanguageApiRef, + AppLanguageSelector.createWithStorage(), + ), + }, +}); diff --git a/packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts b/packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts new file mode 100644 index 0000000000..3acc6e52be --- /dev/null +++ b/packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts @@ -0,0 +1,40 @@ +/* + * 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, + createApiFactory, + featureFlagsApiRef, +} from '@backstage/frontend-plugin-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { LocalStorageFeatureFlags } from '../../../core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags'; + +/** + * Contains the shareable icons installed into the app. + * + * @public + */ +export const FeatureFlagsApi = ApiBlueprint.make({ + name: 'feature-flags', + params: { + // TODO: properly discovery feature flags, maybe rework the whole thing + factory: createApiFactory({ + api: featureFlagsApiRef, + deps: {}, + factory: () => new LocalStorageFeatureFlags(), + }), + }, +}); diff --git a/packages/frontend-app-api/src/extensions/TranslationsApi.ts b/packages/frontend-app-api/src/extensions/TranslationsApi.tsx similarity index 100% rename from packages/frontend-app-api/src/extensions/TranslationsApi.ts rename to packages/frontend-app-api/src/extensions/TranslationsApi.tsx diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 13374e2e17..37594f74d4 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -55,14 +55,10 @@ import { AppThemeProvider } from '../../../core-app-api/src/app/AppThemeProvider // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { LocalStorageFeatureFlags } from '../../../core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultConfigLoader'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs'; // 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 { 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'; @@ -72,7 +68,7 @@ import { alertDisplayAppRootElement, } from '../extensions/elements'; import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; -import { appLanguageApiRef } from '@backstage/core-plugin-api/alpha'; + import { CreateAppRouteBinder } from '../routing'; import { RouteResolver } from '../routing/RouteResolver'; import { resolveRouteBindings } from '../routing/resolveRouteBindings'; @@ -95,6 +91,8 @@ import { AppThemeApi, DarkTheme, LightTheme } from '../extensions/AppThemeApi'; import { IconsApi } from '../extensions/IconsApi'; import { TranslationsApi } from '../extensions/TranslationsApi'; import { ComponentsApi } from '../extensions/ComponentsApi'; +import { AppLanguageApi } from '../extensions/AppLanguageApi'; +import { FeatureFlagsApi } from '../extensions/FeatureFlagsApi'; const DefaultApis = defaultApis.map(factory => ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }), @@ -114,9 +112,11 @@ export const builtinExtensions = [ oauthRequestDialogAppRootElement, alertDisplayAppRootElement, AppThemeApi, + AppLanguageApi, IconsApi, TranslationsApi, ComponentsApi, + FeatureFlagsApi, ...DefaultApis, ].map(def => resolveExtensionDefinition(def)); @@ -349,13 +349,6 @@ function createApiHolder( factoryRegistry.register('default', factory); } - // TODO: properly discovery feature flags, maybe rework the whole thing - factoryRegistry.register('default', { - api: featureFlagsApiRef, - deps: {}, - factory: () => new LocalStorageFeatureFlags(), - }); - factoryRegistry.register('static', { api: identityApiRef, deps: {}, @@ -376,12 +369,6 @@ function createApiHolder( factory: () => routeResolutionApi, }); - factoryRegistry.register('static', { - api: appLanguageApiRef, - deps: {}, - factory: () => AppLanguageSelector.createWithStorage(), - }); - factoryRegistry.register('static', { api: configApiRef, deps: {}, From f3a2b91d9111dda5f7b2a4105876d6f077737b79 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 14:50:31 +0200 Subject: [PATCH 3/9] chore: changeset Signed-off-by: blam --- .changeset/metal-garlics-fetch.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/metal-garlics-fetch.md diff --git a/.changeset/metal-garlics-fetch.md b/.changeset/metal-garlics-fetch.md new file mode 100644 index 0000000000..1cc4fd0353 --- /dev/null +++ b/.changeset/metal-garlics-fetch.md @@ -0,0 +1,6 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-app-api': patch +--- + +Move implementations for some APIs to `ApiExtensions` instead that are discovered from the tree From cbe3ad7a1e4c3fe6df2a0d62b66b5605cb383ac0 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 14:52:26 +0200 Subject: [PATCH 4/9] chore: fixing tests and components Signed-off-by: blam --- .../src/wiring/createApp.test.tsx | 26 ++++++++++++------- .../src/blueprints/ThemeBlueprint.test.ts | 2 +- .../blueprints/TranslationBlueprint.test.ts | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index c79d8eed76..6fc4720da2 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -278,16 +278,24 @@ describe('createApp', () => { ] ] - components [ - - - - ] - themes [ - - - ] apis [ + + themes [ + + + ] + + + + + + components [ + + + + ] + + diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts index e6daf510eb..75d342fdcb 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts @@ -32,7 +32,7 @@ describe('ThemeBlueprint', () => { { "$$type": "@backstage/ExtensionDefinition", "attachTo": { - "id": "app", + "id": "api:app-theme", "input": "themes", }, "configSchema": undefined, diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts index 3c7b271322..65ef4f7ee0 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts @@ -47,7 +47,7 @@ describe('TranslationBlueprint', () => { { "$$type": "@backstage/ExtensionDefinition", "attachTo": { - "id": "app", + "id": "api:translations", "input": "translations", }, "configSchema": undefined, From 3607ea44993fdd0e5c733bd4323c1f282de0e61e Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 15:05:18 +0200 Subject: [PATCH 5/9] chore: removed deprecated property Signed-off-by: blam --- packages/frontend-app-api/api-report.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 27d486b6a8..c837ff09b7 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -6,7 +6,6 @@ import { ConfigApi } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-plugin-api'; -import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; @@ -14,9 +13,6 @@ import { SubRouteRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) export function createApp(options?: { - icons?: { - [key in string]: IconComponent; - }; features?: (FrontendFeature | CreateAppFeatureLoader)[]; configLoader?: () => Promise<{ config: ConfigApi; @@ -50,9 +46,6 @@ export type CreateAppRouteBinder = < // @public export function createSpecializedApp(options?: { - icons?: { - [key in string]: IconComponent; - }; features?: FrontendFeature[]; config?: ConfigApi; bindRoutes?(context: { bind: CreateAppRouteBinder }): void; From 62cce6cecd3a921aee2649b0c7c964dde7a37f0c Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 15:06:33 +0200 Subject: [PATCH 6/9] chore: added another changeset Signed-off-by: blam --- .changeset/yellow-bees-hope.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/yellow-bees-hope.md diff --git a/.changeset/yellow-bees-hope.md b/.changeset/yellow-bees-hope.md new file mode 100644 index 0000000000..dfb0b0b325 --- /dev/null +++ b/.changeset/yellow-bees-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': minor +--- + +Removed deprecated `icons` property passing to `createApp` and `createSpecializedApp`. Use `IconBundleBlueprint` to create extensions instead and include them in the app. From b76af88f1eeea6d9f23c80333d7410f512d37ece Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 15:10:30 +0200 Subject: [PATCH 7/9] chore: review comments Signed-off-by: blam --- .changeset/metal-garlics-fetch.md | 2 +- packages/frontend-app-api/src/extensions/AppThemeApi.tsx | 2 -- packages/frontend-app-api/src/extensions/ComponentsApi.tsx | 2 -- packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts | 2 -- packages/frontend-app-api/src/extensions/IconsApi.ts | 2 -- packages/frontend-app-api/src/extensions/TranslationsApi.tsx | 2 -- 6 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.changeset/metal-garlics-fetch.md b/.changeset/metal-garlics-fetch.md index 1cc4fd0353..076b5f34a5 100644 --- a/.changeset/metal-garlics-fetch.md +++ b/.changeset/metal-garlics-fetch.md @@ -3,4 +3,4 @@ '@backstage/frontend-app-api': patch --- -Move implementations for some APIs to `ApiExtensions` instead that are discovered from the tree +Moved several implementations of built-in APIs from being hardcoded in the app to instead be provided as API extensions. This moves all API-related inputs from the `app` extension to the respective API extensions. For example, extensions created with `ThemeBlueprint` are now attached to the `themes` input of `api:app-theme` rather than the `app` extension. diff --git a/packages/frontend-app-api/src/extensions/AppThemeApi.tsx b/packages/frontend-app-api/src/extensions/AppThemeApi.tsx index 375b9e666b..d0dcad34e8 100644 --- a/packages/frontend-app-api/src/extensions/AppThemeApi.tsx +++ b/packages/frontend-app-api/src/extensions/AppThemeApi.tsx @@ -32,8 +32,6 @@ import { AppThemeSelector } from '@backstage/core-app-api'; /** * Contains the themes installed into the app. - * - * @public */ export const AppThemeApi = ApiBlueprint.makeWithOverrides({ name: 'app-theme', diff --git a/packages/frontend-app-api/src/extensions/ComponentsApi.tsx b/packages/frontend-app-api/src/extensions/ComponentsApi.tsx index 2949e686f8..5803aacf95 100644 --- a/packages/frontend-app-api/src/extensions/ComponentsApi.tsx +++ b/packages/frontend-app-api/src/extensions/ComponentsApi.tsx @@ -25,8 +25,6 @@ import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi'; /** * Contains the shareable components installed into the app. - * - * @public */ export const ComponentsApi = ApiBlueprint.makeWithOverrides({ name: 'components', diff --git a/packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts b/packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts index 3acc6e52be..8016521539 100644 --- a/packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts +++ b/packages/frontend-app-api/src/extensions/FeatureFlagsApi.ts @@ -24,8 +24,6 @@ import { LocalStorageFeatureFlags } from '../../../core-app-api/src/apis/impleme /** * Contains the shareable icons installed into the app. - * - * @public */ export const FeatureFlagsApi = ApiBlueprint.make({ name: 'feature-flags', diff --git a/packages/frontend-app-api/src/extensions/IconsApi.ts b/packages/frontend-app-api/src/extensions/IconsApi.ts index f7cb25de96..7a0ecebb40 100644 --- a/packages/frontend-app-api/src/extensions/IconsApi.ts +++ b/packages/frontend-app-api/src/extensions/IconsApi.ts @@ -26,8 +26,6 @@ import { DefaultIconsApi } from '../apis/implementations/IconsApi'; /** * Contains the shareable icons installed into the app. - * - * @public */ export const IconsApi = ApiBlueprint.makeWithOverrides({ name: 'icons', diff --git a/packages/frontend-app-api/src/extensions/TranslationsApi.tsx b/packages/frontend-app-api/src/extensions/TranslationsApi.tsx index 16defdde10..9c13c3b063 100644 --- a/packages/frontend-app-api/src/extensions/TranslationsApi.tsx +++ b/packages/frontend-app-api/src/extensions/TranslationsApi.tsx @@ -29,8 +29,6 @@ import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementa /** * Contains translations that are installed in the app. - * - * @public */ export const TranslationsApi = ApiBlueprint.makeWithOverrides({ name: 'translations', From 89f6fae9d223b5c991dd1c2ab97453fe38c96dc4 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 15:11:34 +0200 Subject: [PATCH 8/9] chore: small changeset tweak Signed-off-by: blam --- .changeset/yellow-bees-hope.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/yellow-bees-hope.md b/.changeset/yellow-bees-hope.md index dfb0b0b325..1ea8690c20 100644 --- a/.changeset/yellow-bees-hope.md +++ b/.changeset/yellow-bees-hope.md @@ -2,4 +2,4 @@ '@backstage/frontend-app-api': minor --- -Removed deprecated `icons` property passing to `createApp` and `createSpecializedApp`. Use `IconBundleBlueprint` to create extensions instead and include them in the app. +Removed deprecated `icons` property passing to `createApp` and `createSpecializedApp`. Use `IconBundleBlueprint.make` to create extensions instead and include them in the app. From b233ae5c4b8d34101d9ab646251e06ce1c7caaa2 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 21 Aug 2024 15:34:35 +0200 Subject: [PATCH 9/9] chore: include default icons Signed-off-by: blam --- packages/frontend-app-api/src/extensions/IconsApi.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/frontend-app-api/src/extensions/IconsApi.ts b/packages/frontend-app-api/src/extensions/IconsApi.ts index 7a0ecebb40..d770000197 100644 --- a/packages/frontend-app-api/src/extensions/IconsApi.ts +++ b/packages/frontend-app-api/src/extensions/IconsApi.ts @@ -21,8 +21,9 @@ import { createApiFactory, iconsApiRef, } from '@backstage/frontend-plugin-api'; - import { DefaultIconsApi } from '../apis/implementations/IconsApi'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { icons as defaultIcons } from '../../../app-defaults/src/defaults'; /** * Contains the shareable icons installed into the app. @@ -39,7 +40,7 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({ new DefaultIconsApi( inputs.icons .map(i => i.get(IconBundleBlueprint.dataRefs.icons)) - .reduce((acc, bundle) => ({ ...acc, ...bundle }), {}), + .reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons), ), ), });