feat: create template form route
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user