diff options
Diffstat (limited to 'webui/src')
-rw-r--r-- | webui/src/App.js | 48 | ||||
-rw-r--r-- | webui/src/Label.js | 21 | ||||
-rw-r--r-- | webui/src/bug/Bug.js | 65 | ||||
-rw-r--r-- | webui/src/bug/LabelChange.js | 13 | ||||
-rw-r--r-- | webui/src/bug/Message.js | 43 | ||||
-rw-r--r-- | webui/src/bug/SetStatus.js | 13 | ||||
-rw-r--r-- | webui/src/bug/SetTitle.js | 13 | ||||
-rw-r--r-- | webui/src/bug/Timeline.js | 60 | ||||
-rw-r--r-- | webui/src/index.js | 19 | ||||
-rw-r--r-- | webui/src/list/BugRow.js | 67 | ||||
-rw-r--r-- | webui/src/list/List.js | 160 | ||||
-rw-r--r-- | webui/src/list/ListQuery.js | 39 |
12 files changed, 255 insertions, 306 deletions
diff --git a/webui/src/App.js b/webui/src/App.js index 3a693dcb..b2eb6bf0 100644 --- a/webui/src/App.js +++ b/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> + </> + ); +} diff --git a/webui/src/Label.js b/webui/src/Label.js index e7c25c23..826d21f5 100644 --- a/webui/src/Label.js +++ b/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; diff --git a/webui/src/bug/Bug.js b/webui/src/bug/Bug.js index 9b5f84ad..829a4af2 100644 --- a/webui/src/bug/Bug.js +++ b/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; diff --git a/webui/src/bug/LabelChange.js b/webui/src/bug/LabelChange.js index 6301f35f..76b6e6e2 100644 --- a/webui/src/bug/LabelChange.js +++ b/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; diff --git a/webui/src/bug/Message.js b/webui/src/bug/Message.js index 493de8ee..ff039444 100644 --- a/webui/src/bug/Message.js +++ b/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; diff --git a/webui/src/bug/SetStatus.js b/webui/src/bug/SetStatus.js index 58b81630..ab591038 100644 --- a/webui/src/bug/SetStatus.js +++ b/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; diff --git a/webui/src/bug/SetTitle.js b/webui/src/bug/SetTitle.js index f5c48568..d9a09ad5 100644 --- a/webui/src/bug/SetTitle.js +++ b/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; diff --git a/webui/src/bug/Timeline.js b/webui/src/bug/Timeline.js index 3123f45f..d77e0d4b 100644 --- a/webui/src/bug/Timeline.js +++ b/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; diff --git a/webui/src/index.js b/webui/src/index.js index f5d95ccc..885911f5 100644 --- a/webui/src/index.js +++ b/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') diff --git a/webui/src/list/BugRow.js b/webui/src/list/BugRow.js index a045770b..e82d81db 100644 --- a/webui/src/list/BugRow.js +++ b/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; diff --git a/webui/src/list/List.js b/webui/src/list/List.js index d36be8a1..45c2c963 100644 --- a/webui/src/list/List.js +++ b/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; diff --git a/webui/src/list/ListQuery.js b/webui/src/list/ListQuery.js index 9dbe4e53..869bca79 100644 --- a/webui/src/list/ListQuery.js +++ b/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; |