Merge pull request #22312 from backstage/rugvip/icons
frontend-plugin-api: add IconsApi
This commit is contained in:
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added initial `IconsApi` definition.
|
||||
@@ -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 }) => (
|
||||
<ErrorBoundaryFallback
|
||||
{...rest}
|
||||
plugin={plugin && toNewPlugin(plugin)}
|
||||
/>
|
||||
);
|
||||
|
||||
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<string, IconComponent> {
|
||||
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 (
|
||||
<AppContextProvider appContext={appContext}>
|
||||
|
||||
@@ -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"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, IconComponent>;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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<IconsApi>;
|
||||
|
||||
export { IdentityApi };
|
||||
|
||||
export { identityApiRef };
|
||||
|
||||
@@ -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<IconsApi>({
|
||||
id: 'core.icons',
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user