Merge pull request #30988 from dfl-aeb/dfl/fix-scaffolder-techdocs-view

fix(scaffolder): show “View TechDocs” link when template has backstage.io/techdocs-entity and backstage.io/techdocs-entity-path
This commit is contained in:
Fredrik Adelöw
2025-09-09 13:44:40 +02:00
committed by GitHub
5 changed files with 157 additions and 7 deletions
+3
View File
@@ -72,6 +72,8 @@
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/plugin-scaffolder-react": "workspace:^",
"@backstage/plugin-techdocs-common": "workspace:^",
"@backstage/plugin-techdocs-react": "workspace:^",
"@backstage/types": "workspace:^",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.1.0",
@@ -109,6 +111,7 @@
"@backstage/dev-utils": "workspace:^",
"@backstage/plugin-catalog": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-techdocs": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
@@ -27,13 +27,19 @@ import {
TestApiProvider,
mockApis,
} from '@backstage/test-utils';
import { rootRouteRef } from '../../../routes';
import { rootRouteRef, viewTechDocRouteRef } from '../../../routes';
import { TemplateListPage } from './TemplateListPage';
import {
TECHDOCS_ANNOTATION,
TECHDOCS_EXTERNAL_ANNOTATION,
TECHDOCS_EXTERNAL_PATH_ANNOTATION,
} from '@backstage/plugin-techdocs-common';
const mountedRoutes = {
mountedRoutes: {
'/': rootRouteRef,
'/catalog/:namespace/:kind/:name': entityRouteRef,
'/docs/:namespace/:kind/:name': viewTechDocRouteRef,
},
};
@@ -51,6 +57,127 @@ describe('TemplateListPage', () => {
],
});
describe('TechDocs link rendering', () => {
it('shows TechDocs link when template has backstage.io/techdocs-ref', async () => {
const mockCatalogApiWithDocs = catalogApiMock({
entities: [
{
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 'tmpl-a',
annotations: { [TECHDOCS_ANNOTATION]: 'dir:.' },
},
spec: { type: 'service' },
},
],
});
const { findByText } = await renderInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, mockCatalogApiWithDocs],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage />
</TestApiProvider>,
mountedRoutes,
);
expect(await findByText('View TechDocs')).toBeInTheDocument();
});
it('shows TechDocs link when template has backstage.io/techdocs-entity', async () => {
const mockCatalogApiWithExternal = catalogApiMock({
entities: [
{
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 'tmpl-b',
annotations: {
[TECHDOCS_EXTERNAL_ANNOTATION]: 'component:default/other',
},
},
spec: { type: 'service' },
},
],
});
const { findByText } = await renderInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, mockCatalogApiWithExternal],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage />
</TestApiProvider>,
mountedRoutes,
);
expect(await findByText('View TechDocs')).toBeInTheDocument();
});
it('appends path when backstage.io/techdocs-entity-path is set', async () => {
const mockCatalogApiWithPath = catalogApiMock({
entities: [
{
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 'tmpl-c',
annotations: {
[TECHDOCS_EXTERNAL_ANNOTATION]: 'component:default/other',
[TECHDOCS_EXTERNAL_PATH_ANNOTATION]: '/guides/start',
},
},
spec: { type: 'service' },
},
],
});
const { findByText } = await renderInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, mockCatalogApiWithPath],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage />
</TestApiProvider>,
mountedRoutes,
);
const link = (await findByText('View TechDocs')).closest('a')!;
expect(link).toHaveAttribute(
'href',
expect.stringMatching(
/\/docs\/default\/component\/other\/?(index\.html)?#?\/guides\/start|\/docs\/default\/component\/other\/guides\/start/,
),
);
});
});
it('should render the search bar for templates', async () => {
const { getByPlaceholderText } = await renderInTestApp(
<TestApiProvider
@@ -59,6 +59,11 @@ import {
useTranslationRef,
} from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../../translation';
import { buildTechDocsURL } from '@backstage/plugin-techdocs-react';
import {
TECHDOCS_ANNOTATION,
TECHDOCS_EXTERNAL_ANNOTATION,
} from '@backstage/plugin-techdocs-common';
/**
* @alpha
@@ -144,18 +149,25 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
const additionalLinksForEntity = useCallback(
(template: TemplateEntityV1beta3) => {
const { kind, namespace, name } = parseEntityRef(
stringifyEntityRef(template),
);
return template.metadata.annotations?.['backstage.io/techdocs-ref'] &&
viewTechDocsLink
if (
!(
template.metadata.annotations?.[TECHDOCS_ANNOTATION] ||
template.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]
) ||
!viewTechDocsLink
) {
return [];
}
const url = buildTechDocsURL(template, viewTechDocsLink);
return url
? [
{
icon: app.getSystemIcon('docs') ?? DocsIcon,
text: t(
'templateListPage.additionalLinksForEntity.viewTechDocsTitle',
),
url: viewTechDocsLink({ kind, namespace, name }),
url,
},
]
: [];