Skip to content

Commit

Permalink
Merge pull request backstage#26382 from drodil/scaffolder_pagination_…
Browse files Browse the repository at this point in the history
…frontend

feat: allow scaffolder tasks pagination in the frontend
  • Loading branch information
benjdlambert authored Oct 1, 2024
2 parents a0bf75a + 785d68f commit 73d15a3
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 33 deletions.
6 changes: 6 additions & 0 deletions .changeset/green-bottles-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-react': patch
'@backstage/plugin-scaffolder': patch
---

Add support for pagination in scaffolder tasks list
15 changes: 10 additions & 5 deletions plugins/scaffolder-react/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"".
Expand Down
4 changes: 3 additions & 1 deletion plugins/scaffolder-react/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
27 changes: 16 additions & 11 deletions plugins/scaffolder/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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".
Expand Down
28 changes: 28 additions & 0 deletions plugins/scaffolder/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
24 changes: 14 additions & 10 deletions plugins/scaffolder/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -118,6 +118,7 @@ describe('<ListTasksPage />', () => {
lastHeartbeatAt: '',
},
],
totalTasks: 1,
});

scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -194,6 +197,7 @@ describe('<ListTasksPage />', () => {
lastHeartbeatAt: '',
},
],
totalTasks: 1,
})
.mockResolvedValue({
tasks: [
Expand All @@ -212,6 +216,7 @@ describe('<ListTasksPage />', () => {
lastHeartbeatAt: '',
},
],
totalTasks: 1,
});

scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({
Expand Down Expand Up @@ -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();
Expand Down
22 changes: 18 additions & 4 deletions plugins/scaffolder/src/components/ListTasksPage/ListTasksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -51,23 +51,29 @@ 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);

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
console.warn(
'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 />;
Expand Down Expand Up @@ -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={[
{
Expand Down

0 comments on commit 73d15a3

Please sign in to comment.