diff --git a/.changeset/grumpy-lamps-wonder.md b/.changeset/grumpy-lamps-wonder.md new file mode 100644 index 0000000000..c7fe8ebc35 --- /dev/null +++ b/.changeset/grumpy-lamps-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added a menu to the header of template page for direct access to editing the template diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx index 26a9dd946c..5cdae85e8a 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -29,6 +29,8 @@ import { } from '@backstage/plugin-scaffolder-react'; import { TemplateWizardPage } from './TemplateWizardPage'; import { rootRouteRef } from '../../routes'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { ANNOTATION_EDIT_URL } from '@backstage/catalog-model'; jest.mock('react-router-dom', () => { return { @@ -50,12 +52,32 @@ const scaffolderApiMock: jest.Mocked = { listTasks: jest.fn(), }; +const catalogApiMock: jest.Mocked = { + getEntityByRef: jest.fn(), +} as any; + const analyticsMock = new MockAnalyticsApi(); const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [analyticsApiRef, analyticsMock], + [catalogApiRef, catalogApiMock], ); +const entityRefResponse = { + apiVersion: 'v1', + kind: 'service', + metadata: { + name: 'test', + annotations: { + [ANNOTATION_EDIT_URL]: 'http://localhost:3000', + }, + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, +}; describe('TemplateWizardPage', () => { it('captures expected analytics events', async () => { scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' }); @@ -74,6 +96,7 @@ describe('TemplateWizardPage', () => { ], title: 'React JSON Schema Form Test', }); + catalogApiMock.getEntityByRef.mockResolvedValue(entityRefResponse); const { findByRole, getByRole } = await renderInTestApp( @@ -117,4 +140,64 @@ describe('TemplateWizardPage', () => { context: { entityRef: 'template:default/test' }, }); }); + describe('scaffolder page context menu', () => { + it('should render if editUrl is set to url', async () => { + catalogApiMock.getEntityByRef.mockResolvedValue({ + apiVersion: 'v1', + kind: 'service', + metadata: { + name: 'test', + annotations: { + [ANNOTATION_EDIT_URL]: 'http://localhost:3000', + }, + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, + }); + const { queryByTestId } = await renderInTestApp( + + + , + + , + { + mountedRoutes: { + '/create': rootRouteRef, + }, + }, + ); + expect(queryByTestId('menu-button')).toBeInTheDocument(); + }); + it('should not render if editUrl is undefined', async () => { + catalogApiMock.getEntityByRef.mockResolvedValue({ + apiVersion: 'v1', + kind: 'service', + metadata: { + name: 'test', + // annotations are not set + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, + }); + const { queryByTestId } = await renderInTestApp( + + + , + + , + { + mountedRoutes: { + '/create': rootRouteRef, + }, + }, + ); + expect(queryByTestId('menu-button')).not.toBeInTheDocument(); + }); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 611330e76b..053cf6decf 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -15,7 +15,11 @@ */ import React from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; -import { stringifyEntityRef } from '@backstage/catalog-model'; +import useAsync from 'react-use/lib/useAsync'; +import { + stringifyEntityRef, + ANNOTATION_EDIT_URL, +} from '@backstage/catalog-model'; import { AnalyticsContext, useApi, @@ -30,6 +34,8 @@ import { FieldExtensionOptions, ReviewStepProps, } from '@backstage/plugin-scaffolder-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; + import { Workflow } from '@backstage/plugin-scaffolder-react/alpha'; import { JsonValue } from '@backstage/types'; import { Header, Page } from '@backstage/core-components'; @@ -40,6 +46,8 @@ import { selectedTemplateRouteRef, } from '../../routes'; +import { TemplateWizardPageContextMenu } from './TemplateWizardPageContextMenu'; + /** * @alpha */ @@ -62,6 +70,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { const taskRoute = useRouteRef(scaffolderTaskRouteRef); const { secrets } = useTemplateSecrets(); const scaffolderApi = useApi(scaffolderApiRef); + const catalogApi = useApi(catalogApiRef); const navigate = useNavigate(); const { templateName, namespace } = useRouteRefParams( selectedTemplateRouteRef, @@ -73,6 +82,11 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { name: templateName, }); + const { value: editUrl } = useAsync(async () => { + const data = await catalogApi.getEntityByRef(templateRef); + return data?.metadata.annotations?.[ANNOTATION_EDIT_URL]; + }, [templateRef, catalogApi]); + const onCreate = async (values: Record) => { const { taskId } = await scaffolderApi.scaffold({ templateRef, @@ -93,7 +107,9 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { title="Create a new component" subtitle="Create new software components using standard templates in your organization" {...props.headerOptions} - /> + > + + ({ + button: { + color: theme.page.fontColor, + }, +})); + +export type TemplateWizardPageContextMenuProps = { + editUrl?: string; +}; + +export function TemplateWizardPageContextMenu( + props: TemplateWizardPageContextMenuProps, +) { + const { editUrl } = props; + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(); + + if (!editUrl) { + return null; + } + + const onOpen = (event: React.SyntheticEvent) => { + setAnchorEl(event.currentTarget); + }; + + const onClose = () => { + setAnchorEl(undefined); + }; + + return ( + <> + + + + + + window.open(editUrl, '_blank')}> + + + + + + + + + ); +}