feat: create template form route

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-09-19 13:59:04 +02:00
parent c576a36d14
commit 7a3d6227de
10 changed files with 223 additions and 117 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Create a separate route for the template form editor so we refresh it without being redirected to scaffolder edit page.
+1
View File
@@ -614,6 +614,7 @@ export const scaffolderPlugin: BackstagePlugin<
edit: SubRouteRef<undefined>;
editor: SubRouteRef<undefined>;
customFields: SubRouteRef<undefined>;
templateForm: SubRouteRef<undefined>;
},
{
registerComponent: ExternalRouteRef<undefined, true>;
@@ -40,6 +40,7 @@ import {
scaffolderListTaskRouteRef,
scaffolderTaskRouteRef,
selectedTemplateRouteRef,
templateFormRouteRef,
} from '../../routes';
import { ErrorPage } from '@backstage/core-components';
@@ -54,6 +55,7 @@ import { TemplateListPage, TemplateWizardPage } from '../../next';
import { OngoingTask } from '../OngoingTask';
import {
TemplatePage,
TemplateFormPage,
TemplateEditorPage,
CustomFieldsPage,
} from '../../next/TemplateEditorPage';
@@ -169,11 +171,7 @@ export const Router = (props: PropsWithChildren<RouterProps>) => {
path={editRouteRef.path}
element={
<SecretsContextProvider>
<TemplateEditorPage
customFieldExtensions={fieldExtensions}
layouts={customLayouts}
formProps={props.formProps}
/>
<TemplateEditorPage />
</SecretsContextProvider>
}
/>
@@ -185,6 +183,18 @@ export const Router = (props: PropsWithChildren<RouterProps>) => {
</SecretsContextProvider>
}
/>
<Route
path={templateFormRouteRef.path}
element={
<SecretsContextProvider>
<TemplateFormPage
layouts={customLayouts}
formProps={props.formProps}
fieldExtensions={fieldExtensions}
/>
</SecretsContextProvider>
}
/>
<Route path={actionsRouteRef.path} element={<ActionsPage />} />
<Route
@@ -13,18 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import React from 'react';
import { Content, Header, Page } from '@backstage/core-components';
import {
TemplateDirectoryAccess,
WebFileSystemAccess,
} from '../../lib/filesystem';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
import {
FieldExtensionOptions,
FormProps,
type LayoutOptions,
} from '@backstage/plugin-scaffolder-react';
import { WebFileSystemAccess } from '../../lib/filesystem';
import { TemplateEditorIntro } from './TemplateEditorIntro';
import { ScaffolderPageContextMenu } from '@backstage/plugin-scaffolder-react/alpha';
import { useNavigate } from 'react-router-dom';
@@ -35,42 +26,21 @@ import {
customFieldsRouteRef,
rootRouteRef,
scaffolderListTaskRouteRef,
templateFormRouteRef,
} from '../../routes';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { scaffolderTranslationRef } from '../../translation';
import { WebFileSystemStore } from '../../lib/filesystem/WebFileSystemAccess';
import { createExampleTemplate } from '../../lib/filesystem/createExampleTemplate';
type Selection =
| {
type: 'local';
directory: TemplateDirectoryAccess;
}
| {
type: 'create-template';
}
| {
type: 'form';
}
| {
type: 'field-explorer';
};
interface TemplateEditorPageProps {
defaultPreviewTemplate?: string;
customFieldExtensions?: FieldExtensionOptions<any, any>[];
layouts?: LayoutOptions[];
formProps?: FormProps;
}
export function TemplateEditorPage(props: TemplateEditorPageProps) {
const [selection, setSelection] = useState<Selection>();
export function TemplateEditorPage() {
const navigate = useNavigate();
const actionsLink = useRouteRef(actionsRouteRef);
const tasksLink = useRouteRef(scaffolderListTaskRouteRef);
const createLink = useRouteRef(rootRouteRef);
const editorLink = useRouteRef(editorRouteRef);
const customFieldsLink = useRouteRef(customFieldsRouteRef);
const templateFormLink = useRouteRef(templateFormRouteRef);
const { t } = useTranslationRef(scaffolderTranslationRef);
const scaffolderPageContextMenuProps = {
@@ -80,19 +50,14 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
onCreateClicked: () => navigate(createLink()),
};
let content: JSX.Element | null = null;
if (selection?.type === 'form') {
content = (
<TemplateFormPreviewer
defaultPreviewTemplate={props.defaultPreviewTemplate}
customFieldExtensions={props.customFieldExtensions}
onClose={() => setSelection(undefined)}
layouts={props.layouts}
formProps={props.formProps}
/>
);
} else {
content = (
return (
<Page themeId="home">
<Header
title={t('templateEditorPage.title')}
subtitle={t('templateEditorPage.subtitle')}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
<Content>
<TemplateEditorIntro
onSelect={option => {
@@ -111,25 +76,13 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
})
.catch(() => {});
} else if (option === 'form') {
setSelection({ type: 'form' });
navigate(templateFormLink());
} else if (option === 'field-explorer') {
navigate(customFieldsLink());
}
}}
/>
</Content>
);
}
return (
<Page themeId="home">
<Header
title={t('templateEditorPage.title')}
subtitle={t('templateEditorPage.subtitle')}
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
{content}
</Page>
);
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useState } from 'react';
import React, { ReactNode, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
@@ -49,18 +49,29 @@ const useStyles = makeStyles(
},
toolbar: {
display: 'grid',
justifyItems: 'flex-end',
gridTemplateColumns: 'auto 1fr',
gridGap: theme.spacing(1),
padding: theme.spacing(0, 1),
backgroundColor: theme.palette.background.paper,
},
toolbarCustomActions: {
display: 'grid',
alignItems: 'center',
gridAutoFlow: 'Column',
gridGap: theme.spacing(1),
},
toolbarDefaultActions: {
justifySelf: 'end',
},
}),
{ name: 'ScaffolderTemplateEditorToolbar' },
);
export function TemplateEditorToolbar(props: {
children?: ReactNode;
fieldExtensions?: FieldExtensionOptions<any, any>[];
}) {
const { fieldExtensions } = props;
const { children, fieldExtensions } = props;
const classes = useStyles();
const [showFieldsDrawer, setShowFieldsDrawer] = useState(false);
const [showActionsDrawer, setShowActionsDrawer] = useState(false);
@@ -69,14 +80,19 @@ export function TemplateEditorToolbar(props: {
return (
<AppBar className={classes.appbar} position="relative">
<Toolbar className={classes.toolbar}>
<ButtonGroup variant="outlined" color="primary">
<div className={classes.toolbarCustomActions}>{children}</div>
<ButtonGroup
className={classes.toolbarDefaultActions}
variant="outlined"
color="primary"
>
<Tooltip title="Custom Fields Explorer">
<Button size="small" onClick={() => setShowFieldsDrawer(true)}>
<Button onClick={() => setShowFieldsDrawer(true)}>
<ExtensionIcon />
</Button>
</Tooltip>
<Tooltip title="Installed Actions Documentation">
<Button size="small" onClick={() => setShowActionsDrawer(true)}>
<Button onClick={() => setShowActionsDrawer(true)}>
<DescriptionIcon />
</Button>
</Tooltip>
@@ -0,0 +1,76 @@
/*
* Copyright 2024 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 React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { makeStyles } from '@material-ui/core/styles';
import { Page, Header, Content } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import {
FormProps,
LayoutOptions,
FieldExtensionOptions,
} from '@backstage/plugin-scaffolder-react';
import { editRouteRef } from '../../routes';
import { scaffolderTranslationRef } from '../../translation';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
const useStyles = makeStyles({
root: {
padding: 0,
},
});
interface TemplateFormPageProps {
layouts?: LayoutOptions[];
formProps?: FormProps;
fieldExtensions?: FieldExtensionOptions<any, any>[];
defaultPreviewTemplate?: string;
}
export function TemplateFormPage(props: TemplateFormPageProps) {
const classes = useStyles();
const navigate = useNavigate();
const editLink = useRouteRef(editRouteRef);
const { t } = useTranslationRef(scaffolderTranslationRef);
const handleClose = useCallback(() => {
navigate(editLink());
}, [navigate, editLink]);
return (
<Page themeId="home">
<Header
title={t('templateEditorPage.title')}
subtitle={t('templateEditorPage.subtitle')}
/>
<Content className={classes.root}>
<TemplateFormPreviewer
layouts={props.layouts}
formProps={props.formProps}
customFieldExtensions={props.fieldExtensions}
defaultPreviewTemplate={props.defaultPreviewTemplate}
onClose={handleClose}
/>
</Content>
</Page>
);
}
@@ -13,18 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import {
catalogApiRef,
humanizeEntityRef,
} from '@backstage/plugin-catalog-react';
import FormControl from '@material-ui/core/FormControl';
import IconButton from '@material-ui/core/IconButton';
import InputLabel from '@material-ui/core/InputLabel';
import LinearProgress from '@material-ui/core/LinearProgress';
import MenuItem from '@material-ui/core/MenuItem';
import Paper from '@material-ui/core/Paper';
import FormControl from '@material-ui/core/FormControl';
import Input from '@material-ui/core/Input';
import Select from '@material-ui/core/Select';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import MenuItem from '@material-ui/core/MenuItem';
import { makeStyles } from '@material-ui/core/styles';
import CloseIcon from '@material-ui/icons/Close';
import React, { useCallback, useState } from 'react';
@@ -35,6 +38,7 @@ import {
FieldExtensionOptions,
FormProps,
} from '@backstage/plugin-scaffolder-react';
import { TemplateEditorToolbar } from './TemplateEditorToolbar';
import { TemplateEditorForm } from './TemplateEditorForm';
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
@@ -94,27 +98,40 @@ export type ScaffolderTemplateFormPreviewerClassKey =
const useStyles = makeStyles(
theme => ({
root: {
height: '100%',
gridArea: 'pageContent',
display: 'grid',
gridTemplateAreas: `
"controls controls"
"toolbar toolbar"
"textArea preview"
`,
gridTemplateRows: 'auto 1fr',
gridTemplateColumns: '1fr 1fr',
},
controls: {
gridArea: 'controls',
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
margin: theme.spacing(1),
toolbar: {
gridArea: 'toolbar',
},
textArea: {
gridArea: 'textArea',
height: '100%',
},
preview: {
gridArea: 'preview',
position: 'relative',
borderLeft: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.default,
},
scroll: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
padding: theme.spacing(1),
},
formControl: {
minWidth: 120,
maxWidth: 300,
},
}),
{ name: 'ScaffolderTemplateFormPreviewer' },
@@ -137,8 +154,9 @@ export const TemplateFormPreviewer = ({
const { t } = useTranslationRef(scaffolderTranslationRef);
const alertApi = useApi(alertApiRef);
const catalogApi = useApi(catalogApiRef);
const [selectedTemplate, setSelectedTemplate] = useState('');
const [errorText, setErrorText] = useState<string>();
const [selectedTemplate, setSelectedTemplate] =
useState<TemplateOption | null>(null);
const [templateOptions, setTemplateOptions] = useState<TemplateOption[]>([]);
const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate);
@@ -188,29 +206,45 @@ export const TemplateFormPreviewer = ({
return (
<>
{loading && <LinearProgress />}
<main className={classes.root}>
<div className={classes.controls}>
<FormControl variant="outlined" size="small" fullWidth>
<InputLabel id="select-template-label">
{t('templateEditorPage.templateFormPreviewer.title')}
</InputLabel>
<Select
value={selectedTemplate}
label={t('templateEditorPage.templateFormPreviewer.title')}
labelId="select-template-label"
onChange={e => handleSelectChange(e.target.value)}
>
{templateOptions.map((option, idx) => (
<MenuItem key={idx} value={option.value as any}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<IconButton size="medium" onClick={onClose}>
<CloseIcon />
</IconButton>
<Paper
className={classes.root}
component="main"
variant="outlined"
square
>
<div className={classes.toolbar}>
<TemplateEditorToolbar fieldExtensions={customFieldExtensions}>
<Tooltip title="Close editor">
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</Tooltip>
<FormControl className={classes.formControl}>
<Select
displayEmpty
value={selectedTemplate}
onChange={e => handleSelectChange(e.target.value)}
input={<Input />}
renderValue={selected => {
if (!selected) {
return t('templateEditorPage.templateFormPreviewer.title');
}
return (selected as Entity).metadata.title;
}}
inputProps={{
'aria-label': t(
'templateEditorPage.templateFormPreviewer.title',
),
}}
>
{templateOptions.map((option, index) => (
<MenuItem key={index} value={option.value as any}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
</TemplateEditorToolbar>
</div>
<div className={classes.textArea}>
<TemplateEditorTextArea
@@ -220,16 +254,18 @@ export const TemplateFormPreviewer = ({
/>
</div>
<div className={classes.preview}>
<TemplateEditorForm
content={templateYaml}
contentIsSpec
fieldExtensions={customFieldExtensions}
setErrorText={setErrorText}
layouts={layouts}
formProps={formProps}
/>
<div className={classes.scroll}>
<TemplateEditorForm
content={templateYaml}
contentIsSpec
fieldExtensions={customFieldExtensions}
setErrorText={setErrorText}
layouts={layouts}
formProps={formProps}
/>
</div>
</div>
</main>
</Paper>
</>
);
};
@@ -15,6 +15,7 @@
*/
export { TemplatePage } from './TemplatePage';
export { TemplateFormPage } from './TemplateFormPage';
export { TemplateEditorPage } from './TemplateEditorPage';
export { CustomFieldsPage } from './CustomFieldsPage';
export type { ScaffolderCustomFieldExplorerClassKey } from './CustomFieldExplorer';
+2
View File
@@ -70,6 +70,7 @@ import {
editRouteRef,
editorRouteRef,
customFieldsRouteRef,
templateFormRouteRef,
} from './routes';
import {
MyGroupsPicker,
@@ -111,6 +112,7 @@ export const scaffolderPlugin = createPlugin({
edit: editRouteRef,
editor: editorRouteRef,
customFields: customFieldsRouteRef,
templateForm: templateFormRouteRef,
},
externalRoutes: {
registerComponent: registerComponentRouteRef,
+7 -1
View File
@@ -86,7 +86,13 @@ export const editorRouteRef = createSubRouteRef({
});
export const customFieldsRouteRef = createSubRouteRef({
id: 'scaffolder/edit',
id: 'scaffolder/customFields',
parent: rootRouteRef,
path: '/custom-fields',
});
export const templateFormRouteRef = createSubRouteRef({
id: 'scaffolder/editorForm',
parent: rootRouteRef,
path: '/template-form',
});