Collect the routes of a EntityLayout with useElementFilter

Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
Dominik Henneke
2021-08-23 15:35:57 +02:00
parent 874419a5cf
commit c8e7a7d6e4
3 changed files with 90 additions and 37 deletions
@@ -14,17 +14,35 @@
* limitations under the License.
*/
import {
ApiProvider,
ApiRegistry,
FeatureFlagged,
} from '@backstage/core-app-api';
import {
FeatureFlagsApi,
featureFlagsApiRef,
} from '@backstage/core-plugin-api';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { ExploreLayout } from './ExploreLayout';
const featureFlagsApi: jest.Mocked<FeatureFlagsApi> = {
isActive: jest.fn(),
save: jest.fn(),
getRegisteredFlags: jest.fn(),
registerFlag: jest.fn(),
};
const mockApis = ApiRegistry.with(featureFlagsApiRef, featureFlagsApi);
describe('<ExploreLayout />', () => {
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<>{children}</>
<ApiProvider apis={mockApis}>{children}</ApiProvider>
);
beforeEach(() => {
afterEach(() => {
jest.resetAllMocks();
});
@@ -78,4 +96,50 @@ describe('<ExploreLayout />', () => {
expect(getByText('Browse the ACME Corp ecosystem')).toBeInTheDocument(),
);
});
it('renders feature flagged route', async () => {
featureFlagsApi.isActive.mockReturnValue(true);
const { getByText } = await renderInTestApp(
<Wrapper>
<ExploreLayout subtitle="Browse the ACME Corp ecosystem">
<FeatureFlagged with="test-flag">
<ExploreLayout.Route path="/tools" title="Tools">
<div>Tools Content</div>
</ExploreLayout.Route>
</FeatureFlagged>
<FeatureFlagged without="test-flag">
<ExploreLayout.Route path="/tools-v2" title="Tools V2">
<div>Tools V2 Content</div>
</ExploreLayout.Route>
</FeatureFlagged>
</ExploreLayout>
</Wrapper>,
);
await waitFor(() => expect(getByText('Tools')).toBeInTheDocument());
});
it('skips feature flagged route', async () => {
featureFlagsApi.isActive.mockReturnValue(false);
const { getByText } = await renderInTestApp(
<Wrapper>
<ExploreLayout subtitle="Browse the ACME Corp ecosystem">
<FeatureFlagged with="test-flag">
<ExploreLayout.Route path="/tools" title="Tools">
<div>Tools Content</div>
</ExploreLayout.Route>
</FeatureFlagged>
<FeatureFlagged without="test-flag">
<ExploreLayout.Route path="/tools-v2" title="Tools V2">
<div>Tools V2 Content</div>
</ExploreLayout.Route>
</FeatureFlagged>
</ExploreLayout>
</Wrapper>,
);
await waitFor(() => expect(getByText('Tools V2')).toBeInTheDocument());
});
});
@@ -14,11 +14,13 @@
* limitations under the License.
*/
import { TabProps } from '@material-ui/core';
import { Children, default as React, Fragment, isValidElement } from 'react';
import { attachComponentData } from '@backstage/core-plugin-api';
import { Header, Page, RoutedTabs } from '@backstage/core-components';
import {
attachComponentData,
useElementFilter,
} from '@backstage/core-plugin-api';
import { TabProps } from '@material-ui/core';
import { default as React } from 'react';
// TODO: This layout could be a shared based component if it was possible to create custom TabbedLayouts
// A generalized version of createSubRoutesFromChildren, etc. would be required
@@ -30,41 +32,14 @@ type SubRoute = {
tabProps?: TabProps<React.ElementType, { component?: React.ElementType }>;
};
const dataKey = 'plugin.explore.exploreLayoutRoute';
const Route: (props: SubRoute) => null = () => null;
attachComponentData(Route, dataKey, true);
// 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,
): 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);
}
if (child.type !== routeType) {
throw new Error('Child of ExploreLayout must be an ExploreLayout.Route');
}
const { path, title, children, tabProps } = child.props;
return [{ path, title, children, tabProps }];
});
}
type ExploreLayoutProps = {
title?: string;
subtitle?: string;
@@ -88,7 +63,16 @@ export const ExploreLayout = ({
subtitle,
children,
}: ExploreLayoutProps) => {
const routes = createSubRoutesFromChildren(children);
const routes = useElementFilter(children, elements =>
elements
.selectByComponentData({
key: dataKey,
withStrictError:
'Child of ExploreLayout must be an ExploreLayout.Route',
})
.getElements<SubRoute>()
.map(child => child.props),
);
return (
<Page themeId="home">