Merge pull request #148 from MichaelMure/sandhose/webui-update-deps

Michael MurΓ© created

Updates WebUI dependencies & rework the bug list pagination

Change summary

webui/package-lock.json      | 244 ++++++++++++++++++++++++++++++-------
webui/package.json           |  11 
webui/src/App.js             |  48 +++---
webui/src/Label.js           |  21 +-
webui/src/bug/Bug.js         |  65 +++++----
webui/src/bug/LabelChange.js |  13 +
webui/src/bug/Message.js     |  43 +++---
webui/src/bug/SetStatus.js   |  13 +
webui/src/bug/SetTitle.js    |  13 +
webui/src/bug/Timeline.js    |  60 ++++-----
webui/src/index.js           |  19 ++
webui/src/list/BugRow.js     |  67 +++++----
webui/src/list/List.js       | 160 +++++-------------------
webui/src/list/ListQuery.js  |  39 ++++-
14 files changed, 458 insertions(+), 358 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
             }
           }
         },
@@ -5225,9 +5284,9 @@
       }
     },
     "eslint-config-prettier": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.1.0.tgz",
-      "integrity": "sha512-zILwX9/Ocz4SV2vX7ox85AsrAgXV3f2o2gpIicdMIOra48WYqgUnWNH/cR/iHtmD2Vb3dLSC3LiEJnS05Gkw7w==",
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.3.0.tgz",
+      "integrity": "sha512-sZwhSTHVVz78+kYD3t5pCWSYEdVSBR0PXnwjDRsUs8ytIrK8PLXw+6FKp8r3Z7rx4ZszdetWlXYKOHoUrrwPlA==",
       "dev": true,
       "requires": {
         "get-stdin": "^6.0.0"
@@ -5437,9 +5496,9 @@
       }
     },
     "eslint-plugin-prettier": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.0.1.tgz",
-      "integrity": "sha512-/PMttrarPAY78PLvV3xfWibMOdMDl57hmlQ2XqFeA37wd+CJ7WSxV7txqjVPHi/AAFKd2lX0ZqfsOc/i5yFCSQ==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz",
+      "integrity": "sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA==",
       "dev": true,
       "requires": {
         "prettier-linter-helpers": "^1.0.0"
@@ -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
         }
       }
     },
@@ -7307,9 +7385,9 @@
       "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
     },
     "graphql": {
-      "version": "14.2.0",
-      "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.2.0.tgz",
-      "integrity": "sha512-dlFHRtxsL4sBy1C1e3v64IUd5ndZhAOHZ/z3Dr4Nm6+cvr9elrnz4BhMF9h9mRBBnhUCGLc4GH4xvPbKG6sUeA==",
+      "version": "14.3.0",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.3.0.tgz",
+      "integrity": "sha512-MdfI4v7kSNC3NhB7cF8KNijDsifuWO2XOtzpyququqaclO8wVuChYv+KogexDwgP5sp7nFI9Z6N4QHgoLkfjrg==",
       "requires": {
         "iterall": "^1.2.2"
       }
@@ -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",
@@ -13096,9 +13244,9 @@
       "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks="
     },
     "prettier": {
-      "version": "1.16.4",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz",
-      "integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==",
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.1.tgz",
+      "integrity": "sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg==",
       "dev": true
     },
     "prettier-linter-helpers": {
@@ -13381,24 +13529,26 @@
       }
     },
     "react-apollo": {
-      "version": "2.5.3",
-      "resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-2.5.3.tgz",
-      "integrity": "sha512-sBh7M3h4xdbck8NFnn1nlUG0mD0hCTeIcvov4A8hagxjOyTVSakoum+DP6rYNaHuAZFDS3oNurNNSbGZmgoU6g==",
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-2.5.6.tgz",
+      "integrity": "sha512-WWX5UykTtmW6+awjqEsSWSdvVyZv/vsavUgpdI4ddn4CBdz47INC+iTdJBnYaUFMB24GmqjFFSoSd98gu1xqKA==",
       "requires": {
-        "apollo-utilities": "^1.2.1",
+        "apollo-utilities": "^1.3.0",
         "hoist-non-react-statics": "^3.3.0",
         "lodash.isequal": "^4.5.0",
         "prop-types": "^15.7.2",
-        "ts-invariant": "^0.3.2",
+        "ts-invariant": "^0.4.2",
         "tslib": "^1.9.3"
       },
       "dependencies": {
-        "hoist-non-react-statics": {
-          "version": "3.3.0",
-          "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
-          "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
+        "apollo-utilities": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.0.tgz",
+          "integrity": "sha512-wQjV+FdWcTWmWUFlChG5rS0vHKy5OsXC6XlV9STRstQq6VbXANwHy6DHnTEQAfLXWAbNcPgBu+nBUpR3dFhwrA==",
           "requires": {
-            "react-is": "^16.7.0"
+            "fast-json-stable-stringify": "^2.0.0",
+            "ts-invariant": "^0.4.0",
+            "tslib": "^1.9.3"
           }
         },
         "prop-types": {
@@ -13412,9 +13562,9 @@
           }
         },
         "ts-invariant": {
-          "version": "0.3.2",
-          "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.3.2.tgz",
-          "integrity": "sha512-QsY8BCaRnHiB5T6iE4DPlJMAKEG3gzMiUco9FEt1jUXQf0XP6zi0idT0i0rMTu8A326JqNSDsmlkA9dRSh1TRg==",
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.2.tgz",
+          "integrity": "sha512-PTAAn8lJPEdRBJJEs4ig6MVZWfO12yrFzV7YaPslmyhG7+4MA279y4BXT3f72gXeVl0mC1aAWq2rMX4eKTWU/Q==",
           "requires": {
             "tslib": "^1.9.3"
           }

webui/package.json πŸ”—

@@ -5,20 +5,21 @@
   "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",
+    "graphql": "^14.3.0",
     "moment": "^2.24.0",
     "react": "^16.8.6",
-    "react-apollo": "^2.5.3",
+    "react-apollo": "^2.5.6",
     "react-dom": "^16.8.6",
     "react-router": "^5.0.0",
     "react-router-dom": "^5.0.0",
     "react-scripts": "^2.1.8"
   },
   "devDependencies": {
-    "eslint-config-prettier": "^4.1.0",
-    "eslint-plugin-prettier": "^3.0.1",
-    "prettier": "^1.16.4"
+    "eslint-config-prettier": "^4.3.0",
+    "eslint-plugin-prettier": "^3.1.0",
+    "prettier": "^1.17.1"
   },
   "scripts": {
     "start": "react-scripts start",

webui/src/App.js πŸ”—

@@ -1,39 +1,39 @@
 import AppBar from '@material-ui/core/AppBar';
 import CssBaseline from '@material-ui/core/CssBaseline';
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import Toolbar from '@material-ui/core/Toolbar';
-import Typography from '@material-ui/core/Typography';
 import React from 'react';
-import { Route, Switch, withRouter } from 'react-router';
+import { Route, Switch } from 'react-router';
 import { Link } from 'react-router-dom';
 
 import BugQuery from './bug/BugQuery';
 import ListQuery from './list/ListQuery';
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   appTitle: {
+    ...theme.typography.title,
     color: 'white',
     textDecoration: 'none',
   },
-});
+}));
 
-const App = ({ location, classes }) => (
-  <React.Fragment>
-    <CssBaseline />
-    <AppBar position="static" color="primary">
-      <Toolbar>
-        <Link to="/" className={classes.appTitle}>
-          <Typography variant="title" color="inherit">
-            git-bug webui
-          </Typography>
-        </Link>
-      </Toolbar>
-    </AppBar>
-    <Switch>
-      <Route path="/" exact component={ListQuery} />
-      <Route path="/bug/:id" exact component={BugQuery} />
-    </Switch>
-  </React.Fragment>
-);
+export default function App() {
+  const classes = useStyles();
 
-export default withStyles(styles)(withRouter(App));
+  return (
+    <>
+      <CssBaseline />
+      <AppBar position="static" color="primary">
+        <Toolbar>
+          <Link to="/" className={classes.appTitle}>
+            git-bug webui
+          </Link>
+        </Toolbar>
+      </AppBar>
+      <Switch>
+        <Route path="/" exact component={ListQuery} />
+        <Route path="/bug/:id" exact component={BugQuery} />
+      </Switch>
+    </>
+  );
+}

webui/src/Label.js πŸ”—

@@ -1,5 +1,5 @@
 import React from 'react';
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import {
   getContrastRatio,
   darken,
@@ -42,7 +42,7 @@ const _genStyle = background => ({
 // Generate a style object (text, background and border colors) from the label
 const genStyle = label => _genStyle(getColor(label));
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   label: {
     ...theme.typography.body2,
     padding: '0 6px',
@@ -53,12 +53,15 @@ const styles = theme => ({
     borderBottom: 'solid 1.5px',
     verticalAlign: 'bottom',
   },
-});
+}));
 
-const Label = ({ label, classes }) => (
-  <span className={classes.label} style={genStyle(label)}>
-    {label}
-  </span>
-);
+function Label({ label }) {
+  const classes = useStyles();
+  return (
+    <span className={classes.label} style={genStyle(label)}>
+      {label}
+    </span>
+  );
+}
 
-export default withStyles(styles)(Label);
+export default Label;

webui/src/bug/Bug.js πŸ”—

@@ -1,4 +1,4 @@
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import Typography from '@material-ui/core/Typography/Typography';
 import gql from 'graphql-tag';
 import React from 'react';
@@ -7,7 +7,7 @@ import Date from '../Date';
 import TimelineQuery from './TimelineQuery';
 import Label from '../Label';
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   main: {
     maxWidth: 800,
     margin: 'auto',
@@ -48,38 +48,41 @@ const styles = theme => ({
       display: 'block',
     },
   },
-});
+}));
 
-const Bug = ({ bug, classes }) => (
-  <main className={classes.main}>
-    <div className={classes.header}>
-      <span className={classes.title}>{bug.title}</span>
-      <span className={classes.id}>{bug.humanId}</span>
+function Bug({ bug }) {
+  const classes = useStyles();
+  return (
+    <main className={classes.main}>
+      <div className={classes.header}>
+        <span className={classes.title}>{bug.title}</span>
+        <span className={classes.id}>{bug.humanId}</span>
 
-      <Typography color={'textSecondary'}>
-        <Author author={bug.author} />
-        {' opened this bug '}
-        <Date date={bug.createdAt} />
-      </Typography>
-    </div>
-
-    <div className={classes.container}>
-      <div className={classes.timeline}>
-        <TimelineQuery id={bug.id} />
+        <Typography color={'textSecondary'}>
+          <Author author={bug.author} />
+          {' opened this bug '}
+          <Date date={bug.createdAt} />
+        </Typography>
       </div>
-      <div className={classes.sidebar}>
-        <Typography variant={'subheading'}>Labels</Typography>
-        <ul className={classes.labelList}>
-          {bug.labels.map(l => (
-            <li className={classes.label}>
-              <Label label={l} key={l} />
-            </li>
-          ))}
-        </ul>
+
+      <div className={classes.container}>
+        <div className={classes.timeline}>
+          <TimelineQuery id={bug.id} />
+        </div>
+        <div className={classes.sidebar}>
+          <Typography variant={'subheading'}>Labels</Typography>
+          <ul className={classes.labelList}>
+            {bug.labels.map(l => (
+              <li className={classes.label}>
+                <Label label={l} key={l} />
+              </li>
+            ))}
+          </ul>
+        </div>
       </div>
-    </div>
-  </main>
-);
+    </main>
+  );
+}
 
 Bug.fragment = gql`
   fragment Bug on Bug {
@@ -97,4 +100,4 @@ Bug.fragment = gql`
   }
 `;
 
-export default withStyles(styles)(Bug);
+export default Bug;

webui/src/bug/LabelChange.js πŸ”—

@@ -1,11 +1,11 @@
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import gql from 'graphql-tag';
 import React from 'react';
 import Author from '../Author';
 import Date from '../Date';
 import Label from '../Label';
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   main: {
     ...theme.typography.body2,
     marginLeft: theme.spacing.unit + 40,
@@ -13,10 +13,11 @@ const styles = theme => ({
   author: {
     fontWeight: 'bold',
   },
-});
+}));
 
-const LabelChange = ({ op, classes }) => {
+function LabelChange({ op }) {
   const { added, removed } = op;
+  const classes = useStyles();
   return (
     <div className={classes.main}>
       <Author author={op.author} className={classes.author} />
@@ -37,7 +38,7 @@ const LabelChange = ({ op, classes }) => {
       <Date date={op.date} />
     </div>
   );
-};
+}
 
 LabelChange.fragment = gql`
   fragment LabelChange on TimelineItem {
@@ -54,4 +55,4 @@ LabelChange.fragment = gql`
   }
 `;
 
-export default withStyles(styles)(LabelChange);
+export default LabelChange;

webui/src/bug/Message.js πŸ”—

@@ -1,4 +1,4 @@
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import Paper from '@material-ui/core/Paper';
 import gql from 'graphql-tag';
 import React from 'react';
@@ -6,7 +6,7 @@ import Author from '../Author';
 import { Avatar } from '../Author';
 import Date from '../Date';
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   author: {
     fontWeight: 'bold',
   },
@@ -44,24 +44,27 @@ const styles = theme => ({
     padding: '1rem',
     whiteSpace: 'pre-wrap',
   },
-});
+}));
 
-const Message = ({ op, classes }) => (
-  <article className={classes.container}>
-    <Avatar author={op.author} className={classes.avatar} />
-    <Paper elevation={1} className={classes.bubble}>
-      <header className={classes.header}>
-        <div className={classes.title}>
-          <Author className={classes.author} author={op.author} />
-          <span> commented </span>
-          <Date date={op.createdAt} />
-        </div>
-        {op.edited && <div className={classes.tag}>Edited</div>}
-      </header>
-      <section className={classes.body}>{op.message}</section>
-    </Paper>
-  </article>
-);
+function Message({ op }) {
+  const classes = useStyles();
+  return (
+    <article className={classes.container}>
+      <Avatar author={op.author} className={classes.avatar} />
+      <Paper elevation={1} className={classes.bubble}>
+        <header className={classes.header}>
+          <div className={classes.title}>
+            <Author className={classes.author} author={op.author} />
+            <span> commented </span>
+            <Date date={op.createdAt} />
+          </div>
+          {op.edited && <div className={classes.tag}>Edited</div>}
+        </header>
+        <section className={classes.body}>{op.message}</section>
+      </Paper>
+    </article>
+  );
+}
 
 Message.createFragment = gql`
   fragment Create on TimelineItem {
@@ -95,4 +98,4 @@ Message.commentFragment = gql`
   }
 `;
 
-export default withStyles(styles)(Message);
+export default Message;

webui/src/bug/SetStatus.js πŸ”—

@@ -1,17 +1,18 @@
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import gql from 'graphql-tag';
 import React from 'react';
 import Author from '../Author';
 import Date from '../Date';
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   main: {
     ...theme.typography.body2,
     marginLeft: theme.spacing.unit + 40,
   },
-});
+}));
 
-const SetStatus = ({ op, classes }) => {
+function SetStatus({ op }) {
+  const classes = useStyles();
   return (
     <div className={classes.main}>
       <Author author={op.author} bold />
@@ -19,7 +20,7 @@ const SetStatus = ({ op, classes }) => {
       <Date date={op.date} />
     </div>
   );
-};
+}
 
 SetStatus.fragment = gql`
   fragment SetStatus on TimelineItem {
@@ -35,4 +36,4 @@ SetStatus.fragment = gql`
   }
 `;
 
-export default withStyles(styles)(SetStatus);
+export default SetStatus;

webui/src/bug/SetTitle.js πŸ”—

@@ -1,10 +1,10 @@
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import gql from 'graphql-tag';
 import React from 'react';
 import Author from '../Author';
 import Date from '../Date';
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   main: {
     ...theme.typography.body2,
     marginLeft: theme.spacing.unit + 40,
@@ -12,9 +12,10 @@ const styles = theme => ({
   bold: {
     fontWeight: 'bold',
   },
-});
+}));
 
-const SetTitle = ({ op, classes }) => {
+function SetTitle({ op }) {
+  const classes = useStyles();
   return (
     <div className={classes.main}>
       <Author author={op.author} className={classes.bold} />
@@ -25,7 +26,7 @@ const SetTitle = ({ op, classes }) => {
       <Date date={op.date} />
     </div>
   );
-};
+}
 
 SetTitle.fragment = gql`
   fragment SetTitle on TimelineItem {
@@ -42,4 +43,4 @@ SetTitle.fragment = gql`
   }
 `;
 
-export default withStyles(styles)(SetTitle);
+export default SetTitle;

webui/src/bug/Timeline.js πŸ”—

@@ -1,51 +1,43 @@
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import React from 'react';
 import LabelChange from './LabelChange';
 import Message from './Message';
 import SetStatus from './SetStatus';
 import SetTitle from './SetTitle';
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   main: {
     '& > *:not(:last-child)': {
       marginBottom: theme.spacing.unit * 2,
     },
   },
-});
+}));
 
-class Timeline extends React.Component {
-  props: {
-    ops: Array,
-    fetchMore: any => any,
-    classes: any,
-  };
+const componentMap = {
+  CreateTimelineItem: Message,
+  AddCommentTimelineItem: Message,
+  LabelChangeTimelineItem: LabelChange,
+  SetTitleTimelineItem: SetTitle,
+  SetStatusTimelineItem: SetStatus,
+};
 
-  render() {
-    const { ops, classes } = this.props;
+function Timeline({ ops }) {
+  const classes = useStyles();
 
-    return (
-      <div className={classes.main}>
-        {ops.map((op, index) => {
-          switch (op.__typename) {
-            case 'CreateTimelineItem':
-              return <Message key={index} op={op} />;
-            case 'AddCommentTimelineItem':
-              return <Message key={index} op={op} />;
-            case 'LabelChangeTimelineItem':
-              return <LabelChange key={index} op={op} />;
-            case 'SetTitleTimelineItem':
-              return <SetTitle key={index} op={op} />;
-            case 'SetStatusTimelineItem':
-              return <SetStatus key={index} op={op} />;
+  return (
+    <div className={classes.main}>
+      {ops.map((op, index) => {
+        const Component = componentMap[op.__typename];
 
-            default:
-              console.log('unsupported operation type ' + op.__typename);
-              return null;
-          }
-        })}
-      </div>
-    );
-  }
+        if (!Component) {
+          console.warn('unsupported operation type ' + op.__typename);
+          return null;
+        }
+
+        return <Component key={index} op={op} />;
+      })}
+    </div>
+  );
 }
 
-export default withStyles(styles)(Timeline);
+export default Timeline;

webui/src/index.js πŸ”—

@@ -1,22 +1,31 @@
+import { install } from '@material-ui/styles';
+import ThemeProvider from '@material-ui/styles/ThemeProvider';
+import { createMuiTheme } from '@material-ui/core/styles';
 import ApolloClient from 'apollo-boost';
 import React from 'react';
 import { ApolloProvider } from 'react-apollo';
 import ReactDOM from 'react-dom';
 import { BrowserRouter } from 'react-router-dom';
 
-import App from './App';
+install();
+
+// TODO(sandhose): this is temporary until Material-UI v4 goes out
+const App = React.lazy(() => import('./App'));
+
+const theme = createMuiTheme();
 
 const client = new ApolloClient({
   uri: '/graphql',
-  connectToDevTools: true,
 });
 
 ReactDOM.render(
   <ApolloProvider client={client}>
     <BrowserRouter>
-      <React.Fragment>
-        <App />
-      </React.Fragment>
+      <ThemeProvider theme={theme}>
+        <React.Suspense fallback={'Loading…'}>
+          <App />
+        </React.Suspense>
+      </ThemeProvider>
     </BrowserRouter>
   </ApolloProvider>,
   document.getElementById('root')

webui/src/list/BugRow.js πŸ”—

@@ -1,4 +1,4 @@
-import { withStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/styles';
 import TableCell from '@material-ui/core/TableCell/TableCell';
 import TableRow from '@material-ui/core/TableRow/TableRow';
 import Tooltip from '@material-ui/core/Tooltip/Tooltip';
@@ -33,7 +33,7 @@ const Status = ({ status, className }) => {
   }
 };
 
-const styles = theme => ({
+const useStyles = makeStyles(theme => ({
   cell: {
     display: 'flex',
     alignItems: 'center',
@@ -53,36 +53,39 @@ const styles = theme => ({
   labels: {
     paddingLeft: theme.spacing.unit,
   },
-});
+}));
 
-const BugRow = ({ bug, classes }) => (
-  <TableRow hover>
-    <TableCell className={classes.cell}>
-      <Status status={bug.status} className={classes.status} />
-      <div className={classes.expand}>
-        <Link to={'bug/' + bug.humanId}>
-          <div className={classes.expand}>
-            <Typography variant={'title'} className={classes.title}>
-              {bug.title}
-            </Typography>
-            {bug.labels.length > 0 && (
-              <span className={classes.labels}>
-                {bug.labels.map(l => (
-                  <Label key={l} label={l} />
-                ))}
-              </span>
-            )}
-          </div>
-        </Link>
-        <Typography color={'textSecondary'}>
-          {bug.humanId} opened
-          <Date date={bug.createdAt} />
-          by {bug.author.displayName}
-        </Typography>
-      </div>
-    </TableCell>
-  </TableRow>
-);
+function BugRow({ bug }) {
+  const classes = useStyles();
+  return (
+    <TableRow hover>
+      <TableCell className={classes.cell}>
+        <Status status={bug.status} className={classes.status} />
+        <div className={classes.expand}>
+          <Link to={'bug/' + bug.humanId}>
+            <div className={classes.expand}>
+              <Typography variant={'title'} className={classes.title}>
+                {bug.title}
+              </Typography>
+              {bug.labels.length > 0 && (
+                <span className={classes.labels}>
+                  {bug.labels.map(l => (
+                    <Label key={l} label={l} />
+                  ))}
+                </span>
+              )}
+            </div>
+          </Link>
+          <Typography color={'textSecondary'}>
+            {bug.humanId} opened
+            <Date date={bug.createdAt} />
+            by {bug.author.displayName}
+          </Typography>
+        </div>
+      </TableCell>
+    </TableRow>
+  );
+}
 
 BugRow.fragment = gql`
   fragment BugRow on Bug {
@@ -99,4 +102,4 @@ BugRow.fragment = gql`
   }
 `;
 
-export default withStyles(styles)(BugRow);
+export default BugRow;

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;