feat: allow scaffolder tasks pagination in the frontend
Requires #26378, relates to #25856 Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-react': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Add support for pagination in scaffolder tasks list
|
||||
@@ -232,8 +232,13 @@ export interface ScaffolderApi {
|
||||
): Promise<TemplateParameterSchema>;
|
||||
listActions(): Promise<ListActionsResponse>;
|
||||
// (undocumented)
|
||||
listTasks?(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
|
||||
listTasks?(options: {
|
||||
filterByOwnership: 'owned' | 'all';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{
|
||||
tasks: ScaffolderTask[];
|
||||
totalTasks?: number;
|
||||
}>;
|
||||
retry?(taskId: string): Promise<void>;
|
||||
scaffold(
|
||||
@@ -581,10 +586,10 @@ export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets;
|
||||
// src/api/types.d.ts:164:5 - (ae-undocumented) Missing documentation for "getTemplateParameterSchema".
|
||||
// src/api/types.d.ts:172:5 - (ae-undocumented) Missing documentation for "getTask".
|
||||
// src/api/types.d.ts:185:5 - (ae-undocumented) Missing documentation for "listTasks".
|
||||
// src/api/types.d.ts:190:5 - (ae-undocumented) Missing documentation for "getIntegrationsList".
|
||||
// src/api/types.d.ts:195:5 - (ae-undocumented) Missing documentation for "streamLogs".
|
||||
// src/api/types.d.ts:196:5 - (ae-undocumented) Missing documentation for "dryRun".
|
||||
// src/api/types.d.ts:197:5 - (ae-undocumented) Missing documentation for "autocomplete".
|
||||
// src/api/types.d.ts:193:5 - (ae-undocumented) Missing documentation for "getIntegrationsList".
|
||||
// src/api/types.d.ts:198:5 - (ae-undocumented) Missing documentation for "streamLogs".
|
||||
// src/api/types.d.ts:199:5 - (ae-undocumented) Missing documentation for "dryRun".
|
||||
// src/api/types.d.ts:200:5 - (ae-undocumented) Missing documentation for "autocomplete".
|
||||
// src/components/types.d.ts:7:1 - (ae-undocumented) Missing documentation for "TemplateGroupFilter".
|
||||
// src/extensions/types.d.ts:13:5 - (ae-undocumented) Missing documentation for "uiSchema".
|
||||
// src/extensions/types.d.ts:30:5 - (ae-undocumented) Missing documentation for ""ui:options"".
|
||||
|
||||
@@ -223,7 +223,9 @@ export interface ScaffolderApi {
|
||||
|
||||
listTasks?(options: {
|
||||
filterByOwnership: 'owned' | 'all';
|
||||
}): Promise<{ tasks: ScaffolderTask[] }>;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{ tasks: ScaffolderTask[]; totalTasks?: number }>;
|
||||
|
||||
getIntegrationsList(
|
||||
options: ScaffolderGetIntegrationsListOptions,
|
||||
|
||||
@@ -552,8 +552,13 @@ export class ScaffolderClient implements ScaffolderApi_2 {
|
||||
// (undocumented)
|
||||
listActions(): Promise<ListActionsResponse_2>;
|
||||
// (undocumented)
|
||||
listTasks(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
|
||||
listTasks(options: {
|
||||
filterByOwnership: 'owned' | 'all';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{
|
||||
tasks: ScaffolderTask_2[];
|
||||
totalTasks?: number;
|
||||
}>;
|
||||
// (undocumented)
|
||||
retry?(taskId: string): Promise<void>;
|
||||
@@ -670,16 +675,16 @@ export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets_2;
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/api.d.ts:23:5 - (ae-undocumented) Missing documentation for "listTasks".
|
||||
// src/api.d.ts:28:5 - (ae-undocumented) Missing documentation for "getIntegrationsList".
|
||||
// src/api.d.ts:29:5 - (ae-undocumented) Missing documentation for "getTemplateParameterSchema".
|
||||
// src/api.d.ts:30:5 - (ae-undocumented) Missing documentation for "scaffold".
|
||||
// src/api.d.ts:31:5 - (ae-undocumented) Missing documentation for "getTask".
|
||||
// src/api.d.ts:32:5 - (ae-undocumented) Missing documentation for "streamLogs".
|
||||
// src/api.d.ts:33:5 - (ae-undocumented) Missing documentation for "dryRun".
|
||||
// src/api.d.ts:36:5 - (ae-undocumented) Missing documentation for "listActions".
|
||||
// src/api.d.ts:37:5 - (ae-undocumented) Missing documentation for "cancelTask".
|
||||
// src/api.d.ts:38:5 - (ae-undocumented) Missing documentation for "retry".
|
||||
// src/api.d.ts:39:5 - (ae-undocumented) Missing documentation for "autocomplete".
|
||||
// src/api.d.ts:31:5 - (ae-undocumented) Missing documentation for "getIntegrationsList".
|
||||
// src/api.d.ts:32:5 - (ae-undocumented) Missing documentation for "getTemplateParameterSchema".
|
||||
// src/api.d.ts:33:5 - (ae-undocumented) Missing documentation for "scaffold".
|
||||
// src/api.d.ts:34:5 - (ae-undocumented) Missing documentation for "getTask".
|
||||
// src/api.d.ts:35:5 - (ae-undocumented) Missing documentation for "streamLogs".
|
||||
// src/api.d.ts:36:5 - (ae-undocumented) Missing documentation for "dryRun".
|
||||
// src/api.d.ts:39:5 - (ae-undocumented) Missing documentation for "listActions".
|
||||
// src/api.d.ts:40:5 - (ae-undocumented) Missing documentation for "cancelTask".
|
||||
// src/api.d.ts:41:5 - (ae-undocumented) Missing documentation for "retry".
|
||||
// src/api.d.ts:42:5 - (ae-undocumented) Missing documentation for "autocomplete".
|
||||
// src/components/OngoingTask/OngoingTask.d.ts:6:22 - (ae-undocumented) Missing documentation for "OngoingTask".
|
||||
// src/components/fields/EntityPicker/schema.d.ts:15:22 - (ae-undocumented) Missing documentation for "EntityPickerFieldSchema".
|
||||
// src/components/fields/EntityTagsPicker/schema.d.ts:4:22 - (ae-undocumented) Missing documentation for "EntityTagsPickerFieldSchema".
|
||||
|
||||
@@ -356,6 +356,34 @@ describe('api', () => {
|
||||
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) => {
|
||||
|
||||
@@ -24,23 +24,22 @@ import { ResponseError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { Observable } from '@backstage/types';
|
||||
import qs from 'qs';
|
||||
import queryString from 'qs';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import {
|
||||
ListActionsResponse,
|
||||
LogEvent,
|
||||
ScaffolderApi,
|
||||
ScaffolderDryRunOptions,
|
||||
ScaffolderDryRunResponse,
|
||||
ScaffolderGetIntegrationsListOptions,
|
||||
ScaffolderGetIntegrationsListResponse,
|
||||
ScaffolderScaffoldOptions,
|
||||
ScaffolderScaffoldResponse,
|
||||
ScaffolderStreamLogsOptions,
|
||||
ScaffolderGetIntegrationsListOptions,
|
||||
ScaffolderGetIntegrationsListResponse,
|
||||
ScaffolderTask,
|
||||
ScaffolderDryRunOptions,
|
||||
ScaffolderDryRunResponse,
|
||||
TemplateParameterSchema,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import queryString from 'qs';
|
||||
import {
|
||||
EventSourceMessage,
|
||||
fetchEventSource,
|
||||
@@ -74,7 +73,9 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
|
||||
async listTasks(options: {
|
||||
filterByOwnership: 'owned' | 'all';
|
||||
}): Promise<{ tasks: ScaffolderTask[] }> {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{ tasks: ScaffolderTask[]; totalTasks?: number }> {
|
||||
if (!this.identityApi) {
|
||||
throw new Error(
|
||||
'IdentityApi is not available in the ScaffolderClient, please pass through the IdentityApi to the ScaffolderClient constructor in order to use the listTasks method',
|
||||
@@ -83,9 +84,12 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const { userEntityRef } = await this.identityApi.getBackstageIdentity();
|
||||
|
||||
const query = queryString.stringify(
|
||||
options.filterByOwnership === 'owned' ? { createdBy: userEntityRef } : {},
|
||||
);
|
||||
const query = queryString.stringify({
|
||||
createdBy:
|
||||
options.filterByOwnership === 'owned' ? userEntityRef : undefined,
|
||||
limit: options.limit,
|
||||
offset: options.offset,
|
||||
});
|
||||
|
||||
const response = await this.fetchApi.fetch(`${baseUrl}/v2/tasks?${query}`);
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -25,8 +25,8 @@ import React from 'react';
|
||||
import { identityApiRef } from '@backstage/core-plugin-api';
|
||||
import { ListTasksPage } from './ListTasksPage';
|
||||
import {
|
||||
scaffolderApiRef,
|
||||
ScaffolderApi,
|
||||
scaffolderApiRef,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
import { rootRouteRef } from '../../routes';
|
||||
@@ -64,7 +64,7 @@ describe('<ListTasksPage />', () => {
|
||||
};
|
||||
catalogApi.getEntityByRef.mockResolvedValue(entity);
|
||||
|
||||
scaffolderApiMock.listTasks.mockResolvedValue({ tasks: [] });
|
||||
scaffolderApiMock.listTasks.mockResolvedValue({ tasks: [], totalTasks: 0 });
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<TestApiProvider
|
||||
@@ -118,6 +118,7 @@ describe('<ListTasksPage />', () => {
|
||||
lastHeartbeatAt: '',
|
||||
},
|
||||
],
|
||||
totalTasks: 1,
|
||||
});
|
||||
|
||||
scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({
|
||||
@@ -145,6 +146,8 @@ describe('<ListTasksPage />', () => {
|
||||
|
||||
expect(scaffolderApiMock.listTasks).toHaveBeenCalledWith({
|
||||
filterByOwnership: 'owned',
|
||||
limit: 5,
|
||||
offset: 0,
|
||||
});
|
||||
expect(getByText('List template tasks')).toBeInTheDocument();
|
||||
expect(getByText('All tasks that have been started')).toBeInTheDocument();
|
||||
@@ -194,6 +197,7 @@ describe('<ListTasksPage />', () => {
|
||||
lastHeartbeatAt: '',
|
||||
},
|
||||
],
|
||||
totalTasks: 1,
|
||||
})
|
||||
.mockResolvedValue({
|
||||
tasks: [
|
||||
@@ -212,6 +216,7 @@ describe('<ListTasksPage />', () => {
|
||||
lastHeartbeatAt: '',
|
||||
},
|
||||
],
|
||||
totalTasks: 1,
|
||||
});
|
||||
|
||||
scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({
|
||||
@@ -244,6 +249,8 @@ describe('<ListTasksPage />', () => {
|
||||
|
||||
expect(scaffolderApiMock.listTasks).toHaveBeenCalledWith({
|
||||
filterByOwnership: 'all',
|
||||
limit: 5,
|
||||
offset: 0,
|
||||
});
|
||||
expect(await findByText('One Template')).toBeInTheDocument();
|
||||
expect(await findByText('OtherUser')).toBeInTheDocument();
|
||||
|
||||
@@ -28,8 +28,8 @@ import { CatalogFilterLayout } from '@backstage/plugin-catalog-react';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ScaffolderTask,
|
||||
scaffolderApiRef,
|
||||
ScaffolderTask,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { OwnerListPicker } from './OwnerListPicker';
|
||||
import {
|
||||
@@ -51,6 +51,8 @@ export interface MyTaskPageProps {
|
||||
const ListTaskPageContent = (props: MyTaskPageProps) => {
|
||||
const { initiallySelectedFilter = 'owned' } = props;
|
||||
const { t } = useTranslationRef(scaffolderTranslationRef);
|
||||
const [limit, setLimit] = useState(5);
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
const rootLink = useRouteRef(rootRouteRef);
|
||||
@@ -58,7 +60,11 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
|
||||
const [ownerFilter, setOwnerFilter] = useState(initiallySelectedFilter);
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
if (scaffolderApi.listTasks) {
|
||||
return scaffolderApi.listTasks?.({ filterByOwnership: ownerFilter });
|
||||
return scaffolderApi.listTasks?.({
|
||||
filterByOwnership: ownerFilter,
|
||||
limit,
|
||||
offset: page * limit,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -66,8 +72,8 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
|
||||
'listTasks is not implemented in the scaffolderApi, please make sure to implement this method.',
|
||||
);
|
||||
|
||||
return Promise.resolve({ tasks: [] });
|
||||
}, [scaffolderApi, ownerFilter]);
|
||||
return Promise.resolve({ tasks: [], totalTasks: 0 });
|
||||
}, [scaffolderApi, ownerFilter, limit, page]);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
@@ -96,7 +102,15 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<Table<ScaffolderTask>
|
||||
onRowsPerPageChange={pageSize => {
|
||||
setPage(0);
|
||||
setLimit(pageSize);
|
||||
}}
|
||||
onPageChange={newPage => setPage(newPage)}
|
||||
options={{ pageSize: limit, emptyRowsWhenPaging: false }}
|
||||
data={value?.tasks ?? []}
|
||||
page={page}
|
||||
totalCount={value?.totalTasks ?? 0}
|
||||
title={t('listTaskPage.content.tableTitle')}
|
||||
columns={[
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user