diff --git a/.changeset/catalog-compose-my-techdocs.md b/.changeset/catalog-compose-my-techdocs.md
new file mode 100644
index 0000000000..6340c87a89
--- /dev/null
+++ b/.changeset/catalog-compose-my-techdocs.md
@@ -0,0 +1,9 @@
+---
+'@backstage/plugin-catalog': patch
+---
+
+The entity `` now uses an external route ref to link to TechDocs
+sites. This external route must now be bound in order for the "View TechDocs"
+link to continue working. See the [create-app changelog][cacl] for details.
+
+[cacl]: https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md
diff --git a/.changeset/create-incerteza-desafinado-tomara.md b/.changeset/create-incerteza-desafinado-tomara.md
new file mode 100644
index 0000000000..87e82d6b7a
--- /dev/null
+++ b/.changeset/create-incerteza-desafinado-tomara.md
@@ -0,0 +1,28 @@
+---
+'@backstage/create-app': patch
+---
+
+Wire up TechDocs, which now relies on the composability API for routing.
+
+First, ensure you've mounted ``. If you already updated
+to use the composable `` (see below), no action is
+necessary. Otherwise, update your `App.tsx` so that `` is
+mounted:
+
+```diff
+ } />
++ }
++ />
+```
+
+Next, ensure links from the Catalog Entity Page to its TechDocs site are bound:
+
+```diff
+ bindRoutes({ bind }) {
+ bind(catalogPlugin.externalRoutes, {
+ createComponent: scaffolderPlugin.routes.root,
++ viewTechDoc: techdocsPlugin.routes.docRoot,
+ });
+```
diff --git a/.changeset/techdocs-jobim-gilberto-moraes.md b/.changeset/techdocs-jobim-gilberto-moraes.md
new file mode 100644
index 0000000000..e5d23dd16a
--- /dev/null
+++ b/.changeset/techdocs-jobim-gilberto-moraes.md
@@ -0,0 +1,9 @@
+---
+'@backstage/plugin-techdocs': minor
+---
+
+The TechDocs plugin has completed the migration to the Composability API. In
+order to update to this version, please ensure you've made all necessary
+changes to your `App.tsx` file as outlined in the [create-app changelog][cacl].
+
+[cacl]: https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 1132368a91..df4013202e 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -55,6 +55,7 @@ import { TechRadarPage } from '@backstage/plugin-tech-radar';
import {
DefaultTechDocsHome,
TechDocsIndexPage,
+ techdocsPlugin,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
@@ -93,6 +94,7 @@ const app = createApp({
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
+ viewTechDoc: techdocsPlugin.routes.docRoot,
});
bind(apiDocsPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx
index 57491c9222..288351ccdf 100644
--- a/packages/create-app/templates/default-app/packages/app/src/App.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx
@@ -16,6 +16,7 @@ import { TechRadarPage } from '@backstage/plugin-tech-radar';
import {
DefaultTechDocsHome,
TechDocsIndexPage,
+ techdocsPlugin,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
@@ -31,6 +32,7 @@ const app = createApp({
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
+ viewTechDoc: techdocsPlugin.routes.docRoot,
});
bind(apiDocsPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md
index 63ff79ea0f..ec2aeafd00 100644
--- a/plugins/catalog/api-report.md
+++ b/plugins/catalog/api-report.md
@@ -128,6 +128,14 @@ const catalogPlugin: BackstagePlugin<
},
{
createComponent: ExternalRouteRef;
+ viewTechDoc: ExternalRouteRef<
+ {
+ name: string;
+ kind: string;
+ namespace: string;
+ },
+ true
+ >;
}
>;
export { catalogPlugin };
@@ -399,7 +407,7 @@ export const Router: ({
// src/components/EntityLayout/EntityLayout.d.ts:43:5 - (ae-forgotten-export) The symbol "EntityLayoutProps" needs to be exported by the entry point index.d.ts
// src/components/EntityLayout/EntityLayout.d.ts:44:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts
// src/components/EntityPageLayout/EntityPageLayout.d.ts:17:5 - (ae-forgotten-export) The symbol "EntityPageLayoutProps" needs to be exported by the entry point index.d.ts
-// src/plugin.d.ts:17:5 - (ae-forgotten-export) The symbol "ColumnBreakpoints" needs to be exported by the entry point index.d.ts
+// src/plugin.d.ts:22:5 - (ae-forgotten-export) The symbol "ColumnBreakpoints" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
```
diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
index 70b9781ec8..539cce7da3 100644
--- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
+++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
@@ -29,6 +29,7 @@ import {
ApiRegistry,
ConfigReader,
} from '@backstage/core-app-api';
+import { viewTechDocRouteRef } from '../../routes';
describe('', () => {
it('renders info', async () => {
@@ -203,4 +204,138 @@ describe('', () => {
);
expect(getByText('View Source').closest('a')).not.toHaveAttribute('href');
});
+
+ it('renders techdocs link', async () => {
+ const entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ annotations: {
+ 'backstage.io/techdocs-ref': './',
+ },
+ },
+ spec: {
+ owner: 'guest',
+ type: 'service',
+ lifecycle: 'production',
+ },
+ };
+ const apis = ApiRegistry.with(
+ scmIntegrationsApiRef,
+ ScmIntegrationsApi.fromConfig(
+ new ConfigReader({
+ integrations: {
+ github: [
+ {
+ host: 'github.com',
+ token: '...',
+ },
+ ],
+ },
+ }),
+ ),
+ );
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/docs/:namespace/:kind/:name': viewTechDocRouteRef,
+ },
+ },
+ );
+
+ expect(getByText('View TechDocs').closest('a')).toHaveAttribute(
+ 'href',
+ '/docs/default/Component/software',
+ );
+ });
+
+ it('renders disabled techdocs link when no docs exist', async () => {
+ const entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ },
+ spec: {
+ owner: 'guest',
+ type: 'service',
+ lifecycle: 'production',
+ },
+ };
+ const apis = ApiRegistry.with(
+ scmIntegrationsApiRef,
+ ScmIntegrationsApi.fromConfig(
+ new ConfigReader({
+ integrations: {
+ github: [
+ {
+ host: 'github.com',
+ token: '...',
+ },
+ ],
+ },
+ }),
+ ),
+ );
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(getByText('View TechDocs').closest('a')).not.toHaveAttribute('href');
+ });
+
+ it('renders disbaled techdocs link when route is not bound', async () => {
+ const entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ annotations: {
+ 'backstage.io/techdocs-ref': './',
+ },
+ },
+ spec: {
+ owner: 'guest',
+ type: 'service',
+ lifecycle: 'production',
+ },
+ };
+ const apis = ApiRegistry.with(
+ scmIntegrationsApiRef,
+ ScmIntegrationsApi.fromConfig(
+ new ConfigReader({
+ integrations: {
+ github: [
+ {
+ host: 'github.com',
+ token: '...',
+ },
+ ],
+ },
+ }),
+ ),
+ );
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(getByText('View TechDocs').closest('a')).not.toHaveAttribute('href');
+ });
});
diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx
index d6227f9344..113ebe47bd 100644
--- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx
+++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx
@@ -49,7 +49,8 @@ import {
IconLinkVerticalProps,
InfoCardVariants,
} from '@backstage/core-components';
-import { useApi } from '@backstage/core-plugin-api';
+import { useApi, useRouteRef } from '@backstage/core-plugin-api';
+import { viewTechDocRouteRef } from '../../routes';
const useStyles = makeStyles({
gridItemCard: {
@@ -81,6 +82,8 @@ export function AboutCard({ variant }: AboutCardProps) {
const classes = useStyles();
const { entity } = useEntity();
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
+ const viewTechdocLink = useRouteRef(viewTechDocRouteRef);
+
const entitySourceLocation = getEntitySourceLocation(
entity,
scmIntegrationsApi,
@@ -105,11 +108,17 @@ export function AboutCard({ variant }: AboutCardProps) {
};
const viewInTechDocs: IconLinkVerticalProps = {
label: 'View TechDocs',
- disabled: !entity.metadata.annotations?.['backstage.io/techdocs-ref'],
+ disabled:
+ !entity.metadata.annotations?.['backstage.io/techdocs-ref'] ||
+ !viewTechdocLink,
icon: ,
- href: `/docs/${entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE}/${
- entity.kind
- }/${entity.metadata.name}`,
+ href:
+ viewTechdocLink &&
+ viewTechdocLink({
+ namespace: entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE,
+ kind: entity.kind,
+ name: entity.metadata.name,
+ }),
};
const viewApi: IconLinkVerticalProps = {
title: hasApis ? '' : 'No APIs available',
diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts
index c375e76239..c83716b32d 100644
--- a/plugins/catalog/src/plugin.ts
+++ b/plugins/catalog/src/plugin.ts
@@ -21,7 +21,7 @@ import {
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { CatalogClientWrapper } from './CatalogClientWrapper';
-import { createComponentRouteRef } from './routes';
+import { createComponentRouteRef, viewTechDocRouteRef } from './routes';
import {
createApiFactory,
createComponentExtension,
@@ -50,6 +50,7 @@ export const catalogPlugin = createPlugin({
},
externalRoutes: {
createComponent: createComponentRouteRef,
+ viewTechDoc: viewTechDocRouteRef,
},
});
diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts
index d1ee3e520a..63d9ccfb87 100644
--- a/plugins/catalog/src/routes.ts
+++ b/plugins/catalog/src/routes.ts
@@ -20,3 +20,9 @@ export const createComponentRouteRef = createExternalRouteRef({
id: 'create-component',
optional: true,
});
+
+export const viewTechDocRouteRef = createExternalRouteRef({
+ id: 'view-techdoc',
+ optional: true,
+ params: ['namespace', 'kind', 'name'],
+});
diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md
index af12b42ad2..1533f9de49 100644
--- a/plugins/techdocs/api-report.md
+++ b/plugins/techdocs/api-report.md
@@ -271,6 +271,11 @@ export const TechDocsPicker: () => null;
const techdocsPlugin: BackstagePlugin<
{
root: RouteRef;
+ docRoot: RouteRef<{
+ name: string;
+ kind: string;
+ namespace: string;
+ }>;
entityContent: RouteRef;
},
{}
@@ -374,7 +379,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
//
// src/home/components/EntityListDocsTable.d.ts:11:5 - (ae-forgotten-export) The symbol "columnFactories" needs to be exported by the entry point index.d.ts
// src/home/components/EntityListDocsTable.d.ts:12:5 - (ae-forgotten-export) The symbol "actionFactories" needs to be exported by the entry point index.d.ts
-// src/plugin.d.ts:24:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts
+// src/plugin.d.ts:29:5 - (ae-forgotten-export) The symbol "TabsConfig" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
```
diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx
index 3fb109e790..45b0dc756c 100644
--- a/plugins/techdocs/src/Router.tsx
+++ b/plugins/techdocs/src/Router.tsx
@@ -18,11 +18,6 @@ import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { Route, Routes } from 'react-router-dom';
-import {
- rootRouteRef,
- rootDocsRouteRef,
- rootCatalogDocsRouteRef,
-} from './routes';
import { TechDocsIndexPage } from './home/components/TechDocsIndexPage';
import { TechDocsPage as TechDocsReaderPage } from './reader/components/TechDocsPage';
import { EntityPageDocs } from './EntityPageDocs';
@@ -33,9 +28,9 @@ const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
export const Router = () => {
return (
- } />
+ } />
}
/>
@@ -58,10 +53,7 @@ export const EmbeddedDocsRouter = (_props: Props) => {
return (
- }
- />
+ } />
);
};
diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx
index 0b1d6e8db2..48ea67585b 100644
--- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx
+++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx
@@ -30,6 +30,7 @@ import {
configApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
+import { rootDocsRouteRef } from '../../routes';
jest.mock('@backstage/plugin-catalog-react', () => {
const actual = jest.requireActual('@backstage/plugin-catalog-react');
@@ -73,6 +74,11 @@ describe('TechDocs Home', () => {
,
+ {
+ mountedRoutes: {
+ '/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
+ },
+ },
);
// Header
diff --git a/plugins/techdocs/src/home/components/DocsCardGrid.test.tsx b/plugins/techdocs/src/home/components/DocsCardGrid.test.tsx
index 4adada48fa..8b0e31326c 100644
--- a/plugins/techdocs/src/home/components/DocsCardGrid.test.tsx
+++ b/plugins/techdocs/src/home/components/DocsCardGrid.test.tsx
@@ -19,6 +19,7 @@ import { render } from '@testing-library/react';
import React from 'react';
import { configApiRef } from '@backstage/core-plugin-api';
import { DocsCardGrid } from './DocsCardGrid';
+import { rootDocsRouteRef } from '../../routes';
// Hacky way to mock a specific boolean config value.
const getOptionalBooleanMock = jest.fn().mockReturnValue(false);
@@ -71,16 +72,21 @@ describe('Entity Docs Card Grid', () => {
},
]}
/>,
+ {
+ mountedRoutes: {
+ '/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
+ },
+ },
),
);
expect(await findByText('testName')).toBeInTheDocument();
expect(await findByText('testName2')).toBeInTheDocument();
const [button1, button2] = await findAllByRole('button');
expect(button1.getAttribute('href')).toContain(
- '/default/testkind/testname',
+ '/docs/default/testkind/testname',
);
expect(button2.getAttribute('href')).toContain(
- '/default/testkind2/testname2',
+ '/docs/default/testkind2/testname2',
);
});
@@ -104,6 +110,11 @@ describe('Entity Docs Card Grid', () => {
},
]}
/>,
+ {
+ mountedRoutes: {
+ '/techdocs/:namespace/:kind/:name/*': rootDocsRouteRef,
+ },
+ },
),
);
@@ -112,7 +123,7 @@ describe('Entity Docs Card Grid', () => {
'techdocs.legacyUseCaseSensitiveTripletPaths',
);
expect(button.getAttribute('href')).toContain(
- '/SomeNamespace/TestKind/testName',
+ '/techdocs/SomeNamespace/TestKind/testName',
);
});
});
diff --git a/plugins/techdocs/src/home/components/DocsCardGrid.tsx b/plugins/techdocs/src/home/components/DocsCardGrid.tsx
index 8b7e114b09..c8b856032f 100644
--- a/plugins/techdocs/src/home/components/DocsCardGrid.tsx
+++ b/plugins/techdocs/src/home/components/DocsCardGrid.tsx
@@ -15,10 +15,9 @@
*/
import React from 'react';
-import { generatePath } from 'react-router-dom';
import { Entity } from '@backstage/catalog-model';
-import { useApi, configApiRef } from '@backstage/core-plugin-api';
+import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core';
import { rootDocsRouteRef } from '../../routes';
@@ -33,6 +32,8 @@ export const DocsCardGrid = ({
}: {
entities: Entity[] | undefined;
}) => {
+ const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef);
+
// Lower-case entity triplets by default, but allow override.
const toLowerMaybe = useApi(configApiRef).getOptionalBoolean(
'techdocs.legacyUseCaseSensitiveTripletPaths',
@@ -53,7 +54,7 @@ export const DocsCardGrid = ({
{entity.metadata.description}