diff --git a/.changeset/small-ways-hunt.md b/.changeset/small-ways-hunt.md
new file mode 100644
index 0000000000..f0b8dc88cc
--- /dev/null
+++ b/.changeset/small-ways-hunt.md
@@ -0,0 +1,6 @@
+---
+'@backstage/core': patch
+'@backstage/plugin-catalog': patch
+---
+
+Add `if` prop to `EntityLayout.Route` to conditionally render tabs
diff --git a/packages/core/src/components/TabbedLayout/index.ts b/packages/core/src/components/TabbedLayout/index.ts
index 744b56959e..fe72b199ec 100644
--- a/packages/core/src/components/TabbedLayout/index.ts
+++ b/packages/core/src/components/TabbedLayout/index.ts
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { TabbedLayout } from './TabbedLayout';
+export { RoutedTabs } from './RoutedTabs';
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index b6b306c260..d87ca5b7d9 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -33,6 +33,7 @@
"@backstage/catalog-client": "^0.3.10",
"@backstage/catalog-model": "^0.7.7",
"@backstage/core": "^0.7.6",
+ "@backstage/core-api": "^0.2.17",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@backstage/integration-react": "^0.1.1",
diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx
index 7356ccc27b..c5b51b8334 100644
--- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx
+++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx
@@ -101,4 +101,39 @@ describe('EntityLayout', () => {
expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument();
expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument();
});
+
+ it('should conditionally render tabs', async () => {
+ const shouldRenderTab = (e: Entity) => e.metadata.name === 'my-entity';
+ const shouldNotRenderTab = (e: Entity) => e.metadata.name === 'some-entity';
+
+ const rendered = await renderInTestApp(
+
+
+
+
+ tabbed-test-content
+
+
+ tabbed-test-content-2
+
+
+ tabbed-test-content-3
+
+
+
+ ,
+ );
+
+ expect(rendered.queryByText('tabbed-test-title')).toBeInTheDocument();
+ expect(rendered.queryByText('tabbed-test-title-2')).not.toBeInTheDocument();
+ expect(rendered.queryByText('tabbed-test-title-3')).toBeInTheDocument();
+ });
});
diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx
index 6a9fa38e73..e3a8a89b71 100644
--- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx
+++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx
@@ -26,7 +26,7 @@ import {
IconComponent,
Page,
Progress,
- TabbedLayout,
+ RoutedTabs,
} from '@backstage/core';
import {
EntityContext,
@@ -34,14 +34,70 @@ import {
getEntityRelations,
useEntityCompoundName,
} from '@backstage/plugin-catalog-react';
-import { Box } from '@material-ui/core';
+import { attachComponentData } from '@backstage/core-api';
+import { Box, TabProps } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
-import { default as React, useContext, useState } from 'react';
+import {
+ default as React,
+ Children,
+ Fragment,
+ isValidElement,
+ useContext,
+ useState,
+} from 'react';
import { useNavigate } from 'react-router';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
+type SubRoute = {
+ path: string;
+ title: string;
+ children: JSX.Element;
+ if?: (entity: Entity) => boolean;
+ tabProps?: TabProps;
+};
+
+const Route: (props: SubRoute) => null = () => null;
+
+// This causes all mount points that are discovered within this route to use the path of the route itself
+attachComponentData(Route, 'core.gatherMountPoints', true);
+
+function createSubRoutesFromChildren(
+ childrenProps: React.ReactNode,
+ entity: Entity | undefined,
+): SubRoute[] {
+ // Directly comparing child.type with Route will not work with in
+ // combination with react-hot-loader in storybook
+ // https://github.com/gaearon/react-hot-loader/issues/304
+ const routeType = (
+
+
+
+ ).type;
+
+ return Children.toArray(childrenProps).flatMap(child => {
+ if (!isValidElement(child)) {
+ return [];
+ }
+
+ if (child.type === Fragment) {
+ return createSubRoutesFromChildren(child.props.children, entity);
+ }
+
+ if (child.type !== routeType) {
+ throw new Error('Child of EntityLayout must be an EntityLayout.Route');
+ }
+
+ const { path, title, children, if: condition, tabProps } = child.props;
+ if (condition && entity && !condition(entity)) {
+ return [];
+ }
+
+ return [{ path, title, children, tabProps }];
+ });
+}
+
const EntityLayoutTitle = ({
entity,
title,
@@ -139,6 +195,7 @@ export const EntityLayout = ({
const { kind, namespace, name } = useEntityCompoundName();
const { entity, loading, error } = useContext(EntityContext);
+ const routes = createSubRoutesFromChildren(children, entity);
const { headerTitle, headerType } = headerProps(
kind,
namespace,
@@ -175,7 +232,7 @@ export const EntityLayout = ({
{loading && }
- {entity && {children}}
+ {entity && }
{error && (
@@ -192,4 +249,4 @@ export const EntityLayout = ({
);
};
-EntityLayout.Route = TabbedLayout.Route;
+EntityLayout.Route = Route;