From ed8d9ce67cbd95ccea4ad19386b549f6380c0f36 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 7 Mar 2026 16:36:45 +0100 Subject: [PATCH] further NFS icon migration and alignment Signed-off-by: Patrik Oldsberg --- .../src/layout/Sidebar/Items.tsx | 15 ++++++++++- .../IconsApi/DefaultIconsApi.test.ts | 27 ++++++++++++++++++- .../IconsApi/DefaultIconsApi.ts | 26 +++++++++++++++++- .../frontend-plugin-api/src/icons/types.ts | 8 +++++- plugins/api-docs/src/alpha.tsx | 4 +-- plugins/app-visualizer/src/plugin.tsx | 2 +- plugins/app/src/extensions/AppNav.tsx | 3 +++ .../extensions/{IconsApi.ts => IconsApi.tsx} | 10 ++++++- .../src/alpha/plugin.tsx | 2 +- plugins/catalog/src/alpha/pages.tsx | 2 +- plugins/catalog/src/alpha/plugin.tsx | 2 +- plugins/devtools/src/alpha/plugin.tsx | 2 +- plugins/scaffolder/src/alpha/plugin.tsx | 2 +- plugins/search/src/alpha.tsx | 2 +- plugins/techdocs/src/alpha/index.tsx | 2 +- plugins/user-settings/src/alpha.tsx | 2 +- 16 files changed, 95 insertions(+), 16 deletions(-) rename plugins/app/src/extensions/{IconsApi.ts => IconsApi.tsx} (85%) 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: {