diff --git a/.changeset/chatty-seals-tap.md b/.changeset/chatty-seals-tap.md
index 384a89e6ce..5511fe4520 100644
--- a/.changeset/chatty-seals-tap.md
+++ b/.changeset/chatty-seals-tap.md
@@ -1,6 +1,5 @@
---
'@backstage/plugin-catalog': patch
-'example-app': patch
---
Add link from Template entity to the scaffolder launch page for the template in the AboutCard.
diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md
index 970d8d3251..915e89d0c6 100644
--- a/plugins/catalog/api-report.md
+++ b/plugins/catalog/api-report.md
@@ -105,6 +105,13 @@ export const catalogPlugin: BackstagePlugin<
},
true
>;
+ selectedTemplateRoute: ExternalRouteRef<
+ {
+ namespace: string;
+ templateName: string;
+ },
+ true
+ >;
},
CatalogInputPluginOptions
>;
diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
index 869d6f1757..43af858300 100644
--- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
+++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx
@@ -30,7 +30,7 @@ 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 { viewTechDocRouteRef } from '../../routes';
+import { selectedTemplateRouteRef, viewTechDocRouteRef } from '../../routes';
import { AboutCard } from './AboutCard';
describe('', () => {
@@ -505,4 +505,165 @@ describe('', () => {
expect(screen.getByText('View TechDocs')).toBeVisible();
expect(screen.getByText('View TechDocs').closest('a')).toBeNull();
});
+
+ it('renders launch template link', async () => {
+ const entity = {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
+ kind: 'Template',
+ metadata: {
+ name: 'create-react-app-template',
+ namespace: 'default',
+ },
+ };
+
+ await renderInTestApp(
+
+
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/create/templates/:namespace/:templateName':
+ selectedTemplateRouteRef,
+ },
+ },
+ );
+
+ expect(screen.getByText('Launch Template')).toBeVisible();
+ expect(screen.getByText('Launch Template').closest('a')).toHaveAttribute(
+ 'href',
+ '/create/templates/default/create-react-app-template',
+ );
+ });
+
+ it.each([
+ {
+ testName: 'entity is not a template',
+ entity: {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
+ kind: 'Component',
+ metadata: {
+ name: 'create-react-app-template',
+ namespace: 'default',
+ },
+ },
+ },
+ {
+ testName: 'apiVersion is not scaffolder.backstage.io/v1beta3',
+ entity: {
+ apiVersion: 'v1',
+ kind: 'Template',
+ metadata: {
+ name: 'create-react-app-template',
+ namespace: 'default',
+ },
+ },
+ },
+ ])(
+ 'should not render launch template link when $testName',
+ async ({ entity }) => {
+ await renderInTestApp(
+
+
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ '/create/templates/:namespace/:templateName':
+ selectedTemplateRouteRef,
+ },
+ },
+ );
+
+ expect(screen.queryByText('Launch Template')).toBeNull();
+ },
+ );
+
+ it('renders disabled launch template link when route is not bound', async () => {
+ const entity = {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
+ kind: 'Template',
+ metadata: {
+ name: 'create-react-app-template',
+ namespace: 'default',
+ },
+ };
+
+ await renderInTestApp(
+
+
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+
+ expect(screen.getByText('Launch Template')).toBeVisible();
+ expect(screen.getByText('Launch Template').closest('a')).toBeNull();
+ });
});
diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx
index 4be201b555..2933aea9e8 100644
--- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx
+++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx
@@ -30,6 +30,7 @@ import {
alertApiRef,
errorApiRef,
useApi,
+ useApp,
useRouteRef,
} from '@backstage/core-plugin-api';
import {
@@ -90,8 +91,8 @@ export interface AboutCardProps {
/**
* Exported publicly via the EntityAboutCard
*/
-export function AboutCard(props: AboutCardProps) {
- const { variant } = props;
+export function AboutCard({ variant }: AboutCardProps) {
+ const app = useApp();
const classes = useStyles();
const { entity } = useEntity();
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
@@ -132,9 +133,12 @@ export function AboutCard(props: AboutCardProps) {
const subHeaderLinks = [viewInSource, viewInTechDocs];
if (isTemplateEntityV1beta3(entity)) {
+ const Icon = app.getSystemIcon('scaffolder') ?? AddIcon;
+
const launchTemplate: IconLinkVerticalProps = {
label: 'Launch Template',
- icon: ,
+ icon: ,
+ disabled: !templateRoute,
href:
templateRoute &&
templateRoute({
diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md
index f7cf6024ec..eccada5d51 100644
--- a/plugins/scaffolder-backend-module-gitlab/api-report.md
+++ b/plugins/scaffolder-backend-module-gitlab/api-report.md
@@ -26,8 +26,8 @@ export const createGitlabProjectAccessTokenAction: (options: {
integrations: ScmIntegrationRegistry;
}) => TemplateAction<
{
- projectId: string | number;
repoUrl: string;
+ projectId: string | number;
token?: string | undefined;
name?: string | undefined;
accessLevel?: number | undefined;
@@ -44,8 +44,8 @@ export const createGitlabProjectDeployTokenAction: (options: {
}) => TemplateAction<
{
name: string;
- projectId: string | number;
repoUrl: string;
+ projectId: string | number;
token?: string | undefined;
username?: string | undefined;
scopes?: string[] | undefined;
@@ -63,8 +63,8 @@ export const createGitlabProjectVariableAction: (options: {
{
key: string;
value: string;
- projectId: string | number;
repoUrl: string;
+ projectId: string | number;
variableType: string;
token?: string | undefined;
variableProtected?: boolean | undefined;