Merge pull request #19279 from jrwpatterson/feat/reference-external-docs

Feat/reference external entityRef for techdocs
This commit is contained in:
Fredrik Adelöw
2023-08-31 13:12:52 +02:00
committed by GitHub
8 changed files with 211 additions and 57 deletions
@@ -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('<AboutCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
@@ -347,6 +348,62 @@ describe('<AboutCard />', () => {
).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(
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</TestApiProvider>,
{
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',
@@ -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: <DocsIcon />,
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];
+20 -5
View File
@@ -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 (
<TechDocsReaderPage entityRef={entityRef}>
+10 -6
View File
@@ -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 <MissingAnnotationEmptyState annotation={TECHDOCS_ANNOTATION} />;
return <MissingAnnotationEmptyState annotation={[TECHDOCS_ANNOTATION]} />;
}
return element;