Merge pull request #15341 from RoadieHQ/scaffolder-examples

add examples of scaffolder action inputs to docs
This commit is contained in:
Fredrik Adelöw
2023-01-10 14:01:28 +01:00
committed by GitHub
13 changed files with 305 additions and 15 deletions
+69
View File
@@ -0,0 +1,69 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
This patch adds changes to provide examples alongside scaffolder task actions.
The `createTemplateAction` function now takes a list of examples e.g.
```typescript
const actionExamples = [
{
description: 'Example 1',
example: yaml.stringify({
steps: [
{
action: 'test:action',
id: 'test',
input: {
input1: 'value',
},
},
],
}),
},
];
export function createTestAction() {
return createTemplateAction({
id: 'test:action',
examples: [
{
description: 'Example 1',
examples: actionExamples
}
],
...,
});
```
These examples can be retrieved later from the api.
```bash
curl http://localhost:7007/api/scaffolder/v2/actions
```
```json
[
{
"id": "test:action",
"examples": [
{
"description": "Example 1",
"example": "steps:\n - action: test:action\n id: test\n input:\n input1: value\n"
}
],
"schema": {
"input": {
"type": "object",
"properties": {
"input1": {
"title": "Input 1",
"type": "string"
}
}
}
}
}
]
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Show action example yaml on the scaffolder actions documentation page.
+4
View File
@@ -855,6 +855,10 @@ export class TaskWorker {
export type TemplateAction<Input extends JsonObject> = {
id: string;
description?: string;
examples?: {
description: string;
example: string;
}[];
supportsDryRun?: boolean;
schema?: {
input?: Schema;
@@ -19,6 +19,28 @@ import { ScmIntegrations } from '@backstage/integration';
import { CatalogApi } from '@backstage/catalog-client';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { createTemplateAction } from '../../createTemplateAction';
import yaml from 'yaml';
const id = 'catalog:register';
const examples = [
{
description: 'Register with the catalog',
example: yaml.stringify({
steps: [
{
action: id,
id: 'register-with-catalog',
name: 'Register with the catalog',
input: {
catalogInfoUrl:
'http://github.com/backstage/backstage/blob/master/catalog-info.yaml',
},
},
],
}),
},
];
/**
* Registers entities from a catalog descriptor file in the workspace into the software catalog.
@@ -34,9 +56,10 @@ export function createCatalogRegisterAction(options: {
| { catalogInfoUrl: string; optional?: boolean }
| { repoContentsUrl: string; catalogInfoPath?: string; optional?: boolean }
>({
id: 'catalog:register',
id,
description:
'Registers entities from a catalog descriptor file in the workspace into the software catalog.',
examples,
schema: {
input: {
oneOf: [
@@ -20,14 +20,47 @@ import * as yaml from 'yaml';
import { Entity } from '@backstage/catalog-model';
import { resolveSafeChildPath } from '@backstage/backend-common';
const id = 'catalog:write';
const examples = [
{
description: 'Write a catalog yaml file',
example: yaml.stringify({
steps: [
{
action: id,
id: 'create-catalog-info-file',
name: 'Create catalog file',
input: {
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'test',
annotations: {},
},
spec: {
type: 'service',
lifecycle: 'production',
owner: 'default/owner',
},
},
},
},
],
}),
},
];
/**
* Writes a catalog descriptor file containing the provided entity to a path in the workspace.
* @public
*/
export function createCatalogWriteAction() {
return createTemplateAction<{ filePath?: string; entity: Entity }>({
id: 'catalog:write',
id,
description: 'Writes the catalog-info.yaml for your template',
examples,
schema: {
input: {
type: 'object',
@@ -20,6 +20,7 @@ import os from 'os';
import { Writable } from 'stream';
import { createDebugLogAction } from './log';
import { join } from 'path';
import yaml from 'yaml';
describe('debug:log', () => {
const logStream = {
@@ -91,4 +92,43 @@ describe('debug:log', () => {
expect.stringContaining('Hello Backstage!'),
);
});
it('should log the workspace content from an example, if active', async () => {
const example = action.examples?.find(
sample => sample.description === 'List the workspace directory',
)?.example as string;
expect(typeof example).toEqual('string');
const context = {
...mockContext,
...yaml.parse(example).steps[0],
};
await action.handler(context);
expect(logStream.write).toHaveBeenCalledTimes(1);
expect(logStream.write).toHaveBeenCalledWith(
expect.stringContaining('README.md'),
);
expect(logStream.write).toHaveBeenCalledWith(
expect.stringContaining(join('a-directory', 'index.md')),
);
});
it('should log message from an example', async () => {
const example = action.examples?.find(
sample => sample.description === 'Write a debug message',
)?.example as string;
const context = {
...mockContext,
...yaml.parse(example).steps[0],
};
await action.handler(context);
expect(logStream.write).toHaveBeenCalledTimes(1);
expect(logStream.write).toHaveBeenCalledWith(
expect.stringContaining('Hello Backstage!'),
);
});
});
@@ -17,6 +17,42 @@
import { readdir, stat } from 'fs-extra';
import { relative, join } from 'path';
import { createTemplateAction } from '../../createTemplateAction';
import yaml from 'yaml';
const id = 'debug:log';
const examples = [
{
description: 'Write a debug message',
example: yaml.stringify({
steps: [
{
action: id,
id: 'write-debug-line',
name: 'Write "Hello Backstage!" log line',
input: {
message: 'Hello Backstage!',
},
},
],
}),
},
{
description: 'List the workspace directory',
example: yaml.stringify({
steps: [
{
action: id,
id: 'write-workspace-directory',
name: 'List the workspace directory',
input: {
listWorkspace: true,
},
},
],
}),
},
];
/**
* Writes a message into the log or lists all files in the workspace
@@ -30,9 +66,10 @@ import { createTemplateAction } from '../../createTemplateAction';
*/
export function createDebugLogAction() {
return createTemplateAction<{ message?: string; listWorkspace?: boolean }>({
id: 'debug:log',
id,
description:
'Writes a message into the log or lists all files in the workspace.',
examples,
schema: {
input: {
type: 'object',
@@ -66,6 +66,7 @@ export type ActionContext<Input extends JsonObject> = {
export type TemplateAction<Input extends JsonObject> = {
id: string;
description?: string;
examples?: { description: string; example: string }[];
supportsDryRun?: boolean;
schema?: {
input?: Schema;
@@ -292,6 +292,7 @@ export async function createRouter(
return {
id: action.id,
description: action.description,
examples: action.examples,
schema: action.schema,
};
});
+18 -8
View File
@@ -38,6 +38,23 @@ import { UIOptionsType } from '@rjsf/utils';
import { UiSchema } from '@rjsf/utils';
import { z } from 'zod';
// @public
export type Action = {
id: string;
description?: string;
schema?: {
input?: JSONSchema7;
output?: JSONSchema7;
};
examples?: ActionExample[];
};
// @public
export type ActionExample = {
description: string;
example: string;
};
// @alpha
export function createNextScaffolderFieldExtension<
TReturnValue = unknown,
@@ -196,14 +213,7 @@ export interface LayoutOptions<P = any> {
export type LayoutTemplate<T = any> = FormProps_2<T>['ObjectFieldTemplate'];
// @public
export type ListActionsResponse = Array<{
id: string;
description?: string;
schema?: {
input?: JSONSchema7;
output?: JSONSchema7;
};
}>;
export type ListActionsResponse = Array<Action>;
// @public
export type LogEvent = {
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import React, { Fragment } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { scaffolderApiRef } from '../../api';
import {
@@ -27,10 +27,15 @@ import {
TableContainer,
TableHead,
TableRow,
Grid,
makeStyles,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@material-ui/core';
import { JSONSchema7, JSONSchema7Definition } from 'json-schema';
import classNames from 'classnames';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { useApi } from '@backstage/core-plugin-api';
import {
@@ -39,8 +44,10 @@ import {
Header,
Page,
ErrorPage,
CodeSnippet,
MarkdownContent,
} from '@backstage/core-components';
import { ActionExample } from '../../types';
const useStyles = makeStyles(theme => ({
code: {
@@ -68,6 +75,34 @@ const useStyles = makeStyles(theme => ({
},
}));
const ExamplesTable = (props: { examples: ActionExample[] }) => {
return (
<Grid container>
{props.examples.map((example, index) => {
return (
<Fragment key={`example-${index}`}>
<Grid item lg={3}>
<Box padding={4}>
<Typography>{example.description}</Typography>
</Box>
</Grid>
<Grid item lg={9}>
<Box padding={1}>
<CodeSnippet
text={example.example}
showLineNumbers
showCopyCodeButton
language="yaml"
/>
</Box>
</Grid>
</Fragment>
);
})}
</Grid>
);
};
export const ActionsPage = () => {
const api = useApi(scaffolderApiRef);
const classes = useStyles();
@@ -181,6 +216,18 @@ export const ActionsPage = () => {
{renderTable(action.schema.output)}
</Box>
)}
{action.examples && (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h5">Examples</Typography>
</AccordionSummary>
<AccordionDetails>
<Box pb={2}>
<ExamplesTable examples={action.examples} />
</Box>
</AccordionDetails>
</Accordion>
)}
</Box>
);
});
+2
View File
@@ -22,6 +22,8 @@
export { scaffolderApiRef, ScaffolderClient } from './api';
export type {
Action,
ActionExample,
ListActionsResponse,
LogEvent,
ScaffolderApi,
+21 -3
View File
@@ -43,18 +43,36 @@ export type ScaffolderTask = {
};
/**
* The response shape for the `listActions` call to the `scaffolder-backend`
* A single action example
*
* @public
*/
export type ListActionsResponse = Array<{
export type ActionExample = {
description: string;
example: string;
};
/**
* The response shape for a single action in the `listActions` call to the `scaffolder-backend`
*
* @public
*/
export type Action = {
id: string;
description?: string;
schema?: {
input?: JSONSchema7;
output?: JSONSchema7;
};
}>;
examples?: ActionExample[];
};
/**
* The response shape for the `listActions` call to the `scaffolder-backend`
*
* @public
*/
export type ListActionsResponse = Array<Action>;
/** @public */
export type ScaffolderOutputLink = {