diff options
Diffstat (limited to 'webui/src')
-rw-r--r-- | webui/src/App.css | 28 | ||||
-rw-r--r-- | webui/src/App.js | 53 | ||||
-rw-r--r-- | webui/src/App.test.js | 9 | ||||
-rw-r--r-- | webui/src/Bug.js | 38 | ||||
-rw-r--r-- | webui/src/BugPage.js | 29 | ||||
-rw-r--r-- | webui/src/BugSummary.js | 53 | ||||
-rw-r--r-- | webui/src/Comment.js | 44 | ||||
-rw-r--r-- | webui/src/ListPage.js | 46 | ||||
-rw-r--r-- | webui/src/index.css | 5 | ||||
-rw-r--r-- | webui/src/index.js | 31 | ||||
-rw-r--r-- | webui/src/registerServiceWorker.js | 117 |
11 files changed, 271 insertions, 182 deletions
diff --git a/webui/src/App.css b/webui/src/App.css deleted file mode 100644 index c5c6e8a6..00000000 --- a/webui/src/App.css +++ /dev/null @@ -1,28 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - animation: App-logo-spin infinite 20s linear; - height: 80px; -} - -.App-header { - background-color: #222; - height: 150px; - padding: 20px; - color: white; -} - -.App-title { - font-size: 1.5em; -} - -.App-intro { - font-size: large; -} - -@keyframes App-logo-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} diff --git a/webui/src/App.js b/webui/src/App.js index 95bd38af..1d6382e5 100644 --- a/webui/src/App.js +++ b/webui/src/App.js @@ -1,19 +1,40 @@ -import React, { Component } from 'react' -import './App.css' +import React from "react"; +import { withRouter, Switch, Route } from "react-router"; +import { Link } from "react-router-dom"; +import { withStyles } from "@material-ui/core/styles"; -class App extends Component { - render() { - return ( - <div className="App"> - <header className="App-header"> - <h1 className="App-title">Git bug</h1> - </header> - <p className="App-intro"> - Here will appear a Web UI ! - </p> - </div> - ); +import AppBar from "@material-ui/core/AppBar"; +import CssBaseline from "@material-ui/core/CssBaseline"; +import Toolbar from "@material-ui/core/Toolbar"; +import Typography from "@material-ui/core/Typography"; + +import BugPage from "./BugPage"; +import ListPage from "./ListPage"; + +const styles = theme => ({ + appTitle: { + 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(1) + </Typography> + </Link> + </Toolbar> + </AppBar> + <Switch> + <Route path="/" exact component={ListPage} /> + <Route path="/bug/:id" exact component={BugPage} /> + </Switch> + </React.Fragment> +); -export default App; +export default withStyles(styles)(withRouter(App)); diff --git a/webui/src/App.test.js b/webui/src/App.test.js deleted file mode 100644 index a754b201..00000000 --- a/webui/src/App.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(<App />, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/webui/src/Bug.js b/webui/src/Bug.js new file mode 100644 index 00000000..28558a13 --- /dev/null +++ b/webui/src/Bug.js @@ -0,0 +1,38 @@ +import React from "react"; +import gql from "graphql-tag"; +import { withStyles } from "@material-ui/core/styles"; + +import Comment from "./Comment"; +import BugSummary from "./BugSummary"; + +const styles = theme => ({ + main: { + maxWidth: 600, + margin: "auto", + marginTop: theme.spacing.unit * 4 + } +}); + +const Bug = ({ bug, classes }) => ( + <main className={classes.main}> + <BugSummary bug={bug} /> + + {bug.comments.map((comment, index) => ( + <Comment key={index} comment={comment} /> + ))} + </main> +); + +Bug.fragment = gql` + fragment Bug on Bug { + ...BugSummary + comments { + ...Comment + } + } + + ${BugSummary.fragment} + ${Comment.fragment} +`; + +export default withStyles(styles)(Bug); diff --git a/webui/src/BugPage.js b/webui/src/BugPage.js new file mode 100644 index 00000000..ec0872eb --- /dev/null +++ b/webui/src/BugPage.js @@ -0,0 +1,29 @@ +import React from "react"; +import { Query } from "react-apollo"; +import gql from "graphql-tag"; + +import CircularProgress from "@material-ui/core/CircularProgress"; + +import Bug from "./Bug"; + +const QUERY = gql` + query GetBug($id: BugID!) { + bug(id: $id) { + ...Bug + } + } + + ${Bug.fragment} +`; + +const BugPage = ({ match }) => ( + <Query query={QUERY} variables={{ id: match.params.id }}> + {({ loading, error, data }) => { + if (loading) return <CircularProgress />; + if (error) return <p>Error.</p>; + return <Bug bug={data.bug} />; + }} + </Query> +); + +export default BugPage; diff --git a/webui/src/BugSummary.js b/webui/src/BugSummary.js new file mode 100644 index 00000000..469ab9a8 --- /dev/null +++ b/webui/src/BugSummary.js @@ -0,0 +1,53 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import gql from "graphql-tag"; +import { withStyles } from "@material-ui/core/styles"; + +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import Chip from "@material-ui/core/Chip"; +import Typography from "@material-ui/core/Typography"; + +const styles = theme => ({ + labelList: { + display: "flex", + flexWrap: "wrap", + marginTop: theme.spacing.unit + }, + label: { + marginRight: theme.spacing.unit + }, + summary: { + marginBottom: theme.spacing.unit * 2 + } +}); + +const BugSummary = ({ bug, classes }) => ( + <Card className={classes.summary}> + <CardContent> + <Typography variant="headline" component="h2"> + {bug.title} + </Typography> + <Typography variant="subheading" component="h3" title={bug.id}> + <Link to={"/bug/" + bug.id.slice(0, 8)}>#{bug.id.slice(0, 8)}</Link> •{" "} + {bug.status.toUpperCase()} + </Typography> + <div className={classes.labelList}> + {bug.labels.map(label => ( + <Chip key={label} label={label} className={classes.label} /> + ))} + </div> + </CardContent> + </Card> +); + +BugSummary.fragment = gql` + fragment BugSummary on Bug { + id + title + status + labels + } +`; + +export default withStyles(styles)(BugSummary); diff --git a/webui/src/Comment.js b/webui/src/Comment.js new file mode 100644 index 00000000..a4fd1b40 --- /dev/null +++ b/webui/src/Comment.js @@ -0,0 +1,44 @@ +import React from "react"; +import gql from "graphql-tag"; +import { withStyles } from "@material-ui/core/styles"; + +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 Typography from "@material-ui/core/Typography"; + +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); diff --git a/webui/src/ListPage.js b/webui/src/ListPage.js new file mode 100644 index 00000000..c873eefa --- /dev/null +++ b/webui/src/ListPage.js @@ -0,0 +1,46 @@ +import React from "react"; +import { Query } from "react-apollo"; +import gql from "graphql-tag"; +import { withStyles } from "@material-ui/core/styles"; + +import CircularProgress from "@material-ui/core/CircularProgress"; + +import BugSummary from "./BugSummary"; + +const QUERY = gql` + { + bugs: allBugs { + ...BugSummary + } + } + + ${BugSummary.fragment} +`; + +const styles = theme => ({ + main: { + maxWidth: 600, + margin: "auto", + marginTop: theme.spacing.unit * 4 + } +}); + +const List = withStyles(styles)(({ bugs, classes }) => ( + <main className={classes.main}> + {bugs.map(bug => ( + <BugSummary bug={bug} key={bug.id} /> + ))} + </main> +)); + +const ListPage = () => ( + <Query query={QUERY}> + {({ loading, error, data }) => { + if (loading) return <CircularProgress />; + if (error) return <p>Error.</p>; + return <List bugs={data.bugs} />; + }} + </Query> +); + +export default ListPage; diff --git a/webui/src/index.css b/webui/src/index.css deleted file mode 100644 index b4cc7250..00000000 --- a/webui/src/index.css +++ /dev/null @@ -1,5 +0,0 @@ -body { - margin: 0; - padding: 0; - font-family: sans-serif; -} diff --git a/webui/src/index.js b/webui/src/index.js index 296b3911..8c4137fe 100644 --- a/webui/src/index.js +++ b/webui/src/index.js @@ -1,8 +1,25 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; -import registerServiceWorker from './registerServiceWorker'; +import React from "react"; +import ReactDOM from "react-dom"; +import { BrowserRouter } from "react-router-dom"; +import ApolloClient from "apollo-boost"; +import { ApolloProvider } from "react-apollo"; +import CssBaseline from "@material-ui/core/CssBaseline"; -ReactDOM.render(<App />, document.getElementById('root')); -// registerServiceWorker(); +import App from "./App"; + +const client = new ApolloClient({ + uri: "/graphql", + connectToDevTools: true +}); + +ReactDOM.render( + <ApolloProvider client={client}> + <BrowserRouter> + <React.Fragment> + <App /> + <CssBaseline /> + </React.Fragment> + </BrowserRouter> + </ApolloProvider>, + document.getElementById("root") +); diff --git a/webui/src/registerServiceWorker.js b/webui/src/registerServiceWorker.js deleted file mode 100644 index a3e6c0cf..00000000 --- a/webui/src/registerServiceWorker.js +++ /dev/null @@ -1,117 +0,0 @@ -// In production, we register a service worker to serve assets from local cache. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on the "N+1" visit to a page, since previously -// cached resources are updated in the background. - -// To learn more about the benefits of this model, read https://goo.gl/KwvDNy. -// This link also includes instructions on opting out of this behavior. - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.1/8 is considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) -); - -export default function register() { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Lets check if a service worker still exists or not. - checkValidServiceWorker(swUrl); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://goo.gl/SC7cgQ' - ); - }); - } else { - // Is not local host. Just register service worker - registerValidSW(swUrl); - } - }); - } -} - -function registerValidSW(swUrl) { - navigator.serviceWorker - .register(swUrl) - .then(registration => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the old content will have been purged and - // the fresh content will have been added to the cache. - // It's the perfect time to display a "New content is - // available; please refresh." message in your web app. - console.log('New content is available; please refresh.'); - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); - } - } - }; - }; - }) - .catch(error => { - console.error('Error during service worker registration:', error); - }); -} - -function checkValidServiceWorker(swUrl) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl) - .then(response => { - // Ensure service worker exists, and that we really are getting a JS file. - if ( - response.status === 404 || - response.headers.get('content-type').indexOf('javascript') === -1 - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl); - } - }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); -} - -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); - } -} |