+
+
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByText('Browse the ACME Corp ecosystem')).toBeInTheDocument(),
+ );
+ });
+});
diff --git a/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx b/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx
new file mode 100644
index 0000000000..7e64ed815c
--- /dev/null
+++ b/plugins/explore/src/components/ExploreLayout/ExploreLayout.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { attachComponentData, Header, Page, RoutedTabs } from '@backstage/core';
+import { TabProps } from '@material-ui/core';
+import { Children, default as React, Fragment, isValidElement } 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
+
+type SubRoute = {
+ path: string;
+ title: string;
+ children: JSX.Element;
+ 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,
+): 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;
+ children?: React.ReactNode;
+};
+
+/**
+ * Explore is a compound component, which allows you to define a custom layout
+ *
+ * @example
+ * ```jsx
+ *
+ *
+ *
This is rendered under /example/anything-here route
+ *
+ *
+ * ```
+ */
+export const ExploreLayout = ({
+ title,
+ subtitle,
+ children,
+}: ExploreLayoutProps) => {
+ const routes = createSubRoutesFromChildren(children);
+
+ return (
+
+
+
+
+ );
+};
+
+ExploreLayout.Route = Route;
diff --git a/plugins/explore/src/components/ExploreLayout/index.ts b/plugins/explore/src/components/ExploreLayout/index.ts
new file mode 100644
index 0000000000..6cbae79a71
--- /dev/null
+++ b/plugins/explore/src/components/ExploreLayout/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { ExploreLayout } from './ExploreLayout';
diff --git a/plugins/explore/src/components/ExplorePage/ExplorePage.test.tsx b/plugins/explore/src/components/ExplorePage/ExplorePage.test.tsx
new file mode 100644
index 0000000000..9034ee72c0
--- /dev/null
+++ b/plugins/explore/src/components/ExplorePage/ExplorePage.test.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { renderInTestApp } from '@backstage/test-utils';
+import React from 'react';
+import { useOutlet } from 'react-router';
+import { ExplorePage } from './ExplorePage';
+
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ useLocation: jest.fn().mockReturnValue({
+ search: '',
+ }),
+ useOutlet: jest.fn().mockReturnValue('Route Children'),
+}));
+
+jest.mock('../DefaultExplorePage', () => ({
+ ...jest.requireActual('../DefaultExplorePage'),
+ DefaultExplorePage: jest.fn().mockReturnValue('DefaultExplorePageMock'),
+}));
+
+describe('ExplorePage', () => {
+ it('renders provided router element', async () => {
+ const { getByText } = await renderInTestApp();
+
+ expect(getByText('Route Children')).toBeInTheDocument();
+ });
+
+ it('renders default explorer page when no router children are provided', async () => {
+ (useOutlet as jest.Mock).mockReturnValueOnce(null);
+ const { getByText } = await renderInTestApp();
+
+ expect(getByText('DefaultExplorePageMock')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/explore/src/components/ExplorePage/ExplorePage.tsx b/plugins/explore/src/components/ExplorePage/ExplorePage.tsx
index 3f9394a1ab..e3601614d2 100644
--- a/plugins/explore/src/components/ExplorePage/ExplorePage.tsx
+++ b/plugins/explore/src/components/ExplorePage/ExplorePage.tsx
@@ -13,22 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { configApiRef, Header, Page, useApi } from '@backstage/core';
+
import React from 'react';
-import { ExploreTabs } from './ExploreTabs';
+import { useOutlet } from 'react-router';
+import { DefaultExplorePage } from '../DefaultExplorePage';
export const ExplorePage = () => {
- const configApi = useApi(configApiRef);
- const organizationName =
- configApi.getOptionalString('organization.name') ?? 'Backstage';
- return (
-
-
+ const outlet = useOutlet();
-
-
- );
+ return <>{outlet || }>;
};
diff --git a/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx b/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx
deleted file mode 100644
index 0415dd97ae..0000000000
--- a/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2021 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { TabbedLayout } from '@backstage/core';
-import React from 'react';
-import { DomainExplorerContent } from '../DomainExplorerContent';
-import { GroupsExplorerContent } from '../GroupsExplorerContent';
-import { ToolExplorerContent } from '../ToolExplorerContent';
-
-export const ExploreTabs = () => (
-
-
-
-
-
-
-
-
-
-
-
-);
diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx
index 38c4678d5a..4e92784818 100644
--- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx
+++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx
@@ -40,6 +40,12 @@ describe('', () => {
);
+ const mountedRoutes = {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ };
+
beforeEach(() => {
jest.resetAllMocks();
@@ -69,11 +75,7 @@ describe('', () => {
,
- {
- mountedRoutes: {
- '/catalog/:namespace/:kind/:name': entityRouteRef,
- },
- },
+ mountedRoutes,
);
await waitFor(() => {
@@ -81,6 +83,19 @@ describe('', () => {
});
});
+ it('renders a custom title', async () => {
+ catalogApi.getEntities.mockResolvedValue({ items: [] });
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ mountedRoutes,
+ );
+
+ await waitFor(() => expect(getByText('Our Teams')).toBeInTheDocument());
+ });
+
it('renders a friendly error if it cannot collect domains', async () => {
const catalogError = new Error('Network timeout');
catalogApi.getEntities.mockRejectedValueOnce(catalogError);
@@ -89,11 +104,7 @@ describe('', () => {
,
- {
- mountedRoutes: {
- '/catalog/:namespace/:kind/:name': entityRouteRef,
- },
- },
+ mountedRoutes,
);
await waitFor(() =>
diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx
index c4d46206e4..bf2363f16c 100644
--- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx
+++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.tsx
@@ -13,14 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { Content, ContentHeader, SupportButton } from '@backstage/core';
import React from 'react';
import { GroupsDiagram } from './GroupsDiagram';
-export const GroupsExplorerContent = () => {
+type GroupsExplorerContentProps = {
+ title?: string;
+};
+
+export const GroupsExplorerContent = ({
+ title,
+}: GroupsExplorerContentProps) => {
return (
-
+ Explore your groups.
diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx
index 125a72eb12..5cc444ace5 100644
--- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx
+++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx
@@ -80,6 +80,18 @@ describe('', () => {
});
});
+ it('renders a custom title', async () => {
+ exploreToolsConfigApi.getTools.mockResolvedValue([]);
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ );
+
+ await waitFor(() => expect(getByText('Our Tools')).toBeInTheDocument());
+ });
+
it('renders empty state', async () => {
exploreToolsConfigApi.getTools.mockResolvedValue([]);
diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx
index e00606eeb2..8dcf1957d0 100644
--- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx
+++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import {
Content,
ContentHeader,
@@ -61,9 +62,13 @@ const Body = () => {
);
};
-export const ToolExplorerContent = () => (
+type ToolExplorerContentProps = {
+ title?: string;
+};
+
+export const ToolExplorerContent = ({ title }: ToolExplorerContentProps) => (
-
+ Discover the tools in your ecosystem.
diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts
new file mode 100644
index 0000000000..6cbae79a71
--- /dev/null
+++ b/plugins/explore/src/components/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { ExploreLayout } from './ExploreLayout';
diff --git a/plugins/explore/src/extensions.tsx b/plugins/explore/src/extensions.tsx
index cdf43d3035..88ea2561fa 100644
--- a/plugins/explore/src/extensions.tsx
+++ b/plugins/explore/src/extensions.tsx
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-import { createRoutableExtension } from '@backstage/core';
+import {
+ createComponentExtension,
+ createRoutableExtension,
+} from '@backstage/core';
import { explorePlugin } from './plugin';
import { exploreRouteRef } from './routes';
@@ -25,3 +28,36 @@ export const ExplorePage = explorePlugin.provide(
mountPoint: exploreRouteRef,
}),
);
+
+export const DomainExplorerContent = explorePlugin.provide(
+ createComponentExtension({
+ component: {
+ lazy: () =>
+ import('./components/DomainExplorerContent').then(
+ m => m.DomainExplorerContent,
+ ),
+ },
+ }),
+);
+
+export const GroupsExplorerContent = explorePlugin.provide(
+ createComponentExtension({
+ component: {
+ lazy: () =>
+ import('./components/GroupsExplorerContent').then(
+ m => m.GroupsExplorerContent,
+ ),
+ },
+ }),
+);
+
+export const ToolExplorerContent = explorePlugin.provide(
+ createComponentExtension({
+ component: {
+ lazy: () =>
+ import('./components/ToolExplorerContent').then(
+ m => m.ToolExplorerContent,
+ ),
+ },
+ }),
+);
diff --git a/plugins/explore/src/index.ts b/plugins/explore/src/index.ts
index 70a00f5bbb..bee46a31ec 100644
--- a/plugins/explore/src/index.ts
+++ b/plugins/explore/src/index.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+export { ExploreLayout } from './components';
export * from './extensions';
-export { explorePlugin } from './plugin';
+export { explorePlugin, explorePlugin as plugin } from './plugin';
export * from './routes';