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;