diff --git a/assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx b/assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx
index 84cf453358..a6327a56cb 100644
--- a/assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx
+++ b/assets/js/common/ActivityLogOverview/ActivityLogOverview.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
import { noop } from 'lodash';
import {
EOS_BUG_REPORT_OUTLINED,
@@ -21,6 +21,7 @@ import {
import ActivityLogDetailModal from '@common/ActivityLogDetailsModal';
import { format } from 'date-fns';
+import { defaultItemsPerPage } from '../Table/Table';
const logLevelToIcon = {
[LEVEL_DEBUG]: ,
@@ -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: [
{
diff --git a/assets/js/common/ActivityLogOverview/ActivityLogOverview.stories.jsx b/assets/js/common/ActivityLogOverview/ActivityLogOverview.stories.jsx
index 007e2418da..362b4240d2 100644
--- a/assets/js/common/ActivityLogOverview/ActivityLogOverview.stories.jsx
+++ b/assets/js/common/ActivityLogOverview/ActivityLogOverview.stories.jsx
@@ -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';
@@ -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,
+ },
},
};
@@ -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')],
},
};
diff --git a/assets/js/common/ActivityLogOverview/ActivityLogOverview.test.jsx b/assets/js/common/ActivityLogOverview/ActivityLogOverview.test.jsx
index 8dc2011869..12925b92cc 100644
--- a/assets/js/common/ActivityLogOverview/ActivityLogOverview.test.jsx
+++ b/assets/js/common/ActivityLogOverview/ActivityLogOverview.test.jsx
@@ -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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ // 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',
+ });
+ });
+ });
});
diff --git a/assets/js/common/Table/Table.jsx b/assets/js/common/Table/Table.jsx
index 48d72c0c64..e87a1e776f 100644
--- a/assets/js/common/Table/Table.jsx
+++ b/assets/js/common/Table/Table.jsx
@@ -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,
@@ -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);
diff --git a/assets/js/lib/api/activityLogs.js b/assets/js/lib/api/activityLogs.js
index 8a69fb9135..7f58b9c1d3 100644
--- a/assets/js/lib/api/activityLogs.js
+++ b/assets/js/lib/api/activityLogs.js
@@ -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,
+ });
diff --git a/assets/js/pages/ActivityLogPage/ActivityLogPage.jsx b/assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
index 9129c4dd3c..2dbafb53fd 100644
--- a/assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
+++ b/assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
@@ -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';
@@ -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 (
<>
@@ -33,6 +36,10 @@ function ActivityLogPage() {
onCloseActivityLogEntryDetails={() =>
setActivityLogDetailModalOpen(false)
}
+ currentPaginationData={currentPaginationData}
+ loadActivityLog={(filtersAndPagination) => {
+ loadActivityLog(filtersAndPagination);
+ }}
/>
>
);