diff --git a/.changeset/rotten-seas-fetch.md b/.changeset/rotten-seas-fetch.md
new file mode 100644
index 000000000..30bffeff9
--- /dev/null
+++ b/.changeset/rotten-seas-fetch.md
@@ -0,0 +1,5 @@
+---
+'app': minor
+---
+
+Instead of embedding all MUI4 icons into the bundle, we can depend in app.getSystemIcon() icon catalog. This PR allows users to extend it via dynamic plugins and therefore cleanup our bundle of unnecessary icons.
diff --git a/app-config.dynamic-plugins.yaml b/app-config.dynamic-plugins.yaml
index efc3c0241..79e015526 100644
--- a/app-config.dynamic-plugins.yaml
+++ b/app-config.dynamic-plugins.yaml
@@ -71,11 +71,14 @@ dynamicPlugins:
- hasAnnotation: backstage.io/kubernetes-id
- hasAnnotation: backstage.io/kubernetes-namespace
backstage.plugin-lighthouse:
+ appIcons:
+ - name: lighthouse
+ importName: LighthouseIcon
dynamicRoutes:
- path: /lighthouse
importName: LighthousePage
menuItem:
- icon: Assessment
+ icon: lighthouse
text: Lighthouse
mountPoints:
- mountPoint: entity.page.overview/cards
@@ -181,11 +184,14 @@ dynamicPlugins:
anyOf:
- isNexusRepositoryManagerAvailable
janus-idp.backstage-plugin-ocm:
+ appIcons:
+ - name: ocmIcon
+ importName: OcmIcon
dynamicRoutes:
- path: /ocm
importName: OcmPage
menuItem:
- icon: Storage
+ icon: ocmIcon
text: Clusters
mountPoints:
- mountPoint: entity.page.overview/context
diff --git a/dynamic-plugins.default.yaml b/dynamic-plugins.default.yaml
index 0d9308f4b..b118b1455 100644
--- a/dynamic-plugins.default.yaml
+++ b/dynamic-plugins.default.yaml
@@ -411,11 +411,14 @@ plugins:
dynamicPlugins:
frontend:
janus-idp.backstage-plugin-ocm:
+ appIcons:
+ - name: ocmIcon
+ importName: OcmIcon
dynamicRoutes:
- path: /ocm
importName: OcmPage
menuItem:
- icon: Storage
+ icon: ocmIcon
text: Clusters
mountPoints:
- mountPoint: entity.page.overview/context
@@ -651,11 +654,14 @@ plugins:
dynamicPlugins:
frontend:
backstage.plugin-lighthouse:
+ appIcons:
+ - name: lighthouse
+ importName: LighthouseIcon
dynamicRoutes:
- path: /lighthouse
importName: LighthousePage
menuItem:
- icon: Assessment
+ icon: lighthouse
text: Lighthouse
mountPoints:
- mountPoint: entity.page.overview/cards
diff --git a/dynamic-plugins/imports/package.json b/dynamic-plugins/imports/package.json
index 615430b46..3c1515d4f 100644
--- a/dynamic-plugins/imports/package.json
+++ b/dynamic-plugins/imports/package.json
@@ -18,7 +18,7 @@
"@janus-idp/backstage-plugin-acr": "1.2.4",
"@janus-idp/backstage-plugin-jfrog-artifactory": "1.2.4",
"@janus-idp/backstage-plugin-nexus-repository-manager": "1.4.4",
- "@janus-idp/backstage-plugin-ocm": "3.3.4",
+ "@janus-idp/backstage-plugin-ocm": "3.5.0",
"@janus-idp/backstage-plugin-quay": "1.4.6",
"@janus-idp/backstage-plugin-tekton": "3.1.3",
"@janus-idp/backstage-plugin-topology": "1.16.4"
diff --git a/dynamic-plugins/wrappers/backstage-plugin-lighthouse/package.json b/dynamic-plugins/wrappers/backstage-plugin-lighthouse/package.json
index 684092a53..180e52884 100644
--- a/dynamic-plugins/wrappers/backstage-plugin-lighthouse/package.json
+++ b/dynamic-plugins/wrappers/backstage-plugin-lighthouse/package.json
@@ -20,7 +20,8 @@
"export-dynamic": "janus-cli package export-dynamic-plugin"
},
"dependencies": {
- "@backstage/plugin-lighthouse": "0.4.10"
+ "@backstage/plugin-lighthouse": "0.4.10",
+ "@mui/icons-material": "5.14.16"
},
"devDependencies": {
"@backstage/cli": "0.23.1",
diff --git a/dynamic-plugins/wrappers/backstage-plugin-lighthouse/src/index.ts b/dynamic-plugins/wrappers/backstage-plugin-lighthouse/src/index.ts
index 8507e8583..64e9d5ff0 100644
--- a/dynamic-plugins/wrappers/backstage-plugin-lighthouse/src/index.ts
+++ b/dynamic-plugins/wrappers/backstage-plugin-lighthouse/src/index.ts
@@ -1 +1,2 @@
export * from '@backstage/plugin-lighthouse';
+export { default as LighthouseIcon } from '@mui/icons-material/Assessment';
diff --git a/packages/app/config.d.ts b/packages/app/config.d.ts
index 26354f4d1..6dd56e8a2 100644
--- a/packages/app/config.d.ts
+++ b/packages/app/config.d.ts
@@ -54,24 +54,22 @@ export interface Config {
/** @deepVisibility frontend */
frontend?: {
[key: string]: {
- dynamicRoutes: ({
- [key: string]: any;
- } & {
+ dynamicRoutes?: {
path: string;
module?: string;
importName?: string;
- menuItem: {
+ menuItem?: {
icon: string;
text: string;
};
- })[];
+ }[];
routeBindings?: {
bindTarget: string;
bindMap: {
[key: string]: string;
};
}[];
- mountPoints: {
+ mountPoints?: {
mountPoint: string;
module?: string;
importName?: string;
@@ -108,6 +106,11 @@ export interface Config {
};
};
}[];
+ appIcons?: {
+ module?: string;
+ importName?: string;
+ name: string;
+ }[];
};
};
};
diff --git a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx
index 54720c089..7b1a84bc2 100644
--- a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx
+++ b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx
@@ -57,7 +57,7 @@ const DynamicRoot = ({
// Fills registry of remote components
const initializeRemoteModules = useCallback(async () => {
- const { dynamicRoutes, mountPoints, routeBindings } =
+ const { dynamicRoutes, mountPoints, routeBindings, appIcons } =
await extractDynamicConfig();
const requiredModules = [
@@ -83,6 +83,12 @@ const DynamicRoot = ({
bindRoutes({ bind }) {
bindAppRoutes(bind, remotePlugins, routeBindings);
},
+ icons: Object.fromEntries(
+ appIcons.map(({ scope, module, importName, name }) => [
+ name,
+ remotePlugins[scope][module][importName],
+ ]),
+ ),
themes: defaultThemes,
components: defaultAppComponents,
});
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index ac4c35c7e..96d97a27c 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -17,7 +17,7 @@ import AppsIcon from '@mui/icons-material/Apps';
import ExtensionIcon from '@mui/icons-material/Extension';
import HomeIcon from '@mui/icons-material/Home';
import LibraryBooks from '@mui/icons-material/LibraryBooks';
-import MenuIcon from '@mui/icons-material/Menu';
+import MuiMenuIcon from '@mui/icons-material/Menu';
import MapIcon from '@mui/icons-material/MyLocation';
import SchoolIcon from '@mui/icons-material/School';
import SearchIcon from '@mui/icons-material/Search';
@@ -25,7 +25,7 @@ import { makeStyles } from 'tss-react/mui';
import React, { PropsWithChildren, useContext } from 'react';
import { SidebarLogo } from './SidebarLogo';
import DynamicRootContext from '../DynamicRoot/DynamicRootContext';
-import * as MuiIcons from '@mui/icons-material';
+import { useApp } from '@backstage/core-plugin-api';
const useStyles = makeStyles()({
sidebarItem: {
@@ -51,6 +51,13 @@ const SideBarItemWrapper = (props: SidebarItemProps) => {
);
};
+const MenuIcon = ({ icon }: { icon: string }) => {
+ const app = useApp();
+
+ const Icon = app.getSystemIcon(icon) || (() => null);
+ return ;
+};
+
export const Root = ({ children }: PropsWithChildren<{}>) => {
const { dynamicRoutes } = useContext(DynamicRootContext);
return (
@@ -61,7 +68,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => {
- }>
+ }>
{/* Global nav, not org-specific */}
) => {
if (menuItem) {
return (
}
to={path}
text={menuItem.text}
/>
diff --git a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts
index 37bcb659d..ca5e27626 100644
--- a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts
+++ b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts
@@ -4,6 +4,7 @@ import { isKind } from '@backstage/plugin-catalog';
import { hasAnnotation, isType } from '../../components/catalog/utils';
import {
DynamicModuleEntry,
+ MenuItem,
RouteBinding,
ScalprumMountPointConfigRaw,
ScalprumMountPointConfigRawIf,
@@ -25,12 +26,22 @@ type DynamicRoute = {
module: string;
importName: string;
path: string;
+ menuItem?: MenuItem;
};
export type MountPoint = {
+ scope: string;
mountPoint: string;
- module?: string;
- importName?: string;
+ module: string;
+ importName: string;
+ config?: ScalprumMountPointConfigRaw;
+};
+
+type AppIcon = {
+ scope: string;
+ name: string;
+ module: string;
+ importName: string;
};
type CustomProperties = {
@@ -41,6 +52,7 @@ type CustomProperties = {
})[];
routeBindings?: RouteBinding[];
mountPoints?: MountPoint[];
+ appIcons?: AppIcon[];
};
const conditionsArrayMapper = (
@@ -88,13 +100,8 @@ async function extractDynamicConfig() {
const dynamicConfig = (appsConfig as AppConfig[]).reduce<{
routeBindings: RouteBinding[];
dynamicRoutes: DynamicRoute[];
- mountPoints: {
- scope: string;
- module: string;
- importName: string;
- mountPoint: string;
- config?: ScalprumMountPointConfigRaw;
- }[];
+ appIcons: AppIcon[];
+ mountPoints: MountPoint[];
}>(
(acc, { data }) => {
if (data?.dynamicPlugins?.frontend) {
@@ -123,30 +130,43 @@ async function extractDynamicConfig() {
);
acc.mountPoints.push(
- ...Object.entries(data.dynamicPlugins.frontend).reduce<
- {
- scope: string;
- module: string;
- importName: string;
- mountPoint: string;
- }[]
- >((accMountPoints, [scope, { mountPoints }]) => {
- accMountPoints.push(
- ...(mountPoints ?? []).map(point => ({
- ...point,
- module: point.module ?? 'PluginRoot',
- importName: point.importName ?? 'default',
- scope,
- })),
- );
- return accMountPoints;
- }, []),
+ ...Object.entries(data.dynamicPlugins.frontend).reduce(
+ (accMountPoints, [scope, { mountPoints }]) => {
+ accMountPoints.push(
+ ...(mountPoints ?? []).map(point => ({
+ ...point,
+ module: point.module ?? 'PluginRoot',
+ importName: point.importName ?? 'default',
+ scope,
+ })),
+ );
+ return accMountPoints;
+ },
+ [],
+ ),
+ );
+
+ acc.appIcons.push(
+ ...Object.entries(data.dynamicPlugins.frontend).reduce(
+ (accAppIcons, [scope, { appIcons }]) => {
+ accAppIcons.push(
+ ...(appIcons ?? []).map(icon => ({
+ ...icon,
+ module: icon.module ?? 'PluginRoot',
+ importName: icon.importName ?? 'default',
+ scope,
+ })),
+ );
+ return accAppIcons;
+ },
+ [],
+ ),
);
}
return acc;
},
- { routeBindings: [], dynamicRoutes: [], mountPoints: [] },
- ) || { routeBindings: [], dynamicRoutes: [], mountPoints: [] }; // fallback to empty arrays
+ { routeBindings: [], dynamicRoutes: [], mountPoints: [], appIcons: [] },
+ ) || { routeBindings: [], dynamicRoutes: [], mountPoints: [], appIcons: [] }; // fallback to empty arrays
return dynamicConfig;
}
diff --git a/showcase-docs/dynamic-plugins.md b/showcase-docs/dynamic-plugins.md
index 4b9378f22..4105116f6 100644
--- a/showcase-docs/dynamic-plugins.md
+++ b/showcase-docs/dynamic-plugins.md
@@ -450,6 +450,7 @@ Similarly to traditional Backstage instances, there are 3 types of functionality
- Full new page that declares a completely new route in the app
- Extension to existing page via router `bind`ings
- Use of mount points within the application
+- Extend internal library of available icons
The overall configuration is as follows:
@@ -461,8 +462,28 @@ dynamicPlugins:
dynamicRoutes: ...
mountPoints: ...
routeBindings: ...
+ appIcons: ...
```
+#### Extend internal library of available icons
+
+Backstage offers an internal catalog of system icons available across the application. This is traditionally used within Catalog items as icons for links for example. Dynamic plugins also use this catalog when fetching icons for [dynamically configured routes with sidebar navigation menu entry](#dynamic-routes). Therefore if a plugin requires a custom icon to be used for menu item, this icon must be added to the internal icon catalog. This is done via `appIcons` configuration:
+
+```yaml
+# app-config.yaml
+dynamicPlugins:
+ frontend:
+ : # same as `scalprum.name` key in plugin's `package.json`
+ appIcons:
+ - name: fooIcon # unique icon name
+ module: CustomModule # optional, same as key in `scalprum.exposedModules` key in plugin's `package.json`
+ importName: FooIcon # optional, actual component name that should be rendered
+```
+
+- `name` - Unique name in the app's internal icon catalog.
+- `module` - Optional. Since dynamic plugins can expose multiple distinct modules, you may need to specify which set of assets you want to access within the plugin. If not provided, the default module named `PluginRoot` is used. This is the same as the key in `scalprum.exposedModules` key in plugin's `package.json`.
+- `importName` - Optional. The actual component name that should be rendered as a standalone page. If not specified the `default` export is used.
+
#### Dynamic routes
Traditionally, [Backstage full page extensions](https://backstage.io/docs/plugins/composability/#using-extensions-in-an-app) are done within the `packages/app/src/App.tsx` file. It may look like this:
@@ -499,7 +520,7 @@ dynamicPlugins:
module: CustomModule # optional, same as key in `scalprum.exposedModules` key in plugin's `package.json`
importName: FooPluginPage # optional, actual component name that should be rendered
menuItem: # optional, allows you to populate main sidebar navigation
- icon: Storage # MUI4 icon to render in the sidebar
+ icon: fooIcon # Backstage system icon
text: Foo Plugin Page # menu item text
```
@@ -508,7 +529,7 @@ Each plugin can expose multiple routes and each route is required to define its
- `path` - Unique path in the app. Cannot override existing routes with the exception of the `/` home route: the main home page can be replaced via the dynamic plugins mechanism.
- `module` - Optional. Since dynamic plugins can expose multiple distinct modules, you may need to specify which set of assets you want to access within the plugin. If not provided, the default module named `PluginRoot` is used. This is the same as the key in `scalprum.exposedModules` key in plugin's `package.json`.
- `importName` - Optional. The actual component name that should be rendered as a standalone page. If not specified the `default` export is used.
-- `menuItem` - This property allows users to extend the main sidebar navigation and point to their new route. It accepts `text` and `icon` properties. `icon` is a Material UI 4 icon name.
+- `menuItem` - This property allows users to extend the main sidebar navigation and point to their new route. It accepts `text` and `icon` properties. `icon` refers to a Backstage system icon name. See [Backstage system icons](https://backstage.io/docs/getting-started/app-custom-theme/#icons) for the list of default icons and [Extending Icons Library](#extend-internal-library-of-available-icons) to extend this with dynamic plugins.
#### Bind to existing plugins