webui: rework of the bug page with a timeline

Michael Muré created

Change summary

webui/src/App.js               |  8 +-
webui/src/bug/Bug.js           | 78 +++++++++++++++++++++++++++++------
webui/src/bug/BugQuery.js      |  6 +-
webui/src/bug/Comment.js       | 43 -------------------
webui/src/bug/Message.js       | 75 ++++++++++++++++++++++++++++++++++
webui/src/bug/Timeline.js      | 43 +++++++++++++++++++
webui/src/bug/TimelineQuery.js | 39 ++++++++++++++++++
webui/src/list/ListQuery.js    |  7 +-
8 files changed, 231 insertions(+), 68 deletions(-)

Detailed changes

webui/src/App.js 🔗

@@ -7,8 +7,8 @@ import React from 'react'
 import { Route, Switch, withRouter } from 'react-router'
 import { Link } from 'react-router-dom'
 
-import BugPage from './bug/BugPage'
-import ListPage from './list/ListPage'
+import BugQuery from './bug/BugQuery'
+import ListQuery from './list/ListQuery'
 
 const styles = theme => ({
   appTitle: {
@@ -30,8 +30,8 @@ const App = ({location, classes}) => (
       </Toolbar>
     </AppBar>
     <Switch>
-      <Route path="/" exact component={ListPage}/>
-      <Route path="/bug/:id" exact component={BugPage}/>
+      <Route path="/" exact component={ListQuery}/>
+      <Route path="/bug/:id" exact component={BugQuery}/>
     </Switch>
   </React.Fragment>
 )

webui/src/bug/Bug.js 🔗

@@ -1,39 +1,89 @@
 import { withStyles } from '@material-ui/core/styles'
+import Tooltip from '@material-ui/core/Tooltip/Tooltip'
+import Typography from '@material-ui/core/Typography/Typography'
 import gql from 'graphql-tag'
+import * as moment from 'moment'
 import React from 'react'
-
-import Comment from './Comment'
+import TimelineQuery from './TimelineQuery'
 
 const styles = theme => ({
   main: {
     maxWidth: 600,
     margin: 'auto',
     marginTop: theme.spacing.unit * 4
+  },
+  header: {},
+  title: {
+    ...theme.typography.headline
+  },
+  id: {
+    ...theme.typography.subheading,
+    marginLeft: 15,
+  },
+  container: {
+    display: 'flex'
+  },
+  timeline: {
+    width: '70%',
+    marginTop: 20,
+    marginRight: 20,
+  },
+  sidebar: {
+    width: '30%'
+  },
+  label: {
+    backgroundColor: '#da9898',
+    borderRadius: '3px',
+    paddingLeft: '10px',
+    margin: '2px 20px auto 2px',
+    fontWeight: 'bold',
   }
 })
 
 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>
+
+      <Typography color={'textSecondary'}>
+        <Tooltip title={bug.author.email}><span>{bug.author.name}</span></Tooltip>
+        <span> opened this bug </span>
+        <Tooltip title={moment(bug.createdAt).format('MMMM D, YYYY, h:mm a')}>
+          <span> {moment(bug.createdAt).fromNow()} </span>
+        </Tooltip>
+      </Typography>
+    </div>
 
-    {bug.comments.edges.map(({cursor, node}) => (
-      <Comment key={cursor} comment={node}/>
-    ))}
+    <div className={classes.container}>
+      <div className={classes.timeline}>
+        <TimelineQuery id={bug.id}/>
+      </div>
+      <div className={classes.sidebar}>
+        <Typography variant={'subheading'}>Labels</Typography>
+        {bug.labels.map(l => (
+          <Typography key={l} className={classes.label}>
+            {l}
+          </Typography>
+        ))}
+      </div>
+    </div>
   </main>
 )
 
 Bug.fragment = gql`
   fragment Bug on Bug {
-    comments(first: 10) {
-      edges {
-        cursor
-        node {
-          ...Comment
-        }
-      }
+    id
+    humanId
+    status
+    title
+    labels
+    createdAt
+    author {
+      email
+      name
     }
   }
-  
-  ${Comment.fragment}
 `
 
 export default withStyles(styles)(Bug)

webui/src/bug/BugPage.js → webui/src/bug/BugQuery.js 🔗

@@ -17,14 +17,14 @@ const QUERY = gql`
   ${Bug.fragment}
 `
 
-const BugPage = ({match}) => (
+const BugQuery = ({match}) => (
   <Query query={QUERY} variables={{id: match.params.id}}>
     {({loading, error, data}) => {
       if (loading) return <CircularProgress/>
-      if (error) return <p>Error.</p>
+      if (error) return <p>Error: {error}</p>
       return <Bug bug={data.defaultRepository.bug}/>
     }}
   </Query>
 )
 
-export default BugPage
+export default BugQuery

webui/src/bug/Comment.js 🔗

@@ -1,43 +0,0 @@
-import Avatar from '@material-ui/core/Avatar'
-import Card from '@material-ui/core/Card'
-import CardContent from '@material-ui/core/CardContent'
-import CardHeader from '@material-ui/core/CardHeader'
-import { withStyles } from '@material-ui/core/styles'
-import Typography from '@material-ui/core/Typography'
-import gql from 'graphql-tag'
-import React from 'react'
-
-const styles = theme => ({
-  comment: {
-    marginBottom: theme.spacing.unit
-  }
-})
-
-const Comment = withStyles(styles)(({comment, classes}) => (
-  <Card className={classes.comment}>
-    <CardHeader
-      avatar={
-        <Avatar aria-label={comment.author.name}>
-          {comment.author.name[0].toUpperCase()}
-        </Avatar>
-      }
-      title={comment.author.name}
-      subheader={comment.author.email}
-    />
-    <CardContent>
-      <Typography component="p">{comment.message}</Typography>
-    </CardContent>
-  </Card>
-))
-
-Comment.fragment = gql`
-  fragment Comment on Comment {
-    message
-    author {
-      name
-      email
-    }
-  }
-`
-
-export default withStyles(styles)(Comment)

webui/src/bug/Message.js 🔗

@@ -0,0 +1,75 @@
+import { withStyles } from '@material-ui/core/styles'
+import Tooltip from '@material-ui/core/Tooltip/Tooltip'
+import Typography from '@material-ui/core/Typography'
+import gql from 'graphql-tag'
+import * as moment from 'moment'
+import React from 'react'
+
+const styles = theme => ({
+  header: {
+    ...theme.typography.body2,
+    padding: '3px 3px 3px 6px',
+    backgroundColor: '#f1f8ff',
+    border: '1px solid #d1d5da',
+    borderTopLeftRadius: 3,
+    borderTopRightRadius: 3,
+  },
+  author: {
+    ...theme.typography.body2,
+    fontWeight: 'bold'
+  },
+  message: {
+    borderLeft: '1px solid #d1d5da',
+    borderRight: '1px solid #d1d5da',
+    borderBottom: '1px solid #d1d5da',
+    borderBottomLeftRadius: 3,
+    borderBottomRightRadius: 3,
+    backgroundColor: '#fff',
+    minHeight: 50
+  }
+})
+
+const Message = ({message, classes}) => (
+  <div>
+    <div className={classes.header}>
+      <Tooltip title={message.author.email}>
+        <span className={classes.author}>{message.author.name}</span>
+      </Tooltip>
+      <span> commented </span>
+      <Tooltip title={moment(message.date).format('MMMM D, YYYY, h:mm a')}>
+        <span> {moment(message.date).fromNow()} </span>
+      </Tooltip>
+    </div>
+    <div className={classes.message}>
+      <Typography>{message.message}</Typography>
+    </div>
+  </div>
+)
+
+Message.createFragment = gql`
+  fragment Create on Operation {
+    ... on CreateOperation {
+      date
+      author {
+        name
+        email
+      }
+      message
+    }
+  }
+`
+
+Message.commentFragment = gql`
+  fragment Comment on Operation {
+    ... on AddCommentOperation {
+      date
+      author {
+        name
+        email
+      }
+      message
+    }
+  }
+`
+
+export default withStyles(styles)(Message)

webui/src/bug/Timeline.js 🔗

@@ -0,0 +1,43 @@
+import { withStyles } from '@material-ui/core/styles'
+import React from 'react'
+import Message from './Message'
+
+const styles = theme => ({
+  main: {
+    '& > *:not(:last-child)': {
+      marginBottom: 10
+    }
+  }
+})
+
+class Timeline extends React.Component {
+
+  props: {
+    ops: Array,
+    fetchMore: (any) => any,
+    classes: any,
+  }
+
+  render() {
+    const {ops, classes} = this.props
+
+    return (
+      <div className={classes.main}>
+        { ops.map((op, index) => {
+          switch (op.__typename) {
+            case 'CreateOperation':
+              return <Message key={index} message={op}/>
+            case 'AddCommentOperation':
+              return <Message key={index} message={op}/>
+
+            default:
+              console.log('unsupported operation type ' + op.__typename)
+              return null
+          }
+        })}
+      </div>
+    )
+  }
+}
+
+export default withStyles(styles)(Timeline)

webui/src/bug/TimelineQuery.js 🔗

@@ -0,0 +1,39 @@
+import CircularProgress from '@material-ui/core/CircularProgress'
+import gql from 'graphql-tag'
+import React from 'react'
+import { Query } from 'react-apollo'
+import Timeline from './Timeline'
+import Message from './Message'
+
+const QUERY = gql`
+  query($id: String!, $first: Int = 10, $after: String) {
+    defaultRepository {
+      bug(prefix: $id) {
+        operations(first: $first, after: $after) {
+          nodes {
+            ...Create
+            ...Comment
+          }
+          pageInfo {
+            hasNextPage
+            endCursor
+          }
+        }
+      }
+    }
+  }
+  ${Message.createFragment}
+  ${Message.commentFragment}
+`
+
+const TimelineQuery = ({id}) => (
+  <Query query={QUERY} variables={{id}}>
+    {({loading, error, data, fetchMore}) => {
+      if (loading) return <CircularProgress/>
+      if (error) return <p>Error: {error}</p>
+      return <Timeline ops={data.defaultRepository.bug.operations.nodes} fetchMore={fetchMore}/>
+    }}
+  </Query>
+)
+
+export default TimelineQuery

webui/src/list/ListPage.js → webui/src/list/ListQuery.js 🔗

@@ -3,7 +3,6 @@ import CircularProgress from '@material-ui/core/CircularProgress'
 import gql from 'graphql-tag'
 import React from 'react'
 import { Query } from 'react-apollo'
-
 import BugRow from './BugRow'
 import List from './List'
 
@@ -32,14 +31,14 @@ const QUERY = gql`
   ${BugRow.fragment}
 `
 
-const ListPage = () => (
+const ListQuery = () => (
   <Query query={QUERY}>
     {({loading, error, data, fetchMore}) => {
       if (loading) return <CircularProgress/>
-      if (error) return <p>Error.</p>
+      if (error) return <p>Error: {error}</p>
       return <List bugs={data.defaultRepository.bugs} fetchMore={fetchMore}/>
     }}
   </Query>
 )
 
-export default ListPage
+export default ListQuery