Merge pull request #26773 from backstage/mob/scaffolder-template-editor-changes
[Scaffolder] Enhance template editor usability
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Update the Scaffolder template editor to quickly access installed custom fields and actions when editing a template.
|
||||
@@ -227,6 +227,7 @@ export const scaffolderTranslationRef: TranslationRef<
|
||||
readonly 'ongoingTask.startOverButtonTitle': 'Start Over';
|
||||
readonly 'ongoingTask.hideLogsButtonTitle': 'Hide Logs';
|
||||
readonly 'ongoingTask.showLogsButtonTitle': 'Show Logs';
|
||||
readonly 'templateEditorForm.stepper.emptyText': 'There are no spec parameters in the template to preview.';
|
||||
readonly 'templateTypePicker.title': 'Categories';
|
||||
readonly 'templateEditorPage.title': 'Template Editor';
|
||||
readonly 'templateEditorPage.subtitle': 'Edit, preview, and try out templates and template forms';
|
||||
@@ -238,10 +239,11 @@ export const scaffolderTranslationRef: TranslationRef<
|
||||
readonly 'templateEditorPage.dryRunResultsView.tab.log': 'Log';
|
||||
readonly 'templateEditorPage.dryRunResultsView.tab.files': 'Files';
|
||||
readonly 'templateEditorPage.taskStatusStepper.skippedStepTitle': 'Skipped';
|
||||
readonly 'templateEditorPage.customFieldExplorer.preview.title': 'Example Template Spec';
|
||||
readonly 'templateEditorPage.customFieldExplorer.preview.title': 'Template Spec';
|
||||
readonly 'templateEditorPage.customFieldExplorer.fieldForm.title': 'Field Options';
|
||||
readonly 'templateEditorPage.customFieldExplorer.fieldForm.applyButtonTitle': 'Apply';
|
||||
readonly 'templateEditorPage.customFieldExplorer.selectFieldLabel': 'Choose Custom Field Extension';
|
||||
readonly 'templateEditorPage.customFieldExplorer.fieldPreview.title': 'Field Preview';
|
||||
readonly 'templateEditorPage.templateEditorBrowser.closeConfirmMessage': 'Are you sure? Unsaved changes will be lost';
|
||||
readonly 'templateEditorPage.templateEditorBrowser.saveIconTooltip': 'Save all files';
|
||||
readonly 'templateEditorPage.templateEditorBrowser.reloadIconTooltip': 'Reload directory';
|
||||
|
||||
@@ -120,7 +120,7 @@ const ExamplesTable = (props: { examples: ActionExample[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ActionPageContent = () => {
|
||||
export const ActionPageContent = () => {
|
||||
const api = useApi(scaffolderApiRef);
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
|
||||
@@ -21,12 +21,13 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
|
||||
import TreeItem from '@material-ui/lab/TreeItem';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflowY: 'auto',
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export type FileEntry =
|
||||
| {
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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, { useCallback, useMemo, useState } from 'react';
|
||||
import yaml from 'yaml';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
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 Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { Form } from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import { scaffolderTranslationRef } from '../../translation';
|
||||
import { TemplateEditorForm } from './TemplateEditorForm';
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
gridArea: 'pageContent',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
},
|
||||
controls: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
code: {
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
{ name: 'ScaffolderCustomFieldExtensionsPlaygroud' },
|
||||
);
|
||||
|
||||
export const CustomFieldPlaygroud = ({
|
||||
fieldExtensions = [],
|
||||
}: {
|
||||
fieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
const fieldOptions = fieldExtensions.filter(field => !!field.schema);
|
||||
const [refreshKey, setRefreshKey] = useState(Date.now());
|
||||
const [fieldFormState, setFieldFormState] = useState({});
|
||||
const [selectedField, setSelectedField] = useState(fieldOptions[0]);
|
||||
const sampleFieldTemplate = useMemo(
|
||||
() =>
|
||||
yaml.stringify({
|
||||
parameters: [
|
||||
{
|
||||
title: `${selectedField.name} Example`,
|
||||
properties: {
|
||||
[selectedField.name]: {
|
||||
type: selectedField.schema?.returnValue?.type,
|
||||
'ui:field': selectedField.name,
|
||||
'ui:options': fieldFormState,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
[fieldFormState, selectedField],
|
||||
);
|
||||
|
||||
const fieldComponents = useMemo(() => {
|
||||
return Object.fromEntries(
|
||||
fieldExtensions.map(({ name, component }) => [name, component]),
|
||||
);
|
||||
}, [fieldExtensions]);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
(selection: FieldExtensionOptions) => {
|
||||
setSelectedField(selection);
|
||||
setFieldFormState({});
|
||||
},
|
||||
[setFieldFormState, setSelectedField],
|
||||
);
|
||||
|
||||
const handleFieldConfigChange = useCallback(
|
||||
(state: {}) => {
|
||||
setFieldFormState(state);
|
||||
// Force TemplateEditorForm to re-render since some fields
|
||||
// may not be responsive to ui:option changes
|
||||
setRefreshKey(Date.now());
|
||||
},
|
||||
[setFieldFormState, setRefreshKey],
|
||||
);
|
||||
|
||||
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)
|
||||
}
|
||||
>
|
||||
{fieldOptions.map((option, idx) => (
|
||||
<MenuItem key={idx} value={option as any}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel-code-content"
|
||||
id="panel-code-header"
|
||||
>
|
||||
<Typography variant="h6">
|
||||
{t('templateEditorPage.customFieldExplorer.preview.title')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<div className={classes.code}>
|
||||
<CodeMirror
|
||||
readOnly
|
||||
theme="dark"
|
||||
height="100%"
|
||||
width="100%"
|
||||
extensions={[StreamLanguage.define(yamlSupport)]}
|
||||
value={sampleFieldTemplate}
|
||||
/>
|
||||
</div>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel-preview-content"
|
||||
id="panel-preview-header"
|
||||
>
|
||||
<Typography variant="h6">
|
||||
{t('templateEditorPage.customFieldExplorer.fieldPreview.title')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<TemplateEditorForm
|
||||
key={refreshKey}
|
||||
content={sampleFieldTemplate}
|
||||
contentIsSpec
|
||||
fieldExtensions={fieldExtensions}
|
||||
setErrorText={() => null}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel-options-content"
|
||||
id="panel-options-header"
|
||||
>
|
||||
<Typography variant="h6">
|
||||
{t('templateEditorPage.customFieldExplorer.fieldForm.title')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Form
|
||||
showErrorList={false}
|
||||
fields={{ ...fieldComponents }}
|
||||
noHtml5Validate
|
||||
formData={fieldFormState}
|
||||
formContext={{ fieldFormState }}
|
||||
onSubmit={e => handleFieldConfigChange(e.formData)}
|
||||
validator={validator}
|
||||
schema={selectedField.schema?.uiOptions || {}}
|
||||
experimental_defaultFormStateBehavior={{
|
||||
allOf: 'populateDefaults',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={!selectedField.schema?.uiOptions}
|
||||
>
|
||||
{t(
|
||||
'templateEditorPage.customFieldExplorer.fieldForm.applyButtonTitle',
|
||||
)}
|
||||
</Button>
|
||||
</Form>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useState } from 'react';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import type {
|
||||
FormProps,
|
||||
LayoutOptions,
|
||||
@@ -22,6 +23,7 @@ import type {
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
import { TemplateDirectoryAccess } from '../../lib/filesystem';
|
||||
import { DirectoryEditorProvider } from './DirectoryEditorContext';
|
||||
import { TemplateEditorToolbar } from './TemplateEditorToolbar';
|
||||
import { TemplateEditorBrowser } from './TemplateEditorBrowser';
|
||||
import { DryRunProvider } from './DryRunContext';
|
||||
import { TemplateEditorTextArea } from './TemplateEditorTextArea';
|
||||
@@ -37,17 +39,22 @@ export type ScaffolderTemplateEditorClassKey =
|
||||
| '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: '1fr auto',
|
||||
gridTemplateRows: 'auto 1fr auto',
|
||||
},
|
||||
toolbar: {
|
||||
gridArea: 'toolbar',
|
||||
},
|
||||
browser: {
|
||||
gridArea: 'browser',
|
||||
@@ -56,15 +63,27 @@ const useStyles = makeStyles(
|
||||
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' },
|
||||
);
|
||||
|
||||
@@ -76,13 +95,20 @@ export const TemplateEditor = (props: {
|
||||
formProps?: FormProps;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const [errorText, setErrorText] = useState<string>();
|
||||
|
||||
return (
|
||||
<DirectoryEditorProvider directory={props.directory}>
|
||||
<DryRunProvider>
|
||||
<main className={classes.root}>
|
||||
<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>
|
||||
@@ -90,17 +116,19 @@ export const TemplateEditor = (props: {
|
||||
<TemplateEditorTextArea.DirectoryEditor errorText={errorText} />
|
||||
</section>
|
||||
<section className={classes.preview}>
|
||||
<TemplateEditorForm.DirectoryEditorDryRun
|
||||
setErrorText={setErrorText}
|
||||
fieldExtensions={props.fieldExtensions}
|
||||
layouts={props.layouts}
|
||||
formProps={props.formProps}
|
||||
/>
|
||||
<div className={classes.scroll}>
|
||||
<TemplateEditorForm.DirectoryEditorDryRun
|
||||
setErrorText={setErrorText}
|
||||
fieldExtensions={props.fieldExtensions}
|
||||
layouts={props.layouts}
|
||||
formProps={props.formProps}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className={classes.results}>
|
||||
<DryRunResults />
|
||||
</section>
|
||||
</main>
|
||||
</Paper>
|
||||
</DryRunProvider>
|
||||
</DirectoryEditorProvider>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
@@ -26,23 +27,19 @@ import { FileBrowser } from '../../components/FileBrowser';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { scaffolderTranslationRef } from '../../translation';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
button: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
buttonsGap: {
|
||||
flex: '1 1 auto',
|
||||
},
|
||||
buttonsDivider: {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
grid: {
|
||||
'& svg': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
closeButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
}),
|
||||
{ name: 'ScaffolderTemplateEditorBrowser' },
|
||||
);
|
||||
|
||||
/** The local file browser for the template editor */
|
||||
export function TemplateEditorBrowser(props: { onClose?: () => void }) {
|
||||
@@ -69,12 +66,12 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.buttons}>
|
||||
<Grid className={classes.grid} container spacing={0} alignItems="center">
|
||||
<Tooltip
|
||||
title={t('templateEditorPage.templateEditorBrowser.saveIconTooltip')}
|
||||
>
|
||||
<IconButton
|
||||
className={classes.button}
|
||||
size="small"
|
||||
disabled={directoryEditor.files.every(file => !file.dirty)}
|
||||
onClick={() => directoryEditor.save()}
|
||||
>
|
||||
@@ -86,23 +83,23 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) {
|
||||
'templateEditorPage.templateEditorBrowser.reloadIconTooltip',
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
className={classes.button}
|
||||
onClick={() => directoryEditor.reload()}
|
||||
>
|
||||
<IconButton size="small" onClick={() => directoryEditor.reload()}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className={classes.buttonsGap} />
|
||||
<Tooltip
|
||||
title={t('templateEditorPage.templateEditorBrowser.closeIconTooltip')}
|
||||
>
|
||||
<IconButton className={classes.button} onClick={handleClose}>
|
||||
<IconButton
|
||||
size="small"
|
||||
className={classes.closeButton}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Divider className={classes.buttonsDivider} />
|
||||
</Grid>
|
||||
<Divider />
|
||||
<FileBrowser
|
||||
selected={directoryEditor.selectedFile?.path ?? ''}
|
||||
onSelect={directoryEditor.setSelectedFile}
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
import { useApiHolder } from '@backstage/core-plugin-api';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import React, { Component, ReactNode, useMemo, useState } from 'react';
|
||||
import useDebounce from 'react-use/esm/useDebounce';
|
||||
import yaml from 'yaml';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
LayoutOptions,
|
||||
TemplateParameterSchema,
|
||||
@@ -31,20 +34,11 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-react/alpha';
|
||||
import { useDryRun } from './DryRunContext';
|
||||
import { useDirectoryEditor } from './DirectoryEditorContext';
|
||||
import { scaffolderTranslationRef } from '../../translation';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
containerWrapper: {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
container: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
overflow: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -107,6 +101,7 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
const apiHolder = useApiHolder();
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
|
||||
const [steps, setSteps] = useState<TemplateParameterSchema['steps']>();
|
||||
|
||||
@@ -181,26 +176,28 @@ export function TemplateEditorForm(props: TemplateEditorFormProps) {
|
||||
[contentIsSpec, content, apiHolder],
|
||||
);
|
||||
|
||||
if (!steps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.containerWrapper}>
|
||||
<div className={classes.container}>
|
||||
<ErrorBoundary invalidator={steps} setErrorText={setErrorText}>
|
||||
<Stepper
|
||||
manifest={{ steps, title: 'Template Editor' }}
|
||||
extensions={fieldExtensions}
|
||||
components={fields}
|
||||
onCreate={async options => {
|
||||
await onDryRun?.(options);
|
||||
}}
|
||||
layouts={layouts}
|
||||
formProps={props.formProps}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
{steps ? (
|
||||
<Paper variant="outlined">
|
||||
<ErrorBoundary invalidator={steps} setErrorText={setErrorText}>
|
||||
<Stepper
|
||||
manifest={{ steps, title: 'Template Editor' }}
|
||||
extensions={fieldExtensions}
|
||||
components={fields}
|
||||
onCreate={async options => {
|
||||
await onDryRun?.(options);
|
||||
}}
|
||||
layouts={layouts}
|
||||
formProps={props.formProps}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
{t('templateEditorForm.stepper.emptyText')}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import DescriptionIcon from '@material-ui/icons/Description';
|
||||
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import { ActionPageContent } from '../../components/ActionsPage/ActionsPage';
|
||||
import { CustomFieldPlaygroud } from './CustomFieldPlaygroud';
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
paper: {
|
||||
width: '40%',
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
appbar: {
|
||||
zIndex: 1,
|
||||
},
|
||||
toolbar: {
|
||||
display: 'grid',
|
||||
justifyItems: 'flex-end',
|
||||
padding: theme.spacing(0, 1),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
}),
|
||||
{ name: 'ScaffolderTemplateEditorToolbar' },
|
||||
);
|
||||
|
||||
export function TemplateEditorToolbar(props: {
|
||||
fieldExtensions?: FieldExtensionOptions<any, any>[];
|
||||
}) {
|
||||
const { fieldExtensions } = props;
|
||||
const classes = useStyles();
|
||||
const [showFieldsDrawer, setShowFieldsDrawer] = useState(false);
|
||||
const [showActionsDrawer, setShowActionsDrawer] = useState(false);
|
||||
const [showPublishModal, setShowPublishModal] = useState(false);
|
||||
|
||||
return (
|
||||
<AppBar className={classes.appbar} position="relative">
|
||||
<Toolbar className={classes.toolbar}>
|
||||
<ButtonGroup variant="outlined" color="primary">
|
||||
<Tooltip title="Custom Fields Explorer">
|
||||
<Button size="small" onClick={() => setShowFieldsDrawer(true)}>
|
||||
<ExtensionIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Installed Actions Documentation">
|
||||
<Button size="small" onClick={() => setShowActionsDrawer(true)}>
|
||||
<DescriptionIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button onClick={() => setShowPublishModal(true)}>Publish</Button>
|
||||
</ButtonGroup>
|
||||
<Drawer
|
||||
classes={{ paper: classes.paper }}
|
||||
anchor="right"
|
||||
open={showFieldsDrawer}
|
||||
onClose={() => setShowFieldsDrawer(false)}
|
||||
>
|
||||
<CustomFieldPlaygroud fieldExtensions={fieldExtensions} />
|
||||
</Drawer>
|
||||
<Drawer
|
||||
classes={{ paper: classes.paper }}
|
||||
anchor="right"
|
||||
open={showActionsDrawer}
|
||||
onClose={() => setShowActionsDrawer(false)}
|
||||
>
|
||||
<ActionPageContent />
|
||||
</Drawer>
|
||||
<Dialog
|
||||
onClose={() => setShowPublishModal(false)}
|
||||
open={showPublishModal}
|
||||
aria-labelledby="publish-dialog-title"
|
||||
aria-describedby="publish-dialog-description"
|
||||
>
|
||||
<DialogTitle id="publish-dialog-title">Publish changes</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<DialogContentText id="publish-dialog-slide-description">
|
||||
Follow the instructions below to create or update a template:
|
||||
<ol>
|
||||
<li>Save the template files in a local directory</li>
|
||||
<li>
|
||||
Create a pull request to a new or existing git repository
|
||||
</li>
|
||||
<li>
|
||||
If the template already exists, the changes will be reflected
|
||||
in the software catalog once the pull request gets merged
|
||||
</li>
|
||||
<li>
|
||||
But if you are creating a new template, follow this{' '}
|
||||
<Link to="https://backstage.io/docs/features/software-templates/adding-templates/">
|
||||
documentation
|
||||
</Link>{' '}
|
||||
to register the new template repository in software catalog
|
||||
</li>
|
||||
</ol>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="primary" onClick={() => setShowPublishModal(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
@@ -184,6 +184,11 @@ export const scaffolderTranslationRef = createTranslationRef({
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
templateEditorForm: {
|
||||
stepper: {
|
||||
emptyText: 'There are no spec parameters in the template to preview.',
|
||||
},
|
||||
},
|
||||
templateTypePicker: {
|
||||
title: 'Categories',
|
||||
},
|
||||
@@ -214,8 +219,11 @@ export const scaffolderTranslationRef = createTranslationRef({
|
||||
title: 'Field Options',
|
||||
applyButtonTitle: 'Apply',
|
||||
},
|
||||
fieldPreview: {
|
||||
title: 'Field Preview',
|
||||
},
|
||||
preview: {
|
||||
title: 'Example Template Spec',
|
||||
title: 'Template Spec',
|
||||
},
|
||||
},
|
||||
templateEditorBrowser: {
|
||||
|
||||
Reference in New Issue
Block a user