Merge pull request #9768 from backstage/blam/depracations/scaffolder/more

🧹 more scaffolder frontend changes
This commit is contained in:
Fredrik Adelöw
2022-02-24 14:41:11 +01:00
committed by GitHub
21 changed files with 398 additions and 216 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
- **BREAKING** - the `/v2/tasks` endpoint now takes `templateRef` instead of `templateName` in the POST body. This should be a valid stringified `entityRef`.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': patch
---
- Moved the `JSONSchema` type from `@backstage/catalog-model` to `JSONSchema7`.
- Renamed and prefixed some types ready for exporting.
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-scaffolder': minor
---
- **BREAKING** - `scaffolderApi.scaffold()` now takes one `options` argument instead of 3, the existing arguments should just be wrapped up in one object instead.
- **BREAKING** - `scaffolderApi.scaffold()` now returns an object instead of a single string for the job ID. It's now `{ taskId: string }`
- **BREAKING** - `scaffolderApi.scaffold()` now takes a `templateRef` instead of `templateName` as an argument in the options. This should be a valid stringified `entityRef`.
- **BREAKING** - `scaffolderApi.getIntegrationsList` now returns an object `{ integrations: { type: string, title: string, host: string }[] }` instead of just an array.
@@ -49,6 +49,7 @@ import request from 'supertest';
*/
import { createRouter, DatabaseTaskStore, TaskBroker } from '../index';
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
import { stringifyEntityRef } from '@backstage/catalog-model';
const createCatalogClient = (template: any) =>
({
@@ -146,7 +147,10 @@ describe('createRouter', () => {
const response = await request(app)
.post('/v2/tasks')
.send({
templateName: 'create-react-app-template',
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
storePath: 'https://github.com/backstage/backstage',
},
@@ -165,7 +169,10 @@ describe('createRouter', () => {
const response = await request(app)
.post('/v2/tasks')
.send({
templateName: 'create-react-app-template',
templateRef: stringifyEntityRef({
kind: 'template',
name: 'create-react-app-template',
}),
values: {
required: 'required-value',
},
@@ -20,10 +20,7 @@ import {
UrlReader,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { InputError, NotFoundError } from '@backstage/errors';
@@ -178,14 +175,15 @@ export async function createRouter(
res.json(actionsList);
})
.post('/v2/tasks', async (req, res) => {
const templateName: string = req.body.templateName;
const kind = 'template';
const namespace = DEFAULT_NAMESPACE;
const templateRef: string = req.body.templateRef;
const { kind, namespace, name } = parseEntityRef(templateRef, {
defaultKind: 'template',
});
const values = req.body.values;
const token = getBearerToken(req.headers.authorization);
const template = await findTemplate({
catalogApi: catalogClient,
entityRef: { kind, namespace, name: templateName },
entityRef: { kind, namespace, name },
token: getBearerToken(req.headers.authorization),
});
+145 -36
View File
@@ -12,7 +12,6 @@ import { ComponentProps } from 'react';
import { ComponentType } from 'react';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { Extension } from '@backstage/core-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
@@ -20,7 +19,7 @@ import { FieldProps } from '@rjsf/core';
import { FieldValidation } from '@rjsf/core';
import { IconButton } from '@material-ui/core';
import { JsonObject } from '@backstage/types';
import { JSONSchema } from '@backstage/catalog-model';
import { JSONSchema7 } from 'json-schema';
import { Observable } from '@backstage/types';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
@@ -122,6 +121,38 @@ export type FieldExtensionOptions<
validation?: CustomFieldValidator<TFieldReturnValue>;
};
// Warning: (ae-missing-release-tag) "JobStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
// Warning: (ae-missing-release-tag) "ListActionsResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ListActionsResponse = Array<{
id: string;
description?: string;
schema?: {
input?: JSONSchema7;
output?: JSONSchema7;
};
}>;
// Warning: (ae-missing-release-tag) "LogEvent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type LogEvent = {
type: 'log' | 'completion';
body: {
message: string;
stepId?: string;
status?: ScaffolderTaskStatus;
};
createdAt: string;
id: string;
taskId: string;
};
// Warning: (ae-missing-release-tag) "OwnedEntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -210,34 +241,21 @@ export interface RepoUrlPickerUiOptions {
// @public
export interface ScaffolderApi {
// (undocumented)
getIntegrationsList(options: { allowedHosts: string[] }): Promise<
{
type: string;
title: string;
host: string;
}[]
>;
// Warning: (ae-forgotten-export) The symbol "ScaffolderTask" needs to be exported by the entry point index.d.ts
//
getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions,
): Promise<ScaffolderGetIntegrationsListResponse>;
// (undocumented)
getTask(taskId: string): Promise<ScaffolderTask>;
// Warning: (ae-forgotten-export) The symbol "TemplateParameterSchema" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getTemplateParameterSchema(
templateName: EntityName,
templateRef: string,
): Promise<TemplateParameterSchema>;
// Warning: (ae-forgotten-export) The symbol "ListActionsResponse" needs to be exported by the entry point index.d.ts
listActions(): Promise<ListActionsResponse>;
scaffold(
templateName: string,
values: Record<string, any>,
secrets?: Record<string, string>,
): Promise<string>;
// Warning: (ae-forgotten-export) The symbol "LogEvent" needs to be exported by the entry point index.d.ts
//
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
// (undocumented)
streamLogs(options: { taskId: string; after?: number }): Observable<LogEvent>;
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
}
// @public
@@ -252,28 +270,22 @@ export class ScaffolderClient implements ScaffolderApi {
useLongPollingLogs?: boolean;
});
// (undocumented)
getIntegrationsList(options: { allowedHosts: string[] }): Promise<
{
type: string;
title: string;
host: string;
}[]
>;
getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions,
): Promise<ScaffolderGetIntegrationsListResponse>;
// (undocumented)
getTask(taskId: string): Promise<any>;
getTask(taskId: string): Promise<ScaffolderTask>;
// (undocumented)
getTemplateParameterSchema(
templateName: EntityName,
templateRef: string,
): Promise<TemplateParameterSchema>;
// (undocumented)
listActions(): Promise<ListActionsResponse>;
scaffold(
templateName: string,
values: Record<string, any>,
secrets?: Record<string, string>,
): Promise<string>;
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
// (undocumented)
streamLogs(options: { taskId: string; after?: number }): Observable<LogEvent>;
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
}
// Warning: (ae-missing-release-tag) "ScaffolderFieldExtensions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -281,6 +293,26 @@ export class ScaffolderClient implements ScaffolderApi {
// @public (undocumented)
export const ScaffolderFieldExtensions: React_2.ComponentType;
// Warning: (ae-missing-release-tag) "ScaffolderGetIntegrationsListOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ScaffolderGetIntegrationsListOptions {
// (undocumented)
allowedHosts: string[];
}
// Warning: (ae-missing-release-tag) "ScaffolderGetIntegrationsListResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ScaffolderGetIntegrationsListResponse {
// (undocumented)
integrations: {
type: string;
title: string;
host: string;
}[];
}
// Warning: (ae-missing-release-tag) "ScaffolderPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -316,6 +348,68 @@ export const scaffolderPlugin: BackstagePlugin<
}
>;
// Warning: (ae-missing-release-tag) "ScaffolderScaffoldOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ScaffolderScaffoldOptions {
// (undocumented)
secrets?: Record<string, string>;
// (undocumented)
templateRef: string;
// (undocumented)
values: Record<string, any>;
}
// Warning: (ae-missing-release-tag) "ScaffolderScaffoldResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ScaffolderScaffoldResponse {
// (undocumented)
taskId: string;
}
// Warning: (ae-missing-release-tag) "ScaffolderStreamLogsOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ScaffolderStreamLogsOptions {
// (undocumented)
after?: number;
// (undocumented)
taskId: string;
}
// Warning: (ae-missing-release-tag) "ScaffolderTask" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ScaffolderTask = {
id: string;
spec: TaskSpec;
status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled';
lastHeartbeatAt: string;
createdAt: string;
};
// Warning: (ae-missing-release-tag) "ScaffolderTaskOutput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ScaffolderTaskOutput = {
entityRef?: string;
remoteUrl?: string;
links?: ScaffolderOutputLink[];
} & {
[key: string]: unknown;
};
// Warning: (ae-missing-release-tag) "ScaffolderTaskStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ScaffolderTaskStatus =
| 'open'
| 'processing'
| 'failed'
| 'completed'
| 'skipped';
// @public
export const TaskPage: ({ loadingText }: TaskPageProps) => JSX.Element;
@@ -348,6 +442,17 @@ export type TemplateListProps = {
};
};
// Warning: (ae-missing-release-tag) "TemplateParameterSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type TemplateParameterSchema = {
title: string;
steps: Array<{
title: string;
schema: JsonObject;
}>;
};
// Warning: (ae-missing-release-tag) "TemplateTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -357,4 +462,8 @@ export const TemplateTypePicker: () => JSX.Element | null;
export const useTemplateSecrets: () => {
setSecret: (input: Record<string, string>) => void;
};
// Warnings were encountered during analysis:
//
// src/types.d.ts:32:5 - (ae-forgotten-export) The symbol "ScaffolderOutputLink" needs to be exported by the entry point index.d.ts
```
+1
View File
@@ -34,6 +34,7 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@types/json-schema": "^7.0.9",
"@backstage/catalog-client": "^0.7.0",
"@backstage/catalog-model": "^0.10.0",
"@backstage/config": "^0.1.14",
+3 -1
View File
@@ -62,7 +62,9 @@ describe('api', () => {
'dev.azure.com',
'bitbucket.org',
];
const integrations = await apiClient.getIntegrationsList({ allowedHosts });
const { integrations } = await apiClient.getIntegrationsList({
allowedHosts,
});
integrations.forEach(integration =>
expect(allowedHosts).toContain(integration.host),
);
+35 -86
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { EntityName } from '@backstage/catalog-model';
import { parseEntityRef } from '@backstage/catalog-model';
import {
createApiRef,
DiscoveryApi,
@@ -22,11 +22,21 @@ import {
} from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { JsonObject, JsonValue, Observable } from '@backstage/types';
import { Field, FieldValidation } from '@rjsf/core';
import { Observable } from '@backstage/types';
import qs from 'qs';
import ObservableImpl from 'zen-observable';
import { ListActionsResponse, ScaffolderTask, Status } from './types';
import {
ListActionsResponse,
LogEvent,
ScaffolderApi,
TemplateParameterSchema,
ScaffolderScaffoldOptions,
ScaffolderScaffoldResponse,
ScaffolderStreamLogsOptions,
ScaffolderGetIntegrationsListOptions,
ScaffolderGetIntegrationsListResponse,
ScaffolderTask,
} from './types';
/**
* Utility API reference for the {@link ScaffolderApi}.
@@ -37,70 +47,6 @@ export const scaffolderApiRef = createApiRef<ScaffolderApi>({
id: 'plugin.scaffolder.service',
});
type TemplateParameterSchema = {
title: string;
steps: Array<{
title: string;
schema: JsonObject;
}>;
};
export type LogEvent = {
type: 'log' | 'completion';
body: {
message: string;
stepId?: string;
status?: Status;
};
createdAt: string;
id: string;
taskId: string;
};
export type CustomField = {
name: string;
component: Field;
validation: (data: JsonValue, field: FieldValidation) => void;
};
/**
* An API to interact with the scaffolder backend.
*
* @public
*/
export interface ScaffolderApi {
getTemplateParameterSchema(
templateName: EntityName,
): Promise<TemplateParameterSchema>;
/**
* Executes the scaffolding of a component, given a template and its
* parameter values.
*
* @param templateName - Name of the Template entity for the scaffolder to use. New project is going to be created out of this template.
* @param values - Parameters for the template, e.g. name, description
* @param secrets - Optional secrets to pass to as the secrets parameter to the template.
*/
scaffold(
templateName: string,
values: Record<string, any>,
secrets?: Record<string, string>,
): Promise<string>;
getTask(taskId: string): Promise<ScaffolderTask>;
getIntegrationsList(options: {
allowedHosts: string[];
}): Promise<{ type: string; title: string; host: string }[]>;
/**
* Returns a list of all installed actions.
*/
listActions(): Promise<ListActionsResponse>;
streamLogs(options: { taskId: string; after?: number }): Observable<LogEvent>;
}
/**
* An API to interact with the scaffolder backend.
*
@@ -124,8 +70,10 @@ export class ScaffolderClient implements ScaffolderApi {
this.useLongPollingLogs = options.useLongPollingLogs ?? false;
}
async getIntegrationsList(options: { allowedHosts: string[] }) {
return [
async getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions,
): Promise<ScaffolderGetIntegrationsListResponse> {
const integrations = [
...this.scmIntegrationsApi.azure.list(),
...this.scmIntegrationsApi.bitbucket.list(),
...this.scmIntegrationsApi.github.list(),
@@ -133,17 +81,24 @@ export class ScaffolderClient implements ScaffolderApi {
]
.map(c => ({ type: c.type, title: c.title, host: c.config.host }))
.filter(c => options.allowedHosts.includes(c.host));
return {
integrations,
};
}
async getTemplateParameterSchema(
templateName: EntityName,
templateRef: string,
): Promise<TemplateParameterSchema> {
const { namespace, kind, name } = templateName;
const { namespace, kind, name } = parseEntityRef(templateRef, {
defaultKind: 'template',
});
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const templatePath = [namespace, kind, name]
.map(s => encodeURIComponent(s))
.join('/');
const url = `${baseUrl}/v2/templates/${templatePath}/parameter-schema`;
const response = await this.fetchApi.fetch(url);
@@ -159,15 +114,12 @@ export class ScaffolderClient implements ScaffolderApi {
* Executes the scaffolding of a component, given a template and its
* parameter values.
*
* @param templateName - Template name for the scaffolder to use. New project is going to be created out of this template.
* @param values - Parameters for the template, e.g. name, description
* @param secrets - Optional secrets to pass to as the secrets parameter to the template.
* @param options - The {@link ScaffolderScaffoldOptions} the scaffolding.
*/
async scaffold(
templateName: string,
values: Record<string, any>,
secrets: Record<string, string> = {},
): Promise<string> {
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse> {
const { templateRef, values, secrets = {} } = options;
const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`;
const response = await this.fetchApi.fetch(url, {
method: 'POST',
@@ -175,7 +127,7 @@ export class ScaffolderClient implements ScaffolderApi {
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateName,
templateRef,
values: { ...values },
secrets,
}),
@@ -188,10 +140,10 @@ export class ScaffolderClient implements ScaffolderApi {
}
const { id } = (await response.json()) as { id: string };
return id;
return { taskId: id };
}
async getTask(taskId: string) {
async getTask(taskId: string): Promise<ScaffolderTask> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`;
@@ -203,10 +155,7 @@ export class ScaffolderClient implements ScaffolderApi {
return await response.json();
}
streamLogs(options: {
taskId: string;
after?: number;
}): Observable<LogEvent> {
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent> {
if (this.useLongPollingLogs) {
return this.streamLogsPolling(options);
}
@@ -14,11 +14,12 @@
* limitations under the License.
*/
import React from 'react';
import { ScaffolderApi, scaffolderApiRef } from '../../api';
import { scaffolderApiRef } from '../../api';
import { ActionsPage } from './ActionsPage';
import { rootRouteRef } from '../../routes';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { ApiProvider } from '@backstage/core-app-api';
import { ScaffolderApi } from '../../types';
const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
scaffold: jest.fn(),
@@ -28,8 +28,7 @@ import {
TableRow,
makeStyles,
} from '@material-ui/core';
import { JSONSchema } from '@backstage/catalog-model';
import { JSONSchema7Definition } from 'json-schema';
import { JSONSchema7, JSONSchema7Definition } from 'json-schema';
import classNames from 'classnames';
import { useApi } from '@backstage/core-plugin-api';
@@ -87,7 +86,7 @@ export const ActionsPage = () => {
);
}
const formatRows = (input: JSONSchema) => {
const formatRows = (input: JSONSchema7) => {
const properties = input.properties;
if (!properties) {
return undefined;
@@ -95,7 +94,7 @@ export const ActionsPage = () => {
return Object.entries(properties).map(entry => {
const [key] = entry;
const props = entry[1] as unknown as JSONSchema;
const props = entry[1] as unknown as JSONSchema7;
const codeClassname = classNames(classes.code, {
[classes.codeRequired]: input.required?.includes(key),
});
@@ -115,7 +114,7 @@ export const ActionsPage = () => {
});
};
const renderTable = (input: JSONSchema) => {
const renderTable = (input: JSONSchema7) => {
if (!input.properties) {
return undefined;
}
@@ -145,7 +144,7 @@ export const ActionsPage = () => {
<>
<Typography variant="h6">{name}</Typography>
{input.map((i, index) => (
<div key={index}>{renderTable(i as unknown as JSONSchema)}</div>
<div key={index}>{renderTable(i as unknown as JSONSchema7)}</div>
))}
</>
);
@@ -49,7 +49,7 @@ import React, { memo, useEffect, useMemo, useState } from 'react';
import { generatePath, useNavigate, useParams } from 'react-router';
import useInterval from 'react-use/lib/useInterval';
import { rootRouteRef } from '../../routes';
import { Status, TaskOutput } from '../../types';
import { ScaffolderTaskStatus, ScaffolderTaskOutput } from '../../types';
import { useTaskEventStream } from '../hooks/useEventStream';
import { TaskPageLinks } from './TaskPageLinks';
@@ -86,7 +86,7 @@ const useStyles = makeStyles((theme: Theme) =>
type TaskStep = {
id: string;
name: string;
status: Status;
status: ScaffolderTaskStatus;
startedAt?: string;
endedAt?: string;
};
@@ -217,7 +217,11 @@ export const TaskStatusStepper = memo(
},
);
const hasLinks = ({ entityRef, remoteUrl, links = [] }: TaskOutput): boolean =>
const hasLinks = ({
entityRef,
remoteUrl,
links = [],
}: ScaffolderTaskOutput): boolean =>
!!(entityRef || remoteUrl || links.length > 0);
/**
@@ -19,12 +19,12 @@ import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { Box } from '@material-ui/core';
import LanguageIcon from '@material-ui/icons/Language';
import React from 'react';
import { TaskOutput } from '../../types';
import { ScaffolderTaskOutput } from '../../types';
import { IconLink } from './IconLink';
import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api';
type TaskPageLinksProps = {
output: TaskOutput;
output: ScaffolderTaskOutput;
};
export const TaskPageLinks = ({ output }: TaskPageLinksProps) => {
@@ -23,7 +23,8 @@ import { ThemeProvider } from '@material-ui/core';
import { act, fireEvent, within } from '@testing-library/react';
import React from 'react';
import { MemoryRouter, Route } from 'react-router';
import { ScaffolderApi, scaffolderApiRef } from '../../api';
import { scaffolderApiRef } from '../../api';
import { ScaffolderApi } from '../../types';
import { rootRouteRef } from '../../routes';
import { TemplatePage } from './TemplatePage';
import {
@@ -41,17 +41,13 @@ import {
useApiHolder,
useRouteRef,
} from '@backstage/core-plugin-api';
import { stringifyEntityRef } from '@backstage/catalog-model';
const useTemplateParameterSchema = (templateName: string) => {
const useTemplateParameterSchema = (templateRef: string) => {
const scaffolderApi = useApi(scaffolderApiRef);
const { value, loading, error } = useAsync(
() =>
scaffolderApi.getTemplateParameterSchema({
name: templateName,
kind: 'template',
namespace: 'default',
}),
[scaffolderApi, templateName],
() => scaffolderApi.getTemplateParameterSchema(templateRef),
[scaffolderApi, templateRef],
);
return { schema: value, loading, error };
};
@@ -141,11 +137,15 @@ export const TemplatePage = ({
);
const handleCreate = async () => {
const id = await scaffolderApi.scaffold(
templateName,
formState,
secretsContext?.secrets,
);
const { taskId } = await scaffolderApi.scaffold({
templateRef: stringifyEntityRef({
name: templateName,
kind: 'template',
namespace: 'default',
}),
values: formState,
secrets: secretsContext?.secrets,
});
const formParams = qs.stringify(
{ formData: formState },
@@ -158,7 +158,7 @@ export const TemplatePage = ({
// extra back/forward slots.
window.history?.replaceState(null, document.title, newUrl);
navigate(generatePath(`${rootLink()}/tasks/:taskId`, { taskId: id }));
navigate(generatePath(`${rootLink()}/tasks/:taskId`, { taskId }));
};
if (error) {
@@ -23,7 +23,9 @@ import {
scmAuthApiRef,
ScmAuthApi,
} from '@backstage/integration-react';
import { scaffolderApiRef, ScaffolderApi } from '../../../api';
import { scaffolderApiRef } from '../../../api';
import { ScaffolderApi } from '../../../types';
import {
SecretsContextProvider,
SecretsContext,
@@ -32,10 +34,12 @@ import { act, fireEvent } from '@testing-library/react';
describe('RepoUrlPicker', () => {
const mockScaffolderApi: Partial<ScaffolderApi> = {
getIntegrationsList: async () => [
{ host: 'github.com', type: 'github', title: 'github.com' },
{ host: 'dev.azure.com', type: 'azure', title: 'dev.azure.com' },
],
getIntegrationsList: async () => ({
integrations: [
{ host: 'github.com', type: 'github', title: 'github.com' },
{ host: 'dev.azure.com', type: 'azure', title: 'dev.azure.com' },
],
}),
};
const mockIntegrationsApi: Partial<ScmIntegrationsApi> = {
@@ -23,11 +23,11 @@ describe('RepoUrlPickerHostField', () => {
it('renders the default host properly', async () => {
const mockOnChange = jest.fn();
const mockScaffolderApi = {
getIntegrationsList: jest
.fn()
.mockResolvedValue([
getIntegrationsList: jest.fn().mockResolvedValue({
integrations: [
{ host: 'github.com', title: 'github.com', type: 'github' },
]),
],
}),
};
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
@@ -46,10 +46,12 @@ describe('RepoUrlPickerHostField', () => {
it('should provide a dropdown when multiple hosts are returned that can be selected', async () => {
const mockOnChange = jest.fn();
const mockScaffolderApi = {
getIntegrationsList: jest.fn().mockResolvedValue([
{ host: 'github.com', title: 'github.com', type: 'github' },
{ host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' },
]),
getIntegrationsList: jest.fn().mockResolvedValue({
integrations: [
{ host: 'github.com', title: 'github.com', type: 'github' },
{ host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' },
],
}),
};
const { getByRole, getByText, getByTestId } = await renderInTestApp(
@@ -73,10 +75,12 @@ describe('RepoUrlPickerHostField', () => {
it('should not display hosts that dont have integration config set correctly', async () => {
const mockOnChange = jest.fn();
const mockScaffolderApi = {
getIntegrationsList: jest.fn().mockResolvedValue([
{ host: 'github.com', title: 'github.com', type: 'github' },
{ host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' },
]),
getIntegrationsList: jest.fn().mockResolvedValue({
integrations: [
{ host: 'github.com', title: 'github.com', type: 'github' },
{ host: 'gitlab.com', title: 'gitlab.com', type: 'gitlab' },
],
}),
};
const { getByRole, getByText, getByTestId } = await renderInTestApp(
@@ -30,11 +30,13 @@ export const RepoUrlPickerHost = (props: {
const { host, hosts, onChange, rawErrors } = props;
const scaffolderApi = useApi(scaffolderApiRef);
const { value: integrations, loading } = useAsync(async () => {
return await scaffolderApi.getIntegrationsList({
allowedHosts: hosts ?? [],
});
});
const { value: { integrations } = { integrations: [] }, loading } = useAsync(
async () => {
return await scaffolderApi.getIntegrationsList({
allowedHosts: hosts ?? [],
});
},
);
useEffect(() => {
// If there is no host chosen currently
@@ -15,14 +15,19 @@
*/
import { useImmerReducer } from 'use-immer';
import { useEffect } from 'react';
import { scaffolderApiRef, LogEvent } from '../../api';
import { ScaffolderTask, Status, TaskOutput } from '../../types';
import { scaffolderApiRef } from '../../api';
import {
ScaffolderTask,
ScaffolderTaskStatus,
ScaffolderTaskOutput,
LogEvent,
} from '../../types';
import { useApi } from '@backstage/core-plugin-api';
import { Subscription } from '@backstage/types';
type Step = {
id: string;
status: Status;
status: ScaffolderTaskStatus;
endedAt?: string;
startedAt?: string;
};
@@ -34,16 +39,16 @@ export type TaskStream = {
completed: boolean;
task?: ScaffolderTask;
steps: { [stepId in string]: Step };
output?: TaskOutput;
output?: ScaffolderTaskOutput;
};
type ReducerLogEntry = {
createdAt: string;
body: {
stepId?: string;
status?: Status;
status?: ScaffolderTaskStatus;
message: string;
output?: TaskOutput;
output?: ScaffolderTaskOutput;
};
};
+15 -1
View File
@@ -21,7 +21,21 @@
*/
export { scaffolderApiRef, ScaffolderClient } from './api';
export type { ScaffolderApi } from './api';
export type {
JobStatus,
ListActionsResponse,
LogEvent,
ScaffolderApi,
ScaffolderGetIntegrationsListOptions,
ScaffolderGetIntegrationsListResponse,
ScaffolderScaffoldOptions,
ScaffolderScaffoldResponse,
ScaffolderStreamLogsOptions,
ScaffolderTask,
ScaffolderTaskOutput,
ScaffolderTaskStatus,
TemplateParameterSchema,
} from './types';
export {
createScaffolderFieldExtension,
ScaffolderFieldExtensions,
+90 -27
View File
@@ -13,31 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JSONSchema } from '@backstage/catalog-model';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { JsonObject, Observable } from '@backstage/types';
import { JSONSchema7 } from 'json-schema';
export type ScaffolderTaskStatus =
| 'open'
| 'processing'
| 'failed'
| 'completed'
| 'skipped';
export type Status = 'open' | 'processing' | 'failed' | 'completed' | 'skipped';
export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
export type Job = {
id: string;
metadata: {
entity: any;
values: any;
remoteUrl?: string;
catalogInfoUrl?: string;
};
status: JobStatus;
stages: Stage[];
error?: Error;
};
export type Stage = {
name: string;
log: string[];
status: JobStatus;
startedAt: string;
endedAt?: string;
};
export type ScaffolderTask = {
id: string;
@@ -51,24 +38,100 @@ export type ListActionsResponse = Array<{
id: string;
description?: string;
schema?: {
input?: JSONSchema;
output?: JSONSchema;
input?: JSONSchema7;
output?: JSONSchema7;
};
}>;
type OutputLink = {
type ScaffolderOutputLink = {
title?: string;
icon?: string;
url?: string;
entityRef?: string;
};
export type TaskOutput = {
export type ScaffolderTaskOutput = {
/** @deprecated use the `links` property to link out to relevant resources */
entityRef?: string;
/** @deprecated use the `links` property to link out to relevant resources */
remoteUrl?: string;
links?: OutputLink[];
links?: ScaffolderOutputLink[];
} & {
[key: string]: unknown;
};
export type TemplateParameterSchema = {
title: string;
steps: Array<{
title: string;
schema: JsonObject;
}>;
};
export type LogEvent = {
type: 'log' | 'completion';
body: {
message: string;
stepId?: string;
status?: ScaffolderTaskStatus;
};
createdAt: string;
id: string;
taskId: string;
};
export interface ScaffolderScaffoldOptions {
templateRef: string;
values: Record<string, any>;
secrets?: Record<string, string>;
}
export interface ScaffolderScaffoldResponse {
taskId: string;
}
export interface ScaffolderGetIntegrationsListOptions {
allowedHosts: string[];
}
export interface ScaffolderGetIntegrationsListResponse {
integrations: { type: string; title: string; host: string }[];
}
export interface ScaffolderStreamLogsOptions {
taskId: string;
after?: number;
}
/**
* An API to interact with the scaffolder backend.
*
* @public
*/
export interface ScaffolderApi {
getTemplateParameterSchema(
templateRef: string,
): Promise<TemplateParameterSchema>;
/**
* Executes the scaffolding of a component, given a template and its
* parameter values.
*
* @param options - The {@link ScaffolderScaffoldOptions} the scaffolding.
*/
scaffold(
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
getTask(taskId: string): Promise<ScaffolderTask>;
getIntegrationsList(
options: ScaffolderGetIntegrationsListOptions,
): Promise<ScaffolderGetIntegrationsListResponse>;
/**
* Returns a list of all installed actions.
*/
listActions(): Promise<ListActionsResponse>;
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
}