Merge remote-tracking branch 'origin/master' into feat/bitrise_pagination
This commit is contained in:
@@ -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`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Update GitHub publisher to display a more helpful error message when repository access update fails.
|
||||
@@ -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`.
|
||||
|
||||
@@ -38,4 +38,8 @@ export class TemplateActionRegistry {
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
list(): TemplateAction<any>[] {
|
||||
return [...this.actions.values()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -44,6 +44,7 @@ export type ActionContext<Input extends InputBase> = {
|
||||
|
||||
export type TemplateAction<Input extends InputBase> = {
|
||||
id: string;
|
||||
description?: string;
|
||||
schema?: {
|
||||
input?: Schema;
|
||||
output?: Schema;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<ScaffolderApi>({
|
||||
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<ListActionsResponse>;
|
||||
|
||||
streamLogs({
|
||||
taskId,
|
||||
after,
|
||||
@@ -227,4 +230,13 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns ListActionsResponse containing all registered actions.
|
||||
*/
|
||||
async listActions(): Promise<ListActionsResponse> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const response = await fetch(`${baseUrl}/v2/actions`);
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ScaffolderApi> = {
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ActionsPage />
|
||||
</ApiProvider>,
|
||||
{
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ActionsPage />
|
||||
</ApiProvider>,
|
||||
{
|
||||
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(
|
||||
<ApiProvider apis={apis}>
|
||||
<ActionsPage />
|
||||
</ApiProvider>,
|
||||
{
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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 <Progress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
statusMessage="Failed to load installed actions"
|
||||
status="500"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<TableRow key={key}>
|
||||
<TableCell>
|
||||
<div className={codeClassname}>{key}</div>
|
||||
</TableCell>
|
||||
<TableCell>{props.title}</TableCell>
|
||||
<TableCell>{props.description}</TableCell>
|
||||
<TableCell>
|
||||
<span className={classes.code}>{props.type}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const renderTable = (input: JSONSchema) => {
|
||||
if (!input.properties) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Title</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{formatRows(input)}</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTables = (name: string, input?: JSONSchema7Definition[]) => {
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6">{name}</Typography>
|
||||
{input.map((i, index) => (
|
||||
<div key={index}>{renderTable((i as unknown) as JSONSchema)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const items = value?.map(action => {
|
||||
if (action.id.startsWith('legacy:')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const oneOf = renderTables('oneOf', action.schema?.input?.oneOf);
|
||||
return (
|
||||
<Box pb={4} key={action.id}>
|
||||
<Typography variant="h4" className={classes.code}>
|
||||
{action.id}
|
||||
</Typography>
|
||||
<Typography>{action.description}</Typography>
|
||||
{action.schema?.input && (
|
||||
<Box pb={2}>
|
||||
<Typography variant="h5">Input</Typography>
|
||||
{renderTable(action.schema.input)}
|
||||
{oneOf}
|
||||
</Box>
|
||||
)}
|
||||
{action.schema?.output && (
|
||||
<Box pb={2}>
|
||||
<Typography variant="h5">Output</Typography>
|
||||
{renderTable(action.schema.output)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
pageTitleOverride="Create a New Component"
|
||||
title="Installed actions"
|
||||
subtitle="This is the collection of all installed actions"
|
||||
/>
|
||||
<Content>{items}</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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 = () => (
|
||||
<Routes>
|
||||
<Route path="/" element={<ScaffolderPage />} />
|
||||
<Route path="/templates/:templateName" element={<TemplatePage />} />
|
||||
<Route path="/tasks/:taskId" element={<TaskPage />} />
|
||||
<Route path="/actions" element={<ActionsPage />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
getIntegrationsList: jest.fn(),
|
||||
getTask: jest.fn(),
|
||||
streamLogs: jest.fn(),
|
||||
listActions: jest.fn(),
|
||||
};
|
||||
|
||||
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}>;
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user