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:^"