diff --git a/.changeset/modern-trees-add.md b/.changeset/modern-trees-add.md new file mode 100644 index 0000000000..bb5ea78a5b --- /dev/null +++ b/.changeset/modern-trees-add.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs': minor +'@backstage/plugin-catalog': minor +--- + +This change allows a new annotation of `backstage.io/techdocs-entity` this ref allows you to reference another entity for its TechDocs. This allows you have a single TechDoc for all items in a system, for example you might have a frontend and a backend in the same repo. This would allow you to have TechDocs build under a `System` entity while referencing the system e.g.: `backstage.io/techdocs-entity: system:default/example` that will show the systems docs in both the TechDocs button and the TechDocs tab without needing to do duplicate builds and filling the TechDocs page with garbage. diff --git a/ADOPTERS.md b/ADOPTERS.md index 1eb3c68eeb..6cab268750 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -258,3 +258,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Platzi](https://platzi.com/) | [@juancarestre](https://github.com/juancarestre/), [Engineering at Platzi](https://github.com/PlatziDev/) | Backstage allow our developers to get easily engaged with all the internal components, technical documentations and software templates. All new developers reduce its onboarding time via Backstage, and after a couple of integrations it allowed us to create new components with in a couple of clicks. | | [idealo](https://idealo.de) | [Wanis Fahmy](https://github.com/wanisfahmyDE), [Sajjad Pervaiz](https://github.com/sjvaiz), [Tim Heurich](https://github.com/theurichde) | Backstage is our Internal developer portal. Along with other plugins, We use the software catalog, TechDocs and explore plugins to manage, discover and document our software components and platform products. | | [Localiza&Co](https://www.localiza.com/) | [Augusto Amormino](https://github.com/augustoamormino), [Jonas Soares](https://github.com/jonaopower), [Alexandre Amormino](https://github.com/alexandreamormino), [Greg Almeida](https://github.com/sephh) | We're excited to announce our adoption of Backstage as our Internal Developer Portal! Our mission is to elevate the Developer Experience by streaming information access. Backstage will serve as the ultimate hub for developer resources, including documentation, tools, software insights, and metrics. Through Backstage, we're simplifying processes, offering software templates to empowered and efficient development journey, enhancing self-service capabilities with the embedded all best practices. | +| [V2 Digital](https://v2.digital) | [Joe Patterson](https://github.com/jrwpatterson)| We will be using it to be a corporate dashboard plus our software catalog. | diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 0a4f5ee400..70f5ff5e14 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -100,6 +100,21 @@ alongside the entity's source code, the value of this annotation can point to an absolute URL, matching the location reference string format outlined above, for example: `url:https://github.com/backstage/backstage/tree/master` +### backstage.io/techdocs-entity + +```yaml +# Example: +metadata: + annotations: + backstage.io/techdocs-entity: component:default/example +``` + +The value of this annotation informs of an external entity that owns the TechDocs. +This allows you to reference TechDocs from a single source without either duplicating +the TechDocs in the TechDocs page or needing multiple builds of the same docs. + +This is for situations where you have complex systems where they share a single repo, and likely a single TechDoc location. + ### backstage.io/view-url, backstage.io/edit-url ```yaml diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index b78a1a55af..c64018ab97 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -675,3 +675,32 @@ entity. If the value of this annotation is `'local'`, the TechDocs backend will and publish the documentation for them. If the value of the `company.com/techdocs-builder` annotation is anything other than `'local'`, the user is responsible for publishing documentation to the appropriate location in the TechDocs external storage. + +## Reference another components TechDocs + +In systems where you might have multiple entities for example a System with a Website and an API, when served from a Monorepo you might want to keep the TechDocs in one location in the repository. + +In this case you can add the `backstage.io/techdocs-entity` annotation and point to the owners `entityRef` and use its TechDocs. This allows the Subcomponents to read the parents docs, filling the TechDocs link on the `AboutCard` element and the Techdocs tab + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: System +metadata: + name: example + namespace: default + title: Example + description: This is the parent entity + annotations: + backstage.io/techdocs-ref: dir:. + +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-platfrom + title: Example Application Platform + namespace: default + description: This is the child entity + annotations: + backstage.io/techdocs-entity: system:default/example +``` diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index e8d2657e9f..1679e2ddf5 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -14,24 +14,25 @@ * limitations under the License. */ -import { RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { ConfigReader } from '@backstage/core-app-api'; +import { + CatalogApi, + EntityProvider, + catalogApiRef, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { - catalogApiRef, - EntityProvider, - CatalogApi, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import userEvent from '@testing-library/user-event'; -import { screen } from '@testing-library/react'; -import React from 'react'; +import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; + import { AboutCard } from './AboutCard'; +import { ConfigReader } from '@backstage/core-app-api'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import React from 'react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; describe('', () => { const catalogApi: jest.Mocked = { @@ -347,6 +348,62 @@ describe('', () => { ).not.toBeInTheDocument(); }); + it('renders techdocs lin when 3rdparty', async () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'backstage.io/techdocs-entity': 'system:default/example', + }, + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/docs/:namespace/:kind/:name': viewTechDocRouteRef, + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(screen.getByText('View TechDocs').closest('a')).toHaveAttribute( + 'href', + '/docs/default/system/example', + ); + }); + it('renders techdocs link', async () => { const entity = { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 06cc020e51..34aa463c39 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -16,32 +16,10 @@ import { ANNOTATION_EDIT_URL, ANNOTATION_LOCATION, + CompoundEntityRef, DEFAULT_NAMESPACE, stringifyEntityRef, } from '@backstage/catalog-model'; -import { - HeaderIconLinkRow, - IconLinkVerticalProps, - InfoCardVariants, - Link, -} from '@backstage/core-components'; -import { - alertApiRef, - errorApiRef, - useApi, - useApp, - useRouteRef, -} from '@backstage/core-plugin-api'; -import { - ScmIntegrationIcon, - scmIntegrationsApiRef, -} from '@backstage/integration-react'; -import { - catalogApiRef, - getEntitySourceLocation, - useEntity, -} from '@backstage/plugin-catalog-react'; -import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { Card, CardContent, @@ -50,14 +28,42 @@ import { IconButton, makeStyles, } from '@material-ui/core'; -import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; +import { + HeaderIconLinkRow, + IconLinkVerticalProps, + InfoCardVariants, + Link, +} from '@backstage/core-components'; +import React, { useCallback } from 'react'; +import { + ScmIntegrationIcon, + scmIntegrationsApiRef, +} from '@backstage/integration-react'; +import { + alertApiRef, + errorApiRef, + useApi, + useApp, + useRouteRef, +} from '@backstage/core-plugin-api'; +import { + catalogApiRef, + getEntitySourceLocation, + useEntity, +} from '@backstage/plugin-catalog-react'; +import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; + +import { AboutContent } from './AboutContent'; import CachedIcon from '@material-ui/icons/Cached'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; -import React, { useCallback } from 'react'; +import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { parseEntityRef } from '@backstage/catalog-model'; -import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; -import { AboutContent } from './AboutContent'; +const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; + +const TECHDOCS_EXTERNAL_ANNOTATION = 'backstage.io/techdocs-entity'; const useStyles = makeStyles({ gridItemCard: { @@ -110,6 +116,19 @@ export function AboutCard(props: AboutCardProps) { const entityMetadataEditUrl = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; + let techdocsRef: CompoundEntityRef | undefined; + + if (entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]) { + try { + techdocsRef = parseEntityRef( + entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION], + ); + // not a fan of this but we don't care if the parseEntityRef fails + } catch { + techdocsRef = undefined; + } + } + const viewInSource: IconLinkVerticalProps = { label: 'View Source', disabled: !entitySourceLocation, @@ -119,16 +138,24 @@ export function AboutCard(props: AboutCardProps) { const viewInTechDocs: IconLinkVerticalProps = { label: 'View TechDocs', disabled: - !entity.metadata.annotations?.['backstage.io/techdocs-ref'] || - !viewTechdocLink, + !( + entity.metadata.annotations?.[TECHDOCS_ANNOTATION] || + entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION] + ) || !viewTechdocLink, icon: , href: viewTechdocLink && - viewTechdocLink({ - namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, - kind: entity.kind, - name: entity.metadata.name, - }), + (techdocsRef + ? viewTechdocLink({ + namespace: techdocsRef.namespace || DEFAULT_NAMESPACE, + kind: techdocsRef.kind, + name: techdocsRef.name, + }) + : viewTechdocLink({ + namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, + kind: entity.kind, + name: entity.metadata.name, + })), }; const subHeaderLinks = [viewInSource, viewInTechDocs]; diff --git a/plugins/techdocs/src/EntityPageDocs.tsx b/plugins/techdocs/src/EntityPageDocs.tsx index b0e529f9a5..af184bb1a8 100644 --- a/plugins/techdocs/src/EntityPageDocs.tsx +++ b/plugins/techdocs/src/EntityPageDocs.tsx @@ -14,18 +14,33 @@ * limitations under the License. */ +import { + Entity, + getCompoundEntityRef, + parseEntityRef, +} from '@backstage/catalog-model'; + import React from 'react'; - -import { Entity, getCompoundEntityRef } from '@backstage/catalog-model'; - import { TechDocsReaderPage } from './plugin'; -import { TechDocsReaderPageSubheader } from './reader/components/TechDocsReaderPageSubheader'; import { TechDocsReaderPageContent } from './reader/components/TechDocsReaderPageContent'; +import { TechDocsReaderPageSubheader } from './reader/components/TechDocsReaderPageSubheader'; + +const TECHDOCS_EXTERNAL_ANNOTATION = 'backstage.io/techdocs-entity'; type EntityPageDocsProps = { entity: Entity }; export const EntityPageDocs = ({ entity }: EntityPageDocsProps) => { - const entityRef = getCompoundEntityRef(entity); + let entityRef = getCompoundEntityRef(entity); + + if (entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]) { + try { + entityRef = parseEntityRef( + entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION], + ); + } catch { + // not a fan of this but we don't care if the parseEntityRef fails + } + } return ( diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index f821da2a9f..814bf3cf65 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -18,22 +18,24 @@ import React, { PropsWithChildren } from 'react'; import { Route, Routes, useRoutes } from 'react-router-dom'; import { Entity } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { MissingAnnotationEmptyState } from '@backstage/core-components'; - import { EntityPageDocs } from './EntityPageDocs'; +import { MissingAnnotationEmptyState } from '@backstage/core-components'; import { TechDocsIndexPage } from './home/components/TechDocsIndexPage'; import { TechDocsReaderPage } from './reader/components/TechDocsReaderPage'; +import { useEntity } from '@backstage/plugin-catalog-react'; const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; +const TECHDOCS_EXTERNAL_ANNOTATION = 'backstage.io/techdocs-entity'; + /** * Helper that takes in entity and returns true/false if TechDocs is available for the entity * * @public */ export const isTechDocsAvailable = (entity: Entity) => - Boolean(entity?.metadata?.annotations?.[TECHDOCS_ANNOTATION]); + Boolean(entity?.metadata?.annotations?.[TECHDOCS_ANNOTATION]) || + Boolean(entity?.metadata?.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]); /** * Responsible for registering routes for TechDocs, TechDocs Homepage and separate TechDocs page @@ -75,10 +77,12 @@ export const EmbeddedDocsRouter = (props: PropsWithChildren<{}>) => { }, ]); - const projectId = entity.metadata.annotations?.[TECHDOCS_ANNOTATION]; + const projectId = + entity.metadata.annotations?.[TECHDOCS_ANNOTATION] || + entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]; if (!projectId) { - return ; + return ; } return element;