Compared to the backend plugins, where mount points are defined in code and consumed by the backend plugin manager, frontend plugins require additional configuration in the app-config.yaml
. A plugin missing this configuration will not be loaded into the application and will not be displayed.
Similarly to traditional Backstage instances, there are various kinds of functionality a dynamic frontend plugin can offer:
- 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
- Provide additional Utility APIs or replace existing ones
The overall configuration is as follows:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
dynamicRoutes: ...
menuItems: ...
mountPoints: ...
routeBindings: ...
appIcons: ...
apiFactories: ...
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. 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:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
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 namedPluginRoot
is used. This is the same as the key inscalprum.exposedModules
key in plugin'spackage.json
.importName
- Optional. The actual component name that should be rendered as a standalone page. If not specified thedefault
export is used.
Traditionally, Backstage full page extensions are done within the packages/app/src/App.tsx
file. It may look like this:
...
<AppRouter>
<Root>
<FlatRoutes>
{/* Standard routes usually available in each Backstage instance */}
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route path="/settings" element={<UserSettingsPage />} />
...
{/* Additional routes defined by user */}
<Route path="/my-plugin" element={<FooPluginPage />} />
...
</FlatRoutes>
</Root>
</AppRouter>
...
This change is usually coupled with an extension to the main sidebar navigation, achieved by editing packages/app/src/components/Root/Root.tsx
.
In dynamic plugins this mechanism has changed and users are no longer allowed to edit .tsx
files. Instead, they declare their desire to expose additional routes within dynamic plugin configuration:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
dynamicRoutes: # exposes full routes
- path: /my-plugin # unique path in the app, can override `/`
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: fooIcon # Backstage system icon
text: Foo Plugin Page # menu item text
config:
props: ... # optional, React props to pass to the component
Each plugin can expose multiple routes and each route is required to define its path
and importName
(if it differs from the default export).
path
- Unique path in the app. Cannot override existing routes except 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 namedPluginRoot
is used. This is the same as the key inscalprum.exposedModules
key in plugin'spackage.json
.importName
- Optional. The actual component name that should be rendered as a standalone page. If not specified thedefault
export is used.menuItem
- This property allows users to extend the main sidebar navigation and point to their new route. It accepts the following properties:text
: The label shown to the usericon
: refers to a Backstage system icon name. See Backstage system icons for the list of default icons and Extending Icons Library to extend this with dynamic plugins.importName
: optional name of an exportedSidebarItem
component. The component will receive ato
property as well as any properties specified inconfig.props
config
- An optional field which is a holder to passprops
to a custom sidebar item
A custom SidebarItem
offers opportunities to provide a richer user experience such as notification badges. The component should accept the following properties:
export type MySidebarItemProps = {
to: string; // supplied by the sidebar during rendering, this will be the path configured for the dynamicRoute
};
Other properties can be specified as well and configured using the config.props
property on the dynamic route.
Here is an example configuration specifying a custom SidebarItem
component:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
my-dynamic-plugin-package-name:
dynamicRoutes:
- importName: CustomPage
menuItem:
config:
props:
text: Click Me!
importName: SimpleSidebarItem
path: /custom_page
Order and parent-children relationship of plugin menu items which are in main sidebar navigation can be customized with menu items configuration:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
menuItems: # optional, allows you to configure plugin menu items in the main sidebar navigation
<menu_item_name>: # unique name in the plugin menu items list
icon: fooIcon # optional, same as `menuItem.icon` in `dynamicRoutes`
title: Foo Plugin Page # optional, same as `menuItem.text` in `dynamicRoutes`
priority: 10 # optional, defines the order of menu items in the sidebar
parent: favorites # optional, defines parent-child relationships for nested menu items
Up to 3 levels of nested menu items are supported.
-
<menu_item_name> - A unique name in the main sidebar navigation. This can represent either a standalone menu item or a parent menu item. If it represents a plugin menu item, the name must match the corresponding path in
dynamicRoutes
. For example, ifdynamicRoutes
definespath: /my-plugin
, themenu_item_name
must bemy-plugin
.- Handling Complex Paths:
- For simple paths like
path: /my-plugin
, themenu_item_name
should bemy-plugin
. - For more complex paths, such as multi-segment paths like
path: /metrics/users/info
, themenu_item_name
should represent the full path in dot notation (e.g.,metrics.users.info
). - Trailing and leading slashes in paths are ignored. For example:
- For
path: /docs
, themenu_item_name
should bedocs
. - For
path: /metrics/users
, themenu_item_name
should bemetrics.users
.
- For
- For simple paths like
- Handling Complex Paths:
-
icon
- Optional. Defines the icon for the menu item, which refers to a Backstage system icon. See Backstage system icons for the default list, or extend the icon set using dynamic plugins. RHDH also provides additional icons in its internal library. See CommonIcons.tsx for reference. If the icon is already defined in thedynamicRoutes
configuration undermenuItem.icon
, it can be omitted in themenuItems
configuration. -
title
- Optional. Specifies the display title of the menu item. This can also be omitted if it has already been defined in thedynamicRoutes
configuration undermenuItem.text
. -
priority
- Optional. Defines the order in which menu items appear. The default priority is0
, which places the item at the bottom of the list. A higher priority value will position the item higher in the sidebar. -
parent
- Optional. Defines the parent menu item to nest the current item under. If specified, the parent menu item must be defined somewhere else in themenuItems
configuration of any enabled plugin.
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
<package_name>:
dynamicRoutes:
- path: /my-plugin
module: CustomModule
importName: FooPluginPage
menuItem:
icon: fooIcon
text: Foo Plugin Page
menuItems:
my-plugin: # matches `path` in `dynamicRoutes`
priority: 10 # controls order of plugins under the parent menu item
parent: favorites # nests this plugin under the `favorites` parent menu item
favorites: # configuration for the parent menu item
icon: favorite # icon from RHDH system icons
title: Favorites # title for the parent menu item
priority: 100 # controls the order of this top-level menu item
Another extension option available to Backstage is to bind to the external routes of existing plugins. This is traditionally done via the bindRoutes
interface as:
createApp({
bindRoutes({ bind }) {
bind(barPlugin.externalRoutes, {
headerLink: fooPlugin.routes.root,
});
},
});
Dynamic plugins offer similar functionality via routeBindings
configuration:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
routeBindings:
targets: # Declare a new bind target
- name: barPlugin # Optional, defaults to importName. Explicit name of the plugin that exposes the bind target
importName: barPlugin # Required. Explicit import name that reference a BackstagePlugin<{}> implementation.
module: CustomModule # Optional, same as key in `scalprum.exposedModules` key in plugin's `package.json`
bindings:
- bindTarget: 'barPlugin.externalRoutes' # Required. One of the supported or imported bind targets
bindMap: # Required. Map of bindings, same as the `bind` function options argument in the example above
headerLink: 'fooPlugin.routes.root'
This configuration allows you to bind to existing plugins and their routes as well as declare new targets sourced from dynamic plugins:
- Define new targets:
routeBindings.targets
allow you to define new targets. It accepts a list of targets where:importName
is required and has to resolve to aBackstagePlugin<{}>
implementationname
is an optional argument which sets the name of the target. If not provided,importName
is used instead.module
is an optional argument which allows you to specify which set of assets you want to access within the plugin. If not provided, the default module namedPluginRoot
is used. This is the same as the key inscalprum.exposedModules
key in plugin'spackage.json
.
- Declare bindings:
bindTarget
- Required. One of the supported or imported bind targets. This value can refer to any of the new dynamically added targets or available static targets:catalogPlugin.externalRoutes
catalogImportPlugin.externalRoutes
techdocsPlugin.externalRoutes
scaffolderPlugin.externalRoutes
bindMap
: Required. Map of bindings, same as thebind
function options argument in the traditional Backstage example above
Mount points are defined identifiers available across the application. These points specifically allow users to extend existing pages with additional content.
This section aims to allow users dynamic extension of Catalog Components, but can be used to extend additional views in the future as well.
The following mount points are available:
Mount point | Description | Visible even when no plugins are enabled |
---|---|---|
admin.page.plugins |
Administration plugins page | NO |
admin.page.rbac |
Administration RBAC page | NO |
entity.context.menu |
Catalog entity context menu | YES for all entities |
entity.page.overview |
Catalog entity overview page | YES for all entities |
entity.page.topology |
Catalog entity "Topology" tab | NO |
entity.page.issues |
Catalog entity "Issues" tab | NO |
entity.page.pull-requests |
Catalog entity "Pull Requests" tab | NO |
entity.page.ci |
Catalog entity "CI" tab | NO |
entity.page.cd |
Catalog entity "CD" tab | NO |
entity.page.kubernetes |
Catalog entity "Kubernetes" tab | NO |
entity.page.image-registry |
Catalog entity "Image Registry" tab | NO |
entity.page.monitoring |
Catalog entity "Monitoring" tab | NO |
entity.page.lighthouse |
Catalog entity "Lighthouse" tab | NO |
entity.page.api |
Catalog entity "API" tab | YES for entity of kind: Component and spec.type: 'service' |
entity.page.dependencies |
Catalog entity "Dependencies" tab | YES for entity of kind: Component |
entity.page.docs |
Catalog entity "Documentation" tab | YES for entity that satisfies isTechDocsAvailable |
entity.page.definition |
Catalog entity "Definitions" tab | YES for entity of kind: Api |
entity.page.diagram |
Catalog entity "Diagram" tab | YES for entity of kind: System |
search.page.types |
Search result type | YES, default catalog search type is available |
search.page.filters |
Search filters | YES, default catalog kind and lifecycle filters are visible |
search.page.results |
Search results content | YES, default catalog search is present |
Mount points within Catalog aka entity.page.*
are rendered as tabs. They become visible only if at least one plugin contributes to them, or they can render static content (see column 3 in previous table).
Each entity.page.*
mount point has 2 complementary variations:
*/context
type that serves to create React contexts*/cards
type for regular React components
Here is an example of the overall configuration structure of a mount point:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
mountPoints: # optional, uses existing mount points
- mountPoint: <mountPointName>/[cards|context]
module: CustomModule # optional, same as key in `scalprum.exposedModules` key in plugin's `package.json`
importName: FooPluginPage # actual component name that should be rendered
config: # optional, allows you to pass additional configuration to the component
layout: {} # accepts MUI sx properties
if: # Set of conditions that must be met for the component to be rendered
allOf|anyOf|oneOf:
- isMyPluginAvailable # an imported function from the same `module` within the plugin returns boolean
- isKind: component # Check if the entity is of a specific kind
- isType: service # Check if the entity is of a specific type
- hasAnnotation: annotationKey # Check if the entity has a specific annotation key
props: {} # React props to pass to the component
Each mount point supports additional configuration:
-
layout
- Used only in*/cards
type which renders visible content. Allows you to pass MUI sx properties to the component. This is useful when you want to control the layout of the component.entity.page.*
mount points are rendered as CSS grid, so SX property allows you to control the grid layout and exact positioning of the rendered component. -
props
- React props passed to the component. This is useful when you want to pass some additional data to the component. -
if
- Used only in*/cards
type which renders visible content. This is passed to<EntitySwitch.Case if={<here>}
.The following conditions are available:
allOf
: All conditions must be metanyOf
: At least one condition must be metoneOf
: Only one condition must be met
Conditions can be:
isKind
: Accepts a string or a list of string with entity kinds. For exampleisKind: component
will render the component only for entity ofkind: Component
.isType
: Accepts a string or a list of string with entity types. For exampleisType: service
will render the component only for entities ofspec.type: 'service'
.hasAnnotation
: Accepts a string or a list of string with annotation keys. For examplehasAnnotation: my-annotation
will render the component only for entities that havemetadata.annotations['my-annotation']
defined.- Condition imported from the plugin's
module
: Must be function name exported from the samemodule
within the plugin. For exampleisMyPluginAvailable
will render the component only ifisMyPluginAvailable
function returnstrue
. The function must have the following signature:(e: Entity) => boolean
The entity page also supports adding more items to the context menu at the top right of the page. Components targeting the entity.context.menu
mount point have some constraints to follow. The exported component should be some form of dialog wrapper component that accepts an open
boolean property and an onClose
event handler property, like so:
export type SimpleDialogProps = {
open: boolean;
onClose: () => void;
};
The context menu entry can be configured via the props
configuration entry for the mount point. The title
and icon
properties will set the menu item's text and icon. Any system icon or icon added via a dynamic plugin can be used. Here is an example configuration:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
my-dynamic-plugin-package:
appIcons:
- name: dialogIcon
importName: DialogIcon
mountPoints:
- mountPoint: entity.context.menu
importName: SimpleDialog
config:
props:
title: Open Simple Dialog
icon: dialogIcon
The frontend system enables users to customize global headers by specifying configurations in the app-config.yaml
file. Below is an example configuration:
# app-config.yaml
dynamicPlugins:
frontend:
<package_name>: # e.g., preinstalled plugin `red-hat-developer-hub.backstage-plugin-global-header`
mountPoints:
- mountPoint: application/header # mount point for a global header
importName: <header_component> # e.g., `GlobalHeader` for `red-hat-developer-hub.backstage-plugin-global-header`
config:
position: above-main-content # options: `above-main-content` or `above-sidebar`
Each global header entry requires the following attributes:
mountPoint
: Defines where the header will be added. Useapplication/header
to specify it as a global header.importName
: Specifies the component exported by the global header plugin (e.g.,GlobalHeader
).config.position
: Determines the header's position. Supported values are:above-main-content
: Positions the header above the main content area.above-sidebar
: Positions the header above the sidebar.
Users can configure multiple global headers at different positions by adding entries to the mountPoints
field.
The users can add application listeners using the application/listener
mount point. Below is an example that uses the aforesaid mount point:
# app-config.yaml
dynamicPlugins:
frontend:
<package_name>: # plugin_package_name same as `scalprum.name` key in plugin's `package.json`
mountPoints:
- mountPoint: application/listener
importName: <exported listener component>
Users can configure multiple application listeners by adding entries to the mountPoints
field.
The users can add application providers using the application/provider
mount point. Below is an example that uses the aforesaid mount point to configure a context provider:
# app-config.yaml
dynamicPlugins:
frontend:
<package_name>: # plugin_package_name same as `scalprum.name` key in plugin's `package.json`
dynamicRoutes:
- path: /<route>
importName: Component # Component you want to load on the route
mountPoints:
- mountPoint: application/provider
importName: <exported provider component>
Users can configure multiple application providers by adding entries to the mountPoints
field.
Out of the box the frontend system provides an opinionated set of tabs for catalog entity views. This set of tabs can be further customized and extended as needed via the entityTabs
configuration:
# dynamic-plugins-config.yaml
plugins:
- plugin: <plugin_path_or_url>
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
entityTabs:
# Adding a new tab
- path: /new-path
title: My New Tab
mountPoint: entity.page.my-new-tab
# Change an existing tab's title or mount point
- path: /
title: General
mountPoint: entity.page.overview #this can be customized too
Each entity tab entry requires the following attributes:
path
: Specifies the sub-path route in the catalog where this tab will be availabletitle
: The title that is displayed to the usermountPoint
: The base mount point name that will be available on the tab. This name will be expanded to create two mount points per tab, one appended with/context
and the second appended with/cards
.
Dynamic frontend plugins can then be configured to target the mount points exposed by the entityTabs
configuration.
Here are the default catalog entity routes in the default order:
Route | Title | Mount Point | Entity Kind |
---|---|---|---|
/ |
Overview | entity.page.overview |
Any |
/topology |
Topology | entity.page.topology |
Any |
/issues |
Issues | entity.page.issues |
Any |
/pr |
Pull/Merge Requests | entity.page.pull-requests |
Any |
/ci |
CI | entity.page.ci |
Any |
/cd |
CD | entity.page.cd |
Any |
/kubernetes |
Kubernetes | entity.page.kubernetes |
Any |
/image-registry |
Image Registry | entity.page.image-registry |
Any |
/monitoring |
Monitoring | entity.page.monitoring |
Any |
/lighthouse |
Lighthouse | entity.page.lighthouse |
Any |
/api |
Api | entity.page.api |
kind: Service or kind: Component |
/dependencies |
Dependencies | entity.page.dependencies |
kind: Component |
/docs |
Docs | entity.page.docs |
Any |
/definition |
Definition | entity.page.definition |
kind: API |
/system |
Diagram | entity.page.diagram |
kind: System |
The visibility of a tab is derived from the kind of entity that is being displayed along with the visibility guidance mentioned in "Using mount points".
Backstage offers a Utility API mechanism that provide ways for plugins to communicate during their entire life cycle. Utility APIs are registered as:
- Core APIs, which are always present in any Backstage application
- Custom plugin-made API that can be already self-contained within any plugin (including dynamic plugins)
- App API implementations and overrides which needs to be added separately.
and a plugin can potentially expose multiple API Factories. Dynamic plugins allow a couple different ways to take advantage of this functionality.
If a dynamic plugin exports the plugin object returned by createPlugin
, it will be supplied to the createApp
API and all API factories exported by the plugin will be automatically registered and available in the frontend application. Dynamic plugins that follow this pattern should not use the apiFactories
configuration. Also, if a dynamic plugin only contains API factories and follows this pattern, it will just be necessary to add an entry to the dynamicPlugins.frontend
config for the dynamic plugin package name, for example:
# app-config.yaml
dynamicPlugins:
frontend:
my-dynamic-plugin-package-with-api-factories: {}
However, if the dynamic plugin doesn't export it's plugin object then it will be necessary to explicitly configure each API factory that should be registered with the createApp
API via the apiFactories
configuration:
# app-config.yaml
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
apiFactories:
- importName: BarApi # Optional, explicit import name that reference a AnyApiFactory<{}> implementation. Defaults to default export.
module: CustomModule # Optional, same as key in `scalprum.exposedModules` key in plugin's `package.json`
importName
is an optional import name that reference aAnyApiFactory<{}>
implementation. Defaults todefault
export.module
is an optional argument which allows you to specify which set of assets you want to access within the plugin. If not provided, the default module namedPluginRoot
is used. This is the same as the key inscalprum.exposedModules
key in plugin'spackage.json
.
There are a set of API factories already initialized by the Developer Hub application shell. These API factories can be overridden by an API factory provided by a dynamic plugin by specifying the same API ref ID, for example a dynamic plugin could export the following AnyApiFactory<{}>
to cater for some specific use case:
export const customScmAuthApiFactory = createApiFactory({
api: scmAuthApiRef,
deps: { githubAuthApi: githubAuthApiRef },
factory: ({ githubAuthApi }) =>
ScmAuth.merge(
ScmAuth.forGithub(githubAuthApi, { host: 'github.someinstance.com' }),
ScmAuth.forGithub(githubAuthApi, {
host: 'github.someotherinstance.com',
}),
),
});
And the corresponding YAML configuration would look like:
dynamicPlugins:
frontend:
<package_name>:
apiFactories:
- importName: customScmAuthApiFactory
which would override the default ScmAuth
API factory that Developer Hub defaults to.
The Backstage scaffolder component supports specifying custom form fields for the software template wizard, for example:
export const MyNewFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'MyNewFieldExtension',
component: MyNewField,
validation: myNewFieldValidator,
}),
);
These components can be contributed by plugins by exposing the scaffolder field extension component via the scaffolderFieldExtensions
configuration:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
scaffolderFieldExtensions:
- importName: MyNewFieldExtension
A plugin can specify multiple field extensions, in which case each field extension will need to supply an importName
for each field extension.
importName
is an optional import name that should reference the value returned the scaffolder field extension APImodule
is an optional argument which allows you to specify which set of assets you want to access within the plugin. If not provided, the default module namedPluginRoot
is used. This is the same as the key inscalprum.exposedModules
key in plugin'spackage.json
.
The Backstage TechDocs component supports specifying custom addons to extend TechDocs functionality, like rendering a component or accessing and manipulating TechDocs's DOM.
Here is an example of creating an addon:
export const ExampleAddon = techdocsPlugin.provide(
createTechDocsAddonExtension({
name: "ExampleAddon",
location: TechDocsAddonLocations.Content,
component: ExampleTestAddon,
}),
);
These components can be contributed by plugins by exposing the TechDocs addon component via the techdocsAddons
configuration:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in plugin's `package.json`
techdocsAddons:
- importName: ExampleAddon
config:
props: ... # optional, React props to pass to the addon
A plugin can specify multiple addons, in which case each techdocsAddon will need to supply an importName
for each addon.
importName
name of an exportedAddon
componentmodule
is an optional argument which allows you to specify which set of assets you want to access within the plugin. If not provided, the default module namedPluginRoot
is used. This is the same as the key inscalprum.exposedModules
key in plugin'spackage.json
.
The look and feel of a Backstage application is handled by Backstage theming. Out of the box Developer Hub provides a theme with a number of configuration overrides that allow for user customization. It's also possible to provide additional Backstage themes as well as replace the out of box Developer Hub themes from a dynamic plugin.
A dynamic plugin would export a theme provider function with a signature of ({ children }: { children: ReactNode }): React.JSX.Element
, for example:
import { lightTheme } from './lightTheme';
import { darkTheme } from './darkTheme';
import { UnifiedThemeProvider } from '@backstage/theme';
export const lightThemeProvider = ({ children }: { children: ReactNode }) => (
<UnifiedThemeProvider theme={lightTheme} children={children} />
);
export const darkThemeProvider = ({ children }: { children: ReactNode }) => (
<UnifiedThemeProvider theme={darkTheme} children={children} />
);
And then the theme can be declared via the themes
configuration:
dynamicPlugins:
frontend:
<package_name>: # same as `scalprum.name` key in a plugins `package.json`
themes:
- id: light # Using 'light' overrides the app-provided light theme
title: Light
variant: light
icon: someIconReference
importName: lightThemeProvider
- id: dark # Using 'dark' overrides the app-provided dark theme
title: Dark
variant: dark
icon: someIconReference
importName: darkThemeProvider
The required options mirror the AppTheme interface:
id
A required ID value for the theme; use values oflight
ordark
to replace the default provided themes.title
The theme name displayed to the user on the Settings page.variant
Whether the theme islight
ordark
, can only be one of these values.icon
a string reference to a system or app iconimportName
name of the exported theme provider function, the function signature should match({ children }: { children: ReactNode }): React.JSX.Element