From 787bc082622e9f2ff379e98bb8a3263299d0a5cc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 12 Aug 2021 14:25:40 +0200 Subject: [PATCH] Finish TechDocs migration to composability API. Signed-off-by: Eric Peterson --- .changeset/catalog-compose-my-techdocs.md | 9 ++ .../create-incerteza-desafinado-tomara.md | 28 ++++ .changeset/techdocs-jobim-gilberto-moraes.md | 9 ++ packages/app/src/App.tsx | 2 + .../default-app/packages/app/src/App.tsx | 2 + plugins/catalog/api-report.md | 10 +- .../components/AboutCard/AboutCard.test.tsx | 135 ++++++++++++++++++ .../src/components/AboutCard/AboutCard.tsx | 19 ++- plugins/catalog/src/plugin.ts | 3 +- plugins/catalog/src/routes.ts | 6 + plugins/techdocs/api-report.md | 7 +- plugins/techdocs/src/Router.tsx | 14 +- .../components/DefaultTechDocsHome.test.tsx | 6 + .../src/home/components/DocsCardGrid.test.tsx | 17 ++- .../src/home/components/DocsCardGrid.tsx | 7 +- .../src/home/components/DocsTable.test.tsx | 27 +++- .../src/home/components/DocsTable.tsx | 7 +- .../components/LegacyTechDocsHome.test.tsx | 6 + .../components/TechDocsCustomHome.test.tsx | 6 + plugins/techdocs/src/plugin.ts | 1 + plugins/techdocs/src/routes.ts | 7 +- 21 files changed, 293 insertions(+), 35 deletions(-) create mode 100644 .changeset/catalog-compose-my-techdocs.md create mode 100644 .changeset/create-incerteza-desafinado-tomara.md create mode 100644 .changeset/techdocs-jobim-gilberto-moraes.md 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}