From 6a502c145bd8f2e2e1a9c0b103c11f0433c60737 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 4 Feb 2020 20:57:43 +0100 Subject: webui: convert bug list to typescript --- webui/src/list/ListQuery.tsx | 313 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 webui/src/list/ListQuery.tsx (limited to 'webui/src/list/ListQuery.tsx') diff --git a/webui/src/list/ListQuery.tsx b/webui/src/list/ListQuery.tsx new file mode 100644 index 00000000..a9bb15df --- /dev/null +++ b/webui/src/list/ListQuery.tsx @@ -0,0 +1,313 @@ +import { fade, makeStyles, Theme } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; +import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; +import ErrorOutline from '@material-ui/icons/ErrorOutline'; +import Paper from '@material-ui/core/Paper'; +import InputBase from '@material-ui/core/InputBase'; +import Skeleton from '@material-ui/lab/Skeleton'; +import React, { useState, useEffect, useRef } from 'react'; +import { useLocation, useHistory, Link } from 'react-router-dom'; +import { ApolloError } from 'apollo-boost'; +import List from './List'; +import FilterToolbar from './FilterToolbar'; +import { useListBugsQuery } from './ListQuery.generated'; + +type StylesProps = { searching?: boolean }; +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', + }, + header: { + display: 'flex', + padding: theme.spacing(2), + '& > h1': { + ...theme.typography.h6, + margin: theme.spacing(0, 2), + }, + alignItems: 'center', + justifyContent: 'space-between', + }, + search: { + borderRadius: theme.shape.borderRadius, + borderColor: fade(theme.palette.primary.main, 0.2), + borderStyle: 'solid', + borderWidth: '1px', + backgroundColor: fade(theme.palette.primary.main, 0.05), + padding: theme.spacing(0, 1), + width: ({ searching }) => (searching ? '20rem' : '15rem'), + transition: theme.transitions.create([ + 'width', + 'borderColor', + 'backgroundColor', + ]), + }, + searchFocused: { + borderColor: fade(theme.palette.primary.main, 0.4), + backgroundColor: theme.palette.background.paper, + width: '20rem!important', + }, + 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, + }, + message: { + ...theme.typography.h5, + padding: theme.spacing(8), + textAlign: 'center', + borderBottomColor: theme.palette.grey['300'], + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + '& > p': { + margin: '0', + }, + }, + errorBox: { + color: theme.palette.error.main, + '& > pre': { + fontSize: '1rem', + textAlign: 'left', + backgroundColor: theme.palette.grey['900'], + color: theme.palette.common.white, + marginTop: theme.spacing(4), + padding: theme.spacing(2, 3), + }, + }, +})); + +function editParams( + params: URLSearchParams, + callback: (params: URLSearchParams) => void +) { + const cloned = new URLSearchParams(params.toString()); + callback(cloned); + return cloned; +} + +// TODO: factor this out +type PlaceholderProps = { count: number }; +const Placeholder: React.FC = ({ + count, +}: PlaceholderProps) => { + 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.

+
+ ); +}; + +type ErrorProps = { error: ApolloError }; +const Error: React.FC = ({ error }: ErrorProps) => { + const classes = useStyles({}); + return ( +
+ +

There was an error while fetching bug.

+

+ {error.message} +

+
+        {JSON.stringify(error, null, 2)}
+      
+
+ ); +}; + +function ListQuery() { + const location = useLocation(); + const history = useHistory(); + const params = new URLSearchParams(location.search); + const query = params.get('q') || ''; + + const [input, setInput] = useState(query); + + const classes = useStyles({ searching: !!input }); + + // TODO is this the right way to do it? + const lastQuery = useRef(null); + useEffect(() => { + if (query !== lastQuery.current) { + setInput(query); + } + lastQuery.current = query; + }, [query, input, lastQuery]); + + const num = (param: string | null) => (param ? parseInt(param) : null); + const page = { + first: num(params.get('first')), + last: num(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 || 10).toString(); + + const { loading, error, data } = useListBugsQuery({ + variables: { + ...page, + query, + }, + }); + + let nextPage = null; + let previousPage = null; + let count = 0; + if (!loading && !error && data?.defaultRepository?.bugs) { + const bugs = data.defaultRepository.bugs; + count = bugs.totalCount; + // This computes the URL for the next page + if (bugs.pageInfo.hasNextPage) { + 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 + if (bugs.pageInfo.hasPreviousPage) { + previousPage = { + ...location, + search: editParams(params, p => { + p.delete('first'); + p.delete('after'); + p.set('last', perPage); + p.set('before', bugs.pageInfo.startCursor); + }).toString(), + }; + } + } + + // Prepare params without paging for editing filters + const paramsWithoutPaging = editParams(params, p => { + p.delete('first'); + p.delete('last'); + p.delete('before'); + p.delete('after'); + }); + // Returns a new location with the `q` param edited + const queryLocation = (query: string) => ({ + ...location, + search: editParams(paramsWithoutPaging, p => p.set('q', query)).toString(), + }); + + let content; + if (loading) { + content = ; + } else if (error) { + content = ; + } else if (data?.defaultRepository) { + const bugs = data.defaultRepository.bugs; + + if (bugs.totalCount === 0) { + content = ; + } else { + content = ; + } + } + + const formSubmit = (e: React.FormEvent) => { + e.preventDefault(); + history.push(queryLocation(input)); + }; + + return ( + +
+

Issues

+
+ setInput(e.target.value)} + classes={{ + root: classes.search, + focused: classes.searchFocused, + }} + /> + + +
+ + {content} +
+ {previousPage ? ( + + + + ) : ( + + + + )} +
{loading ? 'Loading' : `Total: ${count}`}
+ {nextPage ? ( + + + + ) : ( + + + + )} +
+
+ ); +} + +export default ListQuery; -- cgit