webui: Migrate to Material-UI's new style API

Quentin Gliech created

Change summary

webui/src/App.js             | 48 +++++++++++++-------------
webui/src/Label.js           | 21 ++++++-----
webui/src/bug/Bug.js         | 65 +++++++++++++++++++-----------------
webui/src/bug/LabelChange.js | 13 +++---
webui/src/bug/Message.js     | 43 +++++++++++++-----------
webui/src/bug/SetStatus.js   | 13 +++---
webui/src/bug/SetTitle.js    | 13 +++---
webui/src/bug/Timeline.js    | 60 ++++++++++++++-------------------
webui/src/index.js           | 19 +++++++--
webui/src/list/BugRow.js     | 67 +++++++++++++++++++------------------
10 files changed, 189 insertions(+), 173 deletions(-)

Detailed changes

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>
+    </>
+  );
+}

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;

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;

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;

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;

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;

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;

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;

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')

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;