Collect the routes of a EntityLayout with useElementFilter
Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user