feat(EntityLayout): add 'if' prop to Route component
Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Add `if` prop to `EntityLayout.Route` to conditionally render tabs
|
||||
@@ -14,3 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { TabbedLayout } from './TabbedLayout';
|
||||
export { RoutedTabs } from './RoutedTabs';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
<ApiProvider apis={mockApis}>
|
||||
<EntityContext.Provider value={mockEntityData}>
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="tabbed-test-title">
|
||||
<div>tabbed-test-content</div>
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route
|
||||
path="/some-other-path"
|
||||
title="tabbed-test-title-2"
|
||||
if={shouldNotRenderTab}
|
||||
>
|
||||
<div>tabbed-test-content-2</div>
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route
|
||||
path="/some-other-other-path"
|
||||
title="tabbed-test-title-3"
|
||||
if={shouldRenderTab}
|
||||
>
|
||||
<div>tabbed-test-content-3</div>
|
||||
</EntityLayout.Route>
|
||||
</EntityLayout>
|
||||
</EntityContext.Provider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(rendered.queryByText('tabbed-test-title')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('tabbed-test-title-2')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('tabbed-test-title-3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<React.ElementType, { component?: React.ElementType }>;
|
||||
};
|
||||
|
||||
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 = (
|
||||
<Route path="" title="">
|
||||
<div />
|
||||
</Route>
|
||||
).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 && <Progress />}
|
||||
|
||||
{entity && <TabbedLayout>{children}</TabbedLayout>}
|
||||
{entity && <RoutedTabs routes={routes} />}
|
||||
|
||||
{error && (
|
||||
<Content>
|
||||
@@ -192,4 +249,4 @@ export const EntityLayout = ({
|
||||
);
|
||||
};
|
||||
|
||||
EntityLayout.Route = TabbedLayout.Route;
|
||||
EntityLayout.Route = Route;
|
||||
|
||||
Reference in New Issue
Block a user