diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx
index efe1f97dfb..984488cdbb 100644
--- a/packages/core-components/src/layout/Sidebar/Items.tsx
+++ b/packages/core-components/src/layout/Sidebar/Items.tsx
@@ -116,6 +116,17 @@ const makeSidebarStyles = (sidebarConfig: SidebarConfig) =>
font: 'inherit',
textTransform: 'none',
},
+ itemIcon: {
+ display: 'inline-flex',
+ fontSize: theme.typography.fontSize,
+ lineHeight: 0,
+ '& svg': {
+ width: '1.5em',
+ height: '1.5em',
+ fontSize: 'inherit',
+ flexShrink: 0,
+ },
+ },
closed: {
width: sidebarConfig.drawerWidthClosed,
justifyContent: 'center',
@@ -401,7 +412,9 @@ const SidebarItemBase = forwardRef<
const displayItemIcon = (
-
+
+
+
{!isOpen && hasSubmenu ? : <>>}
);
diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts
index cfe6236b95..b739eea09a 100644
--- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts
+++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.test.ts
@@ -59,6 +59,10 @@ describe('DefaultIconsApi', () => {
expect(result).toBeTruthy();
// @ts-expect-error accessing internal React element structure
expect(result.type).toBe(MyIcon);
+ // @ts-expect-error accessing internal React element structure
+ expect(result.props.fontSize).toBe('inherit');
+ // @ts-expect-error accessing internal React element structure
+ expect(result.props.size).toBe('1em');
});
it('should wrap IconElement values in a component for getIcon()', () => {
@@ -69,10 +73,31 @@ describe('DefaultIconsApi', () => {
expect(icon).toBeDefined();
expect(typeof icon).toBe('function');
// @ts-expect-error testing runtime behavior
- expect(icon({})).toBe(element);
+ const result = icon({});
+ // @ts-expect-error accessing internal React element structure
+ expect(result.type).toBe('span');
+ // @ts-expect-error accessing internal React element structure
+ expect(result.props.style).toEqual({
+ display: 'inline-flex',
+ fontSize: '1.5rem',
+ lineHeight: 0,
+ });
+ // @ts-expect-error accessing internal React element structure
+ expect(result.props.children).toBe(element);
expect(api.getIcon('myIcon')).toBe(icon);
});
+ it('should honor fontSize for getIcon()', () => {
+ const element = createElement('svg');
+ const api = new DefaultIconsApi({ myIcon: element });
+ const icon = api.getIcon('myIcon');
+
+ // @ts-expect-error testing runtime behavior
+ const result = icon({ fontSize: 'small' });
+ // @ts-expect-error accessing internal React element structure
+ expect(result.props.style.fontSize).toBe('1.25rem');
+ });
+
it('should wrap null IconElement in a component for getIcon()', () => {
const api = new DefaultIconsApi({ empty: null });
diff --git a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts
index cc5ef69b6f..1dfdfbdc9e 100644
--- a/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts
+++ b/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts
@@ -21,6 +21,13 @@ import {
} from '@backstage/frontend-plugin-api';
import { createElement, isValidElement } from 'react';
+const legacyFontSizeMap = {
+ inherit: 'inherit',
+ small: '1.25rem',
+ medium: '1.5rem',
+ large: '2.1875rem',
+} as const;
+
/**
* Implementation for the {@link IconsApi}
*
@@ -65,7 +72,24 @@ export class DefaultIconsApi implements IconsApi {
if (el === undefined) {
return undefined;
}
- component = () => el;
+ component = ({ fontSize = 'medium' }) => {
+ if (el === null) {
+ return null;
+ }
+
+ return createElement(
+ // eslint-disable-next-line react/forbid-elements
+ 'span',
+ {
+ style: {
+ display: 'inline-flex',
+ fontSize: legacyFontSizeMap[fontSize],
+ lineHeight: 0,
+ },
+ },
+ el,
+ );
+ };
this.#components.set(key, component);
return component;
}
diff --git a/packages/frontend-plugin-api/src/icons/types.ts b/packages/frontend-plugin-api/src/icons/types.ts
index 1a45bcd8fc..68ae60a2f6 100644
--- a/packages/frontend-plugin-api/src/icons/types.ts
+++ b/packages/frontend-plugin-api/src/icons/types.ts
@@ -38,12 +38,18 @@ export type IconComponent = ComponentType<{
}>;
/**
- * The type used for icon elements throughout Backstage.
+ * The type used for icon elements throughout Backstage. It is recommended to
+ * use icons from `@remixicon/react`.
*
* @remarks
*
* Icons should be exactly 24x24 pixels in size.
*
+ * Using icons from `@remixicon/react` is preferred, but using icons from
+ * `@material-ui/icons` or `AppIcon` and its variants from
+ * `@backstage/core-components` is supported but depreceated. When using these
+ * icons, you must set the `fontSize` to `'inherit'`.
+ *
* @public
*/
export type IconElement = JSX.Element | null;
diff --git a/plugins/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx
index 4afe97dee9..2724619a68 100644
--- a/plugins/api-docs/src/alpha.tsx
+++ b/plugins/api-docs/src/alpha.tsx
@@ -43,7 +43,7 @@ const apiDocsNavItem = NavItemBlueprint.make({
params: {
title: 'APIs',
routeRef: rootRoute,
- icon: () => ,
+ icon: () => ,
},
});
@@ -211,7 +211,7 @@ const apiDocsApisEntityContent = EntityContentBlueprint.make({
export default createFrontendPlugin({
pluginId: 'api-docs',
title: 'APIs',
- icon: ,
+ icon: ,
info: { packageJson: () => import('../package.json') },
routes: {
root: rootRoute,
diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx
index 9fa7b2cd72..869f64db39 100644
--- a/plugins/app-visualizer/src/plugin.tsx
+++ b/plugins/app-visualizer/src/plugin.tsx
@@ -102,7 +102,7 @@ export const visualizerPlugin = createFrontendPlugin({
appVisualizerTreePage,
appVisualizerDetailedPage,
appVisualizerTextPage,
- appVisualizerNavItem,
+ // appVisualizerNavItem,
copyTreeAsJson,
],
});
diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx
index ffa6ced7b0..129a94cd9e 100644
--- a/plugins/app/src/extensions/AppNav.tsx
+++ b/plugins/app/src/extensions/AppNav.tsx
@@ -172,6 +172,7 @@ function NavContentRenderer(props: {
// We want the priority: page (config/params) -> nav item -> plugin -> pluginId
const resolvedTitle = node.instance.getData(coreExtensionData.title);
const pluginTitle = node.spec.plugin.title;
+ const pluginIcon = node.spec.plugin.icon;
const pluginId = node.spec.plugin.pluginId;
const hasExplicitPageTitle =
resolvedTitle !== undefined &&
@@ -194,6 +195,8 @@ function NavContentRenderer(props: {
icon = ;
} else if (resolvedIcon) {
icon = resolvedIcon;
+ } else if (pluginIcon) {
+ icon = pluginIcon;
}
if (!title || !icon) {
diff --git a/plugins/app/src/extensions/IconsApi.ts b/plugins/app/src/extensions/IconsApi.tsx
similarity index 85%
rename from plugins/app/src/extensions/IconsApi.ts
rename to plugins/app/src/extensions/IconsApi.tsx
index d8a97eb7c7..6293c84ed8 100644
--- a/plugins/app/src/extensions/IconsApi.ts
+++ b/plugins/app/src/extensions/IconsApi.tsx
@@ -45,7 +45,15 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({
return new DefaultIconsApi(
inputs.icons
.map(i => i.get(IconBundleBlueprint.dataRefs.icons))
- .reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons),
+ .reduce(
+ (acc, bundle) => ({ ...acc, ...bundle }),
+ Object.fromEntries(
+ Object.entries(defaultIcons).map(([key, Icon]) => [
+ key,
+ ,
+ ]),
+ ),
+ ),
);
},
}),
diff --git a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx
index 8fd614c886..d2987f3197 100644
--- a/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx
+++ b/plugins/catalog-unprocessed-entities/src/alpha/plugin.tsx
@@ -69,7 +69,7 @@ export const catalogUnprocessedEntitiesNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'catalog-unprocessed-entities',
title: 'Unprocessed Entities',
- icon: ,
+ icon: ,
info: { packageJson: () => import('../../package.json') },
routes: {
root: rootRouteRef,
diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx
index ab71cf39dc..6d64c65b74 100644
--- a/plugins/catalog/src/alpha/pages.tsx
+++ b/plugins/catalog/src/alpha/pages.tsx
@@ -59,7 +59,7 @@ export const catalogPage = PageBlueprint.makeWithOverrides({
return originalFactory({
path: '/catalog',
routeRef: rootRouteRef,
- icon: ,
+ icon: ,
title: 'Catalog',
loader: async () => {
const { BaseCatalogPage } = await import('../components/CatalogPage');
diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx
index 8db026016a..243b93db9b 100644
--- a/plugins/catalog/src/alpha/plugin.tsx
+++ b/plugins/catalog/src/alpha/plugin.tsx
@@ -40,7 +40,7 @@ import contextMenuItems from './contextMenuItems';
export default createFrontendPlugin({
pluginId: 'catalog',
title: 'Catalog',
- icon: ,
+ icon: ,
info: {
packageJson: () => import('../../package.json'),
},
diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx
index b1cfa7027f..b991e34bf9 100644
--- a/plugins/devtools/src/alpha/plugin.tsx
+++ b/plugins/devtools/src/alpha/plugin.tsx
@@ -89,7 +89,7 @@ export const devToolsNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'devtools',
title: 'DevTools',
- icon: ,
+ icon: ,
info: { packageJson: () => import('../../package.json') },
routes: {
root: rootRouteRef,
diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx
index 374249a804..0c105f3a7c 100644
--- a/plugins/scaffolder/src/alpha/plugin.tsx
+++ b/plugins/scaffolder/src/alpha/plugin.tsx
@@ -61,7 +61,7 @@ const scaffolderEntityIconLink = EntityIconLinkBlueprint.make({
export default createFrontendPlugin({
pluginId: 'scaffolder',
title: 'Create',
- icon: ,
+ icon: ,
info: { packageJson: () => import('../../package.json') },
routes: {
root: rootRouteRef,
diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx
index a2ecb61e0e..9625f5e946 100644
--- a/plugins/search/src/alpha.tsx
+++ b/plugins/search/src/alpha.tsx
@@ -277,7 +277,7 @@ export const searchNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'search',
title: 'Search',
- icon: ,
+ icon: ,
info: { packageJson: () => import('../package.json') },
extensions: [searchApi, searchPage, searchNavItem],
routes: {
diff --git a/plugins/techdocs/src/alpha/index.tsx b/plugins/techdocs/src/alpha/index.tsx
index 6acc6b5e6d..1ec92edfd3 100644
--- a/plugins/techdocs/src/alpha/index.tsx
+++ b/plugins/techdocs/src/alpha/index.tsx
@@ -280,7 +280,7 @@ const techDocsNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'techdocs',
title: 'Docs',
- icon: ,
+ icon: ,
info: { packageJson: () => import('../../package.json') },
extensions: [
techDocsClientApi,
diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx
index c0342a9b46..87b858fcec 100644
--- a/plugins/user-settings/src/alpha.tsx
+++ b/plugins/user-settings/src/alpha.tsx
@@ -63,7 +63,7 @@ export const settingsNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'user-settings',
title: 'Settings',
- icon: ,
+ icon: ,
info: { packageJson: () => import('../package.json') },
extensions: [userSettingsPage, settingsNavItem],
routes: {