diff --git a/.changeset/fair-grapes-joke.md b/.changeset/fair-grapes-joke.md new file mode 100644 index 0000000000..8ab6516833 --- /dev/null +++ b/.changeset/fair-grapes-joke.md @@ -0,0 +1,46 @@ +--- +'@backstage/create-app': patch +--- + +Register `TechDocs` addons on catalog entity pages, follow the steps below to add them manually: + +```diff +// packages/app/src/components/catalog/EntityPage.tsx + ++ import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; ++ import { ++ ReportIssue, ++ } from '@backstage/plugin-techdocs-module-addons-contrib'; + ++ const techdocsContent = ( ++ ++ ++ ++ ++ ++ ); + +const defaultEntityPage = ( + ... + ++ {techdocsContent} + + ... +); + +const serviceEntityPage = ( + ... + ++ {techdocsContent} + + ... +); + +const websiteEntityPage = ( + ... + ++ {techdocsContent} + + ... +); +``` diff --git a/.changeset/techdocs-buttons-film.md b/.changeset/techdocs-buttons-film.md new file mode 100644 index 0000000000..1af1ead4c7 --- /dev/null +++ b/.changeset/techdocs-buttons-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fix `EntityTechdocsContent` component to use objects instead of `` elements, otherwise "outlet" will be null on sub-pages and add-ons won't render. diff --git a/cypress/src/integration/plugins/catalog.spec.ts b/cypress/src/integration/plugins/catalog.spec.ts index adf9b9a384..50ef3145e5 100644 --- a/cypress/src/integration/plugins/catalog.spec.ts +++ b/cypress/src/integration/plugins/catalog.spec.ts @@ -82,5 +82,118 @@ describe('Catalog', () => { .contains('Sub-page 1') .should('be.visible'); }); + + it('Should render addons on docs tab homepage', () => { + cy.loginAsGuest(); + + cy.visit('/catalog'); + + cy.contains('techdocs-e2e-fixture').click(); + + cy.location().should(loc => { + expect(loc.pathname).to.eq( + '/catalog/default/component/techdocs-e2e-fixture', + ); + }); + + cy.getCatalogDocsTab().click(); + + cy.wait(300); + + cy.getTechDocsShadowRoot() + .find('h1') + .contains('Home page') + .should('be.visible'); + + // highlight a snippet of text + cy.getTechDocsShadowRoot() + .find('article > p') + .then($el => { + const el = $el[0]; + const document = el.ownerDocument; + const range = document.createRange(); + range.selectNodeContents(el); + document?.getSelection()?.removeAllRanges(); + document?.getSelection()?.addRange(range); + }); + + cy.document().trigger('selectionchange'); + + // wait for new issue default debounce time + cy.wait(600); + + // assert that the new issue button has a right url + cy.getTechDocsShadowRoot() + .contains('Open new Github issue') + .should( + 'have.attr', + 'href', + 'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20This%20is%20a%20basic%20documentation%20used%20for%20end-to-end%20tests.&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20This%20is%20a%20basic%20documentation%20used%20for%20end-to-end%20tests.%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%3A7007%2Fcatalog%2Fdefault%2Fcomponent%2Ftechdocs-e2e-fixture%2Fdocs%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fcypress%2Ffixtures%2Fdocs%2Findex.md%3E', + ); + }); + + it('Should render addons on docs tab sup-page', () => { + cy.loginAsGuest(); + + cy.visit('/catalog'); + + cy.contains('techdocs-e2e-fixture').click(); + + cy.location().should(loc => { + expect(loc.pathname).to.eq( + '/catalog/default/component/techdocs-e2e-fixture', + ); + }); + + cy.getCatalogDocsTab().click(); + + cy.wait(300); + + cy.getTechDocsShadowRoot() + .find('h1') + .contains('Home page') + .should('be.visible'); + + cy.getTechDocsShadowRoot().within(() => { + cy.getTechDocsNavigation().find('a').contains('Sub-page 1').click(); + }); + + cy.location().should(loc => { + expect(loc.pathname).to.eq( + '/catalog/default/component/techdocs-e2e-fixture/docs/sub-page-one/', + ); + }); + + cy.getTechDocsShadowRoot() + .find('h1') + .contains('Sub-page 1') + .should('be.visible'); + + // highlight a snippet of text + cy.getTechDocsShadowRoot() + .find('#section-11') + .then($el => { + const el = $el[0]; + const document = el.ownerDocument; + const range = document.createRange(); + range.selectNodeContents(el); + document?.getSelection()?.removeAllRanges(); + document?.getSelection()?.addRange(range); + }); + + cy.document().trigger('selectionchange'); + + // wait for new issue default debounce time + cy.wait(600); + + // assert that the new issue button has a right url + cy.getTechDocsShadowRoot() + .contains('Open new Github issue') + .should( + 'have.attr', + 'href', + 'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20Section%201.1%C2%B6&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20Section%201.1%C2%B6%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%3A7007%2Fcatalog%2Fdefault%2Fcomponent%2Ftechdocs-e2e-fixture%2Fdocs%2Fsub-page-one%2F%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fcypress%2Ffixtures%2Fdocs%2Fsub-page-one.md%3E', + ); + }); }); }); diff --git a/cypress/src/integration/plugins/techdocs.spec.ts b/cypress/src/integration/plugins/techdocs.spec.ts index 1e69ba46ad..31094cb128 100644 --- a/cypress/src/integration/plugins/techdocs.spec.ts +++ b/cypress/src/integration/plugins/techdocs.spec.ts @@ -99,7 +99,7 @@ describe('TechDocs', () => { }); describe('Rendering TechDocs Addons', () => { - it('should render a content addon', () => { + it('should render a content addon in homepage', () => { cy.visit('/docs/default/Component/techdocs-e2e-fixture'); cy.contains('e2e Fixture Documentation'); @@ -130,6 +130,43 @@ describe('TechDocs', () => { 'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20This%20is%20a%20basic%20documentation%20used%20for%20end-to-end%20tests.&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20This%20is%20a%20basic%20documentation%20used%20for%20end-to-end%20tests.%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%3A7007%2Fdocs%2Fdefault%2FComponent%2Ftechdocs-e2e-fixture%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fcypress%2Ffixtures%2Fdocs%2Findex.md%3E', ); }); + + it('should render a content addon in sub-pages', () => { + cy.visit('/docs/default/Component/techdocs-e2e-fixture'); + + cy.contains('e2e Fixture Documentation'); + + // open sub-page + cy.getTechDocsShadowRoot().within(() => { + cy.getTechDocsNavigation().find('a').contains('Sub-page 1').click(); + }); + + // highlight a snippet of text + cy.getTechDocsShadowRoot() + .find('#section-11') + .then($el => { + const el = $el[0]; + const document = el.ownerDocument; + const range = document.createRange(); + range.selectNodeContents(el); + document?.getSelection()?.removeAllRanges(); + document?.getSelection()?.addRange(range); + }); + + cy.document().trigger('selectionchange'); + + // wait for new issue default debounce time + cy.wait(600); + + // assert that the new issue button has a right url + cy.getTechDocsShadowRoot() + .contains('Open new Github issue') + .should( + 'have.attr', + 'href', + 'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20Section%201.1%C2%B6&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20Section%201.1%C2%B6%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%3A7007%2Fdocs%2Fdefault%2FComponent%2Ftechdocs-e2e-fixture%2Fsub-page-one%2F%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fcypress%2Ffixtures%2Fdocs%2Fsub-page-one.md%3E', + ); + }); }); describe('Navigating within TechDocs', () => { diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 22d6cc3ab1..799fe3e4d9 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -140,6 +140,12 @@ import { EntityGoCdContent, isGoCdAvailable } from '@backstage/plugin-gocd'; import React, { ReactNode, useMemo, useState } from 'react'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { + TextSize, + ReportIssue, +} from '@backstage/plugin-techdocs-module-addons-contrib'; + const customEntityFilterKind = ['Component', 'API', 'System']; const EntityLayoutWrapper = (props: { children?: ReactNode }) => { @@ -168,6 +174,15 @@ const EntityLayoutWrapper = (props: { children?: ReactNode }) => { ); }; +const techdocsContent = ( + + + + + + +); + /** * NOTE: This page is designed to work on small screens such as mobile devices. * This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`, @@ -398,7 +413,7 @@ const serviceEntityPage = ( - + {techdocsContent} - + {techdocsContent} + - + {techdocsContent} diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index 84d094410b..6ec4da055d 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -68,6 +68,17 @@ import { RELATION_PROVIDES_API, } from '@backstage/catalog-model'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; + +const techdocsContent = ( + + + + + +); + const cicdContent = ( // This is an example of how you can implement your company's logic in entity page. // You can for example enforce that all components of type 'service' should use GitHubActions @@ -167,7 +178,7 @@ const serviceEntityPage = ( - + {techdocsContent} ); @@ -194,7 +205,7 @@ const websiteEntityPage = ( - + {techdocsContent} ); @@ -213,7 +224,7 @@ const defaultEntityPage = ( - + {techdocsContent} ); diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index a8204bc9af..b2090bc879 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -111,7 +111,9 @@ export type DocsTableRow = { }; // @public -export const EmbeddedDocsRouter: (props: PropsWithChildren<{}>) => JSX.Element; +export const EmbeddedDocsRouter: ( + props: PropsWithChildren<{}>, +) => JSX.Element | null; // @public export const EntityListDocsGrid: () => JSX.Element; @@ -153,7 +155,7 @@ export type EntityListDocsTableProps = { // @public export const EntityTechdocsContent: (props: { children?: ReactNode; -}) => JSX.Element; +}) => JSX.Element | null; // @public export const isTechDocsAvailable: (entity: Entity) => boolean; diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index de63838bc8..12b3ddf611 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -15,7 +15,7 @@ */ import React, { PropsWithChildren } from 'react'; -import { Route, Routes } from 'react-router-dom'; +import { Route, Routes, useRoutes } from 'react-router-dom'; import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; @@ -61,17 +61,25 @@ export const EmbeddedDocsRouter = (props: PropsWithChildren<{}>) => { const { children } = props; const { entity } = useEntity(); + // Using objects instead of elements, otherwise "outlet" will be null on sub-pages and add-ons won't render + const element = useRoutes([ + { + path: '/*', + element: , + children: [ + { + path: '/*', + element: children, + }, + ], + }, + ]); + const projectId = entity.metadata.annotations?.[TECHDOCS_ANNOTATION]; if (!projectId) { return ; } - return ( - - }> - {children} - - - ); + return element; };