From fa13550115144a6f39888960a80cc24890f83536 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 24 Jan 2020 00:43:02 +0100 Subject: [PATCH] webui: enhance the issue list page This starts some ground work for filtering & moves the pagination logic in the query params. Also has a nice loading placeholder. --- webui/package-lock.json | 67 +++++----- webui/package.json | 6 +- webui/src/list/BugRow.js | 3 +- webui/src/list/Filter.js | 32 +++++ webui/src/list/List.js | 48 ++------ webui/src/list/ListQuery.js | 237 ++++++++++++++++++++++++++++++++---- 6 files changed, 297 insertions(+), 96 deletions(-) create mode 100644 webui/src/list/Filter.js diff --git a/webui/package-lock.json b/webui/package-lock.json index 34384e959418d29c9c2826ad2ebdb7ecb269cb7b..8e8648b55d2fa8f1d87f375af23a3cffee277f86 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -2086,14 +2086,14 @@ } }, "@material-ui/core": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.8.3.tgz", - "integrity": "sha512-ZJbfJQmkuZCSQTf0nzpfZwizmDdCq8ruZxnPNFnhoKDqgJpMvV8TJRi8vdI9ls1tMuTqxlhyhw8556fxOpWpFQ==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.0.tgz", + "integrity": "sha512-zrrr8mPU5DDBYaVil4uJYauW41PjSn5otn7cqGsmWOY0t90fypr9nNgM7rRJaPz2AP6oRSDx1kBQt2igf5uelg==", "requires": { "@babel/runtime": "^7.4.4", - "@material-ui/styles": "^4.8.2", + "@material-ui/styles": "^4.9.0", "@material-ui/system": "^4.7.1", - "@material-ui/types": "^4.1.1", + "@material-ui/types": "^5.0.0", "@material-ui/utils": "^4.7.1", "@types/react-transition-group": "^4.2.0", "clsx": "^1.0.2", @@ -2114,26 +2114,38 @@ "@babel/runtime": "^7.4.4" } }, + "@material-ui/lab": { + "version": "4.0.0-alpha.40", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.40.tgz", + "integrity": "sha512-VwXCNFJKfctu9Ot9XP5u2SSzXpm2Fn7F/o08bUfrJDkMCuRc8MCGVnNhT+guZRZa35rR97uWKc3SGQ/LAv8yEg==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.7.1", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0" + } + }, "@material-ui/styles": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.8.2.tgz", - "integrity": "sha512-r5U+93pkpwQOmHTmwyn2sqTio6PHd873xvSHiKP6fdybAXXX6CZgVvh3W8saZNbYr/QXsS8OHmFv7sYJLt5Yfg==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.0.tgz", + "integrity": "sha512-nJHum4RqYBPWsjL/9JET8Z02FZ9gSizlg/7LWVFpIthNzpK6OQ5OSRR4T4x9/p+wK3t1qNn3b1uI4XpnZaPxOA==", "requires": { "@babel/runtime": "^7.4.4", "@emotion/hash": "^0.7.4", - "@material-ui/types": "^4.1.1", + "@material-ui/types": "^5.0.0", "@material-ui/utils": "^4.7.1", "clsx": "^1.0.2", "csstype": "^2.5.2", "hoist-non-react-statics": "^3.2.1", - "jss": "^10.0.0", - "jss-plugin-camel-case": "^10.0.0", - "jss-plugin-default-unit": "^10.0.0", - "jss-plugin-global": "^10.0.0", - "jss-plugin-nested": "^10.0.0", - "jss-plugin-props-sort": "^10.0.0", - "jss-plugin-rule-value-function": "^10.0.0", - "jss-plugin-vendor-prefixer": "^10.0.0", + "jss": "^10.0.3", + "jss-plugin-camel-case": "^10.0.3", + "jss-plugin-default-unit": "^10.0.3", + "jss-plugin-global": "^10.0.3", + "jss-plugin-nested": "^10.0.3", + "jss-plugin-props-sort": "^10.0.3", + "jss-plugin-rule-value-function": "^10.0.3", + "jss-plugin-vendor-prefixer": "^10.0.3", "prop-types": "^15.7.2" } }, @@ -2148,12 +2160,9 @@ } }, "@material-ui/types": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-4.1.1.tgz", - "integrity": "sha512-AN+GZNXytX9yxGi0JOfxHrRTbhFybjUJ05rnsBVjcB+16e466Z0Xe5IxawuOayVZgTBNDxmPKo5j4V6OnMtaSQ==", - "requires": { - "@types/react": "*" - } + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.0.0.tgz", + "integrity": "sha512-UeH2BuKkwDndtMSS0qgx1kCzSMw+ydtj0xx/XbFtxNSTlXydKwzs5gVW5ZKsFlAkwoOOQ9TIsyoCC8hq18tOwg==" }, "@material-ui/utils": { "version": "4.7.1", @@ -2471,9 +2480,9 @@ "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==" }, "@types/react": { - "version": "16.9.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.18.tgz", - "integrity": "sha512-MvjiKX/kUE8o49ipppg49RDZ97p4XfW1WWksp/UlTUSJpisyhzd62pZAMXxAscFLoxfYOflkGANAnGkSeHTFQg==", + "version": "16.9.19", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.19.tgz", + "integrity": "sha512-LJV97//H+zqKWMms0kvxaKYJDG05U2TtQB3chRLF8MPNs+MQh/H1aGlyDUxjaHvu08EAGerdX2z4LTBc7ns77A==", "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" @@ -12382,9 +12391,9 @@ } }, "popper.js": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz", - "integrity": "sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==" + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" }, "portfinder": { "version": "1.0.25", diff --git a/webui/package.json b/webui/package.json index 5926f547f94f1bf5a2be6d664eb9932a854bb997..e1055d7552fa9b317e2aee7d4f82ae98c8d6df50 100644 --- a/webui/package.json +++ b/webui/package.json @@ -3,9 +3,11 @@ "version": "0.1.0", "private": true, "dependencies": { - "@material-ui/core": "^4.3.3", + "@apollo/react-hooks": "^3.1.3", + "@material-ui/core": "^4.9.0", "@material-ui/icons": "^4.2.1", - "@material-ui/styles": "^4.3.3", + "@material-ui/lab": "^4.0.0-alpha.40", + "@material-ui/styles": "^4.9.0", "apollo-boost": "^0.4.7", "graphql": "^14.3.0", "moment": "^2.24.0", diff --git a/webui/src/list/BugRow.js b/webui/src/list/BugRow.js index 23414a36e276cf15c6a95afb414d088fd689ae04..add5c12fcfb8f74db097529f75396ae4bb59cc2e 100644 --- a/webui/src/list/BugRow.js +++ b/webui/src/list/BugRow.js @@ -3,6 +3,7 @@ import TableCell from '@material-ui/core/TableCell/TableCell'; import TableRow from '@material-ui/core/TableRow/TableRow'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import gql from 'graphql-tag'; import React from 'react'; import { Link } from 'react-router-dom'; @@ -18,7 +19,7 @@ const Open = ({ className }) => ( const Closed = ({ className }) => ( - + ); diff --git a/webui/src/list/Filter.js b/webui/src/list/Filter.js new file mode 100644 index 0000000000000000000000000000000000000000..ce457d03bb8dd0dfc36b455f99f0484826d4f93e --- /dev/null +++ b/webui/src/list/Filter.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/styles'; + +const useStyles = makeStyles(theme => ({ + element: { + ...theme.typography.body2, + color: ({ active }) => (active ? '#333' : '#444'), + padding: theme.spacing(0, 1), + fontWeight: ({ active }) => (active ? 500 : 400), + textDecoration: 'none', + display: 'flex', + alignSelf: ({ end }) => (end ? 'flex-end' : 'auto'), + background: 'none', + border: 'none', + }, + icon: { + paddingRight: theme.spacing(0.5), + }, +})); + +function Filter({ active, children, icon: Icon, end, ...props }) { + const classes = useStyles({ active, end }); + + return ( + + ); +} + +export default Filter; diff --git a/webui/src/list/List.js b/webui/src/list/List.js index 54b2fe9721866844f491974705b4f700256da437..63b7354529019f76dbdf69f597304acc5e92cf97 100644 --- a/webui/src/list/List.js +++ b/webui/src/list/List.js @@ -1,49 +1,17 @@ -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 KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; -import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; import React from 'react'; import BugRow from './BugRow'; -const useStyles = makeStyles(theme => ({ - main: { - maxWidth: 600, - margin: 'auto', - marginTop: theme.spacing(4), - }, - pagination: { - ...theme.typography.overline, - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - }, -})); - -function List({ bugs, nextPage, prevPage }) { - const classes = useStyles(); - const { hasNextPage, hasPreviousPage } = bugs.pageInfo; +function List({ bugs }) { return ( -
- - - {bugs.edges.map(({ cursor, node }) => ( - - ))} - -
- -
-
Total: {bugs.totalCount}
- - - - - - -
-
+ + + {bugs.edges.map(({ cursor, node }) => ( + + ))} + +
); } diff --git a/webui/src/list/ListQuery.js b/webui/src/list/ListQuery.js index 869bca79824334de3fed0c1a355c3010aeb71321..9cbfab67c47abefb2664e623b1ec6982658bf83b 100644 --- a/webui/src/list/ListQuery.js +++ b/webui/src/list/ListQuery.js @@ -1,19 +1,90 @@ -// @flow -import CircularProgress from '@material-ui/core/CircularProgress'; +import { makeStyles } from '@material-ui/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Toolbar from '@material-ui/core/Toolbar'; +import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; +import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; +import ErrorOutline from '@material-ui/icons/ErrorOutline'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import Paper from '@material-ui/core/Paper'; +import Filter from './Filter'; +import Skeleton from '@material-ui/lab/Skeleton'; import gql from 'graphql-tag'; -import React, { useState } from 'react'; -import { Query } from 'react-apollo'; +import React from 'react'; +import { useQuery } from '@apollo/react-hooks'; +import { useLocation, Link } from 'react-router-dom'; import BugRow from './BugRow'; import List from './List'; +const useStyles = makeStyles(theme => ({ + main: { + maxWidth: 800, + margin: 'auto', + marginTop: theme.spacing(4), + marginBottom: theme.spacing(4), + overflow: 'hidden', + }, + pagination: { + ...theme.typography.overline, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + toolbar: { + backgroundColor: theme.palette.grey['100'], + borderColor: theme.palette.grey['300'], + borderWidth: '1px 0', + borderStyle: 'solid', + margin: theme.spacing(0, -1), + }, + header: { + ...theme.typography.h6, + padding: theme.spacing(2, 4), + }, + spacer: { + flex: 1, + }, + placeholderRow: { + padding: theme.spacing(1), + borderBottomColor: theme.palette.grey['300'], + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + display: 'flex', + alignItems: 'center', + }, + placeholderRowStatus: { + margin: theme.spacing(1, 2), + }, + placeholderRowText: { + flex: 1, + }, + noBug: { + ...theme.typography.h5, + padding: theme.spacing(8), + textAlign: 'center', + borderBottomColor: theme.palette.grey['300'], + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + '& > p': { + margin: '0', + }, + }, +})); + const QUERY = gql` - query($first: Int, $last: Int, $after: String, $before: String) { + query( + $first: Int + $last: Int + $after: String + $before: String + $query: String + ) { defaultRepository { bugs: allBugs( first: $first last: $last after: $after before: $before + query: $query ) { totalCount edges { @@ -35,30 +106,148 @@ const QUERY = gql` ${BugRow.fragment} `; +function editParams(params, callback) { + const cloned = new URLSearchParams(params.toString()); + callback(cloned); + return cloned; +} + +// TODO: factor this out +const Placeholder = ({ count }) => { + const classes = useStyles(); + return ( + <> + {new Array(count).fill(null).map((_, i) => ( +
+ +
+ + +
+
+ ))} + + ); +}; + +// TODO: factor this out +const NoBug = () => { + const classes = useStyles(); + return ( +
+ +

No results matched your search.

+
+ ); +}; + function ListQuery() { - const [page, setPage] = useState({ first: 10, after: null }); + const classes = useStyles(); + const location = useLocation(); + const params = new URLSearchParams(location.search); + const query = params.get('q'); + const page = { + first: params.get('first'), + last: params.get('last'), + after: params.get('after'), + before: params.get('before'), + }; + + // If nothing set, show the first 10 items + if (!page.first && !page.last) { + page.first = 10; + } const perPage = page.first || page.last; - const nextPage = pageInfo => - setPage({ first: perPage, after: pageInfo.endCursor }); - const prevPage = pageInfo => - setPage({ last: perPage, before: pageInfo.startCursor }); + + const { loading, error, data } = useQuery(QUERY, { + variables: { + ...page, + query, + }, + }); + + let nextPage = null; + let previousPage = null; + let hasNextPage = false; + let hasPreviousPage = false; + let count = 0; + if (!loading && !error && data.defaultRepository.bugs) { + const bugs = data.defaultRepository.bugs; + hasNextPage = bugs.pageInfo.hasNextPage; + hasPreviousPage = bugs.pageInfo.hasPreviousPage; + count = bugs.totalCount; + // This computes the URL for the next page + nextPage = { + ...location, + search: editParams(params, p => { + p.delete('last'); + p.delete('before'); + p.set('first', perPage); + p.set('after', bugs.pageInfo.endCursor); + }).toString(), + }; + // and this for the previous page + previousPage = { + ...location, + search: editParams(params, p => { + p.delete('first'); + p.delete('after'); + p.set('last', perPage); + p.set('before', bugs.pageInfo.startCursor); + }).toString(), + }; + } + + let content; + if (loading) { + content = ; + } else if (error) { + content =

Error: {JSON.stringify(error)}

; + } else { + const bugs = data.defaultRepository.bugs; + + if (bugs.totalCount === 0) { + content = ; + } else { + content = ; + } + } return ( - - {({ loading, error, data }) => { - if (loading) return ; - if (error) return

Error: {error}

; - const bugs = data.defaultRepository.bugs; - return ( - nextPage(bugs.pageInfo)} - prevPage={() => prevPage(bugs.pageInfo)} - /> - ); - }} -
+ +
Issues
+ + {/* TODO */} + + 123 open + + 456 closed +
+ Author + Label + Sort + + {content} +
+ + + +
{loading ? 'Loading' : `Total: ${count}`}
+ + + +
+ ); }