diff --git a/.changeset/chatty-seals-tap.md b/.changeset/chatty-seals-tap.md new file mode 100644 index 0000000000..01d69e1562 --- /dev/null +++ b/.changeset/chatty-seals-tap.md @@ -0,0 +1,6 @@ +--- +'@backstage/create-app': patch +'@backstage/plugin-catalog': patch +--- + +Add link from Template entity to the scaffolder launch page for the template in the AboutCard. diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index bc9eac8c64..d46774a757 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -142,6 +142,7 @@ const app = createApp({ bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, viewTechDoc: techdocsPlugin.routes.docRoot, + createFromTemplate: scaffolderPlugin.routes.selectedTemplate, }); bind(apiDocsPlugin.externalRoutes, { registerApi: catalogImportPlugin.routes.importPage, diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 056402f2a8..8d62f29c52 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -40,12 +40,14 @@ const app = createApp({ bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, viewTechDoc: techdocsPlugin.routes.docRoot, + createFromTemplate: scaffolderPlugin.routes.selectedTemplate, }); bind(apiDocsPlugin.externalRoutes, { registerApi: catalogImportPlugin.routes.importPage, }); bind(scaffolderPlugin.externalRoutes, { registerComponent: catalogImportPlugin.routes.importPage, + viewTechDoc: techdocsPlugin.routes.docRoot, }); bind(orgPlugin.externalRoutes, { catalogIndex: catalogPlugin.routes.catalogIndex, diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 970d8d3251..6bb4fcc4a1 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -105,6 +105,13 @@ export const catalogPlugin: BackstagePlugin< }, true >; + createFromTemplate: ExternalRouteRef< + { + namespace: string; + templateName: string; + }, + true + >; }, CatalogInputPluginOptions >; diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index e76be49515..74f72e3631 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -40,6 +40,7 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", "@backstage/theme": "workspace:^", diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 869d6f1757..e8d2657e9f 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 { createFromTemplateRouteRef, 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': + createFromTemplateRouteRef, + }, + }, + ); + + 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': + createFromTemplateRouteRef, + }, + }, + ); + + 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 cb48be2b26..a08a9be5a4 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { ANNOTATION_EDIT_URL, ANNOTATION_LOCATION, @@ -30,6 +29,7 @@ import { alertApiRef, errorApiRef, useApi, + useApp, useRouteRef, } from '@backstage/core-plugin-api'; import { @@ -41,6 +41,7 @@ import { getEntitySourceLocation, useEntity, } from '@backstage/plugin-catalog-react'; +import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { Card, CardContent, @@ -49,11 +50,13 @@ import { IconButton, makeStyles, } from '@material-ui/core'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import CachedIcon from '@material-ui/icons/Cached'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; import React, { useCallback } from 'react'; -import { viewTechDocRouteRef } from '../../routes'; + +import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutContent } from './AboutContent'; const useStyles = makeStyles({ @@ -90,6 +93,7 @@ export interface AboutCardProps { */ export function AboutCard(props: AboutCardProps) { const { variant } = props; + const app = useApp(); const classes = useStyles(); const { entity } = useEntity(); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); @@ -97,6 +101,7 @@ export function AboutCard(props: AboutCardProps) { const alertApi = useApi(alertApiRef); const errorApi = useApi(errorApiRef); const viewTechdocLink = useRouteRef(viewTechDocRouteRef); + const templateRoute = useRouteRef(createFromTemplateRouteRef); const entitySourceLocation = getEntitySourceLocation( entity, @@ -126,6 +131,26 @@ export function AboutCard(props: AboutCardProps) { }), }; + const subHeaderLinks = [viewInSource, viewInTechDocs]; + + if (isTemplateEntityV1beta3(entity)) { + const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; + + const launchTemplate: IconLinkVerticalProps = { + label: 'Launch Template', + icon: , + disabled: !templateRoute, + href: + templateRoute && + templateRoute({ + templateName: entity.metadata.name, + namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, + }), + }; + + subHeaderLinks.push(launchTemplate); + } + let cardClass = ''; if (variant === 'gridItem') { cardClass = classes.gridItemCard; @@ -179,7 +204,7 @@ export function AboutCard(props: AboutCardProps) { } - subheader={} + subheader={} /> diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index ea6ddcaff2..711f52767a 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -21,7 +21,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { createComponentRouteRef, viewTechDocRouteRef } from './routes'; +import { + createComponentRouteRef, + createFromTemplateRouteRef, + viewTechDocRouteRef, +} from './routes'; import { createApiFactory, createComponentExtension, @@ -77,6 +81,7 @@ export const catalogPlugin = createPlugin({ externalRoutes: { createComponent: createComponentRouteRef, viewTechDoc: viewTechDocRouteRef, + createFromTemplate: createFromTemplateRouteRef, }, __experimentalConfigure( options?: CatalogInputPluginOptions, diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 5c0d195d74..ab70a3c551 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -30,6 +30,12 @@ export const viewTechDocRouteRef = createExternalRouteRef({ params: ['namespace', 'kind', 'name'], }); +export const createFromTemplateRouteRef = createExternalRouteRef({ + id: 'create-from-template', + optional: true, + params: ['namespace', 'templateName'], +}); + export const rootRouteRef = createRouteRef({ id: 'catalog', }); 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; diff --git a/yarn.lock b/yarn.lock index 442d800251..ee0025fdda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5868,6 +5868,7 @@ __metadata: "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" + "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^"