diff --git a/.changeset/red-readers-mix.md b/.changeset/red-readers-mix.md new file mode 100644 index 0000000000..1f1828cb4b --- /dev/null +++ b/.changeset/red-readers-mix.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Introduce scaffolder actions page which lists all available actions along with documentation about their input/output. + +Allow for actions to be extended with a description. + +The list actions page is by default available at `/create/actions`. diff --git a/.changeset/wet-hounds-vanish.md b/.changeset/wet-hounds-vanish.md new file mode 100644 index 0000000000..1a74bfb08d --- /dev/null +++ b/.changeset/wet-hounds-vanish.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Update GitHub publisher to display a more helpful error message when repository access update fails. diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index e6886b1f04..14dc7ef861 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -4,112 +4,13 @@ title: Builtin actions description: Documentation describing the built-in template actions. --- -# Built-in Actions +The scaffolder comes with several built-in actions for fetching content, +registering in the catalog and of course actions for creating and publishing a +git repository. -This is the list of built-in template actions +There are several repository providers supported out of the box such as GitHub, +Azure, GitLab and Bitbucket. -## `fetch:plain` - -Downloads content and places it in the workspacePath or optionally in a -subdirectory specified by the `targetPath` input option. - -input: - -- `url` (required) Relative path or absolute URL pointing to the directory tree - to fetch. -- `targetPath` Target path within the working directory to download the contents - to. - -output: nothing - -## `fetch:cookiecutter` - -Downloads template from `url` and templates with cookiecutter - -input: - -- `url` (required) Relative path or absolute URL pointing to the directory tree - to fetch. -- `targetPath` Target path within the working directory to download the contents - to. -- `values` Values to pass on to cookiecutter for templating. - -output: nothing - -## `catalog:register` - -Registers entity in the software catalog. - -input: - -- `catalogInfoUrl` (required) An absolute URL pointing to the catalog info file - location. - -output: - -- `repoContentsUrl` An absolute URL pointing to the root of a repository - directory tree. -- `catalogInfoPath` A relative path from the repo root pointing to the catalog - info file, defaults to /catalog-info.yaml - -## `publish:github` - -Initializes a git repository of contents in workspacePath and publishes to -GitHub. - -input: - -- `repoUrl` (required) Repository location -- `repoVisibility` (optional, default: 'private') Possible values: 'private', - 'public' or 'internal'. - -output: - -- `remoteUrl` A URL to the repository with the provider -- `repoContentsUrl` A URL to the root of the repository - -## `publish:gitlab` - -Initializes a git repository of contents in workspacePath and publishes to -GitLab. - -input: - -- `repoUrl` (required) Repository location -- `repoVisibility` (optional, default: 'private') Possible values: 'private', - 'public' or 'internal'. - -output: - -- `remoteUrl` A URL to the repository with the provider -- `repoContentsUrl` A URL to the root of the repository - -## `publish:bitbucket` - -Initializes a git repository of contents in workspacePath and publishes to -Bitbucket. - -input: - -- `repoUrl` (required) Repository location -- `repoVisibility` (optional, default: 'private') Possible values: 'private', - 'public'. - -output: - -- `remoteUrl` A URL to the repository with the provider -- `repoContentsUrl` A URL to the root of the repository - -## `publish:azure` - -Initializes a git repository of contents in workspacePath and publishes to -Azure. - -input: - -- `repoUrl` (required) Repository location - -output: - -- `remoteUrl` A URL to the repository with the provider -- `repoContentsUrl` A URL to the root of the repository +A list of all registered actions can be found under `/create/actions`. For local +development you should be able to reach them at +`http://localhost:3000/create/actions`. diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts index 2a841a564b..c9e163bfdd 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts @@ -38,4 +38,8 @@ export class TemplateActionRegistry { } return action; } + + list(): TemplateAction[] { + return [...this.actions.values()]; + } } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index 7cf1ac2120..b034b480e2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -31,6 +31,8 @@ export function createCatalogRegisterAction(options: { | { repoContentsUrl: string; catalogInfoPath?: string } >({ id: 'catalog:register', + description: + 'Registers entities from a catalog descriptor file in the workspace into the software catalog.', schema: { input: { oneOf: [ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts index 2a04ed301d..070aaa9679 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts @@ -38,6 +38,8 @@ export function createFetchCookiecutterAction(options: { values: JsonObject; }>({ id: 'fetch:cookiecutter', + description: + 'Downloads a template from the given URL into the workspace, and runs cookiecutter on it.', schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts index 1eb1a6dc82..00c63535ca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts @@ -28,6 +28,8 @@ export function createFetchPlainAction(options: { return createTemplateAction<{ url: string; targetPath?: string }>({ id: 'fetch:plain', + description: + "Downloads content and places it in the workspace, or optionally in a subdirectory specified by the 'targetPath' input option.", schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 5eaba05ea0..db38960086 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -32,6 +32,8 @@ export function createPublishAzureAction(options: { description?: string; }>({ id: 'publish:azure', + description: + 'Initializes a git repository of the content in the workspace, and publishes it to Azure.', schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 4c3cf30ecd..03881af35d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -167,6 +167,8 @@ export function createPublishBitbucketAction(options: { repoVisibility: 'private' | 'public'; }>({ id: 'publish:bitbucket', + description: + 'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.', schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index be0820d0b1..cb11e3d08a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -43,6 +43,8 @@ export function createPublishGithubAction(options: { repoVisibility: 'private' | 'internal' | 'public'; }>({ id: 'publish:github', + description: + 'Initializes a git repository of contents in workspace and publishes it to GitHub.', schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 9122c828a0..eefdcdf11b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -31,6 +31,8 @@ export function createPublishGitlabAction(options: { repoVisibility: 'private' | 'internal' | 'public'; }>({ id: 'publish:gitlab', + description: + 'Initializes a git repository of the content in the workspace, and publishes it to GitLab.', schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 20b7696d70..955cb72de9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -44,6 +44,7 @@ export type ActionContext = { export type TemplateAction = { id: string; + description?: string; schema?: { input?: Schema; output?: Schema; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index a449eddd5d..d0bfc89529 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -131,25 +131,30 @@ export class GithubPublisher implements PublisherBase { const { data } = await repoCreationPromise; - if (access?.startsWith(`${owner}/`)) { - const [, team] = access.split('/'); - await client.teams.addOrUpdateRepoPermissionsInOrg({ - org: owner, - team_slug: team, - owner, - repo: name, - permission: 'admin', - }); - // no need to add access if it's the person who own's the personal account - } else if (access && access !== owner) { - await client.repos.addCollaborator({ - owner, - repo: name, - username: access, - permission: 'admin', - }); + try { + if (access?.startsWith(`${owner}/`)) { + const [, team] = access.split('/'); + await client.teams.addOrUpdateRepoPermissionsInOrg({ + org: owner, + team_slug: team, + owner, + repo: name, + permission: 'admin', + }); + // no need to add access if it's the person who owns the personal account + } else if (access && access !== owner) { + await client.repos.addCollaborator({ + owner, + repo: name, + username: access, + permission: 'admin', + }); + } + } catch (e) { + throw new Error( + `Failed to add access to '${access}'. Status ${e.status} ${e.message}`, + ); } - return data?.clone_url; } } diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index b7c2582c81..f01b4f8c95 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -261,6 +261,15 @@ describe('createRouter', () => { }); }); + describe('GET /v2/actions', () => { + it('lists available actions', async () => { + const response = await request(app).get('/v2/actions').send(); + expect(response.status).toEqual(200); + expect(response.body[0].id).toBeDefined(); + expect(response.body.length).toBeGreaterThan(8); + }); + }); + describe('POST /v2/tasks', () => { it('rejects template values which do not match the template schema definition', async () => { const response = await request(app) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 3d47d5123b..81be03730b 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -347,6 +347,16 @@ export async function createRouter( } }, ) + .get('/v2/actions', async (_req, res) => { + const actionsList = actionRegistry.list().map(action => { + return { + id: action.id, + description: action.description, + schema: action.schema, + }; + }); + res.json(actionsList); + }) .post('/v2/tasks', async (req, res) => { const templateName: string = req.body.templateName; const values: TemplaterValues = req.body.values; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index eef66a45ca..93fa32b164 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -43,6 +43,7 @@ "@rjsf/core": "^2.4.0", "@rjsf/material-ui": "^2.4.0", "classnames": "^2.2.6", + "json-schema": "^0.2.5", "git-url-parse": "^11.4.4", "humanize-duration": "^3.25.1", "luxon": "^1.25.0", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 2be4caa81f..758cd555e3 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -25,7 +25,7 @@ import { } from '@backstage/core'; import { ScmIntegrations } from '@backstage/integration'; import ObservableImpl from 'zen-observable'; -import { ScaffolderTask, Status } from './types'; +import { ListActionsResponse, ScaffolderTask, Status } from './types'; export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', @@ -72,6 +72,9 @@ export interface ScaffolderApi { allowedHosts: string[]; }): Promise<{ type: string; title: string; host: string }[]>; + // Returns a list of all installed actions. + listActions(): Promise; + streamLogs({ taskId, after, @@ -227,4 +230,13 @@ export class ScaffolderClient implements ScaffolderApi { ); }); } + + /** + * @returns ListActionsResponse containing all registered actions. + */ + async listActions(): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); + const response = await fetch(`${baseUrl}/v2/actions`); + return await response.json(); + } } diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx new file mode 100644 index 0000000000..708d08359a --- /dev/null +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -0,0 +1,167 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ApiRegistry, ApiProvider } from '@backstage/core'; +import React from 'react'; +import { ScaffolderApi, scaffolderApiRef } from '../../api'; +import { ActionsPage } from './ActionsPage'; +import { rootRouteRef } from '../../routes'; +import { renderInTestApp } from '@backstage/test-utils'; + +const scaffolderApiMock: jest.Mocked = { + scaffold: jest.fn(), + getTemplateParameterSchema: jest.fn(), + getIntegrationsList: jest.fn(), + getTask: jest.fn(), + streamLogs: jest.fn(), + listActions: jest.fn(), +}; + +const apis = ApiRegistry.from([[scaffolderApiRef, scaffolderApiMock]]); + +describe('TemplatePage', () => { + beforeEach(() => jest.resetAllMocks()); + + it('renders action with input', async () => { + scaffolderApiMock.listActions.mockResolvedValue([ + { + id: 'test', + description: 'example description', + schema: { + input: { + type: 'object', + required: ['foobar'], + properties: { + foobar: { + title: 'Test title', + type: 'string', + }, + }, + }, + }, + }, + ]); + const rendered = await renderInTestApp( + + + , + { + mountedRoutes: { + '/create/actions': rootRouteRef, + }, + }, + ); + expect(rendered.queryByText('Test title')).toBeInTheDocument(); + expect(rendered.queryByText('example description')).toBeInTheDocument(); + expect(rendered.queryByText('foobar')).toBeInTheDocument(); + expect(rendered.queryByText('output')).not.toBeInTheDocument(); + }); + + it('renders action with input and output', async () => { + scaffolderApiMock.listActions.mockResolvedValue([ + { + id: 'test', + description: 'example description', + schema: { + input: { + type: 'object', + required: ['foobar'], + properties: { + foobar: { + title: 'Test title', + type: 'string', + }, + }, + }, + output: { + type: 'object', + properties: { + buzz: { + title: 'Test output', + type: 'string', + }, + }, + }, + }, + }, + ]); + const rendered = await renderInTestApp( + + + , + { + mountedRoutes: { + '/create/actions': rootRouteRef, + }, + }, + ); + expect(rendered.queryByText('Test title')).toBeInTheDocument(); + expect(rendered.queryByText('example description')).toBeInTheDocument(); + expect(rendered.queryByText('foobar')).toBeInTheDocument(); + expect(rendered.queryByText('Test output')).toBeInTheDocument(); + }); + + it('renders action with oneOf input', async () => { + scaffolderApiMock.listActions.mockResolvedValue([ + { + id: 'test', + description: 'example description', + schema: { + input: { + oneOf: [ + { + type: 'object', + required: ['foo'], + properties: { + foo: { + title: 'Foo title', + description: 'Foo description', + type: 'string', + }, + }, + }, + { + type: 'object', + required: ['bar'], + properties: { + bar: { + title: 'Bar title', + description: 'Bar description', + type: 'string', + }, + }, + }, + ], + }, + }, + }, + ]); + const rendered = await renderInTestApp( + + + , + { + mountedRoutes: { + '/create/actions': rootRouteRef, + }, + }, + ); + expect(rendered.queryByText('oneOf')).toBeInTheDocument(); + expect(rendered.queryByText('Foo title')).toBeInTheDocument(); + expect(rendered.queryByText('Foo description')).toBeInTheDocument(); + expect(rendered.queryByText('Bar title')).toBeInTheDocument(); + expect(rendered.queryByText('Bar description')).toBeInTheDocument(); + }); +}); diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx new file mode 100644 index 0000000000..a06ca50736 --- /dev/null +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -0,0 +1,192 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 from 'react'; +import { useAsync } from 'react-use'; +import { + useApi, + Progress, + Content, + Header, + Page, + ErrorPage, +} from '@backstage/core'; +import { scaffolderApiRef } from '../../api'; +import { + Typography, + Paper, + Table, + TableBody, + Box, + TableCell, + TableContainer, + TableHead, + TableRow, + makeStyles, +} from '@material-ui/core'; +import { JSONSchema } from '@backstage/catalog-model'; +import { JSONSchema7Definition } from 'json-schema'; +import classNames from 'classnames'; + +const useStyles = makeStyles(theme => ({ + code: { + fontFamily: 'Menlo, monospace', + padding: theme.spacing(1), + backgroundColor: + theme.palette.type === 'dark' + ? theme.palette.grey[700] + : theme.palette.grey[300], + display: 'inline-block', + borderRadius: 5, + border: `1px solid ${theme.palette.grey[500]}`, + position: 'relative', + }, + + codeRequired: { + '&::after': { + position: 'absolute', + content: '"*"', + top: 0, + right: theme.spacing(0.5), + fontWeight: 'bolder', + color: theme.palette.error.light, + }, + }, +})); + +export const ActionsPage = () => { + const api = useApi(scaffolderApiRef); + const classes = useStyles(); + const { loading, value, error } = useAsync(async () => { + return api.listActions(); + }); + + if (loading) { + return ; + } + + if (error) { + return ( + + ); + } + + const formatRows = (input: JSONSchema) => { + const properties = input.properties; + if (!properties) { + return undefined; + } + + return Object.entries(properties).map(entry => { + const [key] = entry; + const props = (entry[1] as unknown) as JSONSchema; + const codeClassname = classNames(classes.code, { + [classes.codeRequired]: input.required?.includes(key), + }); + + return ( + + +
{key}
+
+ {props.title} + {props.description} + + {props.type} + +
+ ); + }); + }; + + const renderTable = (input: JSONSchema) => { + if (!input.properties) { + return undefined; + } + return ( + + + + + Name + Title + Description + Type + + + {formatRows(input)} +
+
+ ); + }; + + const renderTables = (name: string, input?: JSONSchema7Definition[]) => { + if (!input) { + return undefined; + } + + return ( + <> + {name} + {input.map((i, index) => ( +
{renderTable((i as unknown) as JSONSchema)}
+ ))} + + ); + }; + + const items = value?.map(action => { + if (action.id.startsWith('legacy:')) { + return undefined; + } + + const oneOf = renderTables('oneOf', action.schema?.input?.oneOf); + return ( + + + {action.id} + + {action.description} + {action.schema?.input && ( + + Input + {renderTable(action.schema.input)} + {oneOf} + + )} + {action.schema?.output && ( + + Output + {renderTable(action.schema.output)} + + )} + + ); + }); + + return ( + +
+ {items} + + ); +}; diff --git a/plugins/scaffolder/src/components/ActionsPage/index.ts b/plugins/scaffolder/src/components/ActionsPage/index.ts new file mode 100644 index 0000000000..64d548e37d --- /dev/null +++ b/plugins/scaffolder/src/components/ActionsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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. + */ + +export { ActionsPage } from './ActionsPage'; diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 12d4ecce82..9a525c0be4 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -19,11 +19,13 @@ import { Routes, Route } from 'react-router'; import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; +import { ActionsPage } from './ActionsPage'; export const Router = () => ( } /> } /> } /> + } /> ); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index 1b11b704ae..3cdf6c9738 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -39,6 +39,7 @@ const scaffolderApiMock: jest.Mocked = { getIntegrationsList: jest.fn(), getTask: jest.fn(), streamLogs: jest.fn(), + listActions: jest.fn(), }; const errorApiMock = { post: jest.fn(), error$: jest.fn() }; diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 0499f4aca2..6a2c2eede4 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { JSONSchema } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/config'; export type Status = 'open' | 'processing' | 'failed' | 'completed'; @@ -54,3 +55,12 @@ export type ScaffolderTask = { lastHeartbeatAt: string; createdAt: string; }; + +export type ListActionsResponse = Array<{ + id: string; + description?: string; + schema?: { + input?: JSONSchema; + output?: JSONSchema; + }; +}>; diff --git a/yarn.lock b/yarn.lock index 2ce59dbf9f..cf95252ca2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8884,10 +8884,10 @@ bluebird@~3.4.1: resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM= -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== body-parser@1.19.0, body-parser@^1.18.3: version "1.19.0" @@ -8992,9 +8992,9 @@ breakword@^1.0.5: dependencies: wcwidth "^1.0.1" -brorand@^1.0.1: +brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= browser-process-hrtime@^1.0.0: @@ -12062,17 +12062,17 @@ element-resize-detector@^1.2.1: batch-processor "1.0.0" elliptic@^6.0.0: - version "6.5.3" - resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" + bn.js "^4.11.9" + brorand "^1.1.0" hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" emitter-component@^1.1.1: version "1.1.1" @@ -14673,7 +14673,7 @@ hash-stream-validation@^0.2.1, hash-stream-validation@^0.2.2: hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" - resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== dependencies: inherits "^2.0.3" @@ -14735,9 +14735,9 @@ history@^5.0.0: dependencies: "@babel/runtime" "^7.7.6" -hmac-drbg@^1.0.0: +hmac-drbg@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= dependencies: hash.js "^1.0.3" @@ -15283,7 +15283,7 @@ inflight@^1.0.4: inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.1: @@ -18480,12 +18480,12 @@ mini-css-extract-plugin@^0.9.0: minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: +minimalistic-crypto-utils@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= minimatch@0.3: