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/.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/.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/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx
index 099993a392..8c6ef1f375 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,37 @@ 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;
+ return Object.fromEntries(
+ iconsApi.listIconKeys().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"
`);
});
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/DefaultIconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts
new file mode 100644
index 0000000000..53a7fa6421
--- /dev/null
+++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.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(): 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
new file mode 100644
index 0000000000..e07f6379ed
--- /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 './DefaultIconsApi';
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: {},
diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md
index 1d42d8bab8..3a7a0f3ee6 100644
--- a/packages/frontend-plugin-api/api-report.md
+++ b/packages/frontend-plugin-api/api-report.md
@@ -976,6 +976,17 @@ export type IconComponent = ComponentType<
}
>;
+// @public
+export interface IconsApi {
+ // (undocumented)
+ getIcon(key: string): IconComponent | undefined;
+ // (undocumented)
+ listIconKeys(): string[];
+}
+
+// @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..aa5b7f4fac
--- /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(): string[];
+}
+
+/**
+ * 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';