Scaffolder: Add ability to create new scaffolder templates

Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Johan Haals
2024-09-19 14:40:00 +02:00
committed by Camila Belo
parent 7f1f4833cc
commit cc3f80cfd7
9 changed files with 192 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Added ability to create a new local scaffolder template to ease onboarding when creating new templates.
+3
View File
@@ -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(options: { name: string; data: string }): 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') {
+6
View File
@@ -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: