How to paginate react-admin lists when the total i

2020-08-04 04:21发布

问题:

Summary: I can't get a total number of records from my GraphQL endpoint. I only know if I have reached the end of my GraphQL records list when I am parsing the response from the endpoint. How can I make my custom pagination component aware that it's on the last page?

Details: I'm using React Admin with AWS AppSync (GraphQL on DynamoDB) using ra-data-graphql. AppSync can't tell you the total number of records available to a list query, and it also limits the number of records you can return to a 1MB payload. Instead, it includes a nextToken value if there are more records to query, which you can include in subsequent list queries.

I have created a custom pagination component that only uses "prev" and "next" links, which is fine. But I need to know when the last page is displayed. Right now, I only know this in the parseResponse() function that I'm passing in to buildQuery() for the list query. At this point, I have access to the nextToken value. If it's empty, then I have fetched the last page of results from AppSync. If I could pass this value, or even a boolean e.g. lastPage to the custom pagination component, I'd be all set. How can I do this in React Admin?

回答1:

There is also a way to adapt AppSync resolver to work with page and perPage native react-admin parameters.

It's a bad practice because query response is limited by 1MB and also full dynamodb query response needs to be parsed and transformed for each page query, however it does the trick.

VTL AppSync resolver Request Mapping Template:

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "query" : {
        "expression": "userId = :userId",
        "expressionValues" : {
            ":userId" : $util.dynamodb.toDynamoDBJson($context.identity.sub)
        }
    }
}

VTL AppSync resolver Response Mapping Template:

#set($result = {})
#set($result.items = [])
#set($result.length = $ctx.result.items.size())
#set($start = $ctx.arguments.perPage * ($ctx.arguments.page - 1))
#set($end = $ctx.arguments.perPage * $ctx.arguments.page - 1)
#if($end > $result.length - 1)
 #set($end = $result.length - 1)
#end

#if($start <= $result.length - 1 && $start >= 0 )
  #set($range = [$start..$end])
  #foreach($i in $range)
     $util.qr($result.items.add($ctx.result.items[$i]))
  #end
#end 

$util.toJson($result)

dataProvider.js

...
const buildQuery = () => (
  raFetchType,
  resourceName,
  params
) => {
  if (resourceName === "getUserToDos" && raFetchType === "GET_LIST") {
    return {
      query: gql`
        query getUserToDos($perPage: Int!, $page: Int!) {
          getUserToDos(perPage: $perPage, page: $page) {
            length
            items {
              todoId
              date
              ...
            }
          }
        }
      `,
      variables: {
        page: params.pagination.page,
        perPage: params.pagination.perPage
      },
      parseResponse: ({ data }) => {
        return {
          data: data.getUserToDos.items.map(item => {
            return { id: item.listingId, ...item };
          }),
          total: data.getUserToDos.length
        };
      }
    };
  }
...


回答2:

To achieve this I created a custom reducer, nextTokenReducer that looks for React Admin's CRUD_GET_LIST_SUCCESS action, the payload of which is the entire response from the AppSync GraphQL endpoint. I can pull the nextToken value out of that:

import { CRUD_GET_LIST_SUCCESS } from "react-admin";

export default (previousState = null, { type, payload }) => {
  if (type === CRUD_GET_LIST_SUCCESS) {
    return payload.nextToken;
  }
  return previousState;
};

I passed this custom reducer to the Admin component in my main App component:

import nextTokenReducer from "./reducers/nextTokenReducer";
...
class App extends Component {
...
  render() {
    const { dataProvider } = this.state;

    if (!dataProvider) {
      return <div>Loading</div>;
    }

    return (
      <Admin
        customReducers={{ nextToken: nextTokenReducer }}
        dataProvider={dataProvider}
      >
        <Resource name="packs" list={PackList} />
      </Admin>
    );
  }
}

I then connected the nextToken store to my custom pagination component. It will display "next", "prev", or nothing based on whether nextToken is in its props:

import React from "react";
import Button from "@material-ui/core/Button";
import ChevronLeft from "@material-ui/icons/ChevronLeft";
import ChevronRight from "@material-ui/icons/ChevronRight";
import Toolbar from "@material-ui/core/Toolbar";

import { connect } from "react-redux";

class CustomPagination extends React.Component {
  render() {
    if (this.props.page === 1 && !this.props.nextToken) {
      return null;
    }
    return (
      <Toolbar>
        {this.props.page > 1 && (
          <Button
            color="primary"
            key="prev"
            icon={<ChevronLeft />}
            onClick={() => this.props.setPage(this.props.page - 1)}
          >
            Prev
          </Button>
        )}
        {this.props.nextToken && (
          <Button
            color="primary"
            key="next"
            icon={<ChevronRight />}
            onClick={() => this.props.setPage(this.props.page + 1)}
            labelposition="before"
          >
            Next
          </Button>
        )}
      </Toolbar>
    );
  }
}

const mapStateToProps = state => ({ nextToken: state.nextToken });

export default connect(mapStateToProps)(CustomPagination);


Finally, I passed the custom pagination component into my list component:

import React from "react";
import { List, Datagrid, DateField, TextField, EditButton } from "react-admin";
import CustomPagination from "./pagination";

export const PackList = props => (
  <List {...props} pagination={<CustomPagination />}>
    <Datagrid>
    ...
    </Datagrid>
  </List>
);


回答3:

In DynamoDB you can query total count with a (second) Scan query.

Given that you use similar Schema:

type Query {
  Post(id: ID!): Post
  allPosts(page: Int, perPage: Int, sortField: String, sortOrder: String, filter: PostFilter): [Post]
  _allPostsMeta(page: Int, perPage: Int, sortField: String, sortOrder: String, filter: PostFilter): ListMetadata
}

type ListMetadata {
    count: Int!
}
...

You can create resolver for Query._allPostsMeta with these VTL templates:

Request VTL Template:

{
    "version" : "2017-02-28", 
    "operation" : "Scan",
    "select": "COUNT"
}

Resolve VTL Template:

#set($result = {"count": $ctx.result.scannedCount})
$util.toJson($result)

React-admin 'GET_LIST' query:

query allPosts($page: Int, $perPage: Int, $sortField: String, $sortOrder: String, $filter: ServiceFilter) {
  items: allPosts(page: $page, perPage: $perPage, sortField: $sortField, sortOrder: $sortOrder, filter: $filter) {
    ...
  }
  total: _allPostsMeta(page: $page, perPage: $perPage, filter: $filter) {
    count
  }
}

This approach is used in ra-data-graphql-simple