Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Activity log pagination #2887

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { noop } from 'lodash';
import {
EOS_BUG_REPORT_OUTLINED,
Expand All @@ -21,6 +21,7 @@ import {

import ActivityLogDetailModal from '@common/ActivityLogDetailsModal';
import { format } from 'date-fns';
import { defaultItemsPerPage } from '../Table/Table';

const logLevelToIcon = {
[LEVEL_DEBUG]: <EOS_BUG_REPORT_OUTLINED className="w-full" />,
Expand Down Expand Up @@ -51,11 +52,56 @@ function ActivityLogOverview({
loading = false,
onActivityLogEntryClick = noop,
onCloseActivityLogEntryDetails = noop,
currentPaginationData,
loadActivityLog = noop,
}) {
const [selectedEntry, setEntry] = useState({});
const [currentlyAppliedFilters, setCurrentlyAppliedFilters] = useState({
first: defaultItemsPerPage,
});

const loadFilteredAndPaginatedActivityLog = (filtersAndPagination) => {
setCurrentlyAppliedFilters(filtersAndPagination);
loadActivityLog(filtersAndPagination);
};

useEffect(() => {
loadFilteredAndPaginatedActivityLog(currentlyAppliedFilters);
}, []);

const onChangeItemsPerPage = (itemsPerPage) =>
loadFilteredAndPaginatedActivityLog({
...currentlyAppliedFilters,
...(currentlyAppliedFilters?.first
? { first: itemsPerPage }
: { last: itemsPerPage }),
});

const onPreviousPage = () =>
loadFilteredAndPaginatedActivityLog({
last: currentlyAppliedFilters?.first || currentlyAppliedFilters?.last,
before: currentPaginationData?.start_cursor,
});

const onNextPage = () =>
loadFilteredAndPaginatedActivityLog({
first: currentlyAppliedFilters?.first || currentlyAppliedFilters?.last,
after: currentPaginationData?.end_cursor,
});

const canNavigateToPreviousPage =
!loading && currentPaginationData?.has_previous_page;
const canNavigateToNextPage =
!loading && currentPaginationData?.has_next_page;

const activityLogTableConfig = {
pagination: true,
cursorPagination: true,
onChangeItemsPerPage,
canNavigateToPreviousPage,
onPreviousPage,
canNavigateToNextPage,
onNextPage,
usePadding: false,
columns: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { action } from '@storybook/addon-actions';
import { activityLogEntryFactory } from '@lib/test-utils/factories/activityLog';
import _ from 'lodash';
import ActivityLogOverview from './ActivityLogOverview';
Expand All @@ -12,16 +13,78 @@ export default {
type: 'array',
},
},
activityLogDetailModalOpen: {
description: 'Whether the activity log entry details modal is open',
control: { type: 'boolean' },
},
loading: {
description: 'Display loading state of the component',
control: { type: 'boolean' },
},
onActivityLogEntryClick: {
description: 'Function to execute when an activity log entry is clicked',
control: { type: 'function' },
},
onCloseActivityLogEntryDetails: {
description:
'Function to execute when the activity log entry details modal is closed',
control: { type: 'function' },
},
currentPaginationData: {
description: 'Current pagination data retrieved from the api response',
control: { type: 'object' },
},
loadActivityLog: {
description: 'Function to execute when a new pagination is applied',
control: { type: 'function' },
},
},
};

export const Default = {
export const FirstPage = {
args: {
activityLog: activityLogEntryFactory.buildList(20),
currentPaginationData: {
first: 10,
last: null,
start_cursor: 'start_cursor',
end_cursor: 'end_cursor',
has_next_page: true,
has_previous_page: false,
},
loadActivityLog: action('loadActivityLog'),
},
};

export const IntermediatePage = {
args: {
...FirstPage.args,
currentPaginationData: {
...FirstPage.args.currentPaginationData,
has_next_page: true,
has_previous_page: true,
},
},
};
export const LastPage = {
args: {
activityLog: activityLogEntryFactory.buildList(20),
currentPaginationData: {
...FirstPage.args.currentPaginationData,
has_next_page: false,
has_previous_page: true,
},
loadActivityLog: action('loadActivityLog'),
},
};
export const OnlyOnePage = {
args: {
...FirstPage.args,
currentPaginationData: {
...FirstPage.args.currentPaginationData,
has_next_page: false,
has_previous_page: false,
},
},
};

Expand All @@ -40,21 +103,21 @@ export const Empty = {

export const UnknwonActivityType = {
args: {
...Default.args,
...FirstPage.args,
activityLog: [activityLogEntryFactory.build({ type: 'foo_bar' })],
},
};

export const UnknwonLevel = {
args: {
...Default.args,
...FirstPage.args,
activityLog: [activityLogEntryFactory.build({ level: 'foo_bar' })],
},
};

export const MissingLevel = {
args: {
...Default.args,
...FirstPage.args,
activityLog: [_.omit(activityLogEntryFactory.build(), 'level')],
},
};
113 changes: 113 additions & 0 deletions assets/js/common/ActivityLogOverview/ActivityLogOverview.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,117 @@ describe('Activity Log Overview', () => {
await userEvent.click(screen.getByLabelText(`entry-${id}`));
expect(onActivityLogEntryClick).toHaveBeenCalled();
});

describe('filtering and pagination', () => {
it('should load the activity log with the default filters', () => {
const loadActivityLog = jest.fn();
render(
<ActivityLogOverview
activityLog={activityLogEntryFactory.buildList(11)}
loadActivityLog={loadActivityLog}
/>
);

expect(loadActivityLog).toHaveBeenCalledTimes(1);
expect(loadActivityLog).toHaveBeenCalledWith({
first: 10,
});
});

it.each`
name | hasNextPage | hasPreviousPage
${'only one page'} | ${false} | ${false}
${'on first page'} | ${true} | ${false}
${'on intermediate page'} | ${true} | ${true}
${'on last page'} | ${false} | ${true}
`(
'should allow relevant pagination given the context: $name',
({ hasNextPage, hasPreviousPage }) => {
render(
<ActivityLogOverview
activityLog={activityLogEntryFactory.buildList(23)}
currentPaginationData={{
first: 7,
last: null,
start_cursor: 'start_cursor',
end_cursor: 'end_cursor',
has_next_page: hasNextPage,
has_previous_page: hasPreviousPage,
}}
/>
);

const prevPageButton = screen.getByLabelText('prev-page');
hasPreviousPage
? expect(prevPageButton).toBeEnabled()
: expect(prevPageButton).toBeDisabled();

const nextPageButton = screen.getByLabelText('next-page');
hasNextPage
? expect(nextPageButton).toBeEnabled()
: expect(nextPageButton).toBeDisabled();
}
);

it('should apply new pagination to activity log', async () => {
const loadActivityLog = jest.fn();

render(
<ActivityLogOverview
activityLog={activityLogEntryFactory.buildList(23)}
currentPaginationData={{
first: 7,
last: null,
start_cursor: 'start_cursor',
end_cursor: 'end_cursor',
has_next_page: true,
has_previous_page: true,
}}
loadActivityLog={loadActivityLog}
/>
);

// initial load
expect(loadActivityLog).toHaveBeenNthCalledWith(1, { first: 10 });

// change items per page from 10 to 20
await userEvent.click(screen.getByRole('button', { name: '10' }));
await userEvent.click(screen.getByRole('option', { name: '20' }));

// just change the number of items from 10 to 20
expect(loadActivityLog).toHaveBeenNthCalledWith(2, { first: 20 });

// navigate to next page
const nextPageButton = screen.getByLabelText('next-page');
await userEvent.click(nextPageButton);

// keep previously selected number of items (20)
// and look for 20 items after the end cursor (aka the last element in the list)
expect(loadActivityLog).toHaveBeenNthCalledWith(3, {
first: 20,
after: 'end_cursor',
});

// change again items per page from 20 to 10
await userEvent.click(screen.getByRole('button', { name: '20' }));
await userEvent.click(screen.getByRole('option', { name: '10' }));

// just change the number of items from 20 to 10 and keep previous cursor
expect(loadActivityLog).toHaveBeenNthCalledWith(4, {
first: 10,
after: 'end_cursor',
});

// navigate to previous page
const prevPageButton = screen.getByLabelText('prev-page');
await userEvent.click(prevPageButton);

// keeps the last selected number of items to show (10)
// and look for 10 items before the start cursor (aka the first element in the list)
expect(loadActivityLog).toHaveBeenNthCalledWith(5, {
last: 10,
before: 'start_cursor',
});
});
});
});
6 changes: 3 additions & 3 deletions assets/js/common/Table/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const getFilterFunction = (column, value) =>
: getDefaultFilterFunction(value, column.key);

const itemsPerPageOptions = [10, 20, 50, 75, 100];
export const defaultItemsPerPage = itemsPerPageOptions[0];

function Table({
config,
Expand Down Expand Up @@ -89,9 +90,8 @@ function Table({

const [filters, setFilters] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [currentItemsPerPage, setCurrentItemsPerPage] = useState(
itemsPerPageOptions[0]
);
const [currentItemsPerPage, setCurrentItemsPerPage] =
useState(defaultItemsPerPage);

const searchParamsEnabled = Boolean(searchParams && setSearchParams);

Expand Down
5 changes: 4 additions & 1 deletion assets/js/lib/api/activityLogs.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { networkClient } from '@lib/network';

export const getActivityLog = () => networkClient.get(`/activity_log`);
export const getActivityLog = (filters = {}) =>
networkClient.get(`/activity_log`, {
params: filters,
});
15 changes: 11 additions & 4 deletions assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';

import PageHeader from '@common/PageHeader';
import ActivityLogOverview from '@common/ActivityLogOverview';
Expand All @@ -10,17 +10,20 @@ function ActivityLogPage() {
const [isLoading, setLoading] = useState(true);
const [activityLogDetailModalOpen, setActivityLogDetailModalOpen] =
useState(false);
const [currentPaginationData, setCurrentPaginationData] = useState({});

useEffect(() => {
getActivityLog()
const loadActivityLog = (filters) => {
setLoading(true);
getActivityLog(filters)
.then((response) => {
setActivityLog(response.data?.data ?? []);
setCurrentPaginationData(response.data?.pagination ?? {});
})
.catch(() => setActivityLog([]))
.finally(() => {
setLoading(false);
});
}, []);
};

return (
<>
Expand All @@ -33,6 +36,10 @@ function ActivityLogPage() {
onCloseActivityLogEntryDetails={() =>
setActivityLogDetailModalOpen(false)
}
currentPaginationData={currentPaginationData}
loadActivityLog={(filtersAndPagination) => {
loadActivityLog(filtersAndPagination);
}}
/>
</>
);
Expand Down
Loading