Merge pull request #23363 from kosukeKK/implements-editUrl-fortemplate

Implements edit url for template
This commit is contained in:
Fredrik Adelöw
2024-03-12 13:32:33 +01:00
committed by GitHub
4 changed files with 198 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Added a menu to the header of template page for direct access to editing the template
@@ -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<ScaffolderApi> = {
listTasks: jest.fn(),
};
const catalogApiMock: jest.Mocked<CatalogApi> = {
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(
<ApiProvider apis={apis}>
@@ -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(
<ApiProvider apis={apis}>
<SecretsContextProvider>
<TemplateWizardPage customFieldExtensions={[]} />,
</SecretsContextProvider>
</ApiProvider>,
{
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(
<ApiProvider apis={apis}>
<SecretsContextProvider>
<TemplateWizardPage customFieldExtensions={[]} />,
</SecretsContextProvider>
</ApiProvider>,
{
mountedRoutes: {
'/create': rootRouteRef,
},
},
);
expect(queryByTestId('menu-button')).not.toBeInTheDocument();
});
});
});
@@ -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<string, JsonValue>) => {
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}
/>
>
<TemplateWizardPageContextMenu editUrl={editUrl} />
</Header>
<Workflow
namespace={namespace}
templateName={templateName}
@@ -0,0 +1,92 @@
/*
* 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,
},
}));
export type TemplateWizardPageContextMenuProps = {
editUrl?: string;
};
export function TemplateWizardPageContextMenu(
props: TemplateWizardPageContextMenuProps,
) {
const { editUrl } = props;
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
if (!editUrl) {
return null;
}
const onOpen = (event: React.SyntheticEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const onClose = () => {
setAnchorEl(undefined);
};
return (
<>
<IconButton
id="long-menu"
aria-label="more"
aria-controls="long-menu"
aria-expanded={!!anchorEl}
aria-haspopup="true"
role="button"
onClick={onOpen}
data-testid="menu-button"
color="inherit"
className={classes.button}
>
<MoreVert />
</IconButton>
<Popover
aria-labelledby="long-menu"
open={Boolean(anchorEl)}
onClose={onClose}
anchorEl={anchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuList>
<MenuItem onClick={() => window.open(editUrl, '_blank')}>
<ListItemIcon>
<Edit fontSize="small" />
</ListItemIcon>
<ListItemText primary="Edit Configuration" />
</MenuItem>
</MenuList>
</Popover>
</>
);
}