refactor: standardize template editor layout
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Standardize template editor pages desktop and mobile layouts.
|
||||
@@ -173,6 +173,7 @@ export type ScaffolderCustomFieldExplorerClassKey =
|
||||
// @public (undocumented)
|
||||
export type ScaffolderTemplateEditorClassKey =
|
||||
| 'root'
|
||||
| 'toolbar'
|
||||
| 'browser'
|
||||
| 'editor'
|
||||
| 'preview'
|
||||
@@ -181,6 +182,7 @@ export type ScaffolderTemplateEditorClassKey =
|
||||
// @public (undocumented)
|
||||
export type ScaffolderTemplateFormPreviewerClassKey =
|
||||
| 'root'
|
||||
| 'toolbar'
|
||||
| 'controls'
|
||||
| 'textArea'
|
||||
| 'preview';
|
||||
@@ -275,9 +277,13 @@ export const scaffolderTranslationRef: TranslationRef<
|
||||
readonly 'ongoingTask.showLogsButtonTitle': 'Show Logs';
|
||||
readonly 'templateEditorForm.stepper.emptyText': 'There are no spec parameters in the template to preview.';
|
||||
readonly 'templateTypePicker.title': 'Categories';
|
||||
readonly 'templateFormPage.title': 'Template Form Playground';
|
||||
readonly 'templateFormPage.subtitle': 'Edit, preview, and try out templates and template forms';
|
||||
readonly 'templateEditorPage.title': 'Manage Templates';
|
||||
readonly 'templateIntroPage.title': 'Manage Templates';
|
||||
readonly 'templateIntroPage.subtitle': 'Edit, preview, and try out templates, forms, and custom fields';
|
||||
readonly 'templateFormPage.title': 'Template Editor';
|
||||
readonly 'templateFormPage.subtitle': 'Edit, preview, and try out templates forms';
|
||||
readonly 'templateCustomFieldPage.title': 'Custom Field Explorer';
|
||||
readonly 'templateCustomFieldPage.subtitle': 'Edit, preview, and try out custom fields';
|
||||
readonly 'templateEditorPage.title': 'Template Editor';
|
||||
readonly 'templateEditorPage.subtitle': 'Edit, preview, and try out templates and template forms';
|
||||
readonly 'templateEditorPage.dryRunResults.title': 'Dry-run results';
|
||||
readonly 'templateEditorPage.dryRunResultsList.title': 'Result {{resultId}}';
|
||||
@@ -303,12 +309,13 @@ export const scaffolderTranslationRef: TranslationRef<
|
||||
readonly 'templateEditorPage.templateEditorIntro.createLocal.title': 'Create New Template';
|
||||
readonly 'templateEditorPage.templateEditorIntro.createLocal.description': 'Create a local template directory, allowing you to both edit and try executing your own template.';
|
||||
readonly 'templateEditorPage.templateEditorIntro.createLocal.unsupportedTooltip': 'Only supported in some Chromium-based browsers';
|
||||
readonly 'templateEditorPage.templateEditorIntro.formEditor.title': 'Template playground';
|
||||
readonly 'templateEditorPage.templateEditorIntro.formEditor.title': 'Template Form Playground';
|
||||
readonly 'templateEditorPage.templateEditorIntro.formEditor.description': 'Preview and edit a template form, either using a sample template or by loading a template from the catalog.';
|
||||
readonly 'templateEditorPage.templateEditorIntro.fieldExplorer.title': 'Custom Field Explorer';
|
||||
readonly 'templateEditorPage.templateEditorIntro.fieldExplorer.description': 'View and play around with available installed custom field extensions.';
|
||||
readonly 'templateEditorPage.templateEditorTextArea.saveIconTooltip': 'Save file';
|
||||
readonly 'templateEditorPage.templateEditorTextArea.refreshIconTooltip': 'Reload file';
|
||||
readonly 'templateEditorPage.templateEditorTextArea.emptyStateParagraph': 'Please select an action on the file menu.';
|
||||
readonly 'templateEditorPage.templateFormPreviewer.title': 'Load Existing Template';
|
||||
readonly 'templateListPage.title': 'Create a new component';
|
||||
readonly 'templateListPage.subtitle': 'Create new software components using standard templates in your organization';
|
||||
@@ -330,6 +337,11 @@ export const scaffolderTranslationRef: TranslationRef<
|
||||
readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog';
|
||||
readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/';
|
||||
readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation';
|
||||
readonly 'templateEditorToolbarFileMenu.button': 'File';
|
||||
readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory';
|
||||
readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory';
|
||||
readonly 'templateEditorToolbarFileMenu.options.closeEditor': 'Close template editor';
|
||||
readonly 'templateEditorToolbarTemplatesMenu.button': 'Templates';
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -370,7 +382,7 @@ export type TemplateWizardPageProps = {
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/alpha/components/TemplateEditorPage/CustomFieldExplorer.d.ts:4:1 - (ae-undocumented) Missing documentation for "ScaffolderCustomFieldExplorerClassKey".
|
||||
// src/alpha/components/TemplateEditorPage/TemplateEditor.d.ts:6:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateEditorClassKey".
|
||||
// src/alpha/components/TemplateEditorPage/TemplateEditor.d.ts:5:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateEditorClassKey".
|
||||
// src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.d.ts:4:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateFormPreviewerClassKey".
|
||||
// src/alpha/components/TemplateListPage/TemplateListPage.d.ts:7:1 - (ae-undocumented) Missing documentation for "TemplateListPageProps".
|
||||
// src/alpha/components/TemplateWizardPage/TemplateWizardPage.d.ts:6:1 - (ae-undocumented) Missing documentation for "TemplateWizardPageProps".
|
||||
|
||||
+49
-33
@@ -19,13 +19,7 @@ import Button from '@material-ui/core/Button';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardHeader from '@material-ui/core/CardHeader';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import yaml from 'yaml';
|
||||
@@ -35,6 +29,10 @@ import validator from '@rjsf/validator-ajv8';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
|
||||
/** @public */
|
||||
export type ScaffolderCustomFieldExplorerClassKey =
|
||||
@@ -48,25 +46,35 @@ const useStyles = makeStyles(
|
||||
root: {
|
||||
gridArea: 'pageContent',
|
||||
display: 'grid',
|
||||
gridGap: theme.spacing(2),
|
||||
gridTemplateAreas: `
|
||||
"controls"
|
||||
"fieldForm"
|
||||
"preview"
|
||||
`,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
gridTemplateAreas: `
|
||||
"controls controls"
|
||||
"fieldForm preview"
|
||||
`,
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
},
|
||||
},
|
||||
controls: {
|
||||
gridArea: 'controls',
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
fieldForm: {
|
||||
gridArea: 'fieldForm',
|
||||
},
|
||||
preview: {
|
||||
gridArea: 'preview',
|
||||
display: 'grid',
|
||||
gridGap: theme.spacing(2),
|
||||
alignContent: 'start',
|
||||
},
|
||||
}),
|
||||
{ name: 'ScaffolderCustomFieldExplorer' },
|
||||
@@ -74,10 +82,8 @@ const useStyles = makeStyles(
|
||||
|
||||
export const CustomFieldExplorer = ({
|
||||
customFieldExtensions = [],
|
||||
onClose,
|
||||
}: {
|
||||
customFieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
onClose?: () => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
@@ -131,29 +137,39 @@ export const CustomFieldExplorer = ({
|
||||
return (
|
||||
<main className={classes.root}>
|
||||
<div className={classes.controls}>
|
||||
<FormControl variant="outlined" size="small" fullWidth>
|
||||
<InputLabel id="select-field-label">
|
||||
{t('templateEditorPage.customFieldExplorer.selectFieldLabel')}
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={selectedField}
|
||||
label={t('templateEditorPage.customFieldExplorer.selectFieldLabel')}
|
||||
labelId="select-field-label"
|
||||
onChange={e =>
|
||||
handleSelectionChange(e.target.value as FieldExtensionOptions)
|
||||
<Autocomplete
|
||||
id="custom-fields-autocomplete"
|
||||
value={selectedField}
|
||||
options={fieldOptions}
|
||||
getOptionLabel={option => option.name}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
aria-label={t(
|
||||
'templateEditorPage.customFieldExplorer.selectFieldLabel',
|
||||
)}
|
||||
placeholder={t(
|
||||
'templateEditorPage.customFieldExplorer.selectFieldLabel',
|
||||
)}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onChange={(_event, option) => {
|
||||
if (option) {
|
||||
handleSelectionChange(option);
|
||||
}
|
||||
>
|
||||
{fieldOptions.map((option, idx) => (
|
||||
<MenuItem key={idx} value={option as any}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<IconButton size="medium" onClick={onClose} aria-label="Close">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
}}
|
||||
disableClearable
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.fieldForm}>
|
||||
<Card>
|
||||
|
||||
+37
-23
@@ -22,16 +22,16 @@ import { StreamLanguage } from '@codemirror/language';
|
||||
import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Accordion from '@material-ui/core/Accordion';
|
||||
import AccordionSummary from '@material-ui/core/AccordionSummary';
|
||||
import AccordionDetails from '@material-ui/core/AccordionDetails';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { Form } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
@@ -48,7 +48,7 @@ const useStyles = makeStyles(
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
},
|
||||
controls: {
|
||||
marginBottom: theme.spacing(2),
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
code: {
|
||||
width: '100%',
|
||||
@@ -114,25 +114,39 @@ export const CustomFieldPlaygroud = ({
|
||||
return (
|
||||
<main className={classes.root}>
|
||||
<div className={classes.controls}>
|
||||
<FormControl variant="outlined" fullWidth>
|
||||
<InputLabel id="select-field-label">
|
||||
{t('templateEditorPage.customFieldExplorer.selectFieldLabel')}
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={selectedField}
|
||||
label={t('templateEditorPage.customFieldExplorer.selectFieldLabel')}
|
||||
labelId="select-field-label"
|
||||
onChange={e =>
|
||||
handleSelectionChange(e.target.value as FieldExtensionOptions)
|
||||
<Autocomplete
|
||||
id="custom-fields-autocomplete"
|
||||
value={selectedField}
|
||||
options={fieldOptions}
|
||||
getOptionLabel={option => option.name}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
aria-label={t(
|
||||
'templateEditorPage.customFieldExplorer.selectFieldLabel',
|
||||
)}
|
||||
placeholder={t(
|
||||
'templateEditorPage.customFieldExplorer.selectFieldLabel',
|
||||
)}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onChange={(_event, option) => {
|
||||
if (option) {
|
||||
handleSelectionChange(option);
|
||||
}
|
||||
>
|
||||
{fieldOptions.map((option, idx) => (
|
||||
<MenuItem key={idx} value={option as any}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
}}
|
||||
disableClearable
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Accordion defaultExpanded>
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
import { Page, Header, Content } from '@backstage/core-components';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
@@ -32,25 +31,19 @@ interface CustomFieldsPageProps {
|
||||
}
|
||||
|
||||
export function CustomFieldsPage(props: CustomFieldsPageProps) {
|
||||
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')}
|
||||
title={t('templateCustomFieldPage.title')}
|
||||
subtitle={t('templateCustomFieldPage.subtitle')}
|
||||
type={t('templateIntroPage.title')}
|
||||
typeLink={editLink()}
|
||||
/>
|
||||
<Content>
|
||||
<CustomFieldExplorer
|
||||
customFieldExtensions={props.fieldExtensions}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<CustomFieldExplorer customFieldExtensions={props.fieldExtensions} />
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
|
||||
+6
-4
@@ -209,7 +209,11 @@ export function DirectoryEditorProvider(props: DirectoryEditorProviderProps) {
|
||||
const { directory } = props;
|
||||
|
||||
const [{ result, error }, { execute }] = useAsync(
|
||||
async (dir: TemplateDirectoryAccess) => {
|
||||
async (dir?: TemplateDirectoryAccess) => {
|
||||
if (!dir) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const manager = new DirectoryEditorManager(dir);
|
||||
await manager.reload();
|
||||
|
||||
@@ -223,9 +227,7 @@ export function DirectoryEditorProvider(props: DirectoryEditorProviderProps) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (directory) {
|
||||
execute(directory);
|
||||
}
|
||||
execute(directory);
|
||||
}, [execute, directory]);
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -13,126 +13,84 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useState } from 'react';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import type {
|
||||
FormProps,
|
||||
LayoutOptions,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { TemplateDirectoryAccess } from '../../../lib/filesystem';
|
||||
import { DirectoryEditorProvider } from './DirectoryEditorContext';
|
||||
import {
|
||||
TemplateEditorLayout,
|
||||
TemplateEditorLayoutToolbar,
|
||||
TemplateEditorLayoutBrowser,
|
||||
TemplateEditorLayoutFiles,
|
||||
TemplateEditorLayoutPreview,
|
||||
TemplateEditorLayoutConsole,
|
||||
} from './TemplateEditorLayout';
|
||||
import { TemplateEditorToolbar } from './TemplateEditorToolbar';
|
||||
import { TemplateEditorToolbarFileMenu } from './TemplateEditorToolbarFileMenu';
|
||||
import { TemplateEditorBrowser } from './TemplateEditorBrowser';
|
||||
import { DryRunProvider } from './DryRunContext';
|
||||
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
|
||||
import { TemplateEditorForm } from './TemplateEditorForm';
|
||||
import { DryRunResults } from './DryRunResults';
|
||||
import { useTemplateDirectory } from './useTemplateDirectory';
|
||||
|
||||
/** @public */
|
||||
export type ScaffolderTemplateEditorClassKey =
|
||||
| 'root'
|
||||
| 'toolbar'
|
||||
| 'browser'
|
||||
| 'editor'
|
||||
| 'preview'
|
||||
| 'results';
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
// Reset and fix sizing to make sure scrolling behaves correctly
|
||||
root: {
|
||||
height: '100%',
|
||||
gridArea: 'pageContent',
|
||||
display: 'grid',
|
||||
gridTemplateAreas: `
|
||||
"toolbar toolbar toolbar"
|
||||
"browser editor preview"
|
||||
"results results results"
|
||||
`,
|
||||
gridTemplateColumns: '1fr 3fr 2fr',
|
||||
gridTemplateRows: 'auto 1fr auto',
|
||||
},
|
||||
toolbar: {
|
||||
gridArea: 'toolbar',
|
||||
},
|
||||
browser: {
|
||||
gridArea: 'browser',
|
||||
overflow: 'auto',
|
||||
},
|
||||
editor: {
|
||||
gridArea: 'editor',
|
||||
overflow: 'auto',
|
||||
borderLeft: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
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),
|
||||
overflow: 'auto',
|
||||
},
|
||||
results: {
|
||||
gridArea: 'results',
|
||||
},
|
||||
}),
|
||||
{ name: 'ScaffolderTemplateEditor' },
|
||||
);
|
||||
|
||||
export const TemplateEditor = (props: {
|
||||
directory?: TemplateDirectoryAccess;
|
||||
fieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
layouts?: LayoutOptions[];
|
||||
onClose?: () => void;
|
||||
formProps?: FormProps;
|
||||
onLoad?: () => void;
|
||||
fieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const { layouts, formProps, fieldExtensions } = props;
|
||||
const [errorText, setErrorText] = useState<string>();
|
||||
const {
|
||||
directory,
|
||||
handleOpenDirectory,
|
||||
handleCreateDirectory,
|
||||
handleCloseDirectory,
|
||||
} = useTemplateDirectory();
|
||||
|
||||
return (
|
||||
<DirectoryEditorProvider directory={props.directory}>
|
||||
<DirectoryEditorProvider directory={directory}>
|
||||
<DryRunProvider>
|
||||
<Paper
|
||||
className={classes.root}
|
||||
component="main"
|
||||
variant="outlined"
|
||||
square
|
||||
>
|
||||
<section className={classes.toolbar}>
|
||||
<TemplateEditorToolbar fieldExtensions={props.fieldExtensions} />
|
||||
</section>
|
||||
<section className={classes.browser}>
|
||||
<TemplateEditorBrowser onClose={props.onClose} />
|
||||
</section>
|
||||
<section className={classes.editor}>
|
||||
<TemplateEditorTextArea.DirectoryEditor
|
||||
errorText={errorText}
|
||||
onLoad={props.onLoad}
|
||||
/>
|
||||
</section>
|
||||
<section className={classes.preview}>
|
||||
<div className={classes.scroll}>
|
||||
<TemplateEditorForm.DirectoryEditorDryRun
|
||||
setErrorText={setErrorText}
|
||||
fieldExtensions={props.fieldExtensions}
|
||||
layouts={props.layouts}
|
||||
formProps={props.formProps}
|
||||
<TemplateEditorLayout>
|
||||
<TemplateEditorLayoutToolbar>
|
||||
<TemplateEditorToolbar fieldExtensions={fieldExtensions}>
|
||||
<TemplateEditorToolbarFileMenu
|
||||
onOpenDirectory={handleOpenDirectory}
|
||||
onCloseDirectory={handleCloseDirectory}
|
||||
onCreateDirectory={handleCreateDirectory}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className={classes.results}>
|
||||
</TemplateEditorToolbar>
|
||||
</TemplateEditorLayoutToolbar>
|
||||
<TemplateEditorLayoutBrowser>
|
||||
<TemplateEditorBrowser onClose={handleCloseDirectory} />
|
||||
</TemplateEditorLayoutBrowser>
|
||||
<TemplateEditorLayoutFiles>
|
||||
<TemplateEditorTextArea.DirectoryEditor errorText={errorText} />
|
||||
</TemplateEditorLayoutFiles>
|
||||
<TemplateEditorLayoutPreview>
|
||||
<TemplateEditorForm.DirectoryEditorDryRun
|
||||
setErrorText={setErrorText}
|
||||
fieldExtensions={fieldExtensions}
|
||||
layouts={layouts}
|
||||
formProps={formProps}
|
||||
/>
|
||||
</TemplateEditorLayoutPreview>
|
||||
<TemplateEditorLayoutConsole>
|
||||
<DryRunResults />
|
||||
</section>
|
||||
</Paper>
|
||||
</TemplateEditorLayoutConsole>
|
||||
</TemplateEditorLayout>
|
||||
</DryRunProvider>
|
||||
</DirectoryEditorProvider>
|
||||
);
|
||||
|
||||
+10
-6
@@ -64,6 +64,10 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
if (!directoryEditor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid className={classes.grid} container spacing={0} alignItems="center">
|
||||
@@ -72,8 +76,8 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={directoryEditor?.files.every(file => !file.dirty)}
|
||||
onClick={() => directoryEditor?.save()}
|
||||
disabled={directoryEditor.files.every(file => !file.dirty)}
|
||||
onClick={() => directoryEditor.save()}
|
||||
>
|
||||
<SaveIcon />
|
||||
</IconButton>
|
||||
@@ -83,7 +87,7 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
|
||||
'templateEditorPage.templateEditorBrowser.reloadIconTooltip',
|
||||
)}
|
||||
>
|
||||
<IconButton size="small" onClick={() => directoryEditor?.reload()}>
|
||||
<IconButton size="small" onClick={() => directoryEditor.reload()}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -101,9 +105,9 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
|
||||
</Grid>
|
||||
<Divider />
|
||||
<FileBrowser
|
||||
selected={directoryEditor?.selectedFile?.path ?? ''}
|
||||
onSelect={directoryEditor?.setSelectedFile}
|
||||
filePaths={directoryEditor?.files.map(file => file.path) ?? []}
|
||||
selected={directoryEditor.selectedFile?.path ?? ''}
|
||||
onSelect={directoryEditor.setSelectedFile}
|
||||
filePaths={directoryEditor.files.map(file => file.path) ?? []}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -239,7 +239,11 @@ export function TemplateEditorFormDirectoryEditorDryRun(
|
||||
? selectedFile.content
|
||||
: undefined;
|
||||
|
||||
return directoryEditor ? (
|
||||
if (!directoryEditor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TemplateEditorForm
|
||||
onDryRun={handleDryRun}
|
||||
fieldExtensions={fieldExtensions}
|
||||
@@ -248,7 +252,7 @@ export function TemplateEditorFormDirectoryEditorDryRun(
|
||||
layouts={layouts}
|
||||
formProps={props.formProps}
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
|
||||
TemplateEditorForm.DirectoryEditorDryRun =
|
||||
|
||||
+15
-15
@@ -40,13 +40,13 @@ const useStyles = makeStyles(theme => ({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cardGrid: {
|
||||
display: 'grid',
|
||||
maxWidth: 1000,
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gridAutoRows: '1fr 1fr',
|
||||
gap: '1rem',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
gridAutoFlow: 'row',
|
||||
display: 'grid',
|
||||
gridGap: theme.spacing(2),
|
||||
gridAutoFlow: 'row',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
gridTemplateRows: '1fr 1fr',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
},
|
||||
},
|
||||
card: {
|
||||
@@ -167,6 +167,15 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
|
||||
Icon={CreateNewFolderIcon}
|
||||
/>
|
||||
|
||||
<ActionCard
|
||||
title={t('templateEditorPage.templateEditorIntro.formEditor.title')}
|
||||
description={t(
|
||||
'templateEditorPage.templateEditorIntro.formEditor.description',
|
||||
)}
|
||||
Icon={ListAltIcon}
|
||||
action={() => props.onSelect?.('form')}
|
||||
/>
|
||||
|
||||
<ActionCard
|
||||
title={t(
|
||||
'templateEditorPage.templateEditorIntro.fieldExplorer.title',
|
||||
@@ -177,15 +186,6 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
|
||||
Icon={FormatListBulletedIcon}
|
||||
action={() => props.onSelect?.('field-explorer')}
|
||||
/>
|
||||
|
||||
<ActionCard
|
||||
title={t('templateEditorPage.templateEditorIntro.formEditor.title')}
|
||||
description={t(
|
||||
'templateEditorPage.templateEditorIntro.formEditor.description',
|
||||
)}
|
||||
Icon={ListAltIcon}
|
||||
action={() => props.onSelect?.('form')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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, { PropsWithChildren } from 'react';
|
||||
import { WithStyles, withStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const TemplateEditorLayout = withStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
height: '100%',
|
||||
gridArea: 'pageContent',
|
||||
display: 'grid',
|
||||
gridTemplateAreas: `
|
||||
"toolbar"
|
||||
"browser"
|
||||
"editor"
|
||||
"preview"
|
||||
"results"
|
||||
`,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
gridTemplateAreas: `
|
||||
"toolbar toolbar toolbar"
|
||||
"browser editor preview"
|
||||
"results results results"
|
||||
`,
|
||||
gridTemplateColumns: '1fr 3fr 2fr',
|
||||
gridTemplateRows: 'auto 1fr auto',
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: 'ScaffolderTemplateEditorLayout' },
|
||||
)(({ children, classes }: PropsWithChildren<WithStyles>) => (
|
||||
<main className={classes.root}>{children}</main>
|
||||
));
|
||||
|
||||
export const TemplateEditorLayoutToolbar = withStyles(
|
||||
{
|
||||
root: {
|
||||
gridArea: 'toolbar',
|
||||
},
|
||||
},
|
||||
{ name: 'ScaffolderTemplateEditorLayoutToolbar' },
|
||||
)(({ children, classes }: PropsWithChildren<WithStyles>) => (
|
||||
<section className={classes.root}>{children}</section>
|
||||
));
|
||||
|
||||
export const TemplateEditorLayoutBrowser = withStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
gridArea: 'browser',
|
||||
overflow: 'auto',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: 'ScaffolderTemplateEditorLayoutBrowser' },
|
||||
)(({ children, classes }: PropsWithChildren<WithStyles>) => (
|
||||
<section className={classes.root}>{children}</section>
|
||||
));
|
||||
|
||||
export const TemplateEditorLayoutFiles = withStyles(
|
||||
{
|
||||
root: {
|
||||
gridArea: 'editor',
|
||||
overflow: 'auto',
|
||||
},
|
||||
},
|
||||
{ name: 'ScaffolderTemplateEditorLayoutFiles' },
|
||||
)(({ children, classes }: PropsWithChildren<WithStyles>) => (
|
||||
<section className={classes.root}>{children}</section>
|
||||
));
|
||||
|
||||
export const TemplateEditorLayoutPreview = withStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
gridArea: 'preview',
|
||||
position: 'relative',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
borderLeft: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
},
|
||||
scroll: {
|
||||
height: '100%',
|
||||
padding: theme.spacing(1),
|
||||
[theme.breakpoints.up('md')]: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
overflow: 'auto',
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: 'ScaffolderTemplateEditorLayoutPreview' },
|
||||
)(({ children, classes }: PropsWithChildren<WithStyles>) => (
|
||||
<section className={classes.root}>
|
||||
<div className={classes.scroll}>{children}</div>
|
||||
</section>
|
||||
));
|
||||
|
||||
export const TemplateEditorLayoutConsole = withStyles(
|
||||
{
|
||||
root: {
|
||||
gridArea: 'results',
|
||||
},
|
||||
},
|
||||
{ name: 'ScaffolderTemplateEditorLayoutConsole' },
|
||||
)(({ children, classes }: PropsWithChildren<WithStyles>) => (
|
||||
<section className={classes.root}>{children}</section>
|
||||
));
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
* 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.
|
||||
@@ -14,63 +14,53 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Content, Header, Page } from '@backstage/core-components';
|
||||
|
||||
import { WebFileSystemAccess } from '../../../lib/filesystem';
|
||||
|
||||
import { TemplateEditorIntro } from './TemplateEditorIntro';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
editorRouteRef,
|
||||
customFieldsRouteRef,
|
||||
rootRouteRef,
|
||||
templateFormRouteRef,
|
||||
} from '../../../routes';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
FormProps,
|
||||
FieldExtensionOptions,
|
||||
type LayoutOptions,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
import { WebFileSystemStore } from '../../../lib/filesystem/WebFileSystemAccess';
|
||||
import { createExampleTemplate } from '../../../lib/filesystem/createExampleTemplate';
|
||||
import { editRouteRef } from '../../../routes';
|
||||
import { TemplateEditor } from './TemplateEditor';
|
||||
|
||||
export function TemplateEditorPage() {
|
||||
const navigate = useNavigate();
|
||||
const createLink = useRouteRef(rootRouteRef);
|
||||
const editorLink = useRouteRef(editorRouteRef);
|
||||
const customFieldsLink = useRouteRef(customFieldsRouteRef);
|
||||
const templateFormLink = useRouteRef(templateFormRouteRef);
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
content: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
{ name: 'ScaffolderTemplateEditorToolbar' },
|
||||
);
|
||||
|
||||
interface TemplatePageProps {
|
||||
defaultPreviewTemplate?: string;
|
||||
fieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
layouts?: LayoutOptions[];
|
||||
formProps?: FormProps;
|
||||
}
|
||||
|
||||
export function TemplateEditorPage(props: TemplatePageProps) {
|
||||
const classes = useStyles();
|
||||
const editLink = useRouteRef(editRouteRef);
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
title={t('templateEditorPage.title')}
|
||||
type="Scaffolder"
|
||||
typeLink={createLink()}
|
||||
subtitle={t('templateEditorPage.subtitle')}
|
||||
type={t('templateIntroPage.title')}
|
||||
typeLink={editLink()}
|
||||
/>
|
||||
<Content>
|
||||
<TemplateEditorIntro
|
||||
onSelect={option => {
|
||||
if (option === 'local') {
|
||||
WebFileSystemAccess.requestDirectoryAccess()
|
||||
.then(directory => WebFileSystemStore.setDirectory(directory))
|
||||
.then(() => navigate(editorLink()))
|
||||
.catch(() => {});
|
||||
} else if (option === 'create-template') {
|
||||
WebFileSystemAccess.requestDirectoryAccess()
|
||||
.then(directory => {
|
||||
createExampleTemplate(directory).then(() => {
|
||||
WebFileSystemStore.setDirectory(directory);
|
||||
navigate(editorLink());
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (option === 'form') {
|
||||
navigate(templateFormLink());
|
||||
} else if (option === 'field-explorer') {
|
||||
navigate(customFieldsLink());
|
||||
}
|
||||
}}
|
||||
<Content className={classes.content}>
|
||||
<TemplateEditor
|
||||
layouts={props.layouts}
|
||||
formProps={props.formProps}
|
||||
fieldExtensions={props.fieldExtensions}
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
|
||||
+10
-17
@@ -20,7 +20,6 @@ import { showPanel } from '@codemirror/view';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
@@ -45,11 +44,14 @@ const useStyles = makeStyles(theme => ({
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
codeMirror: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
errorPanel: {
|
||||
color: theme.palette.error.main,
|
||||
@@ -150,10 +152,10 @@ export function TemplateEditorTextArea(props: {
|
||||
/** A version of the TemplateEditorTextArea that is connected to the DirectoryEditor context */
|
||||
export function TemplateEditorDirectoryEditorTextArea(props: {
|
||||
errorText?: string;
|
||||
onLoad?: () => void;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const directoryEditor = useDirectoryEditor();
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
if (!directoryEditor) {
|
||||
return (
|
||||
@@ -162,16 +164,7 @@ export function TemplateEditorDirectoryEditorTextArea(props: {
|
||||
color="textSecondary"
|
||||
align="center"
|
||||
>
|
||||
Please{' '}
|
||||
<Link
|
||||
className={classes.button}
|
||||
component="button"
|
||||
variant="body1"
|
||||
onClick={props.onLoad}
|
||||
>
|
||||
load
|
||||
</Link>{' '}
|
||||
a template directory.
|
||||
{t('templateEditorPage.templateEditorTextArea.emptyStateParagraph')}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
+10
-8
@@ -31,19 +31,25 @@ import DialogActions from '@material-ui/core/DialogActions';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import DescriptionIcon from '@material-ui/icons/Description';
|
||||
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import { ActionPageContent } from '../../../components/ActionsPage/ActionsPage';
|
||||
import { CustomFieldPlaygroud } from './CustomFieldPlaygroud';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
import { CustomFieldPlaygroud } from './CustomFieldPlaygroud';
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
paper: {
|
||||
width: '40%',
|
||||
width: '90%',
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: '70%',
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
appbar: {
|
||||
zIndex: 1,
|
||||
@@ -83,11 +89,7 @@ export function TemplateEditorToolbar(props: {
|
||||
<AppBar className={classes.appbar} position="relative">
|
||||
<Toolbar className={classes.toolbar}>
|
||||
<div className={classes.toolbarCustomActions}>{children}</div>
|
||||
<ButtonGroup
|
||||
className={classes.toolbarDefaultActions}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
>
|
||||
<ButtonGroup className={classes.toolbarDefaultActions} variant="text">
|
||||
<Tooltip
|
||||
title={t('templateEditorToolbar.customFieldExplorerTooltip')}
|
||||
>
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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, { MouseEvent, useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
import { editRouteRef } from '../../../routes';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
export function TemplateEditorToolbarFileMenu(props: {
|
||||
onOpenDirectory?: () => void;
|
||||
onCreateDirectory?: () => void;
|
||||
onCloseDirectory?: () => void;
|
||||
}) {
|
||||
const { onOpenDirectory, onCreateDirectory, onCloseDirectory } = props;
|
||||
const navigate = useNavigate();
|
||||
const editLink = useRouteRef(editRouteRef);
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleOpenMenu = useCallback(
|
||||
(event: MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
},
|
||||
[setAnchorEl],
|
||||
);
|
||||
|
||||
const handleCloseMenu = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
}, [setAnchorEl]);
|
||||
|
||||
const handleOpenDirectory = useCallback(() => {
|
||||
handleCloseMenu();
|
||||
onOpenDirectory?.();
|
||||
}, [handleCloseMenu, onOpenDirectory]);
|
||||
|
||||
const handleCreateDirectory = useCallback(() => {
|
||||
handleCloseMenu();
|
||||
onCreateDirectory?.();
|
||||
}, [handleCloseMenu, onCreateDirectory]);
|
||||
|
||||
const handleCloseEditor = useCallback(() => {
|
||||
handleCloseMenu();
|
||||
onCloseDirectory?.();
|
||||
navigate(editLink());
|
||||
}, [handleCloseMenu, onCloseDirectory, navigate, editLink]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
aria-controls="file-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleOpenMenu}
|
||||
>
|
||||
{t('templateEditorToolbarFileMenu.button')}
|
||||
</Button>
|
||||
<Menu
|
||||
id="file-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleCloseMenu}
|
||||
getContentAnchorEl={null}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
keepMounted
|
||||
>
|
||||
<MenuItem onClick={handleOpenDirectory} disabled={!onOpenDirectory}>
|
||||
{t('templateEditorToolbarFileMenu.options.openDirectory')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleCreateDirectory}
|
||||
disabled={!onCreateDirectory}
|
||||
divider
|
||||
>
|
||||
{t('templateEditorToolbarFileMenu.options.createDirectory')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleCloseEditor}>
|
||||
{t('templateEditorToolbarFileMenu.options.closeEditor')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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, { MouseEvent, useCallback, useState } from 'react';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
|
||||
const useStyles = makeStyles({
|
||||
menu: {
|
||||
maxHeight: ITEM_HEIGHT * 5,
|
||||
},
|
||||
});
|
||||
|
||||
export type TemplateOption = {
|
||||
label: string;
|
||||
value: Entity;
|
||||
};
|
||||
|
||||
export function TemplateEditorToolbarTemplatesMenu(props: {
|
||||
options: TemplateOption[];
|
||||
selectedOption?: TemplateOption;
|
||||
onSelectOption: (option: TemplateOption) => void;
|
||||
}) {
|
||||
const { options, selectedOption, onSelectOption } = props;
|
||||
const classes = useStyles();
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
const handleOpenMenu = useCallback(
|
||||
(event: MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
},
|
||||
[setAnchorEl],
|
||||
);
|
||||
|
||||
const handleCloseMenu = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
}, [setAnchorEl]);
|
||||
|
||||
const handleSelectOption = useCallback(
|
||||
(option: TemplateOption) => {
|
||||
handleCloseMenu();
|
||||
onSelectOption(option);
|
||||
},
|
||||
[handleCloseMenu, onSelectOption],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
aria-controls="templates-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleOpenMenu}
|
||||
>
|
||||
{t('templateEditorToolbarTemplatesMenu.button')}
|
||||
</Button>
|
||||
<Menu
|
||||
id="templates-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleCloseMenu}
|
||||
getContentAnchorEl={null}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
PaperProps={{
|
||||
className: classes.menu,
|
||||
}}
|
||||
keepMounted
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
selected={!!selectedOption && selectedOption === option}
|
||||
onClick={() => handleSelectOption(option)}
|
||||
>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -61,6 +61,8 @@ export function TemplateFormPage(props: TemplateFormPageProps) {
|
||||
<Header
|
||||
title={t('templateFormPage.title')}
|
||||
subtitle={t('templateFormPage.subtitle')}
|
||||
type={t('templateIntroPage.title')}
|
||||
typeLink={editLink()}
|
||||
/>
|
||||
<Content className={classes.root}>
|
||||
<TemplateFormPreviewer
|
||||
|
||||
+55
-112
@@ -14,22 +14,12 @@
|
||||
* 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 LinearProgress from '@material-ui/core/LinearProgress';
|
||||
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';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import yaml from 'yaml';
|
||||
@@ -38,11 +28,20 @@ import {
|
||||
FieldExtensionOptions,
|
||||
FormProps,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import {
|
||||
TemplateEditorLayout,
|
||||
TemplateEditorLayoutToolbar,
|
||||
TemplateEditorLayoutFiles,
|
||||
TemplateEditorLayoutPreview,
|
||||
} from './TemplateEditorLayout';
|
||||
import { TemplateEditorToolbar } from './TemplateEditorToolbar';
|
||||
import { TemplateEditorToolbarFileMenu } from './TemplateEditorToolbarFileMenu';
|
||||
import {
|
||||
TemplateOption,
|
||||
TemplateEditorToolbarTemplatesMenu,
|
||||
} from './TemplateEditorToolbarTemplatesMenu';
|
||||
import { TemplateEditorForm } from './TemplateEditorForm';
|
||||
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
|
||||
const EXAMPLE_TEMPLATE_PARAMS_YAML = `# Edit the template parameters below to see how they will render in the scaffolder form UI
|
||||
parameters:
|
||||
@@ -83,14 +82,10 @@ steps:
|
||||
name: \${{parameters.name}}
|
||||
`;
|
||||
|
||||
type TemplateOption = {
|
||||
label: string;
|
||||
value: Entity;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ScaffolderTemplateFormPreviewerClassKey =
|
||||
| 'root'
|
||||
| 'toolbar'
|
||||
| 'controls'
|
||||
| 'textArea'
|
||||
| 'preview';
|
||||
@@ -102,36 +97,21 @@ const useStyles = makeStyles(
|
||||
gridArea: 'pageContent',
|
||||
display: 'grid',
|
||||
gridTemplateAreas: `
|
||||
"toolbar"
|
||||
"textArea"
|
||||
"preview"
|
||||
`,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
gridTemplateAreas: `
|
||||
"toolbar toolbar"
|
||||
"textArea preview"
|
||||
`,
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
gridArea: 'toolbar',
|
||||
},
|
||||
textArea: {
|
||||
files: {
|
||||
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' },
|
||||
@@ -140,7 +120,6 @@ const useStyles = makeStyles(
|
||||
export const TemplateFormPreviewer = ({
|
||||
defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML,
|
||||
customFieldExtensions = [],
|
||||
onClose,
|
||||
layouts = [],
|
||||
formProps,
|
||||
}: {
|
||||
@@ -151,16 +130,14 @@ export const TemplateFormPreviewer = ({
|
||||
formProps?: FormProps;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [errorText, setErrorText] = useState<string>();
|
||||
const [selectedTemplate, setSelectedTemplate] =
|
||||
useState<TemplateOption | null>(null);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateOption>();
|
||||
const [templateOptions, setTemplateOptions] = useState<TemplateOption[]>([]);
|
||||
const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate);
|
||||
|
||||
const { loading } = useAsync(
|
||||
useAsync(
|
||||
() =>
|
||||
catalogApi
|
||||
.getEntities({
|
||||
@@ -196,76 +173,42 @@ export const TemplateFormPreviewer = ({
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
// TODO(Rugvip): Afaik this should be Entity, but didn't want to make runtime changes while fixing types
|
||||
(selected: any) => {
|
||||
(selected: TemplateOption) => {
|
||||
setSelectedTemplate(selected);
|
||||
setTemplateYaml(yaml.stringify(selected.spec));
|
||||
setTemplateYaml(yaml.stringify(selected.value.spec));
|
||||
},
|
||||
[setTemplateYaml],
|
||||
[setSelectedTemplate, setTemplateYaml],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <LinearProgress />}
|
||||
<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
|
||||
content={templateYaml}
|
||||
onUpdate={setTemplateYaml}
|
||||
errorText={errorText}
|
||||
<TemplateEditorLayout classes={{ root: classes.root }}>
|
||||
<TemplateEditorLayoutToolbar>
|
||||
<TemplateEditorToolbar fieldExtensions={customFieldExtensions}>
|
||||
<TemplateEditorToolbarFileMenu />
|
||||
<TemplateEditorToolbarTemplatesMenu
|
||||
options={templateOptions}
|
||||
selectedOption={selectedTemplate}
|
||||
onSelectOption={handleSelectChange}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.preview}>
|
||||
<div className={classes.scroll}>
|
||||
<TemplateEditorForm
|
||||
content={templateYaml}
|
||||
contentIsSpec
|
||||
fieldExtensions={customFieldExtensions}
|
||||
setErrorText={setErrorText}
|
||||
layouts={layouts}
|
||||
formProps={formProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
</>
|
||||
</TemplateEditorToolbar>
|
||||
</TemplateEditorLayoutToolbar>
|
||||
<TemplateEditorLayoutFiles classes={{ root: classes.files }}>
|
||||
<TemplateEditorTextArea
|
||||
content={templateYaml}
|
||||
onUpdate={setTemplateYaml}
|
||||
errorText={errorText}
|
||||
/>
|
||||
</TemplateEditorLayoutFiles>
|
||||
<TemplateEditorLayoutPreview>
|
||||
<TemplateEditorForm
|
||||
content={templateYaml}
|
||||
contentIsSpec
|
||||
fieldExtensions={customFieldExtensions}
|
||||
setErrorText={setErrorText}
|
||||
layouts={layouts}
|
||||
formProps={formProps}
|
||||
/>
|
||||
</TemplateEditorLayoutPreview>
|
||||
</TemplateEditorLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Content, Header, Page } from '@backstage/core-components';
|
||||
|
||||
import { WebFileSystemAccess } from '../../../lib/filesystem';
|
||||
|
||||
import { TemplateEditorIntro } from './TemplateEditorIntro';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
rootRouteRef,
|
||||
editorRouteRef,
|
||||
templateFormRouteRef,
|
||||
customFieldsRouteRef,
|
||||
} 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';
|
||||
|
||||
export function TemplateIntroPage() {
|
||||
const navigate = useNavigate();
|
||||
const createLink = useRouteRef(rootRouteRef);
|
||||
const editorLink = useRouteRef(editorRouteRef);
|
||||
const templateFormLink = useRouteRef(templateFormRouteRef);
|
||||
const customFieldsLink = useRouteRef(customFieldsRouteRef);
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
title={t('templateIntroPage.title')}
|
||||
type="Scaffolder"
|
||||
typeLink={createLink()}
|
||||
subtitle={t('templateIntroPage.subtitle')}
|
||||
/>
|
||||
<Content>
|
||||
<TemplateEditorIntro
|
||||
onSelect={option => {
|
||||
if (option === 'local') {
|
||||
WebFileSystemAccess.requestDirectoryAccess()
|
||||
.then(directory => WebFileSystemStore.setDirectory(directory))
|
||||
.then(() => navigate(editorLink()))
|
||||
.catch(() => {});
|
||||
} else if (option === 'create-template') {
|
||||
WebFileSystemAccess.requestDirectoryAccess()
|
||||
.then(directory => {
|
||||
createExampleTemplate(directory).then(() => {
|
||||
WebFileSystemStore.setDirectory(directory);
|
||||
navigate(editorLink());
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (option === 'form') {
|
||||
navigate(templateFormLink());
|
||||
} else if (option === 'field-explorer') {
|
||||
navigate(customFieldsLink());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* 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 useAsyncRetry from 'react-use/esm/useAsyncRetry';
|
||||
|
||||
import { Page, Header, Content, Progress } from '@backstage/core-components';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
FormProps,
|
||||
FieldExtensionOptions,
|
||||
type LayoutOptions,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import { scaffolderTranslationRef } from '../../../translation';
|
||||
import {
|
||||
WebFileSystemAccess,
|
||||
WebFileSystemStore,
|
||||
} from '../../../lib/filesystem';
|
||||
import { TemplateEditor } from './TemplateEditor';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
content: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
{ name: 'ScaffolderTemplateEditorToolbar' },
|
||||
);
|
||||
|
||||
interface TemplatePageProps {
|
||||
defaultPreviewTemplate?: string;
|
||||
fieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
layouts?: LayoutOptions[];
|
||||
formProps?: FormProps;
|
||||
}
|
||||
|
||||
export function TemplatePage(props: TemplatePageProps) {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
const { value, loading, retry } = useAsyncRetry(async () => {
|
||||
const directory = await WebFileSystemStore.getDirectory();
|
||||
if (!directory) return undefined;
|
||||
return WebFileSystemAccess.fromHandle(directory);
|
||||
}, []);
|
||||
|
||||
const handleLoadDirectory = useCallback(() => {
|
||||
WebFileSystemAccess.requestDirectoryAccess()
|
||||
.then(WebFileSystemStore.setDirectory)
|
||||
.then(retry);
|
||||
}, [retry]);
|
||||
|
||||
const handleCloseDirectory = useCallback(() => {
|
||||
WebFileSystemStore.setDirectory(undefined).then(retry);
|
||||
}, [retry]);
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
title={t('templateEditorPage.title')}
|
||||
subtitle={t('templateEditorPage.subtitle')}
|
||||
/>
|
||||
<Content className={classes.content}>
|
||||
{loading ? (
|
||||
<Progress />
|
||||
) : (
|
||||
<TemplateEditor
|
||||
directory={value}
|
||||
layouts={props.layouts}
|
||||
formProps={props.formProps}
|
||||
fieldExtensions={props.fieldExtensions}
|
||||
onClose={handleCloseDirectory}
|
||||
onLoad={handleLoadDirectory}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TemplatePage } from './TemplatePage';
|
||||
export { TemplateFormPage } from './TemplateFormPage';
|
||||
export { TemplateEditorPage } from './TemplateEditorPage';
|
||||
export { TemplateFormPage } from './TemplateFormPage';
|
||||
export { TemplateIntroPage } from './TemplateIntroPage';
|
||||
export { CustomFieldsPage } from './CustomFieldsPage';
|
||||
export type { ScaffolderCustomFieldExplorerClassKey } from './CustomFieldExplorer';
|
||||
export type { ScaffolderTemplateEditorClassKey } from './TemplateEditor';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import useAsyncRetry from 'react-use/esm/useAsyncRetry';
|
||||
|
||||
import {
|
||||
WebFileSystemAccess,
|
||||
WebFileSystemStore,
|
||||
WebDirectoryAccess,
|
||||
} from '../../../lib/filesystem';
|
||||
import { createExampleTemplate } from '../../../lib/filesystem/createExampleTemplate';
|
||||
|
||||
export function useTemplateDirectory(): {
|
||||
directory?: WebDirectoryAccess;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
handleOpenDirectory: () => void;
|
||||
handleCreateDirectory: () => void;
|
||||
handleCloseDirectory: () => void;
|
||||
} {
|
||||
const { value, loading, error, retry } = useAsyncRetry(async () => {
|
||||
const directory = await WebFileSystemStore.getDirectory();
|
||||
if (!directory) return undefined;
|
||||
return WebFileSystemAccess.fromHandle(directory);
|
||||
}, []);
|
||||
|
||||
const handleOpenDirectory = useCallback(() => {
|
||||
WebFileSystemAccess.requestDirectoryAccess()
|
||||
.then(WebFileSystemStore.setDirectory)
|
||||
.then(retry);
|
||||
}, [retry]);
|
||||
|
||||
const handleCreateDirectory = useCallback(() => {
|
||||
WebFileSystemAccess.requestDirectoryAccess()
|
||||
.then(createExampleTemplate)
|
||||
.then(WebFileSystemStore.setDirectory)
|
||||
.then(retry);
|
||||
}, [retry]);
|
||||
|
||||
const handleCloseDirectory = useCallback(() => {
|
||||
WebFileSystemStore.setDirectory(undefined).then(retry);
|
||||
}, [retry]);
|
||||
|
||||
return {
|
||||
directory: value,
|
||||
loading,
|
||||
error,
|
||||
handleOpenDirectory,
|
||||
handleCreateDirectory,
|
||||
handleCloseDirectory,
|
||||
};
|
||||
}
|
||||
@@ -54,8 +54,8 @@ import {
|
||||
import { TemplateListPage, TemplateWizardPage } from '../../alpha/components';
|
||||
import { OngoingTask } from '../OngoingTask';
|
||||
import {
|
||||
TemplatePage,
|
||||
TemplateFormPage,
|
||||
TemplateIntroPage,
|
||||
TemplateEditorPage,
|
||||
CustomFieldsPage,
|
||||
} from '../../alpha/components/TemplateEditorPage';
|
||||
@@ -171,7 +171,7 @@ export const Router = (props: PropsWithChildren<RouterProps>) => {
|
||||
path={editRouteRef.path}
|
||||
element={
|
||||
<SecretsContextProvider>
|
||||
<TemplateEditorPage />
|
||||
<TemplateIntroPage />
|
||||
</SecretsContextProvider>
|
||||
}
|
||||
/>
|
||||
@@ -205,7 +205,7 @@ export const Router = (props: PropsWithChildren<RouterProps>) => {
|
||||
path={editorRouteRef.path}
|
||||
element={
|
||||
<SecretsContextProvider>
|
||||
<TemplatePage
|
||||
<TemplateEditorPage
|
||||
layouts={customLayouts}
|
||||
formProps={props.formProps}
|
||||
fieldExtensions={fieldExtensions}
|
||||
|
||||
@@ -53,7 +53,8 @@ class WebFileAccess implements TemplateFileAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class WebDirectoryAccess implements TemplateDirectoryAccess {
|
||||
/** @internal */
|
||||
export class WebDirectoryAccess implements TemplateDirectoryAccess {
|
||||
constructor(private readonly handle: IterableDirectoryHandle) {}
|
||||
|
||||
async listFiles(): Promise<TemplateFileAccess[]> {
|
||||
|
||||
@@ -91,4 +91,5 @@ export async function createExampleTemplate(
|
||||
for (const [name, data] of Object.entries(files)) {
|
||||
await directory.createFile({ name, data });
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
|
||||
@@ -16,4 +16,8 @@
|
||||
|
||||
export type { TemplateFileAccess, TemplateDirectoryAccess } from './types';
|
||||
export { blobToBase64 } from './helpers';
|
||||
export { WebFileSystemAccess, WebFileSystemStore } from './WebFileSystemAccess';
|
||||
export {
|
||||
WebDirectoryAccess,
|
||||
WebFileSystemAccess,
|
||||
WebFileSystemStore,
|
||||
} from './WebFileSystemAccess';
|
||||
|
||||
@@ -195,12 +195,21 @@ export const scaffolderTranslationRef = createTranslationRef({
|
||||
templateTypePicker: {
|
||||
title: 'Categories',
|
||||
},
|
||||
templateIntroPage: {
|
||||
title: 'Manage Templates',
|
||||
subtitle:
|
||||
'Edit, preview, and try out templates, forms, and custom fields',
|
||||
},
|
||||
templateFormPage: {
|
||||
title: 'Template Form Playground',
|
||||
subtitle: 'Edit, preview, and try out templates and template forms',
|
||||
title: 'Template Editor',
|
||||
subtitle: 'Edit, preview, and try out templates forms',
|
||||
},
|
||||
templateCustomFieldPage: {
|
||||
title: 'Custom Field Explorer',
|
||||
subtitle: 'Edit, preview, and try out custom fields',
|
||||
},
|
||||
templateEditorPage: {
|
||||
title: 'Manage Templates',
|
||||
title: 'Template Editor',
|
||||
subtitle: 'Edit, preview, and try out templates and template forms',
|
||||
dryRunResults: {
|
||||
title: 'Dry-run results',
|
||||
@@ -254,7 +263,7 @@ export const scaffolderTranslationRef = createTranslationRef({
|
||||
unsupportedTooltip: 'Only supported in some Chromium-based browsers',
|
||||
},
|
||||
formEditor: {
|
||||
title: 'Template playground',
|
||||
title: 'Template Form Playground',
|
||||
description:
|
||||
'Preview and edit a template form, either using a sample template or by loading a template from the catalog.',
|
||||
},
|
||||
@@ -267,6 +276,7 @@ export const scaffolderTranslationRef = createTranslationRef({
|
||||
templateEditorTextArea: {
|
||||
saveIconTooltip: 'Save file',
|
||||
refreshIconTooltip: 'Reload file',
|
||||
emptyStateParagraph: 'Please select an action on the file menu.',
|
||||
},
|
||||
templateFormPreviewer: {
|
||||
title: 'Load Existing Template',
|
||||
@@ -316,5 +326,16 @@ export const scaffolderTranslationRef = createTranslationRef({
|
||||
'https://backstage.io/docs/features/software-templates/adding-templates/',
|
||||
},
|
||||
},
|
||||
templateEditorToolbarFileMenu: {
|
||||
button: 'File',
|
||||
options: {
|
||||
openDirectory: 'Open template directory',
|
||||
createDirectory: 'Create template directory',
|
||||
closeEditor: 'Close template editor',
|
||||
},
|
||||
},
|
||||
templateEditorToolbarTemplatesMenu: {
|
||||
button: 'Templates',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user