From e0c9ed759d9e6ef12ce8b04a1fe53ae77b4796f0 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Fri, 23 Apr 2021 16:18:07 -0400 Subject: [PATCH] feat(EntityLayout): add 'if' prop to Route component Signed-off-by: Phil Kuang --- .changeset/small-ways-hunt.md | 6 ++ .../core/src/components/TabbedLayout/index.ts | 1 + plugins/catalog/package.json | 1 + .../EntityLayout/EntityLayout.test.tsx | 35 ++++++++++ .../components/EntityLayout/EntityLayout.tsx | 67 +++++++++++++++++-- 5 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 .changeset/small-ways-hunt.md 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;