Skip to content

Commit 01fb56c

Browse files
authoredMar 21, 2025
feat(sidebar): sidebar items can be removed by setting enabled:false (#2550)
1 parent 8247da8 commit 01fb56c

File tree

7 files changed

+39
-23
lines changed

7 files changed

+39
-23
lines changed
 

‎docs/dynamic-plugins/frontend-plugin-wiring.md

+23-19
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ plugins:
9494
menuItem: # optional, allows you to populate main sidebar navigation
9595
icon: fooIcon # Backstage system icon
9696
text: Foo Plugin Page # menu item text
97+
enabled: false # optional, allows you to remove the menu item when set to false
9798
config:
9899
props: ... # optional, React props to pass to the component
99100
```
@@ -106,10 +107,11 @@ Each plugin can expose multiple routes and each route is required to define its
106107
- `menuItem` - This property allows users to extend the main sidebar navigation and point to their new route. It accepts the following properties:
107108
- `text`: The label shown to the user
108109
- `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.
110+
- `enabled`: Optional. This property allows user to remove a menuItem from sidebar when it is set to `false`.
109111
- `importName`: optional name of an exported `SidebarItem` component. The component will receive a `to` property as well as any properties specified in `config.props`
110112
- `config` - An optional field which is a holder to pass `props` to a custom sidebar item
111113

112-
A custom `SidebarItem` offers opportunities to provide a richer user experience such as notification badges. The component should accept the following properties:
114+
A custom `SidebarItem` offers opportunities to provide a richer user experience such as notification badges. The component should accept the following properties:
113115

114116
```typescript
115117
export type MySidebarItemProps = {
@@ -159,6 +161,7 @@ plugins:
159161
title: Foo Plugin Page # optional, same as `menuItem.text` in `dynamicRoutes`
160162
priority: 10 # optional, defines the order of menu items in the sidebar
161163
parent: favorites # optional, defines parent-child relationships for nested menu items
164+
enabled: false # optional, allows you to remove the menu item when set to false
162165
```
163166
164167
Up to 3 levels of nested menu items are supported.
@@ -176,6 +179,7 @@ Up to 3 levels of nested menu items are supported.
176179
- `title` - Optional. Specifies the display title of the menu item. This can also be omitted if it has already been defined in the `dynamicRoutes` configuration under `menuItem.text`.
177180
- `priority` - Optional. Defines the order in which menu items appear. The default priority is `0`, which places the item at the bottom of the list. A higher priority value will position the item higher in the sidebar.
178181
- `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 the `menuItems` configuration of any enabled plugin.
182+
- `enabled` - Optional. This property allows user to remove a menuItem from sidebar when it is set to `false`.
179183

180184
```yaml
181185
# dynamic-plugins-config.yaml
@@ -234,9 +238,9 @@ plugins:
234238
importName: barPlugin # Required. Explicit import name that reference a BackstagePlugin<{}> implementation.
235239
module: CustomModule # Optional, same as key in `scalprum.exposedModules` key in plugin's `package.json`
236240
bindings:
237-
- bindTarget: 'barPlugin.externalRoutes' # Required. One of the supported or imported bind targets
241+
- bindTarget: "barPlugin.externalRoutes" # Required. One of the supported or imported bind targets
238242
bindMap: # Required. Map of bindings, same as the `bind` function options argument in the example above
239-
headerLink: 'fooPlugin.routes.root'
243+
headerLink: "fooPlugin.routes.root"
240244
```
241245
242246
This configuration allows you to bind to existing plugins and their routes as well as declare new targets sourced from dynamic plugins:
@@ -341,7 +345,7 @@ Each mount point supports additional configuration:
341345
- `hasAnnotation`: Accepts a string or a list of string with annotation keys. For example `hasAnnotation: my-annotation` will render the component only for entities that have `metadata.annotations['my-annotation']` defined.
342346
- Condition imported from the plugin's `module`: Must be function name exported from the same `module` within the plugin. For example `isMyPluginAvailable` will render the component only if `isMyPluginAvailable` function returns `true`. The function must have the following signature: `(e: Entity) => boolean`
343347

344-
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:
348+
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:
345349

346350
```typescript
347351
export type SimpleDialogProps = {
@@ -350,9 +354,9 @@ export type SimpleDialogProps = {
350354
};
351355
```
352356

353-
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:
357+
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:
354358

355-
```yaml
359+
```yaml
356360
# dynamic-plugins-config.yaml
357361
plugins:
358362
- plugin: <plugin_path_or_url>
@@ -371,7 +375,7 @@ plugins:
371375
props:
372376
title: Open Simple Dialog
373377
icon: dialogIcon
374-
```
378+
```
375379

376380
### Adding application header
377381

@@ -407,7 +411,7 @@ The users can add application listeners using the `application/listener` mount p
407411
# app-config.yaml
408412
dynamicPlugins:
409413
frontend:
410-
<package_name>: # plugin_package_name same as `scalprum.name` key in plugin's `package.json`
414+
<package_name>: # plugin_package_name same as `scalprum.name` key in plugin's `package.json`
411415
mountPoints:
412416
- mountPoint: application/listener
413417
importName: <exported listener component>
@@ -423,7 +427,7 @@ The users can add application providers using the `application/provider` mount p
423427
# app-config.yaml
424428
dynamicPlugins:
425429
frontend:
426-
<package_name>: # plugin_package_name same as `scalprum.name` key in plugin's `package.json`
430+
<package_name>: # plugin_package_name same as `scalprum.name` key in plugin's `package.json`
427431
dynamicRoutes:
428432
- path: /<route>
429433
importName: Component # Component you want to load on the route
@@ -459,13 +463,13 @@ plugins:
459463
# Prioritizing tabs (higher priority appears first)
460464
- path: "/pr"
461465
title: "Changed Pull/Merge Requests"
462-
priority: 1 # Added priority field
466+
priority: 1 # Added priority field
463467
mountPoint: "entity.page.pull-requests"
464468
# Negative priority hides default tabs
465469
- path: "/"
466470
title: "Changed Overview"
467471
mountPoint: "entity.page.overview"
468-
priority: -6
472+
priority: -6
469473
```
470474
471475
Each entity tab entry requires the following attributes:
@@ -507,9 +511,9 @@ Backstage offers a Utility API mechanism that provide ways for plugins to commun
507511
- Custom plugin-made API that can be already self-contained within any plugin (including dynamic plugins)
508512
- [App API implementations and overrides](https://backstage.io/docs/api/utility-apis/#app-apis) which needs to be added separately.
509513

510-
and a plugin can potentially expose multiple API Factories. Dynamic plugins allow a couple different ways to take advantage of this functionality.
514+
and a plugin can potentially expose multiple API Factories. Dynamic plugins allow a couple different ways to take advantage of this functionality.
511515

512-
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:
516+
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:
513517

514518
```yaml
515519
# app-config.yaml
@@ -541,9 +545,9 @@ export const customScmAuthApiFactory = createApiFactory({
541545
deps: { githubAuthApi: githubAuthApiRef },
542546
factory: ({ githubAuthApi }) =>
543547
ScmAuth.merge(
544-
ScmAuth.forGithub(githubAuthApi, { host: 'github.someinstance.com' }),
548+
ScmAuth.forGithub(githubAuthApi, { host: "github.someinstance.com" }),
545549
ScmAuth.forGithub(githubAuthApi, {
546-
host: 'github.someotherinstance.com',
550+
host: "github.someotherinstance.com",
547551
}),
548552
),
549553
});
@@ -563,7 +567,7 @@ which would override the default `ScmAuth` API factory that Developer Hub defaul
563567

564568
## Adding custom authentication provider settings
565569

566-
Out of the box the Backstage user settings page supports all of the documented authentication providers, such as "github" or "microsoft". However it is possible to install new authentication providers from a dynamic plugin that either adds additional configuration support for an existing provider or adds a new authentication provider altogether. In either case, these providers are normally listed in the user settings section of the app under the "Authentication Providers" tab. To add entries for an authentication provider from a dynamic plugin, use the `providerSettings` configuration:
570+
Out of the box the Backstage user settings page supports all of the documented authentication providers, such as "github" or "microsoft". However it is possible to install new authentication providers from a dynamic plugin that either adds additional configuration support for an existing provider or adds a new authentication provider altogether. In either case, these providers are normally listed in the user settings section of the app under the "Authentication Providers" tab. To add entries for an authentication provider from a dynamic plugin, use the `providerSettings` configuration:
567571

568572
```yaml
569573
dynamicPlugins:
@@ -579,11 +583,11 @@ Each provider settings entry should define the following attributes:
579583

580584
- `title`: The title for the authentication provider shown above the user's profile image if available.
581585
- `description`: a short description of the authentication provider.
582-
- `provider`: The ID of the authentication provider as provided to the `createApiRef` API call. This value is used to look up the corresponding API factory for the authentication provider to connect the provider's Sign In/Sign Out button.
586+
- `provider`: The ID of the authentication provider as provided to the `createApiRef` API call. This value is used to look up the corresponding API factory for the authentication provider to connect the provider's Sign In/Sign Out button.
583587

584588
## Use a custom SignInPage component
585589

586-
In a Backstage app the SignInPage component is used to connect one or more authentication providers to the application sign-in process. Out of the box in Developer Hub a static SignInPage is already set up and supports all of the built-in authentication providers already. To use a different authentication provider, for example from a dynamic plugin use the `signInPage` configuration:
590+
In a Backstage app the SignInPage component is used to connect one or more authentication providers to the application sign-in process. Out of the box in Developer Hub a static SignInPage is already set up and supports all of the built-in authentication providers already. To use a different authentication provider, for example from a dynamic plugin use the `signInPage` configuration:
587591

588592
```yaml
589593
dynamicPlugins:
@@ -605,7 +609,7 @@ The Backstage scaffolder component supports specifying [custom form fields](http
605609
```typescript
606610
export const MyNewFieldExtension = scaffolderPlugin.provide(
607611
createScaffolderFieldExtension({
608-
name: 'MyNewFieldExtension',
612+
name: "MyNewFieldExtension",
609613
component: MyNewField,
610614
validation: myNewFieldValidator,
611615
}),

‎packages/app/config.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface Config {
7272
menuItem?: {
7373
icon: string;
7474
text: string;
75+
enabled?: boolean;
7576
};
7677
config: {
7778
props?: {

‎packages/app/src/components/DynamicRoot/DynamicRootContext.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type ResolvedDynamicRouteMenuItem =
2020
| {
2121
text: string;
2222
icon: string;
23+
enabled?: boolean;
2324
}
2425
| {
2526
Component: React.ComponentType<any>;
@@ -35,6 +36,7 @@ export type ResolvedMenuItem = {
3536
children?: ResolvedMenuItem[];
3637
to?: string;
3738
priority?: number;
39+
enabled?: boolean;
3840
};
3941

4042
export type DynamicModuleEntry = Pick<

‎packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ describe('extractDynamicConfig', () => {
264264
name: 'foo',
265265
title: 'Foo',
266266
to: '/foo',
267+
enabled: true,
267268
},
268269
],
269270
},

‎packages/app/src/utils/dynamicUI/extractDynamicConfig.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type DynamicRouteMenuItem =
1717
icon: string;
1818
parent?: string;
1919
priority?: number;
20+
enabled?: boolean;
2021
}
2122
| {
2223
module?: string;
@@ -41,6 +42,7 @@ export type MenuItem = {
4142
priority?: number;
4243
to?: string;
4344
parent?: string;
45+
enabled?: boolean;
4446
};
4547

4648
export type DynamicRoute = {

‎packages/app/src/utils/dynamicUI/extractDynamicConfigFrontend.test.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,14 @@ const createMenuItem = (
103103
icon: string,
104104
parent?: string,
105105
children = [],
106+
enabled?: boolean,
106107
) => ({
107108
name,
108109
title,
109110
icon,
110111
parent,
111112
children,
113+
enabled: enabled,
112114
});
113115

114116
describe('buildTree', () => {
@@ -152,15 +154,16 @@ describe('buildTree', () => {
152154
],
153155
},
154156
{
155-
description: 'should filter out items with no title',
157+
description: 'should filter out items with enabled as false or no title',
156158
input: [
157159
createMenuItem('item1', 'Item 1', 'icon1'),
158160
createMenuItem('item2', '', 'icon2'),
159-
createMenuItem('item3', 'Item 3', 'icon3'),
161+
createMenuItem('item3', 'Item 3', 'icon3', undefined, [], false),
162+
createMenuItem('item4', 'Item 4', 'icon4'),
160163
],
161164
expectedOutput: [
162165
createMenuItem('item1', 'Item 1', 'icon1'),
163-
createMenuItem('item3', 'Item 3', 'icon3'),
166+
createMenuItem('item4', 'Item 4', 'icon4'),
164167
],
165168
},
166169
];

‎packages/app/src/utils/dynamicUI/extractDynamicConfigFrontend.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export function buildTree(menuItemsArray: MenuItem[]): MenuItem[] {
5454
});
5555

5656
const filteredItemMap = Object.fromEntries(
57-
Object.entries(itemMap).filter(([_, item]) => item.title),
57+
Object.entries(itemMap).filter(
58+
([_, item]) => item.enabled !== false && item.title,
59+
),
5860
);
5961

6062
const tree: MenuItem[] = [];
@@ -89,6 +91,7 @@ export function extractMenuItems(frontend: FrontendConfig): MenuItem[] {
8991
title: 'text' in mi && mi.text ? mi.text : '',
9092
to: dr.path ?? '',
9193
children: [],
94+
enabled: 'enabled' in mi ? mi.enabled : true,
9295
});
9396
}
9497
});

0 commit comments

Comments
 (0)