aboutsummaryrefslogtreecommitdiffstats
path: root/webui/src/list
diff options
context:
space:
mode:
authorQuentin Gliech <quentingliech@gmail.com>2020-01-24 00:43:02 +0100
committerQuentin Gliech <quentingliech@gmail.com>2020-01-24 01:12:01 +0100
commitfa13550115144a6f39888960a80cc24890f83536 (patch)
treec84027d0163286c08e72ae3f2cae1eb900e38c3d /webui/src/list
parent9f7953161f3ef8a6081b7950b3cc274e34666116 (diff)
downloadgit-bug-fa13550115144a6f39888960a80cc24890f83536.tar.gz
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.
Diffstat (limited to 'webui/src/list')
-rw-r--r--webui/src/list/BugRow.js3
-rw-r--r--webui/src/list/Filter.js32
-rw-r--r--webui/src/list/List.js48
-rw-r--r--webui/src/list/ListQuery.js237
4 files changed, 255 insertions, 65 deletions
diff --git a/webui/src/list/BugRow.js b/webui/src/list/BugRow.js
index 23414a36..add5c12f 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 }) => (
<Tooltip title="Closed">
- <ErrorOutline htmlColor="#cb2431" className={className} />
+ <CheckCircleOutline htmlColor="#cb2431" className={className} />
</Tooltip>
);
diff --git a/webui/src/list/Filter.js b/webui/src/list/Filter.js
new file mode 100644
index 00000000..ce457d03
--- /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 (
+ <button {...props} className={classes.element}>
+ {Icon && <Icon fontSize="small" classes={{ root: classes.icon }} />}
+ <div>{children}</div>
+ </button>
+ );
+}
+
+export default Filter;
diff --git a/webui/src/list/List.js b/webui/src/list/List.js
index 54b2fe97..63b73545 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 (
- <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>
+ <Table>
+ <TableBody>
+ {bugs.edges.map(({ cursor, node }) => (
+ <BugRow bug={node} key={cursor} />
+ ))}
+ </TableBody>
+ </Table>
);
}
diff --git a/webui/src/list/ListQuery.js b/webui/src/list/ListQuery.js
index 869bca79..9cbfab67 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) => (
+ <div key={i} className={classes.placeholderRow}>
+ <Skeleton
+ className={classes.placeholderRowStatus}
+ variant="circle"
+ width={20}
+ height={20}
+ />
+ <div className={classes.placeholderRowText}>
+ <Skeleton height={22} />
+ <Skeleton height={24} width="60%" />
+ </div>
+ </div>
+ ))}
+ </>
+ );
+};
+
+// TODO: factor this out
+const NoBug = () => {
+ const classes = useStyles();
+ return (
+ <div className={classes.noBug}>
+ <ErrorOutline fontSize="large" />
+ <p>No results matched your search.</p>
+ </div>
+ );
+};
+
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 = <Placeholder count={10} />;
+ } else if (error) {
+ content = <p>Error: {JSON.stringify(error)}</p>;
+ } else {
+ const bugs = data.defaultRepository.bugs;
+
+ if (bugs.totalCount === 0) {
+ content = <NoBug />;
+ } else {
+ content = <List bugs={bugs} />;
+ }
+ }
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>
+ <Paper className={classes.main}>
+ <header className={classes.header}>Issues</header>
+ <Toolbar className={classes.toolbar}>
+ {/* TODO */}
+ <Filter active icon={ErrorOutline}>
+ 123 open
+ </Filter>
+ <Filter icon={CheckCircleOutline}>456 closed</Filter>
+ <div className={classes.spacer} />
+ <Filter>Author</Filter>
+ <Filter>Label</Filter>
+ <Filter>Sort</Filter>
+ </Toolbar>
+ {content}
+ <div className={classes.pagination}>
+ <IconButton
+ component={Link}
+ to={previousPage}
+ disabled={!hasPreviousPage}
+ >
+ <KeyboardArrowLeft />
+ </IconButton>
+ <div>{loading ? 'Loading' : `Total: ${count}`}</div>
+ <IconButton component={Link} to={nextPage} disabled={!hasNextPage}>
+ <KeyboardArrowRight />
+ </IconButton>
+ </div>
+ </Paper>
);
}