From acebeb8bd2738a6d7eef25250db24449e45662e8 Mon Sep 17 00:00:00 2001 From: kosukeKK Date: Tue, 5 Mar 2024 14:50:42 +0900 Subject: [PATCH] added ScaffolderWizardContextMenu and revert backend api Signed-off-by: kosukeKK --- .changeset/funny-cameras-speak.md | 5 - .changeset/honest-poems-visit.md | 2 +- .../src/service/router.test.ts | 54 ---------- .../scaffolder-backend/src/service/router.ts | 1 - plugins/scaffolder-react/api-report.md | 1 - .../ScaffolderWizardContextMenu.tsx | 100 ++++++++++++++++++ .../ScaffolderWizardContextMenu/index.ts | 19 ++++ .../src/next/components/index.ts | 1 + plugins/scaffolder-react/src/types.ts | 1 - .../TemplateWizardPage.test.tsx | 74 ++++++++----- .../TemplateWizardPage/TemplateWizardPage.tsx | 21 ++-- 11 files changed, 179 insertions(+), 100 deletions(-) delete mode 100644 .changeset/funny-cameras-speak.md create mode 100644 plugins/scaffolder-react/src/next/components/ScaffolderWizardContextMenu/ScaffolderWizardContextMenu.tsx create mode 100644 plugins/scaffolder-react/src/next/components/ScaffolderWizardContextMenu/index.ts diff --git a/.changeset/funny-cameras-speak.md b/.changeset/funny-cameras-speak.md deleted file mode 100644 index ad206e3cfd..0000000000 --- a/.changeset/funny-cameras-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -Added 'editUrl' to the '/v2/templates/:namespace/:kind/:name/parameter-schema' API response. This 'editUrl' references 'backstage.io/edit-url' in the template metadata annotations diff --git a/.changeset/honest-poems-visit.md b/.changeset/honest-poems-visit.md index 150b10a6e0..a0f392c0fc 100644 --- a/.changeset/honest-poems-visit.md +++ b/.changeset/honest-poems-visit.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-react': minor --- -Added an editUrl parameter to TemplateParameterSchema +Added a header menu for ScaffolderWizard diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index babe93ad13..8bfbd6b915 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -107,7 +107,6 @@ 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: { @@ -879,7 +878,6 @@ 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', @@ -934,7 +932,6 @@ 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: [], }); }); @@ -966,7 +963,6 @@ 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', @@ -987,56 +983,6 @@ 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 a70d6e9800..c0322af283 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -408,7 +408,6 @@ 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/api-report.md b/plugins/scaffolder-react/api-report.md index aab1a70cee..400cf24c53 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -484,7 +484,6 @@ export type TemplateGroupFilter = { export type TemplateParameterSchema = { title: string; description?: string; - editUrl?: string; presentation?: TemplatePresentationV1beta3; steps: Array<{ title: string; diff --git a/plugins/scaffolder-react/src/next/components/ScaffolderWizardContextMenu/ScaffolderWizardContextMenu.tsx b/plugins/scaffolder-react/src/next/components/ScaffolderWizardContextMenu/ScaffolderWizardContextMenu.tsx new file mode 100644 index 0000000000..5702a5ba62 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ScaffolderWizardContextMenu/ScaffolderWizardContextMenu.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import IconButton from '@material-ui/core/IconButton'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import MenuItem from '@material-ui/core/MenuItem'; +import MenuList from '@material-ui/core/MenuList'; +import Popover from '@material-ui/core/Popover'; +import { makeStyles } from '@material-ui/core/styles'; +import Edit from '@material-ui/icons/Edit'; +import MoreVert from '@material-ui/icons/MoreVert'; +import React, { useState } from 'react'; + +const useStyles = makeStyles(theme => ({ + button: { + color: theme.page.fontColor, + }, +})); + +/** + * @alpha + */ +export type ScaffolderWizardContextMenuProps = { + onEditorClicked?: () => void; +}; + +/** + * @alpha + */ +export function ScaffolderWizardContextMenu( + props: ScaffolderWizardContextMenuProps, +) { + const { onEditorClicked } = props; + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(); + + if (!onEditorClicked) { + return null; + } + + const onOpen = (event: React.SyntheticEvent) => { + setAnchorEl(event.currentTarget); + }; + + const onClose = () => { + setAnchorEl(undefined); + }; + + return ( + <> + + + + + + {onEditorClicked && ( + + + + + + + )} + + + + ); +} diff --git a/plugins/scaffolder-react/src/next/components/ScaffolderWizardContextMenu/index.ts b/plugins/scaffolder-react/src/next/components/ScaffolderWizardContextMenu/index.ts new file mode 100644 index 0000000000..51d1458cae --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ScaffolderWizardContextMenu/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + ScaffolderWizardContextMenu, + type ScaffolderWizardContextMenuProps, +} from './ScaffolderWizardContextMenu'; diff --git a/plugins/scaffolder-react/src/next/components/index.ts b/plugins/scaffolder-react/src/next/components/index.ts index 119f983a9f..3ea830002a 100644 --- a/plugins/scaffolder-react/src/next/components/index.ts +++ b/plugins/scaffolder-react/src/next/components/index.ts @@ -25,4 +25,5 @@ export * from './TaskSteps'; export * from './TaskLogStream'; export * from './TemplateCategoryPicker'; export * from './ScaffolderPageContextMenu'; +export * from './ScaffolderWizardContextMenu'; export * from './ScaffolderField'; diff --git a/plugins/scaffolder-react/src/types.ts b/plugins/scaffolder-react/src/types.ts index ee0ac1c28c..c1c26f32bb 100644 --- a/plugins/scaffolder-react/src/types.ts +++ b/plugins/scaffolder-react/src/types.ts @@ -27,7 +27,6 @@ import { TemplatePresentationV1beta3 } from '@backstage/plugin-scaffolder-common export type TemplateParameterSchema = { title: string; description?: string; - editUrl?: string; presentation?: TemplatePresentationV1beta3; steps: Array<{ title: string; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx index 463f40bd9e..f01b0ab003 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -29,6 +29,7 @@ import { } from '@backstage/plugin-scaffolder-react'; import { TemplateWizardPage } from './TemplateWizardPage'; import { rootRouteRef } from '../../routes'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; jest.mock('react-router-dom', () => { return { @@ -50,12 +51,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: { + 'backstage.io/edit-url': 'http://localhost:3000', + }, + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, +}; describe('TemplateWizardPage', () => { it('captures expected analytics events', async () => { scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' }); @@ -74,6 +95,7 @@ describe('TemplateWizardPage', () => { ], title: 'React JSON Schema Form Test', }); + catalogApiMock.getEntityByRef.mockResolvedValue(entityRefResponse); const { findByRole, getByRole } = await renderInTestApp( @@ -119,21 +141,20 @@ describe('TemplateWizardPage', () => { }); describe('scaffolder page context menu', () => { it('should render if editUrl is set to url', async () => { - scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ - steps: [ - { - title: 'Step 1', - schema: { - properties: { - name: { - type: 'string', - }, - }, - }, + catalogApiMock.getEntityByRef.mockResolvedValue({ + apiVersion: 'v1', + kind: 'service', + metadata: { + name: 'test', + annotations: { + 'backstage.io/edit-url': 'http://localhost:3000', }, - ], - title: 'React JSON Schema Form Test', - editUrl: 'http://example.com/load-testing', + }, + spec: { + profile: { + displayName: 'BackUser', + }, + }, }); const { queryByTestId } = await renderInTestApp( @@ -150,21 +171,18 @@ describe('TemplateWizardPage', () => { expect(queryByTestId('menu-button')).toBeInTheDocument(); }); it('should not render if editUrl is undefined', async () => { - scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ - steps: [ - { - title: 'Step 1', - schema: { - properties: { - name: { - type: 'string', - }, - }, - }, + catalogApiMock.getEntityByRef.mockResolvedValue({ + apiVersion: 'v1', + kind: 'service', + metadata: { + name: 'test', + // annotations are not set + }, + spec: { + profile: { + displayName: 'BackUser', }, - ], - title: 'React JSON Schema Form Test', - editUrl: undefined, + }, }); const { queryByTestId } = await renderInTestApp( diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index 5b02454385..7f7cee4c7e 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -30,7 +30,9 @@ import { FieldExtensionOptions, ReviewStepProps, } from '@backstage/plugin-scaffolder-react'; -import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/alpha'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; + +import { ScaffolderWizardContextMenu } from '@backstage/plugin-scaffolder-react/alpha'; import { Workflow } from '@backstage/plugin-scaffolder-react/alpha'; import { JsonValue } from '@backstage/types'; import { Header, Page } from '@backstage/core-components'; @@ -63,6 +65,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, @@ -75,19 +78,19 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { }); const [editUrl, setEditURL] = React.useState(''); - scaffolderApi.getTemplateParameterSchema(templateRef).then(data => { - if (data.editUrl !== undefined) setEditURL(data.editUrl); + catalogApi.getEntityByRef(templateRef).then(data => { + const templateEditUrl = + data?.metadata.annotations?.['backstage.io/edit-url']; + if (templateEditUrl !== undefined) setEditURL(templateEditUrl); }); - const scaffolderPageContextMenuProps = { + + const scaffolderWizardContextMenuProps = { onEditorClicked: editUrl !== '' ? () => { - window.location.href = editUrl; + window.open(editUrl, '_blank'); } : undefined, - onActionsClicked: undefined, - onTasksClicked: undefined, - onCreateClicked: undefined, }; const onCreate = async (values: Record) => { @@ -111,7 +114,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { subtitle="Create new software components using standard templates in your organization" {...props.headerOptions} > - +