webui: Rework pagination

Quentin Gliech created

Change summary

webui/package-lock.json     | 192 ++++++++++++++++++++++++++++++++++----
webui/package.json          |   1 
webui/src/list/List.js      | 160 +++++++------------------------
webui/src/list/ListQuery.js |  39 +++++--
4 files changed, 237 insertions(+), 155 deletions(-)

Detailed changes

webui/package-lock.json 🔗

@@ -849,6 +849,11 @@
       "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
       "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw=="
     },
+    "@emotion/hash": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.1.tgz",
+      "integrity": "sha512-OYpa/Sg+2GDX+jibUfpZVn1YqSVRpYmTLF2eyAfrFTIJSbwyIrc+YscayoykvaOME/wV4BV0Sa0yqdMrgse6mA=="
+    },
     "@material-ui/core": {
       "version": "3.9.3",
       "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-3.9.3.tgz",
@@ -892,6 +897,41 @@
         "recompose": "0.28.0 - 0.30.0"
       }
     },
+    "@material-ui/styles": {
+      "version": "3.0.0-alpha.10",
+      "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-3.0.0-alpha.10.tgz",
+      "integrity": "sha512-qJ5eiupBPRCNlMCDZ2G5h8auBtBtm8uT/oCUAJ/FqhO5oC7POLmmvDN1Cq1cgAmqQnaL6uN5mAM1Gc90GpKr9A==",
+      "requires": {
+        "@babel/runtime": "^7.2.0",
+        "@emotion/hash": "^0.7.1",
+        "@material-ui/utils": "^3.0.0-alpha.2",
+        "classnames": "^2.2.5",
+        "deepmerge": "^3.0.0",
+        "hoist-non-react-statics": "^3.2.1",
+        "jss": "^10.0.0-alpha.7",
+        "jss-plugin-camel-case": "^10.0.0-alpha.7",
+        "jss-plugin-default-unit": "^10.0.0-alpha.7",
+        "jss-plugin-global": "^10.0.0-alpha.7",
+        "jss-plugin-nested": "^10.0.0-alpha.7",
+        "jss-plugin-props-sort": "^10.0.0-alpha.7",
+        "jss-plugin-rule-value-function": "^10.0.0-alpha.7",
+        "jss-plugin-vendor-prefixer": "^10.0.0-alpha.7",
+        "prop-types": "^15.6.0",
+        "warning": "^4.0.1"
+      },
+      "dependencies": {
+        "jss": {
+          "version": "10.0.0-alpha.16",
+          "resolved": "https://registry.npmjs.org/jss/-/jss-10.0.0-alpha.16.tgz",
+          "integrity": "sha512-HmKNNnr82TR5jkWjBcbrx/uim2ief588pWp7zsf4GQpL125zRkEaWYL1SXv5bR6bBvAoTtvJsTAOxDIlLxUNZg==",
+          "requires": {
+            "@babel/runtime": "^7.3.1",
+            "is-in-browser": "^1.1.3",
+            "tiny-warning": "^1.0.2"
+          }
+        }
+      }
+    },
     "@material-ui/system": {
       "version": "3.0.0-alpha.2",
       "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-3.0.0-alpha.2.tgz",
@@ -3103,7 +3143,8 @@
             },
             "ansi-regex": {
               "version": "2.1.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "aproba": {
               "version": "1.2.0",
@@ -3121,11 +3162,13 @@
             },
             "balanced-match": {
               "version": "1.0.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "brace-expansion": {
               "version": "1.1.11",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "balanced-match": "^1.0.0",
                 "concat-map": "0.0.1"
@@ -3138,15 +3181,18 @@
             },
             "code-point-at": {
               "version": "1.1.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "concat-map": {
               "version": "0.0.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "console-control-strings": {
               "version": "1.1.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "core-util-is": {
               "version": "1.0.2",
@@ -3249,7 +3295,8 @@
             },
             "inherits": {
               "version": "2.0.3",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "ini": {
               "version": "1.3.5",
@@ -3259,6 +3306,7 @@
             "is-fullwidth-code-point": {
               "version": "1.0.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "number-is-nan": "^1.0.0"
               }
@@ -3271,17 +3319,20 @@
             "minimatch": {
               "version": "3.0.4",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "brace-expansion": "^1.1.7"
               }
             },
             "minimist": {
               "version": "0.0.8",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "minipass": {
               "version": "2.3.5",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "safe-buffer": "^5.1.2",
                 "yallist": "^3.0.0"
@@ -3298,6 +3349,7 @@
             "mkdirp": {
               "version": "0.5.1",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "minimist": "0.0.8"
               }
@@ -3370,7 +3422,8 @@
             },
             "number-is-nan": {
               "version": "1.0.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "object-assign": {
               "version": "4.1.1",
@@ -3380,6 +3433,7 @@
             "once": {
               "version": "1.4.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "wrappy": "1"
               }
@@ -3455,7 +3509,8 @@
             },
             "safe-buffer": {
               "version": "5.1.2",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "safer-buffer": {
               "version": "2.1.2",
@@ -3485,6 +3540,7 @@
             "string-width": {
               "version": "1.0.2",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "code-point-at": "^1.0.0",
                 "is-fullwidth-code-point": "^1.0.0",
@@ -3502,6 +3558,7 @@
             "strip-ansi": {
               "version": "3.0.1",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "ansi-regex": "^2.0.0"
               }
@@ -3540,11 +3597,13 @@
             },
             "wrappy": {
               "version": "1.0.2",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "yallist": {
               "version": "3.0.3",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             }
           }
         },
@@ -6721,7 +6780,8 @@
         },
         "ansi-regex": {
           "version": "2.1.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -6739,11 +6799,13 @@
         },
         "balanced-match": {
           "version": "1.0.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -6756,15 +6818,18 @@
         },
         "code-point-at": {
           "version": "1.1.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -6867,7 +6932,8 @@
         },
         "inherits": {
           "version": "2.0.3",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -6877,6 +6943,7 @@
         "is-fullwidth-code-point": {
           "version": "1.0.0",
           "bundled": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -6889,17 +6956,20 @@
         "minimatch": {
           "version": "3.0.4",
           "bundled": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
         },
         "minimist": {
           "version": "0.0.8",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -6916,6 +6986,7 @@
         "mkdirp": {
           "version": "0.5.1",
           "bundled": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -6988,7 +7059,8 @@
         },
         "number-is-nan": {
           "version": "1.0.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -6998,6 +7070,7 @@
         "once": {
           "version": "1.4.0",
           "bundled": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -7073,7 +7146,8 @@
         },
         "safe-buffer": {
           "version": "5.1.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -7103,6 +7177,7 @@
         "string-width": {
           "version": "1.0.2",
           "bundled": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -7120,6 +7195,7 @@
         "strip-ansi": {
           "version": "3.0.1",
           "bundled": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -7158,11 +7234,13 @@
         },
         "wrappy": {
           "version": "1.0.2",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.2",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         }
       }
     },
@@ -9380,6 +9458,76 @@
         }
       }
     },
+    "jss-plugin-camel-case": {
+      "version": "10.0.0-alpha.7",
+      "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.0.0-alpha.7.tgz",
+      "integrity": "sha512-Bwrav1ZB0XywdJW6TaEuFhKe1ZpZvUlESh3jsFOvebA9aFTYNCkmHMEqjA5+u9VMxksl3u77nnZHtukpxkzrBA==",
+      "requires": {
+        "@babel/runtime": "^7.0.0",
+        "hyphenate-style-name": "^1.0.2"
+      }
+    },
+    "jss-plugin-default-unit": {
+      "version": "10.0.0-alpha.7",
+      "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.0.0-alpha.7.tgz",
+      "integrity": "sha512-auuJUbQaWMxoHOVFPrfZNZpZm9ab8PZeDyvey8nMt2lbokkmZ53UyAnM/1kNsg5BdAXTItcLDxDB3I4gwNU84g==",
+      "requires": {
+        "@babel/runtime": "^7.0.0"
+      }
+    },
+    "jss-plugin-global": {
+      "version": "10.0.0-alpha.7",
+      "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.0.0-alpha.7.tgz",
+      "integrity": "sha512-OWeoW4szLDgRUKviST+xfilqa8O5uXJCW+O3YonheCRTRJg6rRzlE/b5pfYPoU9UtwvY9n7JvwBX5r3c1lMsEQ==",
+      "requires": {
+        "@babel/runtime": "^7.0.0"
+      }
+    },
+    "jss-plugin-nested": {
+      "version": "10.0.0-alpha.7",
+      "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.0.0-alpha.7.tgz",
+      "integrity": "sha512-wsRzuIZXAc6WMjc61mREW9cUrDxgSI7dK/fx5c7a06IDUfSn+83NJ30J/RB4oBnbQW9SijV/muujz7IJqpn9Gw==",
+      "requires": {
+        "@babel/runtime": "^7.0.0",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-props-sort": {
+      "version": "10.0.0-alpha.7",
+      "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.0.0-alpha.7.tgz",
+      "integrity": "sha512-KXOCaHUk1+KXqE0z3q66/w1fDoy+VsZvI77gLxOqTsTrvIKFLX0jarwXogW3CDlaPQQFTZ6JykJJXtPRTBlstA==",
+      "requires": {
+        "@babel/runtime": "^7.0.0"
+      }
+    },
+    "jss-plugin-rule-value-function": {
+      "version": "10.0.0-alpha.7",
+      "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.0.0-alpha.7.tgz",
+      "integrity": "sha512-ett83hvIM69/LknmrWndrrdiDlfLfP+rneU5qP7gTOWJ7g1P9GuEL1Tc4CWdZUWBX+T58tgIBP0V1pzWCkP0QA==",
+      "requires": {
+        "@babel/runtime": "^7.0.0"
+      }
+    },
+    "jss-plugin-vendor-prefixer": {
+      "version": "10.0.0-alpha.7",
+      "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.0.0-alpha.7.tgz",
+      "integrity": "sha512-YbIVgqq+dLimOBOEYggho1Iuc0roz4PJSZYyaok9n8JnXVIqPnxYJbr8+bMbvzJ5CL3eeJij/e7L2IPCceRKrA==",
+      "requires": {
+        "@babel/runtime": "^7.0.0",
+        "css-vendor": "^1.1.0"
+      },
+      "dependencies": {
+        "css-vendor": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-1.2.1.tgz",
+          "integrity": "sha512-ZpwiWxn5jWNJ7NF3DAb/Dc/+c2lRu+fnovej/adCv3VJsULJSjdXEpUwRcq4fnpAAh98Hi7b0GDnlyoNFcdv1g==",
+          "requires": {
+            "@babel/runtime": "^7.3.1",
+            "is-in-browser": "^1.0.2"
+          }
+        }
+      }
+    },
     "jss-props-sort": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/jss-props-sort/-/jss-props-sort-6.0.0.tgz",

webui/package.json 🔗

@@ -5,6 +5,7 @@
   "dependencies": {
     "@material-ui/core": "^3.9.3",
     "@material-ui/icons": "^3.0.2",
+    "@material-ui/styles": "^3.0.0-alpha.10",
     "apollo-boost": "^0.3.1",
     "graphql": "^14.2.0",
     "moment": "^2.24.0",

webui/src/list/List.js 🔗

@@ -1,134 +1,50 @@
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
+import IconButton from '@material-ui/core/IconButton';
 import Table from '@material-ui/core/Table/Table';
 import TableBody from '@material-ui/core/TableBody/TableBody';
-import TablePagination from '@material-ui/core/TablePagination/TablePagination';
+import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
+import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
 import React from 'react';
 import BugRow from './BugRow';
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   main: {
     maxWidth: 600,
     margin: 'auto',
     marginTop: theme.spacing.unit * 4,
   },
-});
-
-class List extends React.Component {
-  props: {
-    bugs: Array,
-    fetchMore: any => any,
-    classes: any,
-  };
-
-  state = {
-    page: 0,
-    rowsPerPage: 10,
-    lastQuery: {},
-  };
-
-  handleChangePage = (event, page) => {
-    const { bugs, fetchMore } = this.props;
-    const { rowsPerPage } = this.state;
-    const pageInfo = bugs.pageInfo;
-
-    if (page === this.state.page + 1) {
-      if (!pageInfo.hasNextPage) {
-        return;
-      }
-
-      const variables = {
-        after: pageInfo.endCursor,
-        first: rowsPerPage,
-      };
-
-      fetchMore({
-        variables,
-        updateQuery: this.updateQuery,
-      });
-
-      this.setState({ page, lastQuery: variables });
-      return;
-    }
-
-    if (page === this.state.page - 1) {
-      if (!pageInfo.hasPreviousPage) {
-        return;
-      }
-
-      const variables = {
-        before: pageInfo.startCursor,
-        last: rowsPerPage,
-      };
-
-      fetchMore({
-        variables,
-        updateQuery: this.updateQuery,
-      });
-
-      this.setState({ page, lastQuery: variables });
-      return;
-    }
-
-    throw new Error('non neighbour page pagination is not supported');
-  };
-
-  handleChangeRowsPerPage = event => {
-    const { fetchMore } = this.props;
-    const { lastQuery } = this.state;
-    const rowsPerPage = event.target.value;
-
-    const variables = lastQuery;
-
-    if (lastQuery.first) {
-      variables.first = rowsPerPage;
-    } else if (lastQuery.last) {
-      variables.last = rowsPerPage;
-    } else {
-      variables.first = rowsPerPage;
-    }
-
-    fetchMore({
-      variables,
-      updateQuery: this.updateQuery,
-    });
-
-    this.setState({ rowsPerPage, lastQuery: variables });
-  };
-
-  updateQuery = (previousResult, { fetchMoreResult }) => {
-    return fetchMoreResult ? fetchMoreResult : previousResult;
-  };
-
-  render() {
-    const { classes, bugs } = this.props;
-    const { page, rowsPerPage } = this.state;
-
-    return (
-      <main className={classes.main}>
-        <Table className={classes.table}>
-          <TableBody>
-            {bugs.edges.map(({ cursor, node }) => (
-              <BugRow bug={node} key={cursor} />
-            ))}
-          </TableBody>
-        </Table>
-        <TablePagination
-          component="div"
-          count={bugs.totalCount}
-          rowsPerPage={rowsPerPage}
-          page={page}
-          backIconButtonProps={{
-            'aria-label': 'Previous Page',
-          }}
-          nextIconButtonProps={{
-            'aria-label': 'Next Page',
-          }}
-          onChangePage={this.handleChangePage}
-          onChangeRowsPerPage={this.handleChangeRowsPerPage}
-        />
-      </main>
-    );
-  }
+  pagination: {
+    ...theme.typography.overline,
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'flex-end',
+  },
+}));
+
+function List({ bugs, nextPage, prevPage }) {
+  const classes = useStyles();
+  const { hasNextPage, hasPreviousPage } = bugs.pageInfo;
+  return (
+    <main className={classes.main}>
+      <Table className={classes.table}>
+        <TableBody>
+          {bugs.edges.map(({ cursor, node }) => (
+            <BugRow bug={node} key={cursor} />
+          ))}
+        </TableBody>
+      </Table>
+
+      <div className={classes.pagination}>
+        <div>Total: {bugs.totalCount}</div>
+        <IconButton onClick={prevPage} disabled={!hasPreviousPage}>
+          <KeyboardArrowLeft />
+        </IconButton>
+        <IconButton onClick={nextPage} disabled={!hasNextPage}>
+          <KeyboardArrowRight />
+        </IconButton>
+      </div>
+    </main>
+  );
 }
 
-export default withStyles(styles)(List);
+export default List;

webui/src/list/ListQuery.js 🔗

@@ -1,13 +1,13 @@
 // @flow
 import CircularProgress from '@material-ui/core/CircularProgress';
 import gql from 'graphql-tag';
-import React from 'react';
+import React, { useState } from 'react';
 import { Query } from 'react-apollo';
 import BugRow from './BugRow';
 import List from './List';
 
 const QUERY = gql`
-  query($first: Int = 10, $last: Int, $after: String, $before: String) {
+  query($first: Int, $last: Int, $after: String, $before: String) {
     defaultRepository {
       bugs: allBugs(
         first: $first
@@ -35,14 +35,31 @@ const QUERY = gql`
   ${BugRow.fragment}
 `;
 
-const ListQuery = () => (
-  <Query query={QUERY}>
-    {({ loading, error, data, fetchMore }) => {
-      if (loading) return <CircularProgress />;
-      if (error) return <p>Error: {error}</p>;
-      return <List bugs={data.defaultRepository.bugs} fetchMore={fetchMore} />;
-    }}
-  </Query>
-);
+function ListQuery() {
+  const [page, setPage] = useState({ first: 10, after: null });
+
+  const perPage = page.first || page.last;
+  const nextPage = pageInfo =>
+    setPage({ first: perPage, after: pageInfo.endCursor });
+  const prevPage = pageInfo =>
+    setPage({ last: perPage, before: pageInfo.startCursor });
+
+  return (
+    <Query query={QUERY} variables={page}>
+      {({ loading, error, data }) => {
+        if (loading) return <CircularProgress />;
+        if (error) return <p>Error: {error}</p>;
+        const bugs = data.defaultRepository.bugs;
+        return (
+          <List
+            bugs={bugs}
+            nextPage={() => nextPage(bugs.pageInfo)}
+            prevPage={() => prevPage(bugs.pageInfo)}
+          />
+        );
+      }}
+    </Query>
+  );
+}
 
 export default ListQuery;