Repository: codalab/codalab-worksheets (language: python)

Issue:
No error message when adding worksheet fails
Go to worksheets.codalab.org/users.  Click on 'Add' and type in a worksheet name that already exists.  The error message doesn't show up like it does when you click the + in the top bar.


Discussion / hints:

Fix patch:
diff --git a/frontend/src/CodalabApp.js b/frontend/src/CodalabApp.js
index 5c0e0295e..dc2a08f3b 100644
--- a/frontend/src/CodalabApp.js
+++ b/frontend/src/CodalabApp.js
@@ -50,7 +50,7 @@ function CodalabApp() {
                                     <HomePage
                                         {...props}
                                         auth={fakeAuth}
-                                        redirectAuthToDashboard={true}
+                                        redirectAuthToProfile={true}
                                     />
                                 )}
                             />
@@ -61,7 +61,7 @@ function CodalabApp() {
                                     <HomePage
                                         {...props}
                                         auth={fakeAuth}
-                                        redirectAuthToDashboard={false}
+                                        redirectAuthToProfile={false}
                                     />
                                 )}
                             />
diff --git a/frontend/src/components/Dashboard/EditableAvatar.js b/frontend/src/components/Dashboard/EditableAvatar.js
index 03acff5ca..35a05bd2b 100644
--- a/frontend/src/components/Dashboard/EditableAvatar.js
+++ b/frontend/src/components/Dashboard/EditableAvatar.js
@@ -14,6 +14,7 @@ import { getQueryParams } from '../worksheets/NewUpload/NewUpload.jsx';
 import $ from 'jquery';
 import { createAlertText, getDefaultBundleMetadata } from '../../util/worksheet_utils';
 import Avatar from '@material-ui/core/Avatar';
+import ButtonBase from '@material-ui/core/ButtonBase';
 
 const styles = (theme) => ({
     root: {
@@ -34,6 +35,57 @@ const styles = (theme) => ({
         marginTop: 8,
         marginBottom: 0,
     },
+    image: {
+        position: 'relative',
+        [theme.breakpoints.down('xs')]: {
+            width: '100% !important', // Overrides inline-style
+            height: 100,
+        },
+        '&:hover': {
+            zIndex: 1,
+            '& $imageBackdrop': {
+                opacity: 0.6,
+            },
+            '& $imageTitle': {
+                opacity: 0.8,
+            },
+        },
+    },
+    imageButton: {
+        position: 'absolute',
+        left: 0,
+        right: 0,
+        top: 0,
+        bottom: 0,
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        color: theme.palette.common.white,
+    },
+    imageSrc: {
+        position: 'absolute',
+        left: 0,
+        right: 0,
+        top: 0,
+        bottom: 0,
+        backgroundSize: 'cover',
+        backgroundPosition: 'center 40%',
+    },
+    imageBackdrop: {
+        position: 'absolute',
+        left: 0,
+        right: 0,
+        top: 0,
+        bottom: 0,
+        backgroundColor: theme.palette.common.black,
+        opacity: 0,
+        borderRadius: 30,
+        transition: theme.transitions.create('opacity'),
+    },
+    imageTitle: {
+        position: 'relative',
+        opacity: 0,
+    },
 });
 
 const DialogTitle = withStyles(styles)((props) => {
@@ -258,21 +310,34 @@ class EditableAvatar extends React.Component {
     };
     render() {
         const { classes } = this.props;
+        const avatar = this.state.avatar ? (
+            <Avatar className={classes.Avatar} src={this.state.avatar} />
+        ) : (
+            <Avatar className={classes.Avatar}> {this.props.userInfo.user_name.charAt(0)} </Avatar>
+        );
+
         return (
             <div className={classes.box}>
-                {this.state.avatar ? (
-                    <Avatar className={classes.Avatar} src={this.state.avatar} />
-                ) : (
-                    <Avatar className={classes.Avatar}>
-                        {' '}
-                        {this.props.userInfo.user_name.charAt(0)}{' '}
-                    </Avatar>
-                )}
                 {this.props.ownDashboard ? (
                     <div>
-                        <Button variant='outlined' color='primary' onClick={this.handleClickOpen}>
-                            Edit Avatar
-                        </Button>
+                        <ButtonBase
+                            focusRipple
+                            onClick={this.handleClickOpen}
+                            className={classes.image}
+                        >
+                            {avatar}
+                            <span className={classes.imageBackdrop} />
+                            <span className={classes.imageButton}>
+                                <Typography
+                                    component='span'
+                                    variant='subtitle1'
+                                    color='inherit'
+                                    className={classes.imageTitle}
+                                >
+                                    {'edit'}
+                                </Typography>
+                            </span>
+                        </ButtonBase>
                         <Dialog
                             onClose={this.handleClose}
                             aria-labelledby='customized-dialog-title'
@@ -313,7 +378,9 @@ class EditableAvatar extends React.Component {
                             </DialogActions>
                         </Dialog>
                     </div>
-                ) : null}
+                ) : (
+                    <div> {avatar} </div>
+                )}
             </div>
         );
     }
diff --git a/frontend/src/components/Dashboard/MainPanel.js b/frontend/src/components/Dashboard/MainPanel.js
index 5509fb84b..44509df77 100644
--- a/frontend/src/components/Dashboard/MainPanel.js
+++ b/frontend/src/components/Dashboard/MainPanel.js
@@ -16,12 +16,22 @@ import DialogContentText from '@material-ui/core/DialogContentText';
 import DialogActions from '@material-ui/core/DialogActions';
 import { format } from 'timeago.js';
 import { addUTCTimeZone } from '../../util/worksheet_utils';
+import Snackbar from '@material-ui/core/Snackbar';
+import SnackbarContent from '@material-ui/core/SnackbarContent';
+import classNames from 'classnames';
+import IconButton from '@material-ui/core/IconButton';
+import CloseIcon from '@material-ui/icons/Close';
+import ErrorIcon from '@material-ui/icons/Error';
+import SuccessIcon from '@material-ui/icons/CheckCircle';
+import InfoIcon from '@material-ui/icons/Info';
+import WarningIcon from '@material-ui/icons/Warning';
+
 /**
  * This route page displays the new Dashboard, which is the landing page for all the users.
  */
 const kDefaultWorksheetName = 'unnamed';
 
-const styles = ({ palette }) => {
+const styles = ({ palette, spacing, color }) => {
     return {
         wsBox: {
             marginLeft: 20,
@@ -85,6 +95,26 @@ const styles = ({ palette }) => {
             backgroundColor: green[500],
             color: 'white',
             right: 0,
+            marginBottom: 10,
+        },
+        snackbarMessage: {
+            display: 'flex',
+            alignItems: 'center',
+        },
+        snackbarIcon: {
+            marginRight: spacing.large,
+        },
+        snackbarError: {
+            backgroundColor: color.red.base,
+        },
+        snackbarWarning: {
+            backgroundColor: color.yellow.base,
+        },
+        snackbarInfo: {
+            backgroundColor: color.primary.base,
+        },
+        snackbarSuccess: {
+            backgroundColor: color.green.base,
         },
     };
 };
@@ -197,23 +227,31 @@ class MainPanel extends React.Component<{
     }
     /** Renderer. */
     render() {
+        let SnackbarIcon = {
+            error: ErrorIcon,
+            success: SuccessIcon,
+            info: InfoIcon,
+            warning: WarningIcon,
+        }[this.state.snackbarVariant];
         const { classes } = this.props;
         return (
             <div>
                 <Card elevation={0} style={{ height: '100%', backgroundColor: '#f1f1f1' }}>
                     <div className={classes.titleBox} display={'flex'} alignItems={'center'}>
-                        <h3 className={classes.heading}>Worksheets</h3>
+                        <h3 className={classes.heading}>Worksheets &nbsp;&nbsp;</h3>
                         &nbsp;&nbsp;&nbsp;&nbsp;
-                        <Tooltip title='New Worksheet'>
-                            <Button
-                                variant='contained'
-                                className={classes.button}
-                                startIcon={<NewWorksheetIcon />}
-                                onClick={() => this.setState({ newWorksheetShowDialog: true })}
-                            >
-                                ADD
-                            </Button>
-                        </Tooltip>
+                        {this.props.ownDashboard ? (
+                            <Tooltip title='New Worksheet'>
+                                <Button
+                                    variant='contained'
+                                    className={classes.button}
+                                    startIcon={<NewWorksheetIcon />}
+                                    onClick={() => this.setState({ newWorksheetShowDialog: true })}
+                                >
+                                    ADD
+                                </Button>
+                            </Tooltip>
+                        ) : null}
                     </div>
 
                     {this.state.worksheets}
@@ -260,6 +298,43 @@ class MainPanel extends React.Component<{
                         </Button>
                     </DialogActions>
                 </Dialog>
+                <Snackbar
+                    anchorOrigin={{
+                        vertical: 'bottom',
+                        horizontal: 'left',
+                    }}
+                    open={this.state.snackbarShow}
+                    autoHideDuration={5000}
+                    onClose={(e, reason) => {
+                        if (reason !== 'clickaway') this.setState({ snackbarShow: false });
+                    }}
+                >
+                    <SnackbarContent
+                        className={classNames({
+                            [classes.snackbarError]: this.state.snackbarVariant === 'error',
+                            [classes.snackbarWarning]: this.state.snackbarVariant === 'warning',
+                            [classes.snackbarInfo]: this.state.snackbarVariant === 'info',
+                            [classes.snackbarSuccess]: this.state.snackbarVariant === 'success',
+                        })}
+                        message={
+                            <span className={classes.snackbarMessage}>
+                                {SnackbarIcon && <SnackbarIcon className={classes.snackbarIcon} />}
+                                {this.state.snackbarMessage}
+                            </span>
+                        }
+                        action={[
+                            <IconButton
+                                key='close'
+                                aria-label='Close'
+                                color='inherit'
+                                className={classes.close}
+                                onClick={() => this.setState({ snackbarShow: false })}
+                            >
+                                <CloseIcon />
+                            </IconButton>,
+                        ]}
+                    />
+                </Snackbar>
             </div>
         );
     }
diff --git a/frontend/src/components/Dashboard/NewDashboard.js b/frontend/src/components/Dashboard/NewDashboard.js
index d41cc01ce..e2804da3e 100644
--- a/frontend/src/components/Dashboard/NewDashboard.js
+++ b/frontend/src/components/Dashboard/NewDashboard.js
@@ -88,7 +88,10 @@ class NewDashboard extends React.Component<{
                             ></SideBar>
                         </Grid>
                         <Grid item xs>
-                            <MainPanel userInfo={this.state.userInfo}></MainPanel>
+                            <MainPanel
+                                userInfo={this.state.userInfo}
+                                ownDashboard={this.state.ownDashboard}
+                            ></MainPanel>
                         </Grid>
                     </Grid>
                 </div>
diff --git a/frontend/src/components/Dashboard/SideBar.js b/frontend/src/components/Dashboard/SideBar.js
index 77e60749c..c4872f28e 100644
--- a/frontend/src/components/Dashboard/SideBar.js
+++ b/frontend/src/components/Dashboard/SideBar.js
@@ -9,6 +9,8 @@ import { lighten } from '@material-ui/core/es/styles/colorManipulator';
 import { renderSize, renderDuration } from '../../util/worksheet_utils';
 import { BUNDLE_STATES } from '../../constants';
 import { default as AvatarEditorModal } from './EditableAvatar';
+import Tooltip from '@material-ui/core/Tooltip';
+import Button from '@material-ui/core/Button';
 
 const styles = ({ palette }) => {
     return {
@@ -58,13 +60,20 @@ const styles = ({ palette }) => {
             fontSize: 14,
             color: palette.grey[500],
         },
-        name: {
+        fullname: {
             fontFamily: 'Roboto',
             fontStyle: 'normal',
             fontWeight: 500,
             fontSize: 20,
             letterSpacing: 0.15,
         },
+        username: {
+            fontFamily: 'Roboto',
+            fontStyle: 'normal',
+            fontWeight: 400,
+            fontSize: 18,
+            letterSpacing: 0.15,
+        },
         progress: {
             minWidth: '85%',
             maxWidth: '85%',
@@ -164,7 +173,10 @@ class SideBar extends React.Component {
                 ></AvatarEditorModal>
                 {/*<Avatar className={classes.avatar}>{userInfo.user_name.charAt(0)}</Avatar>*/}
                 <div className={classes.box}>
-                    <h3 className={classes.name}>{userInfo.user_name}</h3>
+                    <h3 className={classes.fullname}>
+                        {userInfo.first_name + ' ' + userInfo.last_name}
+                    </h3>
+                    <h4 className={classes.username}>{userInfo.user_name}</h4>
                     {userInfo.affiliation ? (
                         <p className={classes.affiliation}>Affiliation: {userInfo.affiliation}</p>
                     ) : null}
@@ -223,6 +235,17 @@ class SideBar extends React.Component {
                     </div>
                 </div>
                 <Divider />
+                {this.props.ownDashboard ? (
+                    <Tooltip title='Dashboard'>
+                        <Button
+                            variant='contained'
+                            color='primary'
+                            onClick={() => (window.location.href = '/worksheets?name=dashboard')}
+                        >
+                            View Dashboard
+                        </Button>
+                    </Tooltip>
+                ) : null}
                 <div className={classes.placeholderBox}></div>
             </Card>
         );
diff --git a/frontend/src/components/NavBar.js b/frontend/src/components/NavBar.js
index c1a10d09c..5f18eb0fd 100644
--- a/frontend/src/components/NavBar.js
+++ b/frontend/src/components/NavBar.js
@@ -59,6 +59,7 @@ class NavBar extends React.Component<{
             value: '',
             isLoading: false,
             results: [],
+            avatar: '',
         };
     }
 
@@ -76,6 +77,7 @@ class NavBar extends React.Component<{
             success: (data) => {
                 const userInfo = data.data.attributes;
                 userInfo.user_id = data.data.id;
+                this.fetchImg(userInfo.avatar_id);
                 this.setState({ userInfo: userInfo, newWorksheetName: `${userInfo.user_name}-` });
             },
             error: (xhr, status, err) => {
@@ -141,6 +143,41 @@ class NavBar extends React.Component<{
         });
     }
 
+    // Fetch the image file represented by the bundle
+    fetchImg(bundleUuid) {
+        if (bundleUuid == null) return;
+        // Set defaults
+        let url = '/rest/bundles/' + bundleUuid + '/contents/blob/';
+
+        fetch(url)
+            .then(function(response) {
+                if (response.ok) {
+                    return response.arrayBuffer();
+                }
+                throw new Error('Network response was not ok.');
+            })
+            .then(function(data) {
+                let dataUrl =
+                    'data:image/png;base64,' +
+                    btoa(
+                        new Uint8Array(data).reduce(
+                            (data, byte) => data + String.fromCharCode(byte),
+                            '',
+                        ),
+                    );
+                return dataUrl;
+            })
+            .then((dataUrl) => {
+                // Update avatar shown on the page
+                this.setState({
+                    avatar: dataUrl,
+                });
+            })
+            .catch(function(error) {
+                console.log(url, error.responseText);
+            });
+    }
+
     handleResultSelect = (e, { result }) => {
         this.setState({ value: result.plaintextTitle || result.plaintextDescription });
         window.open('/worksheets/' + result.uuid, '_self');
@@ -368,8 +405,8 @@ class NavBar extends React.Component<{
                         )}
                         {this.props.auth.isAuthenticated && (
                             <React.Fragment>
-                                <Link to='/worksheets?name=dashboard'>
-                                    <Button color='primary'>Dashboard</Button>
+                                <Link to='/users'>
+                                    <Button color='primary'>My Profile</Button>
                                 </Link>
                                 <Tooltip title='New Worksheet'>
                                     <IconButton
@@ -413,7 +450,17 @@ class NavBar extends React.Component<{
                                             this.setState({ accountEl: e.currentTarget })
                                         }
                                     >
-                                        <AccountIcon />
+                                        {this.state.avatar ? (
+                                            <div>
+                                                <img
+                                                    src={this.state.avatar}
+                                                    className={classes.avatar}
+                                                    alt='CodaLab'
+                                                />
+                                            </div>
+                                        ) : (
+                                            <AccountIcon />
+                                        )}
                                     </IconButton>
                                 </Tooltip>
                                 <Menu
@@ -555,6 +602,11 @@ const styles = (theme) => ({
     logo: {
         maxHeight: 40,
     },
+    avatar: {
+        maxHeight: 30,
+        maxWidth: 30,
+        borderRadius: 15,
+    },
     snackbarMessage: {
         display: 'flex',
         alignItems: 'center',
diff --git a/frontend/src/routes/HomePage.js b/frontend/src/routes/HomePage.js
index 6b8d3611b..ee78e1014 100644
--- a/frontend/src/routes/HomePage.js
+++ b/frontend/src/routes/HomePage.js
@@ -26,10 +26,9 @@ class HomePage extends React.Component<{
 }> {
     constructor(props) {
         super(props);
-        const { auth, redirectAuthToDashboard } = this.props;
-        if (auth.isAuthenticated && redirectAuthToDashboard) {
-            this.props.history.push('/worksheets?name=dashboard');
-            // this.props.history.push('/dashboard');
+        const { auth, redirectAuthToProfile } = this.props;
+        if (auth.isAuthenticated && redirectAuthToProfile) {
+            this.props.history.push('/users');
         }
     }
 
@@ -95,10 +94,7 @@ class HomePage extends React.Component<{
                                         </React.Fragment>
                                     )}
                                     {auth.isAuthenticated &&
-                                        this.renderButton(
-                                            'Dashboard',
-                                            '/worksheets?name=dashboard',
-                                        )}
+                                        this.renderButton('My Profile', '/users')}
                                 </div>
                             </Grid>
                             <Grid item xs={12} sm={6} container>


Test patch:
diff --git a/tests/ui/README.md b/tests/ui/README.md
index 92e4178ff..fb05c6267 100644
--- a/tests/ui/README.md
+++ b/tests/ui/README.md
@@ -15,8 +15,8 @@ repository, by running `pip install -r requirements.txt`.
 tests and their respective browsers.
     1. Run `sudo ./scripts/test-setup.sh` and place the installed GeckoDriver on your path by running 
     `export PATH=$PATH:/usr/bin/geckodriver`. This is not needed for ChromeDriver.
-    2. An alternative is to simply use [Homebrew](https://brew.sh/) and run `brew install geckodriver && brew cask 
-    install chromedriver`. 
+    2. An alternative is to simply use [Homebrew](https://brew.sh/) and run `brew install geckodriver && brew install 
+    --cask chromedriver`. 
 
 #### Running the tests locally
 
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet1.png b/tests/ui/baselines/worksheet/chrome/worksheet1.png
index 39d653747..fe70041dd 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet1.png and b/tests/ui/baselines/worksheet/chrome/worksheet1.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet10.png b/tests/ui/baselines/worksheet/chrome/worksheet10.png
index ae23ac2f4..44ef7d911 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet10.png and b/tests/ui/baselines/worksheet/chrome/worksheet10.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet2.png b/tests/ui/baselines/worksheet/chrome/worksheet2.png
index f966d5dab..3e2e829ba 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet2.png and b/tests/ui/baselines/worksheet/chrome/worksheet2.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet3.png b/tests/ui/baselines/worksheet/chrome/worksheet3.png
index 3add97839..43f690a27 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet3.png and b/tests/ui/baselines/worksheet/chrome/worksheet3.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet4.png b/tests/ui/baselines/worksheet/chrome/worksheet4.png
index 23a4f0bca..22c3d0b28 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet4.png and b/tests/ui/baselines/worksheet/chrome/worksheet4.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet5.png b/tests/ui/baselines/worksheet/chrome/worksheet5.png
index 850f1c379..7d3f29a92 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet5.png and b/tests/ui/baselines/worksheet/chrome/worksheet5.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet6.png b/tests/ui/baselines/worksheet/chrome/worksheet6.png
index 54d7eabc0..36058a1d1 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet6.png and b/tests/ui/baselines/worksheet/chrome/worksheet6.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet7.png b/tests/ui/baselines/worksheet/chrome/worksheet7.png
index 77137ceef..ca3d70da7 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet7.png and b/tests/ui/baselines/worksheet/chrome/worksheet7.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet8.png b/tests/ui/baselines/worksheet/chrome/worksheet8.png
index 238dba636..ff4a52baa 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet8.png and b/tests/ui/baselines/worksheet/chrome/worksheet8.png differ
diff --git a/tests/ui/baselines/worksheet/chrome/worksheet9.png b/tests/ui/baselines/worksheet/chrome/worksheet9.png
index 32925c434..cde37b5e7 100644
Binary files a/tests/ui/baselines/worksheet/chrome/worksheet9.png and b/tests/ui/baselines/worksheet/chrome/worksheet9.png differ
diff --git a/tests/ui/ui_tester.py b/tests/ui/ui_tester.py
index 7f3b72b65..2eb668df5 100644
--- a/tests/ui/ui_tester.py
+++ b/tests/ui/ui_tester.py
@@ -51,7 +51,7 @@ def add_headless(browser_options):
         self.browser.close()
 
     def login(self, username='codalab', password='codalab'):
-        self.browser.get(self.get_url('/home'))
+        self.browser.get(self.get_url('home'))
         self.click(By.LINK_TEXT, 'LOGIN')
         self.fill_field(By.ID, 'id_login', username)
         self.fill_field(By.ID, 'id_password', password, press_enter=True)
@@ -315,6 +315,8 @@ def __init__(self):
 
     def test(self):
         self.login()
+        self.longer_pause()
+        self.browser.get(self.get_url('worksheets?name=dashboard'))
         self.wait_until_worksheet_content_loads()
         # wait for small worksheet to be resolved from place holder item
         by = By.XPATH