diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 0a4f5ee400..651520cd28 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-ref + +```yaml +# Example: +metadata: + annotations: + backstage.io/techdocs-external-ref: 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/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 06cc020e51..6c28660144 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,38 @@ 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 { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; -import { AboutContent } from './AboutContent'; +import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { parseEntityRef } from '@backstage/catalog-model'; const useStyles = makeStyles({ gridItemCard: { @@ -110,6 +112,19 @@ export function AboutCard(props: AboutCardProps) { const entityMetadataEditUrl = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; + let techdocsRef: CompoundEntityRef | undefined; + + if (entity.metadata.annotations?.['backstage.io/techdocs-external-ref']) { + try { + techdocsRef = parseEntityRef( + entity.metadata.annotations?.['backstage.io/techdocs-external-ref'], + ); + // 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 +134,24 @@ export function AboutCard(props: AboutCardProps) { const viewInTechDocs: IconLinkVerticalProps = { label: 'View TechDocs', disabled: - !entity.metadata.annotations?.['backstage.io/techdocs-ref'] || - !viewTechdocLink, + !( + entity.metadata.annotations?.['backstage.io/techdocs-ref'] || + entity.metadata.annotations?.['backstage.io/techdocs-external-ref'] + ) || !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..56c1341aa2 100644 --- a/plugins/techdocs/src/EntityPageDocs.tsx +++ b/plugins/techdocs/src/EntityPageDocs.tsx @@ -14,18 +14,31 @@ * 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'; type EntityPageDocsProps = { entity: Entity }; export const EntityPageDocs = ({ entity }: EntityPageDocsProps) => { - const entityRef = getCompoundEntityRef(entity); + let entityRef = getCompoundEntityRef(entity); + + if (entity.metadata.annotations?.['backstage.io/techdocs-external-ref']) { + try { + entityRef = parseEntityRef( + entity.metadata.annotations?.['backstage.io/techdocs-external-ref'], + ); + } 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..5c967a54e8 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-external-ref'; + /** * 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,16 @@ 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;