From 6331ec1ebcf5c38cda32773fc2afb3ea54823d4f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Apr 2022 12:21:59 +0200 Subject: [PATCH 1/4] scaffolder: add context menu with links to preview and actions Signed-off-by: Patrik Oldsberg --- .changeset/cold-steaks-provide.md | 5 + plugins/scaffolder/api-report.md | 4 + plugins/scaffolder/src/components/Router.tsx | 10 ++ .../ScaffolderPage/ScaffolderPage.tsx | 10 +- .../ScaffolderPageContextMenu.tsx | 107 ++++++++++++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 .changeset/cold-steaks-provide.md create mode 100644 plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx diff --git a/.changeset/cold-steaks-provide.md b/.changeset/cold-steaks-provide.md new file mode 100644 index 0000000000..8a107f7b53 --- /dev/null +++ b/.changeset/cold-steaks-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added a context menu to the scaffolder page that provides links to the template editor and actions reference. These links and the presence of the context menu can be toggled through the `contextMenu` prop of the scaffolder page. diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index f1ab23c320..20945d08a6 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -217,6 +217,10 @@ export type RouterProps = { filter: (entity: Entity) => boolean; }>; defaultPreviewTemplate?: string; + contextMenu?: { + editor?: boolean; + actions?: boolean; + }; }; // @public diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index f46a4cc111..c3efce1020 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -49,6 +49,15 @@ export type RouterProps = { filter: (entity: Entity) => boolean; }>; defaultPreviewTemplate?: string; + /** + * Options for the context menu on the scaffolder page. + */ + contextMenu?: { + /** Whether to show a link to the template editor */ + editor?: boolean; + /** Whether to show a link to the actions documentation */ + actions?: boolean; + }; }; /** @@ -92,6 +101,7 @@ export const Router = (props: RouterProps) => { } /> diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index a68e320c13..79d176cd11 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -39,6 +39,7 @@ import { TemplateList } from '../TemplateList'; import { TemplateTypePicker } from '../TemplateTypePicker'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; import { usePermission } from '@backstage/plugin-permission-react'; +import { ScaffolderPageContextMenu } from './ScaffolderPageContextMenu'; export type ScaffolderPageProps = { TemplateCardComponent?: @@ -48,11 +49,16 @@ export type ScaffolderPageProps = { title?: React.ReactNode; filter: (entity: Entity) => boolean; }>; + contextMenu?: { + editor?: boolean; + actions?: boolean; + }; }; export const ScaffolderPageContents = ({ TemplateCardComponent, groups, + contextMenu, }: ScaffolderPageProps) => { const registerComponentLink = useRouteRef(registerComponentRouteRef); const otherTemplatesGroup = { @@ -73,7 +79,9 @@ export const ScaffolderPageContents = ({ pageTitleOverride="Create a New Component" title="Create a New Component" subtitle="Create new software components using standard templates" - /> + > + + {allowed && ( diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx new file mode 100644 index 0000000000..b79f8917f6 --- /dev/null +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx @@ -0,0 +1,107 @@ +/* + * 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 { useRouteRef } from '@backstage/core-plugin-api'; +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 Description from '@material-ui/icons/Description'; +import Edit from '@material-ui/icons/Edit'; +import MoreVert from '@material-ui/icons/MoreVert'; +import React, { useState } from 'react'; +import { useNavigate } from 'react-router'; +import { rootRouteRef } from '../../routes'; + +const useStyles = makeStyles({ + button: { + color: 'white', + }, +}); + +export type ScaffolderPageContextMenuProps = { + editor?: boolean; + actions?: boolean; +}; + +export function ScaffolderPageContextMenu( + props: ScaffolderPageContextMenuProps, +) { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(); + const pageLink = useRouteRef(rootRouteRef); + const navigate = useNavigate(); + + const showEditor = props.editor !== false; + const showActions = props.actions !== false; + + if (!showEditor && !showActions) { + return null; + } + + const onOpen = (event: React.SyntheticEvent) => { + setAnchorEl(event.currentTarget); + }; + + const onClose = () => { + setAnchorEl(undefined); + }; + + return ( + <> + + + + + + {showEditor && ( + navigate(`${pageLink()}/preview`)}> + + + + + + )} + {showActions && ( + navigate(`${pageLink()}/actions`)}> + + + + + + )} + + + + ); +} From 8fad3b6ef599ec8e6e0cc9628bd6d1b97be07c82 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Apr 2022 14:10:46 +0200 Subject: [PATCH 2/4] scaffolder: rename and move /preview -> /edit Signed-off-by: Patrik Oldsberg --- .changeset/moody-sheep-marry.md | 5 +++++ plugins/scaffolder/src/components/Router.tsx | 10 ++++++---- .../ScaffolderPage/ScaffolderPageContextMenu.tsx | 2 +- .../TemplateEditorPage.tsx} | 4 ++-- .../index.ts | 2 +- 5 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .changeset/moody-sheep-marry.md rename plugins/scaffolder/src/components/{TemplatePreviewPage/TemplatePreviewPage.tsx => TemplateEditorPage/TemplateEditorPage.tsx} (99%) rename plugins/scaffolder/src/components/{TemplatePreviewPage => TemplateEditorPage}/index.ts (90%) diff --git a/.changeset/moody-sheep-marry.md b/.changeset/moody-sheep-marry.md new file mode 100644 index 0000000000..73f037db32 --- /dev/null +++ b/.changeset/moody-sheep-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +The Template Preview page has been renamed to Template Editor, and is not available at the `/edit` path instead. There is a redirect in place from `/preview`. diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index c3efce1020..3ab3b03dd8 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentType } from 'react'; -import { Routes, Route, useOutlet } from 'react-router'; +import { Routes, Route, useOutlet, Navigate } from 'react-router'; import { Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScaffolderPage } from './ScaffolderPage'; @@ -23,7 +23,7 @@ import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; import { ActionsPage } from './ActionsPage'; import { SecretsContextProvider } from './secrets/SecretsContext'; -import { TemplatePreviewPage } from './TemplatePreviewPage'; +import { TemplateEditorPage } from './TemplateEditorPage'; import { FieldExtensionOptions, @@ -116,16 +116,18 @@ export const Router = (props: RouterProps) => { } /> } /> - } /> + + } /> ); }; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx index b79f8917f6..6d9a89cfab 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.tsx @@ -85,7 +85,7 @@ export function ScaffolderPageContextMenu( > {showEditor && ( - navigate(`${pageLink()}/preview`)}> + navigate(`${pageLink()}/edit`)}> diff --git a/plugins/scaffolder/src/components/TemplatePreviewPage/TemplatePreviewPage.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx similarity index 99% rename from plugins/scaffolder/src/components/TemplatePreviewPage/TemplatePreviewPage.tsx rename to plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx index cb4f8e9896..aa04e8d1c6 100644 --- a/plugins/scaffolder/src/components/TemplatePreviewPage/TemplatePreviewPage.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -92,7 +92,7 @@ const useStyles = makeStyles({ }, }); -export const TemplatePreviewPage = ({ +export const TemplateEditorPage = ({ defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML, customFieldExtensions = [], }: { @@ -200,7 +200,7 @@ export const TemplatePreviewPage = ({ return (
diff --git a/plugins/scaffolder/src/components/TemplatePreviewPage/index.ts b/plugins/scaffolder/src/components/TemplateEditorPage/index.ts similarity index 90% rename from plugins/scaffolder/src/components/TemplatePreviewPage/index.ts rename to plugins/scaffolder/src/components/TemplateEditorPage/index.ts index cdf7bd1e82..506ff08f24 100644 --- a/plugins/scaffolder/src/components/TemplatePreviewPage/index.ts +++ b/plugins/scaffolder/src/components/TemplateEditorPage/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { TemplatePreviewPage } from './TemplatePreviewPage'; +export { TemplateEditorPage } from './TemplateEditorPage'; From 881acb0dbc76a20ba4c2dab2fb831e2822c17a83 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Apr 2022 14:44:26 +0200 Subject: [PATCH 3/4] scaffolder: add editRouteRef and updated route declarations to use route refs Signed-off-by: Patrik Oldsberg --- plugins/scaffolder/src/components/Router.tsx | 15 ++++++++++----- plugins/scaffolder/src/next/Router/Router.tsx | 4 ++-- plugins/scaffolder/src/routes.ts | 6 ++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 3ab3b03dd8..774bc61638 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -32,6 +32,12 @@ import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS, } from '../extensions'; import { useElementFilter } from '@backstage/core-plugin-api'; +import { + actionsRouteRef, + editRouteRef, + scaffolderTaskRouteRef, + selectedTemplateRouteRef, +} from '../routes'; /** * The props for the entrypoint `ScaffolderPage` component the plugin. @@ -96,7 +102,6 @@ export const Router = (props: RouterProps) => { return ( { } /> } /> - } /> - } /> + } /> + } /> ) => { return ( ) => { /> diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 97f942d8b9..8ec9f9b68a 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -45,3 +45,9 @@ export const actionsRouteRef = createSubRouteRef({ parent: rootRouteRef, path: '/actions', }); + +export const editRouteRef = createSubRouteRef({ + id: 'scaffolder/edit', + parent: rootRouteRef, + path: '/edit', +}); From 001c63450eef30be6d0ff411e8620b48d828d916 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Apr 2022 16:26:19 +0200 Subject: [PATCH 4/4] scaffolder: add context menu tests Signed-off-by: Patrik Oldsberg --- .../ScaffolderPageContextMenu.test.tsx | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx new file mode 100644 index 0000000000..2d07416025 --- /dev/null +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 userEvent from '@testing-library/user-event'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { rootRouteRef } from '../../routes'; +import { ScaffolderPageContextMenu } from './ScaffolderPageContextMenu'; + +describe('ScaffolderPageContextMenu', () => { + it('does not render anything if fully disabled', async () => { + await renderInTestApp( +
+ +
, + { mountedRoutes: { '/': rootRouteRef } }, + ); + + expect(screen.getByTestId('container')).toBeEmptyDOMElement(); + }); + + it('renders the editor option', async () => { + await renderInTestApp( +
+ +
, + { + mountedRoutes: { '/': rootRouteRef }, + }, + ); + + await userEvent.click(screen.getByTestId('container').firstElementChild!); + + expect(screen.queryByText('Template Editor')).toBeInTheDocument(); + expect(screen.queryByText('Installed Actions')).not.toBeInTheDocument(); + }); + + it('renders the actions option', async () => { + await renderInTestApp( +
+ +
, + { + mountedRoutes: { '/': rootRouteRef }, + }, + ); + + await userEvent.click(screen.getByTestId('container').firstElementChild!); + + expect(screen.queryByText('Template Editor')).not.toBeInTheDocument(); + expect(screen.queryByText('Installed Actions')).toBeInTheDocument(); + }); + + it('renders all options', async () => { + await renderInTestApp( +
+ +
, + { + mountedRoutes: { '/': rootRouteRef }, + }, + ); + + await userEvent.click(screen.getByTestId('container').firstElementChild!); + + expect(screen.queryByText('Template Editor')).toBeInTheDocument(); + expect(screen.queryByText('Installed Actions')).toBeInTheDocument(); + }); +});