diff --git a/.changeset/green-bottles-live.md b/.changeset/green-bottles-live.md new file mode 100644 index 0000000000..6bed257eee --- /dev/null +++ b/.changeset/green-bottles-live.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Add support for pagination in scaffolder tasks list diff --git a/plugins/scaffolder-react/report.api.md b/plugins/scaffolder-react/report.api.md index d7cf450bf5..73573b25c4 100644 --- a/plugins/scaffolder-react/report.api.md +++ b/plugins/scaffolder-react/report.api.md @@ -232,8 +232,13 @@ export interface ScaffolderApi { ): Promise; listActions(): Promise; // (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; 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"". diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index 3816c4bcbf..bc37eed47e 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -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, diff --git a/plugins/scaffolder/report.api.md b/plugins/scaffolder/report.api.md index b93a59a82c..35b3dad66e 100644 --- a/plugins/scaffolder/report.api.md +++ b/plugins/scaffolder/report.api.md @@ -552,8 +552,13 @@ export class ScaffolderClient implements ScaffolderApi_2 { // (undocumented) listActions(): Promise; // (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; @@ -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". diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index 805828a9fe..1df2dacb17 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -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) => { diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 93db7386f3..df032b536d 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -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) { diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index b9c5a84da3..f7f75bc3e8 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -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('', () => { }; catalogApi.getEntityByRef.mockResolvedValue(entity); - scaffolderApiMock.listTasks.mockResolvedValue({ tasks: [] }); + scaffolderApiMock.listTasks.mockResolvedValue({ tasks: [], totalTasks: 0 }); const { getByText } = await renderInTestApp( ', () => { lastHeartbeatAt: '', }, ], + totalTasks: 1, }); scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ @@ -145,6 +146,8 @@ describe('', () => { 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('', () => { lastHeartbeatAt: '', }, ], + totalTasks: 1, }) .mockResolvedValue({ tasks: [ @@ -212,6 +216,7 @@ describe('', () => { lastHeartbeatAt: '', }, ], + totalTasks: 1, }); scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ @@ -244,6 +249,8 @@ describe('', () => { expect(scaffolderApiMock.listTasks).toHaveBeenCalledWith({ filterByOwnership: 'all', + limit: 5, + offset: 0, }); expect(await findByText('One Template')).toBeInTheDocument(); expect(await findByText('OtherUser')).toBeInTheDocument(); diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx index 7bc34662ef..b07d7c7fd3 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx @@ -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 ; @@ -96,7 +102,15 @@ const ListTaskPageContent = (props: MyTaskPageProps) => { + 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={[ {