Merge pull request #15341 from RoadieHQ/scaffolder-examples
add examples of scaffolder action inputs to docs
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Show action example yaml on the scaffolder actions documentation page.
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
export { scaffolderApiRef, ScaffolderClient } from './api';
|
||||
export type {
|
||||
Action,
|
||||
ActionExample,
|
||||
ListActionsResponse,
|
||||
LogEvent,
|
||||
ScaffolderApi,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user