Merge pull request #9768 from backstage/blam/depracations/scaffolder/more
🧹 more scaffolder frontend changes
This commit is contained in:
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user