diff --git a/plugins/scaffolder-backend/sample-templates/all-templates.yaml b/plugins/scaffolder-backend/sample-templates/all-templates.yaml index 6637c9479b..54a3fb213e 100644 --- a/plugins/scaffolder-backend/sample-templates/all-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/all-templates.yaml @@ -3,6 +3,8 @@ kind: Location metadata: name: example-templates description: A collection of all Backstage example templates +# annotations: +# backstage.io/edit-url: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml spec: targets: - ./remote-templates.yaml diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 8bfbd6b915..babe93ad13 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -107,6 +107,7 @@ describe('createRouter', () => { title: 'Create React App Template', annotations: { 'backstage.io/managed-by-location': 'url:https://dev.azure.com', + 'backstage.io/edit-url': 'url:EDIT_URL', }, }, spec: { @@ -878,6 +879,7 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect(response.body).toEqual({ title: 'Create React App Template', description: 'Create a new CRA website project', + editUrl: 'url:EDIT_URL', steps: [ { title: 'Please enter the following information', @@ -932,6 +934,7 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect(response.body).toEqual({ title: 'Create React App Template', description: 'Create a new CRA website project', + editUrl: 'url:EDIT_URL', steps: [], }); }); @@ -963,6 +966,7 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect(response.body).toEqual({ title: 'Create React App Template', description: 'Create a new CRA website project', + editUrl: 'url:EDIT_URL', steps: [ { title: 'Please enter the following information', @@ -983,6 +987,56 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ ], }); }); + it('should not return editUrl', async () => { + jest + .spyOn(catalogClient, 'getEntityByRef') + .mockImplementationOnce(async () => { + const template = getMockTemplate(); + delete template.metadata.annotations?.['backstage.io/edit-url']; + return template; + }); + const response = await request(app) + .get( + '/v2/templates/default/Template/create-react-app-template/parameter-schema', + ) + .send(); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + title: 'Create React App Template', + description: 'Create a new CRA website project', + steps: [ + { + title: 'Please enter the following information', + schema: { + required: ['requiredParameter1'], + type: 'object', + properties: { + requiredParameter1: { + description: 'Required parameter 1', + type: 'string', + }, + }, + }, + }, + { + title: 'Please enter the following information', + schema: { + type: 'object', + required: ['requiredParameter2'], + 'backstage:permissions': { + tags: ['parameters-tag'], + }, + properties: { + requiredParameter2: { + type: 'string', + description: 'Required parameter 2', + }, + }, + }, + }, + ], + }); + }); }); describe('POST /v2/tasks', () => { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index c0322af283..a70d6e9800 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -408,6 +408,7 @@ export async function createRouter( res.json({ title: template.metadata.title ?? template.metadata.name, ...(presentation ? { presentation } : {}), + editUrl: template.metadata.annotations?.['backstage.io/edit-url'], description: template.metadata.description, 'ui:options': template.metadata['ui:options'], steps: parameters.map(schema => ({ diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx index 81c5a697e0..e431e66ca2 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx @@ -20,7 +20,7 @@ import { renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; -import { act, fireEvent } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { Workflow } from './Workflow'; import { analyticsApiRef } from '@backstage/core-plugin-api'; @@ -75,7 +75,7 @@ describe('', () => { title: 'React JSON Schema Form Test', }); - const { getByRole, getAllByRole, getByText } = await renderInTestApp( + const { getByRole, getByText } = await renderInTestApp( ', () => { ).toBeDefined(); await act(async () => { - fireEvent.click(getAllByRole('button')[1] as HTMLButtonElement); + fireEvent.click(getByText('Make') as HTMLButtonElement); }); expect(onCreate).toHaveBeenCalledWith({ @@ -141,4 +141,72 @@ describe('', () => { age: '53', }); }); + it('renders "edit template" button', async () => { + const onCreate = jest.fn(); + const onError = jest.fn(); + const expectedUrl = '/EDIT_URL'; + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ + editUrl: expectedUrl, + steps: [ + { + title: 'Step 1', + schema: {}, + }, + ], + title: 'React JSON Schema Form Test', + }); + await renderInTestApp( + + + , + ); + const editLink = screen.getByTitle('Edit Template').closest('a'); + expect(editLink).toHaveAttribute('href', expectedUrl); + }); + it('renders disable "edit template" button', async () => { + const onCreate = jest.fn(); + const onError = jest.fn(); + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ + steps: [ + { + title: 'Step 1', + schema: {}, + }, + ], + editUrl: undefined, + title: 'React JSON Schema Form Test', + }); + await renderInTestApp( + + + , + ); + const editLink = screen.getByTitle('Edit Template'); + expect(editLink).toBeVisible(); + expect(editLink).toHaveAttribute('href', '/'); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx index c6030930c5..660f115064 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx @@ -20,6 +20,7 @@ import { InfoCard, MarkdownContent, Progress, + Link, } from '@backstage/core-components'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { makeStyles } from '@material-ui/core'; @@ -29,6 +30,8 @@ import { Stepper, type StepperProps } from '../Stepper/Stepper'; import { SecretsContextProvider } from '../../../secrets/SecretsContext'; import { useFilteredSchemaProperties } from '../../hooks/useFilteredSchemaProperties'; import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; +import IconButton from '@material-ui/core/IconButton'; +import EditIcon from '@material-ui/icons/Edit'; const useStyles = makeStyles({ markdown: { @@ -93,13 +96,23 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { if (error) { return props.onError(error); } - return ( {loading && } {sortedManifest && ( + + + } subheader={