diff --git a/.changeset/five-ducks-hide.md b/.changeset/five-ducks-hide.md
new file mode 100644
index 0000000000..6ce1fec8f7
--- /dev/null
+++ b/.changeset/five-ducks-hide.md
@@ -0,0 +1,31 @@
+---
+'@backstage/frontend-plugin-api': minor
+---
+
+**BREAKING**: The `NavLogoBlueprint` has been removed and replaced by `NavContentBlueprint`, which instead replaces the entire navbar. The default navbar has also been switched to a more minimal implementation.
+
+To use `NavContentBlueprint` to install new logos, you can use it as follows:
+
+```tsx
+NavContentBlueprint.make({
+ params: {
+ component: ({ items }) => {
+ return compatWrapper(
+
+
+
+ {/* Other sidebar content */}
+
+
+ {items.map((item, index) => (
+
+ ))}
+
+
+ {/* Other sidebar content */}
+ ,
+ );
+ },
+ },
+});
+```
diff --git a/.changeset/full-streets-take.md b/.changeset/full-streets-take.md
new file mode 100644
index 0000000000..f04cd92c57
--- /dev/null
+++ b/.changeset/full-streets-take.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-app': minor
+---
+
+Updated the `app/nav` extension to use the new `NavContentBlueprint`, and removed support for extensions created with the now removed `NavLogoBlueprint`.
diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt
index 430e76a2ce..911429490c 100644
--- a/.github/vale/config/vocabularies/Backstage/accept.txt
+++ b/.github/vale/config/vocabularies/Backstage/accept.txt
@@ -288,6 +288,7 @@ namespaced
namespaces
Namespaces
namespacing
+navbar
neuro
newrelic
nginx
diff --git a/docs/frontend-system/building-apps/03-built-in-extensions.md b/docs/frontend-system/building-apps/03-built-in-extensions.md
index 4cca7cebe2..5d0898679b 100644
--- a/docs/frontend-system/building-apps/03-built-in-extensions.md
+++ b/docs/frontend-system/building-apps/03-built-in-extensions.md
@@ -161,10 +161,10 @@ Extension responsible for rendering the logo and items in the app's sidebar.
#### Inputs
-| Name | Description | Type | Optional | Default | Extension creator |
-| ----- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------- | -------------------------------------------------------------------------------------------------------- |
-| logos | A nav logos object. | [createNavLogoExtension.logoElementsDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createnavlogoextension.logoelementsdataref) | true | - | [createNavLogoExtension](https://backstage.io/docs/reference/frontend-plugin-api.createnavlogoextension) |
-| items | Nav items target objects. | [createNavItemExtension.targetDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension.targetdataref) | true | - | [createNavItemExtension](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) |
+| Name | Description | Type | Optional | Default | Extension creator |
+| ------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------- | -------------------------------------------------------------------------------------------------------- |
+| content | Overrides the default content of the navbar. | [NavContentBlueprint.dataRefs.component](https://backstage.io/docs/reference/frontend-plugin-api.navcontentblueprint) | true | - | [NavContentBlueprint](https://backstage.io/docs/reference/frontend-plugin-api.navcontentblueprint) |
+| items | Nav items target objects. | [createNavItemExtension.targetDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension.targetdataref) | true | - | [createNavItemExtension](https://backstage.io/docs/reference/frontend-plugin-api.createnavitemextension) |
### App routes
diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml
index da233b0188..5665324be6 100644
--- a/packages/app-next/app-config.yaml
+++ b/packages/app-next/app-config.yaml
@@ -26,6 +26,13 @@ app:
# - apis.plugin.graphiql.browse.gitlab: true
# - graphiql-endpoint:graphiql/gitlab: true
+ - nav-item:search: false
+ - nav-item:user-settings: false
+ - nav-item:catalog
+ - nav-item:api-docs
+ - nav-item:scaffolder
+ - nav-item:app-visualizer
+
# Pages
- page:catalog/entity:
config:
diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx
index 43cc63d1a4..1eeedfa9be 100644
--- a/packages/app-next/src/App.tsx
+++ b/packages/app-next/src/App.tsx
@@ -44,6 +44,7 @@ import { convertLegacyPlugin } from '@backstage/core-compat-api';
import { convertLegacyPageExtension } from '@backstage/core-compat-api';
import { convertLegacyEntityContentExtension } from '@backstage/plugin-catalog-react/alpha';
import { pluginInfoResolver } from './pluginInfoResolver';
+import { appModuleNav } from './modules/appModuleNav';
/*
@@ -130,6 +131,7 @@ const app = createApp({
appVisualizerPlugin,
kubernetesPlugin,
notFoundErrorPageModule,
+ appModuleNav,
customHomePageModule,
...collectedLegacyPlugins,
],
diff --git a/packages/app-next/src/modules/appModuleNav.tsx b/packages/app-next/src/modules/appModuleNav.tsx
new file mode 100644
index 0000000000..aa7d92bd1b
--- /dev/null
+++ b/packages/app-next/src/modules/appModuleNav.tsx
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { compatWrapper } from '@backstage/core-compat-api';
+import {
+ Link,
+ Sidebar,
+ sidebarConfig,
+ SidebarDivider,
+ SidebarGroup,
+ SidebarItem,
+ SidebarScrollWrapper,
+ SidebarSpace,
+ useSidebarOpenState,
+} from '@backstage/core-components';
+import SearchIcon from '@material-ui/icons/Search';
+import MenuIcon from '@material-ui/icons/Menu';
+import BuildIcon from '@material-ui/icons/Build';
+import {
+ createFrontendModule,
+ NavContentBlueprint,
+} from '@backstage/frontend-plugin-api';
+import { SidebarSearchModal } from '@backstage/plugin-search';
+import { NotificationsSidebarItem } from '@backstage/plugin-notifications';
+import {
+ Settings,
+ UserSettingsSignInAvatar,
+} from '@backstage/plugin-user-settings';
+import { makeStyles } from '@material-ui/core/styles';
+
+const useSidebarLogoStyles = makeStyles({
+ root: {
+ width: sidebarConfig.drawerWidthClosed,
+ height: 3 * sidebarConfig.logoHeight,
+ display: 'flex',
+ flexFlow: 'row nowrap',
+ alignItems: 'center',
+ marginBottom: -14,
+ },
+ link: {
+ width: sidebarConfig.drawerWidthClosed,
+ marginLeft: 24,
+ },
+});
+
+const SidebarLogo = () => {
+ const classes = useSidebarLogoStyles();
+ const { isOpen } = useSidebarOpenState();
+
+ return (
+
+
+ {isOpen ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
+
+export const appModuleNav = createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ NavContentBlueprint.make({
+ params: {
+ component: ({ items }) => {
+ return compatWrapper(
+
+
+ } to="/search">
+
+
+
+ }>
+
+ {items.map((item, index) => (
+
+ ))}
+
+
+
+
+
+ }
+ to="/settings"
+ >
+
+
+
+
+ ,
+ );
+ },
+ },
+ }),
+ ],
+});
diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md
index 6101f3e7c9..cd8f3d02ce 100644
--- a/packages/frontend-plugin-api/report.api.md
+++ b/packages/frontend-plugin-api/report.api.md
@@ -27,6 +27,7 @@ import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { ConfigApi } from '@backstage/core-plugin-api';
import { configApiRef } from '@backstage/core-plugin-api';
+import { ConfigurableExtensionDataRef as ConfigurableExtensionDataRef_2 } from '@backstage/frontend-plugin-api';
import { createApiFactory } from '@backstage/core-plugin-api';
import { createApiRef } from '@backstage/core-plugin-api';
import { createTranslationMessages } from '@backstage/core-plugin-api/alpha';
@@ -39,6 +40,7 @@ import { ErrorApiError } from '@backstage/core-plugin-api';
import { ErrorApiErrorContext } from '@backstage/core-plugin-api';
import { errorApiRef } from '@backstage/core-plugin-api';
import { Expand } from '@backstage/types';
+import { ExtensionBlueprint as ExtensionBlueprint_2 } from '@backstage/frontend-plugin-api';
import { FeatureFlag } from '@backstage/core-plugin-api';
import { FeatureFlagsApi } from '@backstage/core-plugin-api';
import { featureFlagsApiRef } from '@backstage/core-plugin-api';
@@ -49,7 +51,8 @@ import { fetchApiRef } from '@backstage/core-plugin-api';
import { githubAuthApiRef } from '@backstage/core-plugin-api';
import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
-import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api';
+import { IconComponent as IconComponent_2 } from '@backstage/frontend-plugin-api';
+import { IconComponent as IconComponent_3 } from '@backstage/core-plugin-api';
import { IdentityApi } from '@backstage/core-plugin-api';
import { identityApiRef } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
@@ -70,6 +73,7 @@ import { ProfileInfo } from '@backstage/core-plugin-api';
import { ProfileInfoApi } from '@backstage/core-plugin-api';
import { PropsWithChildren } from 'react';
import { ReactNode } from 'react';
+import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api';
import { SessionApi } from '@backstage/core-plugin-api';
import { SessionState } from '@backstage/core-plugin-api';
import { SignInPageProps } from '@backstage/core-plugin-api';
@@ -1413,19 +1417,59 @@ export { identityApiRef };
export { microsoftAuthApiRef };
+// @public
+export const NavContentBlueprint: ExtensionBlueprint_2<{
+ kind: 'nav-content';
+ name: undefined;
+ params: {
+ component: NavContentComponent;
+ };
+ output: ConfigurableExtensionDataRef_2<
+ NavContentComponent,
+ 'core.nav-content.component',
+ {}
+ >;
+ inputs: {};
+ config: {};
+ configInput: {};
+ dataRefs: {
+ component: ConfigurableExtensionDataRef_2<
+ NavContentComponent,
+ 'core.nav-content.component',
+ {}
+ >;
+ };
+}>;
+
+// @public
+export type NavContentComponent = (
+ props: NavContentComponentProps,
+) => JSX.Element | null;
+
+// @public
+export interface NavContentComponentProps {
+ items: Array<{
+ icon: IconComponent_2;
+ title: string;
+ routeRef: RouteRef_2;
+ to: string;
+ text: string;
+ }>;
+}
+
// @public
export const NavItemBlueprint: ExtensionBlueprint<{
kind: 'nav-item';
name: undefined;
params: {
title: string;
- icon: IconComponent_2;
+ icon: IconComponent_3;
routeRef: RouteRef;
};
output: ConfigurableExtensionDataRef<
{
title: string;
- icon: IconComponent_2;
+ icon: IconComponent_3;
routeRef: RouteRef;
},
'core.nav-item.target',
@@ -1438,7 +1482,7 @@ export const NavItemBlueprint: ExtensionBlueprint<{
target: ConfigurableExtensionDataRef<
{
title: string;
- icon: IconComponent_2;
+ icon: IconComponent_3;
routeRef: RouteRef;
},
'core.nav-item.target',
@@ -1447,37 +1491,6 @@ export const NavItemBlueprint: ExtensionBlueprint<{
};
}>;
-// @public
-export const NavLogoBlueprint: ExtensionBlueprint<{
- kind: 'nav-logo';
- name: undefined;
- params: {
- logoIcon: JSX.Element;
- logoFull: JSX.Element;
- };
- output: ConfigurableExtensionDataRef<
- {
- logoIcon?: JSX.Element;
- logoFull?: JSX.Element;
- },
- 'core.nav-logo.logo-elements',
- {}
- >;
- inputs: {};
- config: {};
- configInput: {};
- dataRefs: {
- logoElements: ConfigurableExtensionDataRef<
- {
- logoIcon?: JSX.Element;
- logoFull?: JSX.Element;
- },
- 'core.nav-logo.logo-elements',
- {}
- >;
- };
-}>;
-
export { OAuthApi };
export { OAuthRequestApi };
diff --git a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.test.tsx
new file mode 100644
index 0000000000..a113a2f21d
--- /dev/null
+++ b/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.test.tsx
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createRouteRef } from '@backstage/frontend-plugin-api';
+import { NavContentBlueprint } from './NavContentBlueprint';
+import { createExtensionTester } from '@backstage/frontend-test-utils';
+
+const routeRef = createRouteRef();
+
+describe('NavContentBlueprint', () => {
+ it('should create an extension with sensible defaults', () => {
+ const extension = NavContentBlueprint.make({
+ params: {
+ component: () => Nav content
,
+ },
+ });
+
+ expect(extension).toMatchInlineSnapshot(`
+ {
+ "$$type": "@backstage/ExtensionDefinition",
+ "T": undefined,
+ "attachTo": {
+ "id": "app/nav",
+ "input": "content",
+ },
+ "configSchema": undefined,
+ "disabled": false,
+ "factory": [Function],
+ "inputs": {},
+ "kind": "nav-content",
+ "name": undefined,
+ "output": [
+ [Function],
+ ],
+ "override": [Function],
+ "toString": [Function],
+ "version": "v2",
+ }
+ `);
+ });
+
+ it('should return a valid component', () => {
+ const extension = NavContentBlueprint.make({
+ name: 'test',
+ params: {
+ component: () => Nav content
,
+ },
+ });
+
+ const tester = createExtensionTester(extension);
+
+ expect(
+ tester.get(NavContentBlueprint.dataRefs.component)({ items: [] }),
+ ).toEqual(Nav content
);
+ });
+
+ it('should return a valid component with items', () => {
+ const extension = NavContentBlueprint.make({
+ name: 'test',
+ params: {
+ component: ({ items }) => (
+
+ ),
+ },
+ });
+
+ const tester = createExtensionTester(extension);
+
+ expect(
+ tester.get(NavContentBlueprint.dataRefs.component)({
+ items: [
+ {
+ to: '/',
+ text: 'Home',
+ title: 'Home',
+ icon: () => null,
+ routeRef,
+ },
+ ],
+ }),
+ ).toEqual(
+ ,
+ );
+ });
+});
diff --git a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts
new file mode 100644
index 0000000000..a6524ea49e
--- /dev/null
+++ b/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { IconComponent, RouteRef } from '@backstage/frontend-plugin-api';
+import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
+
+/**
+ * The props for the {@link NavContentComponent}.
+ *
+ * @public
+ */
+export interface NavContentComponentProps {
+ /**
+ * The nav items available to the component. These are all the items created
+ * with the {@link NavItemBlueprint} in the app.
+ *
+ * In addition to the original properties from the nav items, these also
+ * include a resolved route path as `to`, and duplicated `title` as `text` to
+ * simplify rendering.
+ */
+ items: Array<{
+ // Original props from nav items
+ icon: IconComponent;
+ title: string;
+ routeRef: RouteRef;
+
+ // Additional props to simplify item rendering
+ to: string;
+ text: string;
+ }>;
+}
+
+/**
+ * A component that renders the nav bar content, to be passed to the {@link NavContentBlueprint}.
+ *
+ * @public
+ */
+export type NavContentComponent = (
+ props: NavContentComponentProps,
+) => JSX.Element | null;
+
+const componentDataRef = createExtensionDataRef().with({
+ id: 'core.nav-content.component',
+});
+
+/**
+ * Creates an extension that replaces the entire nav bar with your own component.
+ *
+ * @public
+ */
+export const NavContentBlueprint = createExtensionBlueprint({
+ kind: 'nav-content',
+ attachTo: { id: 'app/nav', input: 'content' },
+ output: [componentDataRef],
+ dataRefs: {
+ component: componentDataRef,
+ },
+ *factory(params: { component: NavContentComponent }) {
+ yield componentDataRef(params.component);
+ },
+});
diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx
deleted file mode 100644
index bbd96ea53b..0000000000
--- a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2024 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { NavLogoBlueprint } from './NavLogoBlueprint';
-import { createExtensionTester } from '@backstage/frontend-test-utils';
-
-describe('NavLogoBlueprint', () => {
- it('should create an extension with sensible defaults', () => {
- const extension = NavLogoBlueprint.make({
- params: {
- logoFull: Logo Full
,
- logoIcon: Logo Icon
,
- },
- });
-
- expect(extension).toMatchInlineSnapshot(`
- {
- "$$type": "@backstage/ExtensionDefinition",
- "T": undefined,
- "attachTo": {
- "id": "app/nav",
- "input": "logos",
- },
- "configSchema": undefined,
- "disabled": false,
- "factory": [Function],
- "inputs": {},
- "kind": "nav-logo",
- "name": undefined,
- "output": [
- [Function],
- ],
- "override": [Function],
- "toString": [Function],
- "version": "v2",
- }
- `);
- });
-
- it('should return a valid component ref', () => {
- const logoFull = Logo Full
;
- const logoIcon = Logo Icon
;
-
- const extension = NavLogoBlueprint.make({
- name: 'test',
- params: {
- logoFull,
- logoIcon,
- },
- });
-
- const tester = createExtensionTester(extension);
-
- expect(tester.get(NavLogoBlueprint.dataRefs.logoElements)).toEqual({
- logoFull,
- logoIcon,
- });
- });
-});
diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts
deleted file mode 100644
index 4a37859975..0000000000
--- a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2024 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
-
-const logoElementsDataRef = createExtensionDataRef<{
- logoIcon?: JSX.Element;
- logoFull?: JSX.Element;
-}>().with({ id: 'core.nav-logo.logo-elements' });
-
-/**
- * Creates an extension that replaces the logo in the nav bar with your own.
- *
- * @public
- */
-export const NavLogoBlueprint = createExtensionBlueprint({
- kind: 'nav-logo',
- attachTo: { id: 'app/nav', input: 'logos' },
- output: [logoElementsDataRef],
- dataRefs: {
- logoElements: logoElementsDataRef,
- },
- *factory({
- logoIcon,
- logoFull,
- }: {
- logoIcon: JSX.Element;
- logoFull: JSX.Element;
- }) {
- yield logoElementsDataRef({
- logoIcon,
- logoFull,
- });
- },
-});
diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts
index 85f7a99a2c..047b007226 100644
--- a/packages/frontend-plugin-api/src/blueprints/index.ts
+++ b/packages/frontend-plugin-api/src/blueprints/index.ts
@@ -18,8 +18,12 @@ export { ApiBlueprint } from './ApiBlueprint';
export { AppRootElementBlueprint } from './AppRootElementBlueprint';
export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint';
export { IconBundleBlueprint } from './IconBundleBlueprint';
+export {
+ NavContentBlueprint,
+ type NavContentComponent,
+ type NavContentComponentProps,
+} from './NavContentBlueprint';
export { NavItemBlueprint } from './NavItemBlueprint';
-export { NavLogoBlueprint } from './NavLogoBlueprint';
export { PageBlueprint } from './PageBlueprint';
export { RouterBlueprint } from './RouterBlueprint';
export { SignInPageBlueprint } from './SignInPageBlueprint';
diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md
index 3654a3965a..027b6365b5 100644
--- a/plugins/app/report.api.md
+++ b/plugins/app/report.api.md
@@ -16,6 +16,7 @@ import { FrontendPlugin } from '@backstage/frontend-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { IconComponent as IconComponent_2 } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
+import { NavContentComponent } from '@backstage/frontend-plugin-api';
import { ReactNode } from 'react';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { SignInPageProps } from '@backstage/core-plugin-api';
@@ -100,13 +101,10 @@ const appPlugin: FrontendPlugin<
optional: false;
}
>;
- logos: ExtensionInput<
+ content: ExtensionInput<
ConfigurableExtensionDataRef<
- {
- logoIcon?: JSX.Element;
- logoFull?: JSX.Element;
- },
- 'core.nav-logo.logo-elements',
+ NavContentComponent,
+ 'core.nav-content.component',
{}
>,
{
diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx
index eaec264dca..aa030300fe 100644
--- a/plugins/app/src/extensions/AppNav.tsx
+++ b/plugins/app/src/extensions/AppNav.tsx
@@ -18,93 +18,93 @@ import {
createExtension,
coreExtensionData,
createExtensionInput,
- useRouteRef,
NavItemBlueprint,
- NavLogoBlueprint,
+ NavContentBlueprint,
+ NavContentComponentProps,
+ routeResolutionApiRef,
+ IconComponent,
+ RouteRef,
+ useApi,
+ NavContentComponent,
} from '@backstage/frontend-plugin-api';
-import { makeStyles } from '@material-ui/core/styles';
-import {
- Sidebar,
- useSidebarOpenState,
- Link,
- sidebarConfig,
- SidebarDivider,
- SidebarItem,
-} from '@backstage/core-components';
-// eslint-disable-next-line @backstage/no-relative-monorepo-imports
-import LogoIcon from '../../../../packages/app/src/components/Root/LogoIcon';
-// eslint-disable-next-line @backstage/no-relative-monorepo-imports
-import LogoFull from '../../../../packages/app/src/components/Root/LogoFull';
-
-const useSidebarLogoStyles = makeStyles({
- root: {
- width: sidebarConfig.drawerWidthClosed,
- height: 3 * sidebarConfig.logoHeight,
- display: 'flex',
- flexFlow: 'row nowrap',
- alignItems: 'center',
- marginBottom: -14,
- },
- link: {
- width: sidebarConfig.drawerWidthClosed,
- marginLeft: 24,
- },
-});
-
-const SidebarLogo = (
- props: (typeof NavLogoBlueprint.dataRefs.logoElements)['T'],
-) => {
- const classes = useSidebarLogoStyles();
- const { isOpen } = useSidebarOpenState();
+import { Sidebar, SidebarItem } from '@backstage/core-components';
+import { useMemo } from 'react';
+function DefaultNavContent(props: NavContentComponentProps) {
return (
-
-
- {isOpen
- ? props?.logoFull ??
- : props?.logoIcon ?? }
-
-
+
+ {props.items.map((item, index) => (
+
+ ))}
+
);
-};
+}
-const SidebarNavItem = (
- props: (typeof NavItemBlueprint.dataRefs.target)['T'],
-) => {
- const { icon: Icon, title, routeRef } = props;
- const link = useRouteRef(routeRef);
- if (!link) {
- return null;
- }
- // TODO: Support opening modal, for example, the search one
- return ;
-};
+// This helps defer rendering until the app is being rendered, which is needed
+// because the RouteResolutionApi can't be called until the app has been fully initialized.
+function NavContentRenderer(props: {
+ Content: NavContentComponent;
+ items: Array<{
+ title: string;
+ icon: IconComponent;
+ routeRef: RouteRef;
+ }>;
+}) {
+ const routeResolutionApi = useApi(routeResolutionApiRef);
+
+ const items = useMemo(() => {
+ return props.items.flatMap(item => {
+ const link = routeResolutionApi.resolve(item.routeRef);
+ if (!link) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `NavItemBlueprint: unable to resolve route ref ${item.routeRef}`,
+ );
+ return [];
+ }
+ return [
+ {
+ to: link(),
+ text: item.title,
+ icon: item.icon,
+ title: item.title,
+ routeRef: item.routeRef,
+ },
+ ];
+ });
+ }, [props.items, routeResolutionApi]);
+
+ return ;
+}
export const AppNav = createExtension({
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
inputs: {
items: createExtensionInput([NavItemBlueprint.dataRefs.target]),
- logos: createExtensionInput([NavLogoBlueprint.dataRefs.logoElements], {
+ content: createExtensionInput([NavContentBlueprint.dataRefs.component], {
singleton: true,
optional: true,
}),
},
output: [coreExtensionData.reactElement],
- factory: ({ inputs }) => [
- coreExtensionData.reactElement(
-
-
-
- {inputs.items.map((item, index) => (
-
- ))}
- ,
- ),
- ],
+ *factory({ inputs }) {
+ const Content =
+ inputs.content?.get(NavContentBlueprint.dataRefs.component) ??
+ DefaultNavContent;
+
+ yield coreExtensionData.reactElement(
+
+ item.get(NavItemBlueprint.dataRefs.target),
+ )}
+ Content={Content}
+ />,
+ );
+ },
});