From 7eae3e0c702005920b8a5a6b21f42b926739a7d2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jan 2024 14:15:46 +0100 Subject: [PATCH 1/4] frontend-plugin-api: added IconsApi Signed-off-by: Patrik Oldsberg --- .changeset/flat-wasps-fold.md | 5 +++ packages/frontend-plugin-api/api-report.md | 13 +++++++ .../src/apis/definitions/IconsApi.ts | 38 +++++++++++++++++++ .../src/apis/definitions/index.ts | 1 + 4 files changed, 57 insertions(+) create mode 100644 .changeset/flat-wasps-fold.md create mode 100644 packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts diff --git a/.changeset/flat-wasps-fold.md b/.changeset/flat-wasps-fold.md new file mode 100644 index 0000000000..653dc0d78f --- /dev/null +++ b/.changeset/flat-wasps-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added initial `IconsApi` definition. diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 1d42d8bab8..fd0e1302b7 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -976,6 +976,19 @@ export type IconComponent = ComponentType< } >; +// @public +export interface IconsApi { + // (undocumented) + getIcon(key: string): IconComponent | undefined; + // (undocumented) + listIconKeys(): { + keys: Iterable; + }; +} + +// @public +export const iconsApiRef: ApiRef; + export { IdentityApi }; export { identityApiRef }; diff --git a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts new file mode 100644 index 0000000000..daef2ca40f --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2023 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 { createApiRef } from '@backstage/core-plugin-api'; +import { IconComponent } from '../../icons'; + +/** + * API for accessing app icons. + * + * @public + */ +export interface IconsApi { + getIcon(key: string): IconComponent | undefined; + + listIconKeys(): { keys: Iterable }; +} + +/** + * The `ApiRef` of {@link IconsApi}. + * + * @public + */ +export const iconsApiRef = createApiRef({ + id: 'core.icons', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/index.ts b/packages/frontend-plugin-api/src/apis/definitions/index.ts index f2496ee61a..c0a2101db0 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/index.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/index.ts @@ -40,6 +40,7 @@ export * from './DiscoveryApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; export * from './FetchApi'; +export * from './IconsApi'; export * from './IdentityApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; From 42ebf27c0bd6a2ae10a44bfe6450c4198ac1a2f6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jan 2024 14:18:47 +0100 Subject: [PATCH 2/4] frontend-app-api: added IconsApi implementation and icons option for createApp Signed-off-by: Patrik Oldsberg --- .changeset/dry-lobsters-flash.md | 5 +++ packages/frontend-app-api/api-report.md | 7 ++++ .../apis/implementations/IconsApi/IconsApi.ts | 38 +++++++++++++++++++ .../apis/implementations/IconsApi/index.ts | 17 +++++++++ .../frontend-app-api/src/wiring/createApp.tsx | 21 +++++++++- 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 .changeset/dry-lobsters-flash.md create mode 100644 packages/frontend-app-api/src/apis/implementations/IconsApi/IconsApi.ts create mode 100644 packages/frontend-app-api/src/apis/implementations/IconsApi/index.ts diff --git a/.changeset/dry-lobsters-flash.md b/.changeset/dry-lobsters-flash.md new file mode 100644 index 0000000000..9815bf62f4 --- /dev/null +++ b/.changeset/dry-lobsters-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Added `IconsApi` implementation and the ability to configure icons through the `icons` option for `createApp` and `createSpecializedApp`. This is not a long-term solution as icons should be installable via extensions instead. This is just a stop-gap to make sure there is feature parity with the existing frontend system. diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index e7dd584734..d0aadb7e8c 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -8,12 +8,16 @@ import { ConfigApi } from '@backstage/core-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-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 { RouteRef } from '@backstage/frontend-plugin-api'; 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; @@ -49,6 +53,9 @@ export function createExtensionTree(options: { config: Config }): ExtensionTree; // @public export function createSpecializedApp(options?: { + icons?: { + [key in string]: IconComponent; + }; features?: FrontendFeature[]; config?: ConfigApi; bindRoutes?(context: { bind: CreateAppRouteBinder }): void; diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/IconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/IconsApi.ts new file mode 100644 index 0000000000..f64bbc67f9 --- /dev/null +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/IconsApi.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2023 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 { IconComponent, IconsApi } from '@backstage/frontend-plugin-api'; + +/** + * Implementation for the {@link IconsApi} + * + * @internal + */ +export class DefaultIconsApi implements IconsApi { + #icons: Map; + + constructor(icons: { [key in string]: IconComponent }) { + this.#icons = new Map(Object.entries(icons)); + } + + getIcon(key: string): IconComponent | undefined { + return this.#icons.get(key); + } + + listIconKeys: IconsApi['listIconKeys'] = () => { + return { keys: this.#icons.keys() }; + }; +} diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/index.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/index.ts new file mode 100644 index 0000000000..4a73182e84 --- /dev/null +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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. + */ + +export { DefaultIconsApi } from './IconsApi'; diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 7b2f17193e..832aa2663b 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -29,6 +29,7 @@ import { createTranslationExtension, ExtensionDataRef, FrontendFeature, + iconsApiRef, RouteRef, useRouteRef, } from '@backstage/frontend-plugin-api'; @@ -105,7 +106,10 @@ import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiri // 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'; const DefaultApis = defaultApis.map(factory => createApiExtension({ factory })); @@ -269,6 +273,7 @@ export interface CreateAppFeatureLoader { /** @public */ export function createApp(options?: { + icons?: { [key in string]: IconComponent }; features?: (FrontendFeature | CreateAppFeatureLoader)[]; configLoader?: () => Promise<{ config: ConfigApi }>; bindRoutes?(context: { bind: CreateAppRouteBinder }): void; @@ -303,6 +308,7 @@ export function createApp(options?: { } const app = createSpecializedApp({ + icons: options?.icons, config, features: [...discoveredFeatures, ...providedFeatures], bindRoutes: options?.bindRoutes, @@ -330,6 +336,7 @@ export function createApp(options?: { * @public */ export function createSpecializedApp(options?: { + icons?: { [key in string]: IconComponent }; features?: FrontendFeature[]; config?: ConfigApi; bindRoutes?(context: { bind: CreateAppRouteBinder }): void; @@ -348,7 +355,12 @@ export function createSpecializedApp(options?: { }); const appIdentityProxy = new AppIdentityProxy(); - const apiHolder = createApiHolder(tree, config, appIdentityProxy); + const apiHolder = createApiHolder( + tree, + config, + appIdentityProxy, + options?.icons, + ); const featureFlagApi = apiHolder.get(featureFlagsApiRef); if (featureFlagApi) { @@ -402,6 +414,7 @@ function createApiHolder( tree: AppTree, configApi: ConfigApi, appIdentityProxy: AppIdentityProxy, + icons?: { [key in string]: IconComponent }, ): ApiHolder { const factoryRegistry = new ApiFactoryRegistry(); @@ -470,6 +483,12 @@ function createApiHolder( factory: () => new DefaultComponentsApi(componentsMap), }); + factoryRegistry.register('static', { + api: iconsApiRef, + deps: {}, + factory: () => new DefaultIconsApi({ ...defaultIcons, ...icons }), + }); + factoryRegistry.register('static', { api: appThemeApiRef, deps: {}, From 1fa5041091aa0dafb4f56835e1cab2ca642c45e9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jan 2024 14:20:38 +0100 Subject: [PATCH 3/4] core-compat-api: make compatWrapper use ComponentsApi and IconsApi Signed-off-by: Patrik Oldsberg --- .changeset/chilly-seahorses-bake.md | 5 ++ .../compatWrapper/BackwardsCompatProvider.tsx | 62 ++++++++++++++----- .../src/compatWrapper/compatWrapper.test.tsx | 2 +- 3 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 .changeset/chilly-seahorses-bake.md diff --git a/.changeset/chilly-seahorses-bake.md b/.changeset/chilly-seahorses-bake.md new file mode 100644 index 0000000000..8ab80cf93e --- /dev/null +++ b/.changeset/chilly-seahorses-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': patch +--- + +The backwards compatibility provider will now use the new `ComponentsApi` and `IconsApi` when implementing the old `AppContext`. diff --git a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx index 099993a392..1dd64efb89 100644 --- a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx @@ -18,14 +18,13 @@ import React, { useMemo } from 'react'; import { ReactNode } from 'react'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppContextProvider } from '../../../core-app-api/src/app/AppContext'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - components as defaultComponents, - icons as defaultIcons, -} from '../../../app-defaults/src/defaults'; import { + createPlugin as createNewPlugin, BackstagePlugin as NewBackstagePlugin, appTreeApiRef, + componentsApiRef, + coreComponentRefs, + iconsApiRef, useApi, } from '@backstage/frontend-plugin-api'; import { @@ -71,15 +70,35 @@ function toLegacyPlugin(plugin: NewBackstagePlugin): LegacyBackstagePlugin { return legacy; } +// TODO: Currently a very naive implementation, may need some more work +function toNewPlugin(plugin: LegacyBackstagePlugin): NewBackstagePlugin { + return createNewPlugin({ + id: plugin.getId(), + }); +} + // Recreates the old AppContext APIs using the various new APIs that replaced it function LegacyAppContextProvider(props: { children: ReactNode }) { const appTreeApi = useApi(appTreeApiRef); + const componentsApi = useApi(componentsApiRef); + const iconsApi = useApi(iconsApiRef); const appContext = useMemo(() => { const { tree } = appTreeApi.getTree(); let gatheredPlugins: LegacyBackstagePlugin[] | undefined = undefined; + const ErrorBoundaryFallback = componentsApi.getComponent( + coreComponentRefs.errorBoundaryFallback, + ); + const ErrorBoundaryFallbackWrapper: AppComponents['ErrorBoundaryFallback'] = + ({ plugin, ...rest }) => ( + + ); + return { getPlugins(): LegacyBackstagePlugin[] { if (gatheredPlugins) { @@ -98,24 +117,39 @@ function LegacyAppContextProvider(props: { children: ReactNode }) { return gatheredPlugins; }, - // TODO: Grab these from new API once it exists getSystemIcon(key: string): IconComponent | undefined { - return key in defaultIcons - ? defaultIcons[key as keyof typeof defaultIcons] - : undefined; + return iconsApi.getIcon(key); }, - // TODO: Grab these from new API once it exists getSystemIcons(): Record { - return defaultIcons; + const { keys } = iconsApi.listIconKeys(); + + return Object.fromEntries( + Array.from(keys).map(key => [key, iconsApi.getIcon(key)!]), + ); }, - // TODO: Grab these from new API once it exists getComponents(): AppComponents { - return defaultComponents; + return { + NotFoundErrorPage: componentsApi.getComponent( + coreComponentRefs.notFoundErrorPage, + ), + BootErrorPage() { + throw new Error( + 'The BootErrorPage app component should not be accessed by plugins', + ); + }, + Progress: componentsApi.getComponent(coreComponentRefs.progress), + Router() { + throw new Error( + 'The Router app component should not be accessed by plugins', + ); + }, + ErrorBoundaryFallback: ErrorBoundaryFallbackWrapper, + }; }, }; - }, [appTreeApi]); + }, [appTreeApi, componentsApi, iconsApi]); return ( diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index b2ad7f3d0c..2e7dbd9d37 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -60,7 +60,7 @@ describe('BackwardsCompatProvider', () => { expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(` "plugins: - components: Progress, Router, NotFoundErrorPage, BootErrorPage, ErrorBoundaryFallback + components: NotFoundErrorPage, BootErrorPage, Progress, Router, ErrorBoundaryFallback icons: brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, user, warning" `); }); From d2e576d55aead2de14810485b4c6ac796eda6192 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 18 Jan 2024 17:32:48 +0100 Subject: [PATCH 4/4] frontend-plugin-api: swtich IconApi.listIconKeys to return an array direcly instead Signed-off-by: Patrik Oldsberg --- .../src/compatWrapper/BackwardsCompatProvider.tsx | 4 +--- .../IconsApi/{IconsApi.ts => DefaultIconsApi.ts} | 6 +++--- .../src/apis/implementations/IconsApi/index.ts | 2 +- packages/frontend-plugin-api/api-report.md | 4 +--- .../frontend-plugin-api/src/apis/definitions/IconsApi.ts | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) rename packages/frontend-app-api/src/apis/implementations/IconsApi/{IconsApi.ts => DefaultIconsApi.ts} (91%) diff --git a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx index 1dd64efb89..8c6ef1f375 100644 --- a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx @@ -122,10 +122,8 @@ function LegacyAppContextProvider(props: { children: ReactNode }) { }, getSystemIcons(): Record { - const { keys } = iconsApi.listIconKeys(); - return Object.fromEntries( - Array.from(keys).map(key => [key, iconsApi.getIcon(key)!]), + iconsApi.listIconKeys().map(key => [key, iconsApi.getIcon(key)!]), ); }, diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/IconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts similarity index 91% rename from packages/frontend-app-api/src/apis/implementations/IconsApi/IconsApi.ts rename to packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts index f64bbc67f9..53a7fa6421 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/IconsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts @@ -32,7 +32,7 @@ export class DefaultIconsApi implements IconsApi { return this.#icons.get(key); } - listIconKeys: IconsApi['listIconKeys'] = () => { - return { keys: this.#icons.keys() }; - }; + listIconKeys(): string[] { + return Array.from(this.#icons.keys()); + } } diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/index.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/index.ts index 4a73182e84..e07f6379ed 100644 --- a/packages/frontend-app-api/src/apis/implementations/IconsApi/index.ts +++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { DefaultIconsApi } from './IconsApi'; +export { DefaultIconsApi } from './DefaultIconsApi'; diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index fd0e1302b7..3a7a0f3ee6 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -981,9 +981,7 @@ export interface IconsApi { // (undocumented) getIcon(key: string): IconComponent | undefined; // (undocumented) - listIconKeys(): { - keys: Iterable; - }; + listIconKeys(): string[]; } // @public diff --git a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts index daef2ca40f..aa5b7f4fac 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/IconsApi.ts @@ -25,7 +25,7 @@ import { IconComponent } from '../../icons'; export interface IconsApi { getIcon(key: string): IconComponent | undefined; - listIconKeys(): { keys: Iterable }; + listIconKeys(): string[]; } /**