Skip to content

Commit

Permalink
feat(dynamic-plugins): replace static MUI Icons library import with d…
Browse files Browse the repository at this point in the history
…ynamic appIcons

Signed-off-by: Tomas Coufal <tcoufal@redhat.com>
  • Loading branch information
tumido committed Nov 8, 2023
1 parent 7ed4465 commit d60e5ff
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-seas-fetch.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 8 additions & 2 deletions app-config.dynamic-plugins.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -173,11 +176,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
Expand Down
10 changes: 8 additions & 2 deletions dynamic-plugins.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,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
Expand Down Expand Up @@ -629,11 +632,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
Expand Down
2 changes: 1 addition & 1 deletion dynamic-plugins/imports/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from '@backstage/plugin-lighthouse';
export { default as LighthouseIcon } from '@mui/icons-material/Assessment';
15 changes: 9 additions & 6 deletions packages/app/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -104,6 +102,11 @@ export interface Config {
};
};
}[];
appIcons?: {
module?: string;
importName?: string;
name: string;
}[];
};
};
};
Expand Down
8 changes: 7 additions & 1 deletion packages/app/src/components/DynamicRoot/DynamicRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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,
});
Expand Down
15 changes: 11 additions & 4 deletions packages/app/src/components/Root/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ 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';
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: {
Expand All @@ -51,6 +51,13 @@ const SideBarItemWrapper = (props: SidebarItemProps) => {
);
};

const MenuIcon = ({ icon }: { icon: string }) => {
const app = useApp();

const Icon = app.getSystemIcon(icon) || (() => null);
return <Icon />;
};

export const Root = ({ children }: PropsWithChildren<{}>) => {
const { dynamicRoutes } = useContext(DynamicRootContext);
return (
Expand All @@ -61,7 +68,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => {
<SidebarSearchModal />
</SidebarGroup>
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
<SidebarGroup label="Menu" icon={<MuiMenuIcon />}>
{/* Global nav, not org-specific */}
<SideBarItemWrapper icon={HomeIcon as any} to="/" text="Home" />
<SideBarItemWrapper
Expand Down Expand Up @@ -101,7 +108,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => {
if (menuItem) {
return (
<SideBarItemWrapper
icon={(MuiIcons as any)[menuItem.icon] as any}
icon={() => <MenuIcon icon={menuItem.icon} />}
to={path}
text={menuItem.text}
/>
Expand Down
78 changes: 49 additions & 29 deletions packages/app/src/utils/dynamicUI/extractDynamicConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isKind } from '@backstage/plugin-catalog';
import { hasAnnotation, isType } from '../../components/catalog/utils';
import {
DynamicModuleEntry,
MenuItem,
RouteBinding,
ScalprumMountPointConfigRaw,
ScalprumMountPointConfigRawIf,
Expand All @@ -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 = {
Expand All @@ -41,6 +52,7 @@ type CustomProperties = {
})[];
routeBindings?: RouteBinding[];
mountPoints?: MountPoint[];
appIcons?: AppIcon[];
};

const conditionsArrayMapper = (
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<MountPoint[]>(
(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<AppIcon[]>(
(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;
}
Expand Down
25 changes: 23 additions & 2 deletions showcase-docs/dynamic-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,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:

Expand All @@ -429,8 +430,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:
<package_name>: # 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:
Expand Down Expand Up @@ -467,7 +488,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
```

Expand All @@ -476,7 +497,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

Expand Down

0 comments on commit d60e5ff

Please sign in to comment.