Merge pull request #33122 from johnmcollier/filterstatustasks
feat(scaffolder): Allow filtering by status in scaffolderService.listTasks
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
Updated the `list-scaffolder-tasks` action to support the new "status" filter parameter, allowing the action to return tasks matching a specific status.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-node': patch
|
||||
---
|
||||
|
||||
Added optional `status` filter to `ScaffolderService.listTasks`, allowing callers to retrieve tasks matching a specific status.
|
||||
@@ -63,7 +63,12 @@ describe('createListScaffolderTasksAction', () => {
|
||||
totalTasks: mockTasks.totalTasks ?? 0,
|
||||
});
|
||||
expect(mockScaffolderService.listTasks).toHaveBeenCalledWith(
|
||||
{ createdBy: undefined, limit: undefined, offset: undefined },
|
||||
{
|
||||
createdBy: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
status: undefined,
|
||||
},
|
||||
expect.objectContaining({ credentials: expect.anything() }),
|
||||
);
|
||||
});
|
||||
@@ -106,7 +111,7 @@ describe('createListScaffolderTasksAction', () => {
|
||||
});
|
||||
|
||||
expect(mockScaffolderService.listTasks).toHaveBeenCalledWith(
|
||||
{ createdBy: undefined, limit: 2, offset: 1 },
|
||||
{ createdBy: undefined, limit: 2, offset: 1, status: undefined },
|
||||
expect.objectContaining({ credentials: expect.anything() }),
|
||||
);
|
||||
|
||||
@@ -188,11 +193,102 @@ describe('createListScaffolderTasksAction', () => {
|
||||
createdBy: 'user:default/alice',
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
status: undefined,
|
||||
},
|
||||
expect.objectContaining({ credentials: expect.anything() }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter tasks by a single status when status is provided', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockAuth = mockServices.auth.mock();
|
||||
const mockScaffolderService = scaffolderServiceMock.mock();
|
||||
const completedTasks = generateMockTasks().tasks.filter(
|
||||
t => t.status === 'completed',
|
||||
);
|
||||
|
||||
mockScaffolderService.listTasks.mockResolvedValue({
|
||||
items: completedTasks as ScaffolderTask[],
|
||||
totalItems: completedTasks.length,
|
||||
});
|
||||
|
||||
createListScaffolderTasksAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
auth: mockAuth,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
const result = await mockActionsRegistry.invoke({
|
||||
id: 'test:list-scaffolder-tasks',
|
||||
input: { status: 'completed' },
|
||||
});
|
||||
|
||||
expect(mockScaffolderService.listTasks).toHaveBeenCalledWith(
|
||||
{
|
||||
createdBy: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
status: 'completed',
|
||||
},
|
||||
expect.objectContaining({ credentials: expect.anything() }),
|
||||
);
|
||||
expect(result.output).toEqual({
|
||||
tasks: completedTasks.map(task => ({
|
||||
id: task.id,
|
||||
spec: task.spec,
|
||||
status: task.status,
|
||||
createdAt: task.createdAt,
|
||||
lastHeartbeatAt: task.lastHeartbeatAt,
|
||||
})),
|
||||
totalTasks: completedTasks.length,
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter tasks by multiple statuses when an array is provided', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockAuth = mockServices.auth.mock();
|
||||
const mockScaffolderService = scaffolderServiceMock.mock();
|
||||
const matchingTasks = generateMockTasks().tasks.filter(
|
||||
t => t.status === 'completed' || t.status === 'failed',
|
||||
);
|
||||
|
||||
mockScaffolderService.listTasks.mockResolvedValue({
|
||||
items: matchingTasks as ScaffolderTask[],
|
||||
totalItems: matchingTasks.length,
|
||||
});
|
||||
|
||||
createListScaffolderTasksAction({
|
||||
actionsRegistry: mockActionsRegistry,
|
||||
auth: mockAuth,
|
||||
scaffolderService: mockScaffolderService,
|
||||
});
|
||||
|
||||
const result = await mockActionsRegistry.invoke({
|
||||
id: 'test:list-scaffolder-tasks',
|
||||
input: { status: ['completed', 'failed'] },
|
||||
});
|
||||
|
||||
expect(mockScaffolderService.listTasks).toHaveBeenCalledWith(
|
||||
{
|
||||
createdBy: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
status: ['completed', 'failed'],
|
||||
},
|
||||
expect.objectContaining({ credentials: expect.anything() }),
|
||||
);
|
||||
expect(result.output).toEqual({
|
||||
tasks: matchingTasks.map(task => ({
|
||||
id: task.id,
|
||||
spec: task.spec,
|
||||
status: task.status,
|
||||
createdAt: task.createdAt,
|
||||
lastHeartbeatAt: task.lastHeartbeatAt,
|
||||
})),
|
||||
totalTasks: matchingTasks.length,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NotAllowedError when owned is true without user identity', async () => {
|
||||
const mockActionsRegistry = actionsRegistryServiceMock();
|
||||
const mockAuth = mockServices.auth.mock();
|
||||
|
||||
@@ -40,7 +40,7 @@ This allows you to list scaffolder tasks that have been created.
|
||||
Each task has a unique id, specification, and status (one of open, processing, completed, failed, cancelled, skipped).
|
||||
Each task includes a timestamp for when it was created, and an optional last heartbeat timestamp indicating the most recent activity.
|
||||
Set owned to true to return only tasks created by the current user; omit or set to false for all tasks the credentials can see.
|
||||
Pagination is supported via limit and offset.
|
||||
Filtering by one or multiple statuses is supported. Pagination is supported via limit and offset.
|
||||
`,
|
||||
schema: {
|
||||
input: z =>
|
||||
@@ -65,6 +65,20 @@ Pagination is supported via limit and offset.
|
||||
.min(0)
|
||||
.describe('The offset to start from for pagination')
|
||||
.optional(),
|
||||
status: (() => {
|
||||
const statusEnum = z.enum([
|
||||
'open',
|
||||
'processing',
|
||||
'completed',
|
||||
'failed',
|
||||
'cancelled',
|
||||
'skipped',
|
||||
]);
|
||||
return z
|
||||
.union([statusEnum, z.array(statusEnum).nonempty()])
|
||||
.optional()
|
||||
.describe('Filter tasks by status, or an array of statuses');
|
||||
})(),
|
||||
}),
|
||||
output: z =>
|
||||
z
|
||||
@@ -112,6 +126,7 @@ Pagination is supported via limit and offset.
|
||||
createdBy,
|
||||
limit: input.limit,
|
||||
offset: input.offset,
|
||||
status: input.status,
|
||||
},
|
||||
{ credentials },
|
||||
);
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface ScaffolderService {
|
||||
createdBy?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: ScaffolderTaskStatus | ScaffolderTaskStatus[];
|
||||
},
|
||||
options: ScaffolderServiceRequestOptions,
|
||||
): Promise<{
|
||||
|
||||
@@ -406,6 +406,7 @@ export interface ScaffolderService {
|
||||
createdBy?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: ScaffolderTaskStatus | ScaffolderTaskStatus[];
|
||||
},
|
||||
options: ScaffolderServiceRequestOptions,
|
||||
): Promise<{
|
||||
|
||||
@@ -202,4 +202,61 @@ describe('scaffolderServiceRef', () => {
|
||||
|
||||
expect(result).toEqual({ items: [], totalItems: 0 });
|
||||
});
|
||||
|
||||
it('should serialize a single status as a repeated query param for listTasks', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get('*/api/scaffolder/v2/tasks', (req, res, ctx) => {
|
||||
expect(req.url.searchParams.getAll('status')).toEqual(['completed']);
|
||||
return res(ctx.json({ tasks: [], totalTasks: 0 }));
|
||||
}),
|
||||
);
|
||||
|
||||
const tester = ServiceFactoryTester.from(
|
||||
createServiceFactory({
|
||||
service: createServiceRef<void>({ id: 'unused-dummy' }),
|
||||
deps: {},
|
||||
factory() {},
|
||||
}),
|
||||
{ dependencies: [mockServices.discovery.factory()] },
|
||||
);
|
||||
|
||||
const scaffolder = await tester.getService(scaffolderServiceRef);
|
||||
|
||||
await scaffolder.listTasks(
|
||||
{ status: 'completed' },
|
||||
{ credentials: mockCredentials.user() },
|
||||
);
|
||||
});
|
||||
|
||||
it('should serialize multiple statuses as repeated query params for listTasks', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get('*/api/scaffolder/v2/tasks', (req, res, ctx) => {
|
||||
expect(req.url.searchParams.getAll('status')).toEqual([
|
||||
'completed',
|
||||
'failed',
|
||||
]);
|
||||
return res(ctx.json({ tasks: [], totalTasks: 0 }));
|
||||
}),
|
||||
);
|
||||
|
||||
const tester = ServiceFactoryTester.from(
|
||||
createServiceFactory({
|
||||
service: createServiceRef<void>({ id: 'unused-dummy' }),
|
||||
deps: {},
|
||||
factory() {},
|
||||
}),
|
||||
{ dependencies: [mockServices.discovery.factory()] },
|
||||
);
|
||||
|
||||
const scaffolder = await tester.getService(scaffolderServiceRef);
|
||||
|
||||
await scaffolder.listTasks(
|
||||
{ status: ['completed', 'failed'] },
|
||||
{ credentials: mockCredentials.user() },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface ScaffolderService {
|
||||
createdBy?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: ScaffolderTaskStatus | ScaffolderTaskStatus[];
|
||||
},
|
||||
options: ScaffolderServiceRequestOptions,
|
||||
): Promise<{ items: ScaffolderTask[]; totalItems: number }>;
|
||||
@@ -185,6 +186,7 @@ class DefaultScaffolderService implements ScaffolderService {
|
||||
createdBy?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: ScaffolderTaskStatus | ScaffolderTaskStatus[];
|
||||
},
|
||||
options: ScaffolderServiceRequestOptions,
|
||||
): Promise<{ items: ScaffolderTask[]; totalItems: number }> {
|
||||
@@ -201,6 +203,11 @@ class DefaultScaffolderService implements ScaffolderService {
|
||||
if (request.offset !== undefined) {
|
||||
params.set('offset', String(request.offset));
|
||||
}
|
||||
if (request.status !== undefined) {
|
||||
for (const s of [request.status].flat()) {
|
||||
params.append('status', s);
|
||||
}
|
||||
}
|
||||
|
||||
const query = params.toString();
|
||||
const url = `${baseUrl}/v2/tasks${query ? `?${query}` : ''}`;
|
||||
|
||||
Reference in New Issue
Block a user