@@ -105,8 +105,8 @@ export type ListActions = {};
|
||||
export type ListTasks = {
|
||||
query: {
|
||||
createdBy?: Array<string>;
|
||||
limit?: Array<number>;
|
||||
offset?: Array<number>;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
order?: Array<string>;
|
||||
status?: Array<string>;
|
||||
};
|
||||
@@ -339,7 +339,7 @@ export class DefaultApiClient {
|
||||
): Promise<TypedResponse<ListTasksResponse>> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
|
||||
|
||||
const uriTemplate = `/v2/tasks{?createdBy*,limit*,offset*,order*,status*}`;
|
||||
const uriTemplate = `/v2/tasks{?createdBy*,limit,offset,order*,status*}`;
|
||||
|
||||
const uri = parser.parse(uriTemplate).expand({
|
||||
...request.query,
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Autocomplete400Response {
|
||||
message?: string;
|
||||
name?: string;
|
||||
}
|
||||
+2
-3
@@ -17,13 +17,12 @@
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { JsonValue } from '../models/JsonValue.model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface TemplateParameterSchemaStepsInner {
|
||||
title: JsonValue;
|
||||
description?: JsonValue;
|
||||
title: string;
|
||||
description?: string;
|
||||
schema: any;
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
import { ValidationErrorArgument } from '../models/ValidationErrorArgument.model';
|
||||
import { ValidationErrorPathInner } from '../models/ValidationErrorPathInner.model';
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,6 @@ export interface ValidationError {
|
||||
message: string;
|
||||
instance: any;
|
||||
name: string;
|
||||
argument: any;
|
||||
argument: ValidationErrorArgument;
|
||||
stack: string;
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// ******************************************************************
|
||||
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
|
||||
// ******************************************************************
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ValidationErrorArgument = any | boolean | number | string;
|
||||
@@ -19,6 +19,7 @@ export * from '../models/ActionExample.model';
|
||||
export * from '../models/ActionSchema.model';
|
||||
export * from '../models/Autocomplete200Response.model';
|
||||
export * from '../models/Autocomplete200ResponseResultsInner.model';
|
||||
export * from '../models/Autocomplete400Response.model';
|
||||
export * from '../models/AutocompleteRequest.model';
|
||||
export * from '../models/CancelTask200Response.model';
|
||||
export * from '../models/DryRun200Response.model';
|
||||
@@ -61,4 +62,5 @@ export * from '../models/TemplateInfoEntity.model';
|
||||
export * from '../models/TemplateParameterSchema.model';
|
||||
export * from '../models/TemplateParameterSchemaStepsInner.model';
|
||||
export * from '../models/ValidationError.model';
|
||||
export * from '../models/ValidationErrorArgument.model';
|
||||
export * from '../models/ValidationErrorPathInner.model';
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
@@ -73,7 +72,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"msw": "^1.0.0",
|
||||
"uri-template": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { ScmIntegrations } from '@backstage/integration';
|
||||
import {
|
||||
MockFetchApi,
|
||||
mockApis,
|
||||
registerMswTestHooks,
|
||||
} from '@backstage/test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ScaffolderClient } from './ScaffolderClient';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
|
||||
jest.mock('@microsoft/fetch-event-source');
|
||||
const mockFetchEventSource = fetchEventSource as jest.MockedFunction<
|
||||
typeof fetchEventSource
|
||||
>;
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
describe('api', () => {
|
||||
registerMswTestHooks(server);
|
||||
const githubIntegrationConfig = mockApis.config({
|
||||
data: {
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'hello.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const mockBaseUrl = 'http://backstage/api';
|
||||
|
||||
const discoveryApi = { getBaseUrl: async () => mockBaseUrl };
|
||||
const fetchApi = new MockFetchApi();
|
||||
const identityApi = {
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
signOut: jest.fn(),
|
||||
};
|
||||
|
||||
const scmIntegrationsApi = ScmIntegrations.fromConfig(
|
||||
githubIntegrationConfig,
|
||||
);
|
||||
|
||||
let apiClient: ScaffolderClient;
|
||||
beforeEach(() => {
|
||||
apiClient = new ScaffolderClient({
|
||||
scmIntegrationsApi,
|
||||
discoveryApi,
|
||||
fetchApi,
|
||||
identityApi,
|
||||
});
|
||||
|
||||
jest.restoreAllMocks();
|
||||
identityApi.getBackstageIdentity.mockReturnValue({});
|
||||
});
|
||||
|
||||
it('should return default and custom integrations', async () => {
|
||||
const allowedHosts = [
|
||||
'hello.com',
|
||||
'gitlab.com',
|
||||
'github.com',
|
||||
'dev.azure.com',
|
||||
'bitbucket.org',
|
||||
];
|
||||
const { integrations } = await apiClient.getIntegrationsList({
|
||||
allowedHosts,
|
||||
});
|
||||
integrations.forEach(integration =>
|
||||
expect(allowedHosts).toContain(integration.host),
|
||||
);
|
||||
});
|
||||
|
||||
describe('streamEvents', () => {
|
||||
describe('eventsource', () => {
|
||||
it('should work', async () => {
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({
|
||||
id: '',
|
||||
event: 'log',
|
||||
data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}',
|
||||
});
|
||||
await Promise.resolve();
|
||||
onmessage?.({
|
||||
id: '',
|
||||
event: 'completion',
|
||||
data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}',
|
||||
});
|
||||
});
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete => {
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete });
|
||||
});
|
||||
|
||||
expect(mockFetchEventSource).toHaveBeenCalledWith(
|
||||
'http://backstage/api/v2/tasks/a-random-task-id/eventstream',
|
||||
{
|
||||
fetch: fetchApi.fetch,
|
||||
onmessage: expect.any(Function),
|
||||
onerror: expect.any(Function),
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('longPolling', () => {
|
||||
beforeEach(() => {
|
||||
apiClient = new ScaffolderClient({
|
||||
scmIntegrationsApi,
|
||||
discoveryApi,
|
||||
fetchApi,
|
||||
identityApi,
|
||||
useLongPollingLogs: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(req, res, ctx) => {
|
||||
const { taskId } = req.params;
|
||||
const after = req.url.searchParams.get('after');
|
||||
|
||||
if (taskId === 'a-random-task-id') {
|
||||
if (!after) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
} else if (after === '1') {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete =>
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete }),
|
||||
);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should unsubscribe', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(req, res, ctx) => {
|
||||
const { taskId } = req.params;
|
||||
|
||||
const after = req.url.searchParams.get('after');
|
||||
|
||||
// use assertion to make sure it is not called after unsubscribing
|
||||
expect(after).toBe(null);
|
||||
|
||||
if (taskId === 'a-random-task-id') {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete => {
|
||||
const subscription = apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({
|
||||
next: (...args) => {
|
||||
next(...args);
|
||||
subscription.unsubscribe();
|
||||
complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should continue after error', async () => {
|
||||
const called = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(_req, res, ctx) => {
|
||||
called();
|
||||
|
||||
if (called.mock.calls.length > 1) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete =>
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete }),
|
||||
);
|
||||
|
||||
expect(called).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listTasks', () => {
|
||||
it('should list all tasks', async () => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => {
|
||||
const createdBy = req.url.searchParams.get('createdBy');
|
||||
|
||||
if (createdBy) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
createdBy,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await apiClient.listTasks({ filterByOwnership: 'all' });
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should list tasks with limit and offset', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks?limit=5&offset=0`,
|
||||
(_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await apiClient.listTasks({
|
||||
filterByOwnership: 'all',
|
||||
limit: 5,
|
||||
offset: 0,
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should list task using the current user as owner', async () => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => {
|
||||
const createdBy = req.url.searchParams.get('createdBy');
|
||||
|
||||
if (createdBy) {
|
||||
return res(
|
||||
ctx.json({
|
||||
tasks: [
|
||||
{
|
||||
createdBy,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json({
|
||||
tasks: [
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
identityApi.getBackstageIdentity.mockResolvedValueOnce({
|
||||
userEntityRef: 'user:default/foo',
|
||||
});
|
||||
|
||||
const result = await apiClient.listTasks({ filterByOwnership: 'owned' });
|
||||
expect(identityApi.getBackstageIdentity).toHaveBeenCalled();
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,11 +15,6 @@
|
||||
*/
|
||||
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
DiscoveryApi,
|
||||
FetchApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { Observable } from '@backstage/types';
|
||||
@@ -50,6 +45,9 @@ import {
|
||||
TaskStatus,
|
||||
TypedResponse,
|
||||
} from '../client/src/schema/openapi';
|
||||
import { DiscoveryApi } from '../client/src/schema/openapi/generated/types/discovery';
|
||||
import { FetchApi } from '../client/src/schema/openapi/generated/types/fetch';
|
||||
import { IdentityApi } from './types/IdentityApi';
|
||||
|
||||
/**
|
||||
* An API to interact with the scaffolder backend.
|
||||
@@ -106,8 +104,8 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
request.filterByOwnership === 'owned'
|
||||
? [userEntityRef]
|
||||
: undefined,
|
||||
limit: request.limit ? [request.limit] : undefined,
|
||||
offset: request.offset ? [request.offset] : undefined,
|
||||
limit: request.limit,
|
||||
offset: request.offset,
|
||||
},
|
||||
},
|
||||
options,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a copy of the BackstageUserIdentity, to avoid importing core-plugin-api.
|
||||
*/
|
||||
type BackstageUserIdentity = {
|
||||
/**
|
||||
* The type of identity that this structure represents. In the frontend app
|
||||
* this will currently always be 'user'.
|
||||
*/
|
||||
type: 'user';
|
||||
|
||||
/**
|
||||
* The entityRef of the user in the catalog.
|
||||
* For example User:default/sandra
|
||||
*/
|
||||
userEntityRef: string;
|
||||
|
||||
/**
|
||||
* The user and group entities that the user claims ownership through
|
||||
*/
|
||||
ownershipEntityRefs: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a partial copy of the IdentityApi, to avoid importing core-plugin-api.
|
||||
*/
|
||||
export type IdentityApi = {
|
||||
/**
|
||||
* User identity information within Backstage.
|
||||
*/
|
||||
getBackstageIdentity(): Promise<BackstageUserIdentity>;
|
||||
};
|
||||
Reference in New Issue
Block a user