Merge master branch
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 13 Jun 2018 06:02:51 +0000 (08:02 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 13 Jun 2018 06:02:51 +0000 (08:02 +0200)
Feature #13590

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

src/components/breadcrumbs/breadcrumbs.tsx [new file with mode: 0644]
src/components/main-app-bar/dropdown-menu/dropdown-menu.test.tsx [new file with mode: 0644]
src/components/main-app-bar/dropdown-menu/dropdown-menu.tsx [new file with mode: 0644]
src/components/main-app-bar/main-app-bar.tsx [new file with mode: 0644]
src/components/main-app-bar/search-bar/search-bar.test.tsx [new file with mode: 0644]
src/components/main-app-bar/search-bar/search-bar.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx

diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx
new file mode 100644 (file)
index 0000000..6c2d1fb
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Button, Grid, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import { withStyles } from '@material-ui/core';
+
+export interface Breadcrumb {
+    label: string
+}
+
+interface BreadcrumbsDataProps {
+    items: Breadcrumb[]
+}
+
+interface BreadcrumbsActionProps {
+    onClick: (breadcrumb: Breadcrumb) => any
+}
+
+type BreadcrumbsProps = BreadcrumbsDataProps & BreadcrumbsActionProps & WithStyles<CssRules>;
+
+class Breadcrumbs extends React.Component<BreadcrumbsProps> {
+
+    render() {
+        const { classes, onClick } = this.props;
+        return <Grid container alignItems="center">
+            {
+                this.getInactiveItems().map((item, index) => (
+                    <React.Fragment key={index}>
+                        <Button
+                            color="inherit"
+                            className={classes.inactiveItem}
+                            onClick={() => onClick(item)}
+                        >
+                            {item.label}
+                        </Button>
+                        <ChevronRightIcon color="inherit" className={classes.inactiveItem} />
+                    </React.Fragment>
+                ))
+            }
+            {
+                this.getActiveItem().map((item, index) => (
+                    <Button
+                        color="inherit"
+                        key={index}
+                        onClick={() => onClick(item)}
+                    >
+                        {item.label}
+                    </Button>
+                ))
+            }
+        </Grid>
+    }
+
+    getInactiveItems = () => {
+        return this.props.items.slice(0, -1)
+    }
+
+    getActiveItem = () => {
+        return this.props.items.slice(-1)
+    }
+
+}
+
+type CssRules = 'inactiveItem'
+
+const styles: StyleRulesCallback<CssRules> = theme => {
+    const { unit } = theme.spacing
+    return {
+        inactiveItem: {
+            opacity: 0.6
+        }
+    }
+}
+
+export default withStyles(styles)(Breadcrumbs)
+
diff --git a/src/components/main-app-bar/dropdown-menu/dropdown-menu.test.tsx b/src/components/main-app-bar/dropdown-menu/dropdown-menu.test.tsx
new file mode 100644 (file)
index 0000000..19924ad
--- /dev/null
@@ -0,0 +1,43 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { shallow, configure } from "enzyme";
+import DropdownMenu from "./dropdown-menu";
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+
+import * as Adapter from 'enzyme-adapter-react-16';
+import { MenuItem, IconButton, Menu } from "@material-ui/core";
+
+configure({ adapter: new Adapter() });
+
+describe("<DropdownMenu />", () => {
+    it("renders menu icon", () => {
+        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
+        expect(dropdownMenu.find(ChevronRightIcon)).toHaveLength(1);
+    });
+
+    it("render menu items", () => {
+        const dropdownMenu = shallow(
+            <DropdownMenu id="test-menu" icon={ChevronRightIcon}>
+                <MenuItem>Item 1</MenuItem>
+                <MenuItem>Item 2</MenuItem>
+            </DropdownMenu>
+        );
+        expect(dropdownMenu.find(MenuItem)).toHaveLength(2);
+    });
+
+    it("opens on menu icon click", () => {
+        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
+        dropdownMenu.find(IconButton).simulate("click", {currentTarget: {}});
+        expect(dropdownMenu.state().anchorEl).toBeDefined();
+    });
+    
+    it("closes on menu click", () => {
+        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
+        dropdownMenu.find(Menu).simulate("click", {currentTarget: {}});
+        expect(dropdownMenu.state().anchorEl).toBeUndefined();
+    });
+
+});
\ No newline at end of file
diff --git a/src/components/main-app-bar/dropdown-menu/dropdown-menu.tsx b/src/components/main-app-bar/dropdown-menu/dropdown-menu.tsx
new file mode 100644 (file)
index 0000000..34f9bf3
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Button, Grid, StyleRulesCallback, WithStyles, Menu, MenuItem, IconButton } from '@material-ui/core';
+import { PopoverOrigin } from '@material-ui/core/Popover';
+
+
+interface DropdownMenuDataProps {
+    id: string,
+    icon: React.ComponentType,
+}
+
+
+type DropdownMenuProps = DropdownMenuDataProps;
+
+class DropdownMenu extends React.Component<DropdownMenuProps> {
+
+    state = {
+        anchorEl: undefined
+    }
+
+    transformOrigin: PopoverOrigin = {
+        vertical: "top",
+        horizontal: "center"
+    }
+
+    render() {
+        const { icon: Icon, id, children } = this.props;
+        const { anchorEl } = this.state;
+        return (
+            <div>
+                <IconButton
+                    aria-owns={anchorEl ? id : undefined}
+                    aria-haspopup="true"
+                    color="inherit"
+                    onClick={this.handleOpen}
+
+                >
+                    <Icon />
+                </IconButton>
+                <Menu
+                    id={id}
+                    anchorEl={anchorEl}
+                    open={Boolean(anchorEl)}
+                    onClose={this.handleClose}
+                    onClick={this.handleClose}
+                    anchorOrigin={this.transformOrigin}
+                    transformOrigin={this.transformOrigin}
+                >
+                    {children}
+                </Menu>
+            </div>
+        )
+    }
+
+    handleClose = () => {
+        this.setState({ anchorEl: undefined })
+    }
+
+    handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ anchorEl: event.currentTarget })
+    }
+}
+
+
+export default DropdownMenu
diff --git a/src/components/main-app-bar/main-app-bar.tsx b/src/components/main-app-bar/main-app-bar.tsx
new file mode 100644 (file)
index 0000000..e7b0e20
--- /dev/null
@@ -0,0 +1,127 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { AppBar, Toolbar, Typography, Grid, IconButton, Badge, Paper, Input, StyleRulesCallback, withStyles, WithStyles, Button, MenuItem } from "@material-ui/core";
+import NotificationsIcon from "@material-ui/icons/Notifications";
+import PersonIcon from "@material-ui/icons/Person";
+import HelpIcon from "@material-ui/icons/Help";
+import SearchBar from "./search-bar/search-bar";
+import Breadcrumbs, { Breadcrumb } from "../breadcrumbs/breadcrumbs"
+import DropdownMenu from "./dropdown-menu/dropdown-menu"
+import { User } from "../../models/user";
+
+export interface MainAppBarMenuItem {
+    label: string;
+}
+
+export interface MainAppBarMenuItems {
+    accountMenu: MainAppBarMenuItem[];
+    helpMenu: MainAppBarMenuItem[];
+    anonymousMenu: MainAppBarMenuItem[];
+}
+
+interface MainAppBarDataProps {
+    searchText: string;
+    searchDebounce?: number;
+    breadcrumbs: Breadcrumb[];
+    user?: User;
+    menuItems: MainAppBarMenuItems;
+}
+
+export interface MainAppBarActionProps {
+    onSearch: (searchText: string) => void,
+    onBreadcrumbClick: (breadcrumb: Breadcrumb) => void,
+    onMenuItemClick: (menuItem: MainAppBarMenuItem) => void
+}
+
+type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps & WithStyles<CssRules>;
+
+export class MainAppBar extends React.Component<MainAppBarProps> {
+
+    render() {
+        const { classes, searchText, breadcrumbs, searchDebounce } = this.props
+        return <AppBar className={classes.appBar} position="static">
+            <Toolbar>
+                <Grid
+                    container
+                    justify="space-between"
+                >
+                    <Grid item xs={3}>
+                        <Typography variant="title" color="inherit" noWrap style={{ flexGrow: 1 }}>
+                            <span>Arvados</span><br /><span style={{ fontSize: 12 }}>Workbench 2</span>
+                        </Typography>
+                    </Grid>
+                    <Grid item xs={6} container alignItems="center">
+                        {
+                            this.props.user && <SearchBar
+                                value={searchText}
+                                onSearch={this.props.onSearch}
+                                debounce={searchDebounce}
+                            />
+                        }
+                    </Grid>
+                    <Grid item xs={3} container alignItems="center" justify="flex-end">
+                        {
+                            this.props.user ? this.renderMenuForUser() : this.renderMenuForAnonymous()
+                        }
+                    </Grid>
+                </Grid>
+            </Toolbar>
+            {
+                this.props.user && <Toolbar>
+                    <Breadcrumbs items={breadcrumbs} onClick={this.props.onBreadcrumbClick} />
+                </Toolbar>
+            }
+        </AppBar>
+    }
+
+    renderMenuForUser = () => {
+        const { user } = this.props
+        return (
+            <>
+                <IconButton color="inherit">
+                    <Badge badgeContent={3} color="primary">
+                        <NotificationsIcon />
+                    </Badge>
+                </IconButton>
+                <DropdownMenu icon={PersonIcon} id="account-menu">
+                    <MenuItem>{this.getUserFullname()}</MenuItem>
+                    {this.renderMenuItems(this.props.menuItems.accountMenu)}
+                </DropdownMenu>
+                <DropdownMenu icon={HelpIcon} id="help-menu">
+                    {this.renderMenuItems(this.props.menuItems.helpMenu)}
+                </DropdownMenu>
+            </>
+        )
+    }
+
+    renderMenuForAnonymous = () => {
+        return this.props.menuItems.anonymousMenu.map((item, index) => (
+            <Button color="inherit" onClick={() => this.props.onMenuItemClick(item)}>{item.label}</Button>
+        ))
+    }
+
+    renderMenuItems = (menuItems: MainAppBarMenuItem[]) => {
+        return menuItems.map((item, index) => (
+            <MenuItem key={index} onClick={() => this.props.onMenuItemClick(item)}>{item.label}</MenuItem>
+        ))
+    }
+
+    getUserFullname = () => {
+        const { user } = this.props;
+        return user ? `${user.firstName} ${user.lastName}` : "";
+    }
+
+}
+
+type CssRules = "appBar"
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    appBar: {
+        backgroundColor: "#692498"
+    }
+})
+
+export default withStyles(styles)(MainAppBar)
\ No newline at end of file
diff --git a/src/components/main-app-bar/search-bar/search-bar.test.tsx b/src/components/main-app-bar/search-bar/search-bar.test.tsx
new file mode 100644 (file)
index 0000000..eaadd12
--- /dev/null
@@ -0,0 +1,99 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { mount, configure } from "enzyme";
+import SearchBar, { DEFAULT_SEARCH_DEBOUNCE } from "./search-bar";
+
+import * as Adapter from 'enzyme-adapter-react-16';
+
+configure({ adapter: new Adapter() });
+
+describe("<SearchBar />", () => {
+
+    jest.useFakeTimers();
+
+    let onSearch: () => void;
+
+    beforeEach(() => {
+        onSearch = jest.fn();
+    });
+
+    describe("on submit", () => {
+        it("calls onSearch with initial value passed via props", () => {
+            const searchBar = mount(<SearchBar value="initial value" onSearch={onSearch} />);
+            searchBar.find("form").simulate("submit");
+            expect(onSearch).toBeCalledWith("initial value");
+        });
+
+        it("calls onSearch with current value", () => {
+            const searchBar = mount(<SearchBar value="" onSearch={onSearch} />);
+            searchBar.find("input").simulate("change", { target: { value: "current value" } });
+            searchBar.find("form").simulate("submit");
+            expect(onSearch).toBeCalledWith("current value");
+        });
+
+        it("calls onSearch with new value passed via props", () => {
+            const searchBar = mount(<SearchBar value="" onSearch={onSearch} />);
+            searchBar.find("input").simulate("change", { target: { value: "current value" } });
+            searchBar.setProps({value: "new value"});
+            searchBar.find("form").simulate("submit");
+            expect(onSearch).toBeCalledWith("new value");
+        });
+
+        it("cancels timeout set on input value change", () => {
+            const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={1000} />);
+            searchBar.find("input").simulate("change", { target: { value: "current value" } });
+            searchBar.find("form").simulate("submit");
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toHaveBeenCalledTimes(1);
+            expect(onSearch).toBeCalledWith("current value");
+        });
+
+    });
+
+    describe("on input value change", () => {
+        it("calls onSearch after default timeout", () => {
+            const searchBar = mount(<SearchBar value="" onSearch={onSearch} />);
+            searchBar.find("input").simulate("change", { target: { value: "current value" } });
+            expect(onSearch).not.toBeCalled();
+            jest.advanceTimersByTime(DEFAULT_SEARCH_DEBOUNCE);
+            expect(onSearch).toBeCalledWith("current value");
+        });
+        
+        it("calls onSearch after the time specified in props has passed", () => {
+            const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={2000}/>);
+            searchBar.find("input").simulate("change", { target: { value: "current value" } });
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).not.toBeCalled();
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toBeCalledWith("current value");
+        });
+        
+        it("calls onSearch only once after no change happened during the specified time", () => {
+            const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={1000}/>);
+            searchBar.find("input").simulate("change", { target: { value: "current value" } });
+            jest.advanceTimersByTime(500);
+            searchBar.find("input").simulate("change", { target: { value: "changed value" } });
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toHaveBeenCalledTimes(1);
+        });
+        
+        it("calls onSearch again after the specified time has passed since previous call", () => {
+            const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={1000}/>);
+            searchBar.find("input").simulate("change", { target: { value: "current value" } });
+            jest.advanceTimersByTime(500);
+            searchBar.find("input").simulate("change", { target: { value: "intermediate value" } });
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toBeCalledWith("intermediate value")
+            searchBar.find("input").simulate("change", { target: { value: "latest value" } });
+            jest.advanceTimersByTime(1000);
+            expect(onSearch).toBeCalledWith("latest value")
+            expect(onSearch).toHaveBeenCalledTimes(2);
+            
+        });
+
+    });
+
+});
\ No newline at end of file
diff --git a/src/components/main-app-bar/search-bar/search-bar.tsx b/src/components/main-app-bar/search-bar/search-bar.tsx
new file mode 100644 (file)
index 0000000..944eb42
--- /dev/null
@@ -0,0 +1,110 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
+import SearchIcon from '@material-ui/icons/Search';
+
+interface SearchBarDataProps {
+    value: string;
+}
+
+interface SearchBarActionProps {
+    onSearch: (value: string) => any;
+    debounce?: number;
+}
+
+type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>
+
+interface SearchBarState {
+    value: string;
+}
+
+export const DEFAULT_SEARCH_DEBOUNCE = 1000;
+
+class SearchBar extends React.Component<SearchBarProps> {
+
+    state: SearchBarState = {
+        value: ""
+    }
+
+    timeout: NodeJS.Timer;
+
+    render() {
+        const { classes } = this.props
+        return <Paper className={classes.container}>
+            <form onSubmit={this.handleSubmit}>
+                <input
+                    className={classes.input}
+                    onChange={this.handleChange}
+                    placeholder="Search"
+                    value={this.state.value}
+                />
+                <IconButton className={classes.button}>
+                    <SearchIcon />
+                </IconButton>
+            </form>
+        </Paper>
+    }
+
+    componentDidMount() {
+        this.setState({value: this.props.value});
+    }
+
+    componentWillReceiveProps(nextProps: SearchBarProps) {
+        if (nextProps.value !== this.props.value) {
+            this.setState({ value: nextProps.value });
+        }
+    }
+
+    componentWillUnmount() {
+        clearTimeout(this.timeout);
+    }
+
+    handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+        event.preventDefault();
+        clearTimeout(this.timeout);
+        this.props.onSearch(this.state.value);
+    }
+
+    handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+        clearTimeout(this.timeout);
+        this.setState({ value: event.target.value });
+        this.timeout = setTimeout(
+            () => this.props.onSearch(this.state.value),
+            this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
+        );
+
+    }
+
+}
+
+type CssRules = 'container' | 'input' | 'button'
+
+const styles: StyleRulesCallback<CssRules> = theme => {
+    const { unit } = theme.spacing
+    return {
+        container: {
+            position: 'relative',
+            width: '100%'
+        },
+        input: {
+            border: 'none',
+            borderRadius: unit / 4,
+            boxSizing: 'border-box',
+            padding: unit,
+            paddingRight: unit * 4,
+            width: '100%',
+        },
+        button: {
+            position: 'absolute',
+            top: unit / 2,
+            right: unit / 2,
+            width: unit * 3,
+            height: unit * 3
+        }
+    }
+}
+
+export default withStyles(styles)(SearchBar)
\ No newline at end of file
index d18d113bcbca66ced17a91bfc920e04a7d1be65d..3d0a7ad310b0c135b08a2eade23690aa0627c3ae 100644 (file)
@@ -22,8 +22,10 @@ import { AccountCircle } from "@material-ui/icons";
 import { User } from "../../models/user";
 import Grid from "@material-ui/core/Grid/Grid";
 import { RootState } from "../../store/store";
+import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItems, MainAppBarMenuItem } from '../../components/main-app-bar/main-app-bar';
+import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
+import { push } from 'react-router-redux';
 import projectActions from "../../store/project/project-action"
-
 import ProjectTree from '../../components/project-tree/project-tree';
 import { TreeItem } from "../../components/tree/tree";
 import { Project } from "../../models/project";
@@ -45,7 +47,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     },
     appBar: {
         zIndex: theme.zIndex.drawer + 1,
-        backgroundColor: '#692498'
+        backgroundColor: '#692498',
+        position: "absolute",
+        width: "100%"
     },
     drawerPaper: {
         position: 'relative',
@@ -71,38 +75,73 @@ interface WorkbenchActionProps {
 
 type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
 
+interface NavBreadcrumb extends Breadcrumb {
+    path: string
+}
+
+interface NavMenuItem extends MainAppBarMenuItem {
+    action: () => void
+}
+
 interface WorkbenchState {
     anchorEl: any;
+    breadcrumbs: NavBreadcrumb[];
+    searchText: string;
+    menuItems: {
+        accountMenu: NavMenuItem[],
+        helpMenu: NavMenuItem[],
+        anonymousMenu: NavMenuItem[]
+    };
 }
 
 class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
-    constructor(props: WorkbenchProps) {
-        super(props);
-        this.state = {
-            anchorEl: null
+    state = {
+        anchorEl: null,
+        searchText: "",
+        breadcrumbs: [
+            {
+                label: "Projects",
+                path: "/projects"
+            }, {
+                label: "Project 1",
+                path: "/projects/project-1"
+            }
+        ],
+        menuItems: {
+            accountMenu: [
+                {
+                    label: "Logout",
+                    action: () => this.props.dispatch(authActions.LOGOUT())
+                },
+                {
+                    label: "My account",
+                    action: () => this.props.dispatch(push("/my-account"))
+                }
+            ],
+            helpMenu: [
+                {
+                    label: "Help",
+                    action: () => this.props.dispatch(push("/help"))
+                }
+            ],
+            anonymousMenu: [
+                {
+                    label: "Sign in",
+                    action: () => this.props.dispatch(authActions.LOGIN())
+                }
+            ]
         }
     }
 
-    login = () => {
-        this.props.dispatch(authActions.LOGIN());
-    };
-
-    logout = () => {
-        this.handleClose();
-        this.props.dispatch(authActions.LOGOUT());
-    };
-
-    handleOpenMenu = (event: React.MouseEvent<any>) => {
-        this.setState({
-            anchorEl: event.currentTarget
-        });
-    };
 
-    handleClose = () => {
-        this.setState({
-            anchorEl: null
-        });
-    };
+    mainAppBarActions: MainAppBarActionProps = {
+        onBreadcrumbClick: (breadcrumb: NavBreadcrumb) => this.props.dispatch(push(breadcrumb.path)),
+        onSearch: searchText => {
+            this.setState({ searchText });
+            this.props.dispatch(push(`/search?q=${searchText}`));
+        },
+        onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action()
+    }
 
     toggleProjectTreeItem = (itemId: string) => {
         this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => {
@@ -111,52 +150,18 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
     };
 
     render() {
-        const {classes, user} = this.props;
+        const { classes, user } = this.props;
         return (
             <div className={classes.root}>
-                <AppBar position="absolute" className={classes.appBar}>
-                    <Toolbar>
-                        <Typography variant="title" color="inherit" noWrap style={{flexGrow: 1}}>
-                            <span>Arvados</span><br/><span style={{fontSize: 12}}>Workbench 2</span>
-                        </Typography>
-                        {user ?
-                            <Grid container style={{width: 'auto'}}>
-                                <Grid container style={{width: 'auto'}} alignItems='center'>
-                                    <Typography variant="title" color="inherit" noWrap>
-                                        {user.firstName} {user.lastName}
-                                    </Typography>
-                                </Grid>
-                                <Grid item>
-                                    <IconButton
-                                          aria-owns={this.state.anchorEl ? 'menu-appbar' : undefined}
-                                          aria-haspopup="true"
-                                          onClick={this.handleOpenMenu}
-                                          color="inherit">
-                                      <AccountCircle/>
-                                    </IconButton>
-                                </Grid>
-                                <Menu
-                                  id="menu-appbar"
-                                  anchorEl={this.state.anchorEl}
-                                  anchorOrigin={{
-                                    vertical: 'top',
-                                    horizontal: 'right',
-                                  }}
-                                  transformOrigin={{
-                                    vertical: 'top',
-                                    horizontal: 'right',
-                                  }}
-                                  open={!!this.state.anchorEl}
-                                  onClose={this.handleClose}>
-                                  <MenuItem onClick={this.logout}>Logout</MenuItem>
-                                  <MenuItem onClick={this.handleClose}>My account</MenuItem>
-                                </Menu>
-                            </Grid>
-                            :
-                            <Button color="inherit" onClick={this.login}>Login</Button>
-                        }
-                    </Toolbar>
-                </AppBar>
+                <div className={classes.appBar}>
+                    <MainAppBar
+                        breadcrumbs={this.state.breadcrumbs}
+                        searchText={this.state.searchText}
+                        user={this.props.user}
+                        menuItems={this.state.menuItems}
+                        {...this.mainAppBarActions}
+                    />
+                </div>
                 {user &&
                 <Drawer
                     variant="permanent"
@@ -169,9 +174,10 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                         toggleProjectTreeItem={this.toggleProjectTreeItem}/>
                 </Drawer>}
                 <main className={classes.content}>
-                    <div className={classes.toolbar}/>
+                    <div className={classes.toolbar} />
+                    <div className={classes.toolbar} />
                     <Switch>
-                        <Route path="/project/:name" component={ProjectList}/>
+                        <Route path="/project/:name" component={ProjectList} />
                     </Switch>
                 </main>
             </div>