Merge pull request #26790 from backstage/jhaals/create-template
[Scaffolder] Add ability to create new scaffolder templates
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Added ability to create a new local scaffolder template to ease onboarding when creating new templates.
|
||||
@@ -252,6 +252,9 @@ export const scaffolderTranslationRef: TranslationRef<
|
||||
readonly 'templateEditorPage.templateEditorIntro.loadLocal.title': 'Load Template Directory';
|
||||
readonly 'templateEditorPage.templateEditorIntro.loadLocal.description': 'Load a local template directory, allowing you to both edit and try executing your own template.';
|
||||
readonly 'templateEditorPage.templateEditorIntro.loadLocal.unsupportedTooltip': 'Only supported in some Chromium-based browsers';
|
||||
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': 'Edit Template Form';
|
||||
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';
|
||||
|
||||
@@ -42,6 +42,10 @@ class MockDirectoryAccess implements TemplateDirectoryAccess {
|
||||
);
|
||||
}
|
||||
|
||||
createFile(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async listFiles(): Promise<TemplateFileAccess[]> {
|
||||
return this.files;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,27 @@ class WebDirectoryAccess implements TemplateDirectoryAccess {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createFile(options: { name: string; data: string }): Promise<void> {
|
||||
const { name, data } = options;
|
||||
let file: FileSystemFileHandle;
|
||||
|
||||
// Current create template does not require support for nested directories
|
||||
if (name.includes('/')) {
|
||||
const [dir, path] = name.split('/');
|
||||
const handle = await this.handle.getDirectoryHandle(dir, {
|
||||
create: true,
|
||||
});
|
||||
file = await handle.getFileHandle(path, { create: true });
|
||||
} else {
|
||||
file = await this.handle.getFileHandle(name, {
|
||||
create: true,
|
||||
});
|
||||
}
|
||||
const writable = await file.createWritable();
|
||||
await writable.write(data);
|
||||
await writable.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { TemplateDirectoryAccess } from './types';
|
||||
|
||||
const files = {
|
||||
'template.yaml': `
|
||||
apiVersion: scaffolder.backstage.io/v1beta3
|
||||
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template
|
||||
kind: Template
|
||||
metadata:
|
||||
name: generated-example-template
|
||||
title: Scaffolder Example Template
|
||||
description: An example template for the scaffolder
|
||||
spec:
|
||||
owner: user:guest
|
||||
type: service
|
||||
# These parameters are used to generate the input form in the frontend, and are
|
||||
# used to gather input data for the execution of the template.
|
||||
parameters:
|
||||
- title: Fill in some steps
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
description: Unique name of the component
|
||||
owner:
|
||||
title: Owner
|
||||
type: string
|
||||
description: Owner of the component
|
||||
ui:field: OwnerPicker
|
||||
ui:options:
|
||||
catalogFilter:
|
||||
kind: Group
|
||||
- title: Choose a location
|
||||
required:
|
||||
- repoUrl
|
||||
properties:
|
||||
repoUrl:
|
||||
title: Repository Location
|
||||
type: string
|
||||
ui:field: RepoUrlPicker
|
||||
ui:options:
|
||||
allowedHosts:
|
||||
- github.com
|
||||
steps:
|
||||
- id: fetch-base
|
||||
name: Fetch Base
|
||||
action: fetch:template
|
||||
input:
|
||||
url: ./skeleton
|
||||
values:
|
||||
name: \${{parameters.name}}
|
||||
owner: \${{parameters.owner}}
|
||||
destination: \${{ parameters.repoUrl | parseRepoUrl }}`,
|
||||
'skeleton/README.md': `# This service is named \${{values.name}}!`,
|
||||
'skeleton/catalog-info.yaml': `apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: \${{values.component_id | dump}}
|
||||
{%- if values.description %}
|
||||
description: \${{values.description | dump}}
|
||||
{%- endif %}
|
||||
annotations:
|
||||
github.com/project-slug: \${{values.destination.owner + "/" + values.destination.repo}}
|
||||
backstage.io/techdocs-ref: dir:.
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: experimental
|
||||
owner: \${{values.owner | dump}}`,
|
||||
};
|
||||
|
||||
export async function createExampleTemplate(
|
||||
directory: TemplateDirectoryAccess,
|
||||
) {
|
||||
for (const [name, data] of Object.entries(files)) {
|
||||
await directory.createFile({ name, data });
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,5 @@ export interface TemplateFileAccess {
|
||||
|
||||
export interface TemplateDirectoryAccess {
|
||||
listFiles(): Promise<Array<TemplateFileAccess>>;
|
||||
createFile(options: { name: string; data: string }): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,9 @@ const useStyles = makeStyles(theme => ({
|
||||
|
||||
interface EditorIntroProps {
|
||||
style?: JSX.IntrinsicElements['div']['style'];
|
||||
onSelect?: (option: 'local' | 'form' | 'field-explorer') => void;
|
||||
onSelect?: (
|
||||
option: 'create-template' | 'local' | 'form' | 'field-explorer',
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function TemplateEditorIntro(props: EditorIntroProps) {
|
||||
@@ -93,6 +95,47 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
|
||||
</Card>
|
||||
);
|
||||
|
||||
const cardCreateLocal = (
|
||||
<Card className={classes.card} elevation={4}>
|
||||
<CardActionArea
|
||||
disabled={!supportsLoad}
|
||||
onClick={() => props.onSelect?.('create-template')}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h3"
|
||||
gutterBottom
|
||||
color={supportsLoad ? undefined : 'textSecondary'}
|
||||
style={{ display: 'flex', flexFlow: 'row nowrap' }}
|
||||
>
|
||||
{t('templateEditorPage.templateEditorIntro.createLocal.title')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={supportsLoad ? undefined : 'textSecondary'}
|
||||
>
|
||||
{t(
|
||||
'templateEditorPage.templateEditorIntro.createLocal.description',
|
||||
)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
{!supportsLoad && (
|
||||
<div className={classes.infoIcon}>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={t(
|
||||
'templateEditorPage.templateEditorIntro.createLocal.unsupportedTooltip',
|
||||
)}
|
||||
>
|
||||
<InfoOutlinedIcon />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
const cardFormEditor = (
|
||||
<Card className={classes.card} elevation={4}>
|
||||
<CardActionArea onClick={() => props.onSelect?.('form')}>
|
||||
@@ -140,6 +183,7 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
|
||||
}}
|
||||
>
|
||||
{supportsLoad && cardLoadLocal}
|
||||
{supportsLoad && cardCreateLocal}
|
||||
{cardFormEditor}
|
||||
{!supportsLoad && cardLoadLocal}
|
||||
{cardFieldExplorer}
|
||||
|
||||
@@ -39,12 +39,16 @@ import {
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { scaffolderTranslationRef } from '../../translation';
|
||||
import { WebFileSystemStore } from '../../lib/filesystem/WebFileSystemAccess';
|
||||
import { createExampleTemplate } from '../../lib/filesystem/createExampleTemplate';
|
||||
|
||||
type Selection =
|
||||
| {
|
||||
type: 'local';
|
||||
directory: TemplateDirectoryAccess;
|
||||
}
|
||||
| {
|
||||
type: 'create-template';
|
||||
}
|
||||
| {
|
||||
type: 'form';
|
||||
}
|
||||
@@ -103,6 +107,15 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
|
||||
.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') {
|
||||
setSelection({ type: 'form' });
|
||||
} else if (option === 'field-explorer') {
|
||||
|
||||
@@ -240,6 +240,12 @@ export const scaffolderTranslationRef = createTranslationRef({
|
||||
'Load a local template directory, allowing you to both edit and try executing your own template.',
|
||||
unsupportedTooltip: 'Only supported in some Chromium-based browsers',
|
||||
},
|
||||
createLocal: {
|
||||
title: 'Create New Template',
|
||||
description:
|
||||
'Create a local template directory, allowing you to both edit and try executing your own template.',
|
||||
unsupportedTooltip: 'Only supported in some Chromium-based browsers',
|
||||
},
|
||||
formEditor: {
|
||||
title: 'Edit Template Form',
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user