diff --git a/.changeset/afraid-pugs-confess.md b/.changeset/afraid-pugs-confess.md new file mode 100644 index 0000000000..25a3637f89 --- /dev/null +++ b/.changeset/afraid-pugs-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Collect the routes of a `ExploreLayout` with `useElementFilter` diff --git a/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx b/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx index 2e398e3dae..80944628da 100644 --- a/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx +++ b/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx @@ -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 = { + isActive: jest.fn(), + save: jest.fn(), + getRegisteredFlags: jest.fn(), + registerFlag: jest.fn(), +}; + +const mockApis = ApiRegistry.with(featureFlagsApiRef, featureFlagsApi); + describe('', () => { const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - <>{children} + {children} ); - beforeEach(() => { + afterEach(() => { jest.resetAllMocks(); }); @@ -78,4 +96,50 @@ describe('', () => { expect(getByText('Browse the ACME Corp ecosystem')).toBeInTheDocument(), ); }); + + it('renders feature flagged route', async () => { + featureFlagsApi.isActive.mockReturnValue(true); + + const { getByText } = await renderInTestApp( + + + + +
Tools Content
+
+
+ + +
Tools V2 Content
+
+
+
+
, + ); + + await waitFor(() => expect(getByText('Tools')).toBeInTheDocument()); + }); + + it('skips feature flagged route', async () => { + featureFlagsApi.isActive.mockReturnValue(false); + + const { getByText } = await renderInTestApp( + + + + +
Tools Content
+
+
+ + +
Tools V2 Content
+
+
+
+
, + ); + + await waitFor(() => expect(getByText('Tools V2')).toBeInTheDocument()); + }); }); diff --git a/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx b/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx index c28238427f..bc9553c612 100644 --- a/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx +++ b/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx @@ -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; }; +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 = ( - -
- - ).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() + .map(child => child.props), + ); return (