可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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