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

Change from offset & limit to number and size #9207

Merged
merged 1 commit into from
Jan 7, 2025
Merged

Change from offset & limit to number and size #9207

merged 1 commit into from
Jan 7, 2025

Conversation

seanh
Copy link
Contributor

@seanh seanh commented Dec 12, 2024

Change the list-memberships API's pagination query params from
page[offset] and page[limit] to page[number] and page[size].

This is a better API design because it doesn't enable the client to send
nonsensical values. For example page[offset]=2 and page[size]=10
is nonsensical because offset isn't a multiple of limit: what would
the offset and limit for the previous page link be?

With page and size this class of nonsensical requests is no longer
possible.

This will make it easier to add first, last, prev and next page links to
the backend's API responses, as was attempted in this PR (not merged):
#9181

If the client supplies a page number that is beyond the end of the
group's list of memberships, rather than an error the server simply
responds with empty data:

{
    "meta": {
        "page": {
            "total": 101
        }
    },
    "data": []
}

The client can use the meta.page.total value to figure out how many
pages there should be and re-render its pagination controls.

(This can happen for example if members leave the group after pagination
controls are rendered: the pagination controls may contain links to
pages that no longer exist.)

Testing

Creating a group with lots of members

  1. Log in as devdata_admin

  2. Create a new group

  3. Create an auth client with:
    Authority: localhost
    Grant type: client_credentials
    Trusted: yes

  4. Run this script to add 100 members to the group:

    #!/usr/bin/env python3
    import httpx
    
    auth = ("CLIENT_ID", "CLIENT_SECRET")
    pubid = "GROUP_PUBID"
    
    for i in range(100):
        httpx.post(
            "http://localhost:5000/api/users",
            auth=auth,
            json={
                "authority": "localhost",
                "username": f"user{i}",
                "email": f"user{i}@example.com",
            },
        )
        httpx.post(
            f"http://localhost:5000/api/groups/{pubid}/members/acct:user{i}@localhost",
            auth=auth,
        )

    The script requires httpx so:

    python3 -m venv /tmp/venv
    /tmp/venv/bin/pip install httpx
    /tmp/venv/bin/python script.py
    

Testing the API

Create an API key then:

$ httpx http://localhost:5000/api/groups/{PUBID}/members --method GET --headers Authorization 'Bearer {APIKEY}' --params page[number] 1 --params page[size] 10

Testing the UI

Go to http://localhost:5000/admin/features and enable the group_members feature flag, then go to the group's edit page and test the pagination controls at the bottom.

Comment on lines +8 to +9
validator=Range(min=1),
missing=1,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting page number from 1 instead of 0

@seanh seanh marked this pull request as ready for review December 12, 2024 12:16
@robertknight
Copy link
Member

A related issue: If you are on page 3 with a page size of 20, so the first item has offset 40 and you change the page size to 50, what page do you end up on? There is no 50-item sized page that exactly aligns with offset 40. A possible solution would be to adjust the page number to Math.floor(pageIndex * oldPageSize / newPageSize), so you'll end up on the first page which contains the item that was previously at the top.

Copy link
Member

@robertknight robertknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. There are a few variable names and comments in the frontend that will need an update.

@@ -191,7 +191,7 @@ export default function EditGroupMembersForm({
const config = useContext(Config)!;
const currentUserid = config.context.user.userid;

const [pageIndex, setPageIndex] = useState(0);
const [pageIndex, setPageIndex] = useState(1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be renamed to pageNumber/setPageNumber to reflect the change to 1-based numbers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Done

currentPage={pageIndex + 1}
onChangePage={page => setPageIndex(page - 1)}
currentPage={pageIndex}
onChangePage={page => setPageIndex(page)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to onChangePage={setPageNumber} after renaming mentioned above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Done

offset: pageIndex * pageSize,
limit: pageSize,
pageNumber: pageIndex,
pageSize: pageSize,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Options can be simplified to { pageNumber, pageSize, signal: abort.signal } after the renaming mentioned above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Done

@@ -113,10 +113,10 @@ export type APIOptions = {
signal?: AbortSignal;

/** Index of first item to return in paginated APIs. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated to eg. "1-based number of first page to return ..."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Done

@seanh seanh requested a review from Copilot December 12, 2024 14:49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 5 out of 9 changed files in this pull request and generated no suggestions.

Files not reviewed (4)
  • h/static/scripts/group-forms/utils/api.ts: Evaluated as low risk
  • h/static/scripts/group-forms/utils/test/api-test.js: Evaluated as low risk
  • h/static/scripts/group-forms/components/test/EditGroupMembersForm-test.js: Evaluated as low risk
  • h/views/api/group_members.py: Evaluated as low risk
Comments skipped due to low confidence (2)

tests/functional/api/groups/members_test.py:100

  • The use of 'offset' in the 'group_members_service.get_memberships' call should be reviewed to ensure it aligns with the new pagination logic. Consider updating it to use 'pageNumber' and 'pageSize'.
context.group, offset=4, limit=2

tests/functional/api/groups/members_test.py:282

  • The error message should be updated to reflect the new parameters 'page[number]' and 'page[size]'.
"reason": "page[offset]: -1 is less than minimum value 0\npage[limit]: 0 is less than minimum value 1"
Change the list-memberships API's pagination query params from
`page[offset]` and `page[limit]` to `page[number]` and `page[size]`.

This is a better API design because it doesn't enable the client to send
nonsensical values. For example `page[offset]=2` and `page[size]=10`
is nonsensical because `offset` isn't a multiple of `limit`: what would
the `offset` and `limit` for the previous page link be?

With `page` and `size` this class of nonsensical requests is no longer
possible.

This will make it easier to add first, last, prev and next page links to
the backend's API responses, as was attempted in this PR (not merged):
#9181

If the client supplies a page number that is beyond the end of the
group's list of memberships, rather than an error the server simply
responds with empty date:

    {
        "meta": {
            "page": {
                "total": 101
            }
        },
        "data": []
    }

The client can use the `meta.page.total` value to figure out how many
pages there should be and re-render its pagination controls.

(This can happen for example if members leave the group after pagination
controls are rendered: the pagination controls may contain links to
pages that no longer exist.)
@seanh seanh merged commit 506e82d into main Jan 7, 2025
10 checks passed
@seanh seanh deleted the page-and-size branch January 7, 2025 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants