diff --git a/docs/plugins/index.md b/docs/plugins/index.md
index 6941ff3e18..41ad082d7c 100644
--- a/docs/plugins/index.md
+++ b/docs/plugins/index.md
@@ -27,9 +27,9 @@ This helps the community know what plugins are in development.
You can also use this process if you have an idea for a good plugin but you hope
that someone else will pick up the work.
-## Integrate into the catalog service
+## Integrate into the Service Catalog
If your plugin isn't supposed to live as a standalone page, but rather needs to
-be presented as a part of a catalog service (e.g. a separate tab or a card on an
+be presented as a part of a Service Catalog (e.g. a separate tab or a card on an
"Overview" tab), then check out
[the instruction](integrating-plugin-into-service-catalog.md). on how to do it.
diff --git a/docs/plugins/integrating-plugin-into-service-catalog.md b/docs/plugins/integrating-plugin-into-service-catalog.md
index 7819737c52..4408c27ccc 100644
--- a/docs/plugins/integrating-plugin-into-service-catalog.md
+++ b/docs/plugins/integrating-plugin-into-service-catalog.md
@@ -1,6 +1,6 @@
---
id: integrating-plugin-into-service-catalog
-title: Integrate into the catalog service
+title: Integrate into the Service Catalog
---
> This is an advanced use case and currently is an experimental feature. Expect
@@ -119,7 +119,6 @@ type EntityPageLayoutContentProps = {
};
```
-> The recommended pattern is to get the `entity` in the App and then pass it to
-> the plugin's Router component as a prop. However if it inconvenient for you,
+> You can either pass the entity from App to the plugin's router as a prop or
> use `useEntity` hook from `@backstage/plugin-catalog` directly inside your
> plugin.
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 642eb7b004..cb38726716 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -26,8 +26,7 @@ import * as plugins from './plugins';
import { apis } from './apis';
import { hot } from 'react-hot-loader/root';
import { providers } from './identityProviders';
-import { CatalogRouter } from '@backstage/plugin-catalog';
-// import { ExplorePlugin } from '@backstage/plugin-explore';
+import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Route, Routes, Navigate } from 'react-router';
import { EntityPage } from './components/catalog/EntityPage';
@@ -51,6 +50,7 @@ const app = createApp({
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
+const deprecatedAppRoutes = app.getRoutes();
const AppRoutes = () => (
@@ -58,8 +58,8 @@ const AppRoutes = () => (
path="/catalog/*"
element={}
/>
- } />
- {/* } /> */}
+
+ {...deprecatedAppRoutes}
);
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index b56268036f..913c063f90 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -21,9 +21,14 @@ import {
AboutCard,
} from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
+import { Grid } from '@material-ui/core';
const OverviewPage = ({ entity }: { entity: Entity }) => (
-
+
+
+
+
+
);
const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx
index 27a63ea04a..bfa26b7547 100644
--- a/packages/core-api/src/app/App.tsx
+++ b/packages/core-api/src/app/App.tsx
@@ -136,7 +136,7 @@ export class PrivateAppImpl implements BackstageApp {
return this.icons[key];
}
- getRoutes(): ComponentType<{}> {
+ getRoutes(): JSX.Element[] {
const routes = new Array();
const registeredFeatureFlags = new Array();
@@ -191,14 +191,9 @@ export class PrivateAppImpl implements BackstageApp {
FeatureFlags.registeredFeatureFlags = registeredFeatureFlags;
}
- const rendered = (
-
- {routes}
- } />
-
- );
+ routes.push(} />);
- return () => rendered;
+ return routes;
}
getProvider(): ComponentType<{}> {
diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts
index 6b022c2736..db7a0897b1 100644
--- a/packages/core-api/src/app/types.ts
+++ b/packages/core-api/src/app/types.ts
@@ -168,5 +168,5 @@ export type BackstageApp = {
/**
* Routes component that contains all routes for plugin pages in the app.
*/
- getRoutes(): ComponentType<{}>;
+ getRoutes(): JSX.Element[];
};
diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx
index 410f7592a5..cde39fdd05 100644
--- a/packages/dev-utils/src/devApp/render.tsx
+++ b/packages/dev-utils/src/devApp/render.tsx
@@ -85,7 +85,7 @@ class DevAppBuilder {
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
- const AppRoutes = app.getRoutes();
+ const deprecatedAppRoutes = app.getRoutes();
const sidebar = this.setupSidebar(this.plugins);
@@ -99,7 +99,7 @@ class DevAppBuilder {
{sidebar}
-
+ {deprecatedAppRoutes}
diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts
index f74c3f7166..5dfac68d6a 100644
--- a/packages/e2e-test/src/e2e-test.ts
+++ b/packages/e2e-test/src/e2e-test.ts
@@ -269,7 +269,7 @@ async function createPlugin(pluginName: string, appDir: string) {
/**
* Start serving the newly created app and make sure that the create plugin is rendering correctly
*/
-async function testAppServe(_pluginName: string, appDir: string) {
+async function testAppServe(pluginName: string, appDir: string) {
const startApp = spawnPiped(['yarn', 'start'], {
cwd: appDir,
});
@@ -280,12 +280,11 @@ async function testAppServe(_pluginName: string, appDir: string) {
const browser = new Browser();
await waitForPageWithText(browser, '/', 'Backstage Service Catalog');
- // TODO(shmidt-i): adjust the plugin creation flow with new routing patterns
- // await waitForPageWithText(
- // browser,
- // `/${pluginName}`,
- // `Welcome to ${pluginName}!`,
- // );
+ await waitForPageWithText(
+ browser,
+ `/${pluginName}`,
+ `Welcome to ${pluginName}!`,
+ );
print('Both App and Plugin loaded correctly');
successful = true;
diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx
index 17f9f46830..6a1b80150e 100644
--- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx
+++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React, { useState } from 'react';
+import React, { useState, useContext } from 'react';
import { useParams, useNavigate } from 'react-router';
-import { useEntity } from '../../hooks/useEntity';
+import { EntityContext } from '../../hooks/useEntity';
import {
pageTheme,
PageTheme,
@@ -74,7 +74,7 @@ function headerProps(
export const EntityPageLayout = ({
children,
}: {
- children: React.ReactNode;
+ children?: React.ReactNode;
}) => {
const { optionalNamespaceAndName, kind } = useParams() as {
optionalNamespaceAndName: string;
@@ -82,7 +82,7 @@ export const EntityPageLayout = ({
};
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
- const { entity, loading, error } = useEntity();
+ const { entity, loading, error } = useContext(EntityContext);
const { headerTitle, headerType } = headerProps(
kind,
namespace,
@@ -139,5 +139,4 @@ export const EntityPageLayout = ({
);
};
-
EntityPageLayout.Content = Tabbed.Content;
diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx
index 3eccafbf7a..da1e696fde 100644
--- a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx
+++ b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx
@@ -36,6 +36,21 @@ describe('Tabbed layout', () => {
expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument();
});
+ it('throws if any other component is a child of Tabbed.Layout', async () => {
+ await expect(
+ renderInTestApp(
+
+ tabbed-test-content}
+ />
+
This will cause app to throw
+ ,
+ ),
+ ).rejects.toThrow(/EntityPageLayout component only accepts/);
+ });
+
it('navigates when user clicks different tab', async () => {
const rendered = await renderInTestApp(
diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx
index 693ee10c78..096b2fa708 100644
--- a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx
+++ b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.tsx
@@ -25,7 +25,6 @@ import {
RouteMatch,
} from 'react-router';
import { Tab, HeaderTabs, Content } from '@backstage/core';
-import { Grid } from '@material-ui/core';
import { Helmet } from 'react-helmet';
const getSelectedIndexOrDefault = (
@@ -67,6 +66,11 @@ export const Tabbed = {
// Skip conditionals resolved to falses/nulls/undefineds etc
return;
}
+ if (child.type !== Tabbed.Content) {
+ throw new Error(
+ 'This component only accepts Content elements as direct children. Check the code of the EntityPage.',
+ );
+ }
const pathAndId = (child as JSX.Element).props.path;
// Child here must be then always a functional component without any wrappers
@@ -92,7 +96,7 @@ export const Tabbed = {
matchRoutes(routes as RouteObject[], `/${params['*']}`) ?? [];
const selectedIndex = getSelectedIndexOrDefault(matchedRoute, tabs);
const currentTab = tabs[selectedIndex];
- const title = currentTab.label;
+ const title = currentTab?.label;
const onTabChange = (index: number) =>
// Remove trailing /*
@@ -103,6 +107,7 @@ export const Tabbed = {
const currentRouteElement = useRoutes(routes);
+ if (!currentTab) return null;
return (
<>
-
-
- {currentRouteElement}
-
+
+ {currentRouteElement}
>
);
diff --git a/plugins/catalog/src/Router.tsx b/plugins/catalog/src/components/Router.tsx
similarity index 71%
rename from plugins/catalog/src/Router.tsx
rename to plugins/catalog/src/components/Router.tsx
index 161536c714..61855f56d9 100644
--- a/plugins/catalog/src/Router.tsx
+++ b/plugins/catalog/src/components/Router.tsx
@@ -14,13 +14,14 @@
* limitations under the License.
*/
import React, { ComponentType } from 'react';
-import { CatalogPage } from './components/CatalogPage';
-import { EntityPageLayout } from './components/EntityPageLayout';
+import { CatalogPage } from './CatalogPage';
+import { EntityPageLayout } from './EntityPageLayout';
import { Route, Routes } from 'react-router';
-import { entityRoute, rootRoute, entityRouteDefault } from './routes';
+import { entityRoute, rootRoute } from '../routes';
import { Content } from '@backstage/core';
import { Typography, Link } from '@material-ui/core';
-import { EntityProvider } from './components/EntityProvider';
+import { EntityProvider } from './EntityProvider';
+import { useEntity } from '../hooks/useEntity';
const DefaultEntityPage = () => (
@@ -43,7 +44,17 @@ const DefaultEntityPage = () => (
);
-export const CatalogRouter = ({
+const EntityPageSwitch = ({ EntityPage }: { EntityPage: ComponentType }) => {
+ const { entity } = useEntity();
+ // Loading and error states
+ if (!entity) return ;
+
+ // Otherwise EntityPage provided from the App
+ // Note that EntityPage will include EntityPageLayout already
+ return ;
+};
+
+export const Router = ({
EntityPage = DefaultEntityPage,
}: {
EntityPage?: ComponentType;
@@ -54,15 +65,7 @@ export const CatalogRouter = ({
path={`/${entityRoute.path}`}
element={
-
-
- }
- />
-
-
+
}
/>
diff --git a/plugins/catalog/src/hooks/useEntity.ts b/plugins/catalog/src/hooks/useEntity.ts
index ede45efeef..53b1cff73a 100644
--- a/plugins/catalog/src/hooks/useEntity.ts
+++ b/plugins/catalog/src/hooks/useEntity.ts
@@ -24,13 +24,13 @@ const REDIRECT_DELAY = 2000;
type EntityLoadingStatus = {
entity?: Entity;
- loading: boolean | null;
+ loading: boolean;
error?: Error;
};
export const EntityContext = createContext({
- entity: undefined as any,
- loading: null,
+ entity: undefined,
+ loading: true,
error: undefined,
});
@@ -53,25 +53,20 @@ export const useEntityFromUrl = (): EntityLoadingStatus => {
navigate('/');
}, REDIRECT_DELAY);
}
- }, [errorApi, navigate, error, loading, entity]);
- if (!name) {
- navigate('/catalog');
- return {
- entity: undefined,
- loading: null,
- error: new Error('No name in url'),
- } as never;
- }
+ if (!name) {
+ errorApi.post(new Error('No name provided!'));
+ navigate('/');
+ }
+ }, [errorApi, navigate, error, loading, entity, name]);
return { entity, loading, error };
};
/**
* Always going to return an entity, or throw an error if not a descendant of a EntityProvider.
- * Otherwise the useEntityFromUrl will take care of the `undefined` entity
*/
-export const useEntity = () =>
- useContext & { entity: Entity }>(
- EntityContext as any,
- );
+export const useEntity = () => {
+ const { entity } = useContext<{ entity: Entity }>(EntityContext as any);
+ return { entity };
+};
diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts
index 26285b4814..a6d86e8102 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -19,7 +19,7 @@ export * from './api/CatalogClient';
export * from './api/types';
export * from './routes';
export { useEntityCompoundName } from './components/useEntityCompoundName';
-export * from './Router';
+export { Router } from './components/Router';
export { useEntity } from './hooks/useEntity';
export { AboutCard } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts
index a9606760fb..14e23ff73b 100644
--- a/plugins/catalog/src/routes.ts
+++ b/plugins/catalog/src/routes.ts
@@ -28,8 +28,3 @@ export const entityRoute = createRouteRef({
path: ':kind/:optionalNamespaceAndName/*',
title: 'Entity',
});
-export const entityRouteDefault = createRouteRef({
- icon: NoIcon,
- path: ':kind/:optionalNamespaceAndName/*',
- title: 'Entity',
-});
diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx
index f5a592b7b7..c6ed55c92c 100644
--- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx
+++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx
@@ -26,7 +26,7 @@ import { useJobPolling } from './useJobPolling';
import { Job } from '../../types';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { Button } from '@backstage/core';
-import { entityRouteDefault } from '@backstage/plugin-catalog';
+import { entityRoute } from '@backstage/plugin-catalog';
import { generatePath } from 'react-router-dom';
type Props = {
@@ -72,7 +72,7 @@ export const JobStatusModal = ({
{entity && (