Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 10 Sep 2018 07:39:14 +0000 (09:39 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 10 Sep 2018 07:39:14 +0000 (09:39 +0200)
Feature #14149

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

16 files changed:
src/common/config.ts
src/components/code-snippet/code-snippet.tsx
src/components/default-code-snippet/default-code-snippet.tsx
src/index.tsx
src/store/current-token-dialog/current-token-dialog-actions.tsx [new file with mode: 0644]
src/views-components/current-token-dialog/current-token-dialog.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/main-app-bar/account-menu.tsx [new file with mode: 0644]
src/views-components/main-app-bar/anonymous-menu.tsx [new file with mode: 0644]
src/views-components/main-app-bar/help-menu.tsx
src/views-components/main-app-bar/main-app-bar.test.tsx [deleted file]
src/views-components/main-app-bar/main-app-bar.tsx
src/views-components/main-app-bar/notifications-menu.tsx [new file with mode: 0644]
src/views-components/main-content-bar/main-content-bar.tsx [new file with mode: 0644]
src/views-components/side-panel/side-panel.tsx
src/views/workbench/workbench.tsx

index 061f9c00af0b91e5b1a92e5337ecda44492c2754..1ab73294b01bc9c742c8b52d17bb1c418d080dc9 100644 (file)
@@ -56,8 +56,10 @@ export const fetchConfig = () => {
         .get<ConfigJSON>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
         .then(response => response.data)
         .catch(() => Promise.resolve(getDefaultConfig()))
-        .then(config => Axios.get<Config>(getDiscoveryURL(config.API_HOST)))
-        .then(response => response.data);
+        .then(config => Axios
+            .get<Config>(getDiscoveryURL(config.API_HOST))
+            .then(response => ({ config: response.data, apiHost: config.API_HOST })));
+
 };
 
 export const mockConfig = (config: Partial<Config>): Config => ({
index b622210f008eb1c95c43897c6f94e99560fa5cc2..eb0e709a9b0dfa572860e15b796b436e065b41e3 100644 (file)
@@ -14,7 +14,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         width: '100%',
         height: 'auto',
         maxHeight: '550px',
-        overflow: 'scroll',
+        overflow: 'auto',
         padding: theme.spacing.unit
     }
 });
index 541f390616ef369d801a0067cddabcf89dbcb5ad..b8c0a7be93ba96acebca6e77f4c973859b79876d 100644 (file)
@@ -19,7 +19,7 @@ const theme = createMuiTheme({
         }
     },
     typography: {
-        fontFamily: 'VT323'
+        fontFamily: 'monospace'
     }
 });
 
index 8ab089a89fcb3ff9ede3026c509c8de09d611dca..a76a86ac7d70213a6dad83a030f3f0e523164ac0 100644 (file)
@@ -35,6 +35,7 @@ import { ServiceRepository } from '~/services/services';
 import { initWebSocket } from '~/websocket/websocket';
 import { Config } from '~/common/config';
 import { addRouteChangeHandlers } from './routes/route-change-handlers';
+import { setCurrentTokenDialogApiHost } from '~/store/current-token-dialog/current-token-dialog-actions';
 import { processResourceActionSet } from './views-components/context-menu/action-sets/process-resource-action-set';
 
 const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
@@ -58,13 +59,14 @@ addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
 addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 
 fetchConfig()
-    .then((config) => {
+    .then(({ config, apiHost }) => {
         const history = createBrowserHistory();
         const services = createServices(config);
         const store = configureStore(history, services);
 
         store.subscribe(initListener(history, store, services, config));
         store.dispatch(initAuth());
+        store.dispatch(setCurrentTokenDialogApiHost(apiHost));
 
         const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
         const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props} />;
diff --git a/src/store/current-token-dialog/current-token-dialog-actions.tsx b/src/store/current-token-dialog/current-token-dialog-actions.tsx
new file mode 100644 (file)
index 0000000..030b18e
--- /dev/null
@@ -0,0 +1,26 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { getProperty } from '../properties/properties';
+import { propertiesActions } from '~/store/properties/properties-actions';
+import { RootState } from '~/store/store';
+
+export const CURRENT_TOKEN_DIALOG_NAME = 'currentTokenDialog';
+const API_HOST_PROPERTY_NAME = 'apiHost';
+
+export interface CurrentTokenDialogData {
+    currentToken: string;
+    apiHost: string;
+}
+
+export const setCurrentTokenDialogApiHost = (apiHost: string) =>
+    propertiesActions.SET_PROPERTY({ key: API_HOST_PROPERTY_NAME, value: apiHost });
+
+export const getCurrentTokenDialogData = (state: RootState): CurrentTokenDialogData => ({
+    apiHost: getProperty<string>(API_HOST_PROPERTY_NAME)(state.properties) || '',
+    currentToken: state.auth.apiToken || '',
+});
+
+export const openCurrentTokenDialog = dialogActions.OPEN_DIALOG({ id: CURRENT_TOKEN_DIALOG_NAME, data: {} });
index fca9f05982b6ed559875a7bfeb252e8d4b8f1e5c..ba6c3258f428a3fe882fa47463b623e798f25451 100644 (file)
@@ -5,6 +5,13 @@
 import * as React from 'react';
 import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography, Paper } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
+import { withDialog } from '~/store/dialog/with-dialog';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import { CurrentTokenDialogData, getCurrentTokenDialogData } from '~/store/current-token-dialog/current-token-dialog-actions';
+import { RootState } from '~/store/store';
+import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
 
 type CssRules = 'link' | 'paper' | 'button';
 
@@ -26,65 +33,51 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
-interface CurrentTokenDataProps {
-    currentToken?: string;
-    open: boolean;
-}
-
-interface CurrentTokenActionProps {
-    handleClose: () => void;
-}
-
-type CurrentTokenProps = CurrentTokenDataProps & CurrentTokenActionProps & WithStyles<CssRules>;
-
-export const CurrentTokenDialog = withStyles(styles)(
-    class extends React.Component<CurrentTokenProps> {
+type CurrentTokenProps = CurrentTokenDialogData & WithDialogProps<{}> & WithStyles<CssRules>;
 
-        render() {
-            const { classes, open, handleClose, currentToken } = this.props;
-            return (
-                <Dialog open={open} onClose={handleClose} fullWidth={true} maxWidth='md'>
-                    <DialogTitle>Current Token</DialogTitle>
-                    <DialogContent>
-                        <Typography variant='body1' paragraph={true}>
-                            The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
+export const CurrentTokenDialog = compose(
+    withStyles(styles),
+    connect(getCurrentTokenDialogData),
+    withDialog('currentTokenDialog')
+)(class extends React.Component<CurrentTokenProps> {
+    render() {
+        const { classes, open, closeDialog, ...data } = this.props;
+        return <Dialog
+            open={open}
+            onClose={closeDialog}
+            fullWidth={true}
+            maxWidth='md'>
+            <DialogTitle>Current Token</DialogTitle>
+            <DialogContent>
+                <Typography variant='body1' paragraph={true}>
+                    The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
                             <Typography component='p'>
-                                For more information see
+                        For more information see
                                 <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>
-                                    Getting an API token.
+                            Getting an API token.
                                 </a>
-                            </Typography>
+                    </Typography>
+                </Typography>
+                <Typography variant='body1' paragraph={true}>
+                    Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
                         </Typography>
-
-                        <Typography variant='body1' paragraph={true}>
-                            Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
-                        </Typography>
-
-                        <Paper className={classes.paper} elevation={0}>
-                            <Typography variant='body1'>
-                                HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
-                            </Typography>
-                            <Typography variant='body1'>
-                                export ARVADOS_API_TOKEN={currentToken}
-                            </Typography>
-                            <Typography variant='body1'>
-                                export ARVADOS_API_HOST=api.ardev.roche.com
-                            </Typography>
-                            <Typography variant='body1'>
-                                unset ARVADOS_API_HOST_INSECURE
-                            </Typography>
-                        </Paper>
-                        <Typography variant='body1'>
-                            Arvados
+                <DefaultCodeSnippet lines={[getSnippet(data)]} />
+                <Typography variant='body1'>
+                    Arvados
                             <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
-                            do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
+                    do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
                         </Typography>
-                    </DialogContent>
-                    <DialogActions>
-                        <Button onClick={handleClose} className={classes.button} color="primary">CLOSE</Button>
-                    </DialogActions>
-                </Dialog>
-            );
-        }
+            </DialogContent>
+            <DialogActions>
+                <Button onClick={closeDialog} className={classes.button} color="primary">CLOSE</Button>
+            </DialogActions>
+        </Dialog>;
     }
+}
 );
+
+const getSnippet = ({ apiHost, currentToken }: CurrentTokenDialogData) =>
+`HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
+export ARVADOS_API_TOKEN=${currentToken}
+export ARVADOS_API_HOST=${apiHost}
+unset ARVADOS_API_HOST_INSECURE`;
index 7aae7860ac39999df6262fdb886198f49400a4e2..c0d4797fa50f68ac89f8dc11b1781b636dfbbdd4 100644 (file)
@@ -22,20 +22,24 @@ import { DetailsData } from "./details-data";
 import { DetailsResource } from "~/models/details";
 import { getResource } from '../../store/resources/resources';
 
-type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
+type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
 
 const drawerWidth = 320;
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    container: {
+    root: {
         width: 0,
-        position: 'relative',
-        height: 'auto',
+        overflowX: 'hidden',
         transition: 'width 0.5s ease',
-        '&$opened': {
-            width: drawerWidth
-        }
+        background: theme.palette.background.paper,
+        borderLeft: `1px solid ${theme.palette.divider}`,
+        height: '100%',
+    },
+    opened: {
+        width: drawerWidth,
+    },
+    container: {
+        width: drawerWidth,
     },
-    opened: {},
     drawerPaper: {
         position: 'relative',
         width: drawerWidth
@@ -113,10 +117,9 @@ export const DetailsPanel = withStyles(styles)(
                 const { classes, onCloseDrawer, isOpened, item } = this.props;
                 const { tabsValue } = this.state;
                 return (
-                    <Typography component="div"
-                        className={classnames([classes.container, { [classes.opened]: isOpened }])}>
-                        <Drawer variant="permanent" anchor="right" classes={{ paper: classes.drawerPaper }}>
-                            <Typography component="div" className={classes.headerContainer}>
+                    <div className={classnames([classes.root, { [classes.opened]: isOpened }])}>
+                        <div className={classes.container}>
+                            <div className={classes.headerContainer}>
                                 <Grid container alignItems='center' justify='space-around'>
                                     <Grid item xs={2}>
                                         {item.getIcon(classes.headerIcon)}
@@ -132,7 +135,7 @@ export const DetailsPanel = withStyles(styles)(
                                         </IconButton>
                                     </Grid>
                                 </Grid>
-                            </Typography>
+                            </div>
                             <Tabs value={tabsValue} onChange={this.handleChange}>
                                 <Tab disableRipple label="Details" />
                                 <Tab disableRipple label="Activity" disabled />
@@ -145,8 +148,8 @@ export const DetailsPanel = withStyles(styles)(
                             {tabsValue === 1 && this.renderTabContainer(
                                 <Grid container direction="column" />
                             )}
-                        </Drawer>
-                    </Typography>
+                        </div>
+                    </div>
                 );
             }
         }
diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx
new file mode 100644 (file)
index 0000000..fdd8123
--- /dev/null
@@ -0,0 +1,37 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { MenuItem } from "@material-ui/core";
+import { User, getUserFullname } from "~/models/user";
+import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
+import { UserPanelIcon } from "~/components/icon/icon";
+import { DispatchProp, connect } from 'react-redux';
+import { logout } from "~/store/auth/auth-action";
+import { RootState } from "~/store/store";
+import { openCurrentTokenDialog } from '../../store/current-token-dialog/current-token-dialog-actions';
+
+interface AccountMenuProps {
+    user?: User;
+}
+
+const mapStateToProps = (state: RootState): AccountMenuProps => ({
+    user: state.auth.user
+});
+
+export const AccountMenu = connect(mapStateToProps)(
+    ({ user, dispatch }: AccountMenuProps & DispatchProp<any>) =>
+        user
+            ? <DropdownMenu
+                icon={<UserPanelIcon />}
+                id="account-menu"
+                title="Account Management">
+                <MenuItem>
+                    {getUserFullname(user)}
+                </MenuItem>
+                <MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
+                <MenuItem>My account</MenuItem>
+                <MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
+            </DropdownMenu>
+            : null);
diff --git a/src/views-components/main-app-bar/anonymous-menu.tsx b/src/views-components/main-app-bar/anonymous-menu.tsx
new file mode 100644 (file)
index 0000000..6f77a52
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Button } from '@material-ui/core';
+import { DispatchProp, connect } from 'react-redux';
+import { login } from '~/store/auth/auth-action';
+
+export const AnonymousMenu = connect()(
+    ({ dispatch }: DispatchProp<any>) =>
+        <Button
+            color="inherit"
+            onClick={() => dispatch(login())}>
+            Sign in
+        </Button>);
index de3ed3b8e275a8ad11ccc4983c648c8b2b69d9a1..26604228fc21ac9fbfb79ba21a96a8372324655c 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { MenuItem, Typography } from "@material-ui/core";
+import { MenuItem, Typography, ListSubheader } from "@material-ui/core";
 import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
 import { ImportContactsIcon, HelpIcon } from "~/components/icon/icon";
 import { ArvadosTheme } from '~/common/custom-theme';
@@ -58,15 +58,13 @@ export const HelpMenu = withStyles(styles)(
             icon={<HelpIcon />}
             id="help-menu"
             title="Help">
-            <li className={classes.title}>
-                <Typography variant="body1">Help</Typography>
-            </li>
+            <MenuItem disabled>Help</MenuItem>
             {
                 links.map(link =>
                     <MenuItem key={link.title}>
                         <a href={link.link} target="_blank" className={classes.link}>
-                                <ImportContactsIcon className={classes.icon} />
-                                <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
+                            <ImportContactsIcon className={classes.icon} />
+                            <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
                         </a>
                     </MenuItem>
                 )
diff --git a/src/views-components/main-app-bar/main-app-bar.test.tsx b/src/views-components/main-app-bar/main-app-bar.test.tsx
deleted file mode 100644 (file)
index 69b4dd6..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-// 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 * as Adapter from "enzyme-adapter-react-16";
-import { MainAppBar, MainAppBarProps } from './main-app-bar';
-import { SearchBar } from "~/components/search-bar/search-bar";
-import { Breadcrumbs } from "~/components/breadcrumbs/breadcrumbs";
-import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
-import { Button, MenuItem, IconButton } from "@material-ui/core";
-import { User } from "~/models/user";
-import { MemoryRouter } from 'react-router-dom';
-
-configure({ adapter: new Adapter() });
-
-describe("<MainAppBar />", () => {
-
-    const user: User = {
-        firstName: "Test",
-        lastName: "User",
-        email: "test.user@example.com",
-        uuid: "",
-        ownerUuid: ""
-    };
-
-    it("renders all components and the menu for authenticated user if user prop has value", () => {
-        const mainAppBar = mount(
-            <MemoryRouter>
-                <MainAppBar
-                    {...mockMainAppBarProps({ user })}
-                />
-            </MemoryRouter>
-        );
-        expect(mainAppBar.find(SearchBar)).toHaveLength(1);
-        expect(mainAppBar.find(Breadcrumbs)).toHaveLength(1);
-        expect(mainAppBar.find(DropdownMenu)).toHaveLength(2);
-    });
-
-    it("renders only the menu for anonymous user if user prop is undefined", () => {
-        const menuItems = { accountMenu: [], helpMenu: [], anonymousMenu: [{ label: 'Sign in' }] };
-        const mainAppBar = mount(
-            <MemoryRouter>
-                <MainAppBar
-                    {...mockMainAppBarProps({ user: undefined, menuItems })}
-                />
-            </MemoryRouter>
-        );
-        expect(mainAppBar.find(SearchBar)).toHaveLength(0);
-        expect(mainAppBar.find(Breadcrumbs)).toHaveLength(0);
-        expect(mainAppBar.find(DropdownMenu)).toHaveLength(0);
-        expect(mainAppBar.find(Button)).toHaveLength(1);
-    });
-
-    it("communicates with <SearchBar />", () => {
-        const onSearch = jest.fn();
-        const mainAppBar = mount(
-            <MemoryRouter>
-                <MainAppBar
-                    {...mockMainAppBarProps({ searchText: 'search text', searchDebounce: 2000, onSearch, user })}
-                />
-            </MemoryRouter>
-        );
-        const searchBar = mainAppBar.find(SearchBar);
-        expect(searchBar.prop("value")).toBe("search text");
-        expect(searchBar.prop("debounce")).toBe(2000);
-        searchBar.prop("onSearch")("new search text");
-        expect(onSearch).toBeCalledWith("new search text");
-    });
-
-    it("communicates with menu", () => {
-        const onMenuItemClick = jest.fn();
-        const menuItems = { accountMenu: [{ label: "log out" }], helpMenu: [], anonymousMenu: [] };
-        const mainAppBar = mount(
-            <MemoryRouter>
-                <MainAppBar
-                    {...mockMainAppBarProps({ menuItems, onMenuItemClick, user })}
-                />
-            </MemoryRouter>
-        );
-
-        mainAppBar.find(DropdownMenu).at(0).find(IconButton).simulate("click");
-        mainAppBar.find(DropdownMenu).at(0).find(MenuItem).at(1).simulate("click");
-        expect(onMenuItemClick).toBeCalledWith(menuItems.accountMenu[0]);
-    });
-});
-
-const Breadcrumbs = () => <span>Breadcrumbs</span>;
-
-const mockMainAppBarProps = (props: Partial<MainAppBarProps>): MainAppBarProps => ({
-    searchText: '',
-    breadcrumbs: Breadcrumbs,
-    menuItems: {
-        accountMenu: [],
-        helpMenu: [],
-        anonymousMenu: [],
-    },
-    buildInfo: '',
-    onSearch: jest.fn(),
-    onMenuItemClick: jest.fn(),
-    onDetailsPanelToggle: jest.fn(),
-    ...props,
-});
index 04e0fb804a75dba4ac8eae7116d4606617889aa6..ec2a511a1e89faf2e0261911dbe8e5b679d1882d 100644 (file)
@@ -3,48 +3,38 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { AppBar, Toolbar, Typography, Grid, IconButton, Badge, Button, MenuItem, Tooltip } from "@material-ui/core";
+import { AppBar, Toolbar, Typography, Grid } from "@material-ui/core";
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from '~/common/custom-theme';
 import { Link } from "react-router-dom";
-import { User, getUserFullname } from "~/models/user";
+import { User } from "~/models/user";
 import { SearchBar } from "~/components/search-bar/search-bar";
-import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
-import { DetailsIcon, NotificationIcon, UserPanelIcon, HelpIcon } from "~/components/icon/icon";
 import { Routes } from '~/routes/routes';
+import { NotificationsMenu } from "~/views-components/main-app-bar/notifications-menu";
+import { AccountMenu } from "~/views-components/main-app-bar/account-menu";
+import { AnonymousMenu } from "~/views-components/main-app-bar/anonymous-menu";
+import { HelpMenu } from './help-menu';
 
-type CssRules = 'link';
+type CssRules = 'toolbar' | 'link';
 
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+const styles: StyleRulesCallback<CssRules> = () => ({
     link: {
         textDecoration: 'none',
         color: 'inherit'
+    },
+    toolbar: {
+        height: '56px'
     }
 });
 
-export interface MainAppBarMenuItem {
-    label: string;
-}
-
-export interface MainAppBarMenuItems {
-    accountMenu: MainAppBarMenuItem[];
-    helpMenu: MainAppBarMenuItem[];
-    anonymousMenu: MainAppBarMenuItem[];
-}
-
 interface MainAppBarDataProps {
     searchText: string;
     searchDebounce?: number;
-    breadcrumbs: React.ComponentType<any>;
     user?: User;
-    menuItems: MainAppBarMenuItems;
-    buildInfo: string;
+    buildInfo?: string;
 }
 
 export interface MainAppBarActionProps {
     onSearch: (searchText: string) => void;
-    onMenuItemClick: (menuItem: MainAppBarMenuItem) => void;
-    onDetailsPanelToggle: () => void;
 }
 
 export type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps & WithStyles<CssRules>;
@@ -52,81 +42,44 @@ export type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps & With
 export const MainAppBar = withStyles(styles)(
     (props: MainAppBarProps) => {
         return <AppBar position="static">
-            <Toolbar>
+            <Toolbar className={props.classes.toolbar}>
                 <Grid container justify="space-between">
-                    <Grid item xs={3}>
-                        <Typography variant="headline" color="inherit" noWrap>
+                    <Grid container item xs={3} direction="column" justify="center">
+                        <Typography variant="title" color="inherit" noWrap>
                             <Link to={Routes.ROOT} className={props.classes.link}>
-                                Arvados 2
+                                arvados workbench
                             </Link>
                         </Typography>
-                        <Typography variant="body1" color="inherit" noWrap >
-                            {props.buildInfo}
-                        </Typography>
+                        <Typography variant="caption" color="inherit">{props.buildInfo}</Typography>
                     </Grid>
-                    <Grid item xs={6} container alignItems="center">
-                        {
-                            props.user && <SearchBar
-                                value={props.searchText}
-                                onSearch={props.onSearch}
-                                debounce={props.searchDebounce}
-                            />
-                        }
+                    <Grid
+                        item
+                        xs={6}
+                        container
+                        alignItems="center">
+                        {props.user && <SearchBar
+                            value={props.searchText}
+                            onSearch={props.onSearch}
+                            debounce={props.searchDebounce}
+                        />}
                     </Grid>
-                    <Grid item xs={3} container alignItems="center" justify="flex-end">
-                        {
-                            props.user ? renderMenuForUser(props) : renderMenuForAnonymous(props)
-                        }
+                    <Grid
+                        item
+                        xs={3}
+                        container
+                        alignItems="center"
+                        justify="flex-end"
+                        wrap="nowrap">
+                        {props.user
+                            ? <>
+                                <NotificationsMenu />
+                                <AccountMenu />
+                                <HelpMenu />
+                            </>
+                            : <AnonymousMenu />}
                     </Grid>
                 </Grid>
             </Toolbar>
-            <Toolbar >
-                {props.user && <props.breadcrumbs />}
-                {props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
-                    <Tooltip title="Additional Info">
-                        <DetailsIcon />
-                    </Tooltip>
-                </IconButton>}
-            </Toolbar>
         </AppBar>;
     }
 );
-
-const renderMenuForUser = ({ user, menuItems, onMenuItemClick }: MainAppBarProps) => {
-    return (
-        <>
-            <IconButton color="inherit">
-                <Tooltip title="Notification">
-                    <Badge badgeContent={3} color="primary">
-                        <NotificationIcon />
-                    </Badge>
-                </Tooltip>
-            </IconButton>
-            <DropdownMenu icon={<UserPanelIcon />} id="account-menu" title="Account Management">
-                <MenuItem>
-                    {getUserFullname(user)}
-                </MenuItem>
-                {renderMenuItems(menuItems.accountMenu, onMenuItemClick)}
-            </DropdownMenu>
-            <DropdownMenu icon={<HelpIcon />} id="help-menu" title="Help">
-                {renderMenuItems(menuItems.helpMenu, onMenuItemClick)}
-            </DropdownMenu>
-        </>
-    );
-};
-
-const renderMenuForAnonymous = ({ onMenuItemClick, menuItems }: MainAppBarProps) => {
-    return menuItems.anonymousMenu.map((item, index) => (
-        <Button key={index} color="inherit" onClick={() => onMenuItemClick(item)}>
-            {item.label}
-        </Button>
-    ));
-};
-
-const renderMenuItems = (menuItems: MainAppBarMenuItem[], onMenuItemClick: (menuItem: MainAppBarMenuItem) => void) => {
-    return menuItems.map((item, index) => (
-        <MenuItem key={index} onClick={() => onMenuItemClick(item)}>
-            {item.label}
-        </MenuItem>
-    ));
-};
diff --git a/src/views-components/main-app-bar/notifications-menu.tsx b/src/views-components/main-app-bar/notifications-menu.tsx
new file mode 100644 (file)
index 0000000..5781ec1
--- /dev/null
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Badge, MenuItem } from '@material-ui/core';
+import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
+import { NotificationIcon } from '~/components/icon/icon';
+
+export const NotificationsMenu = 
+    () =>
+        <DropdownMenu
+            icon={
+                <Badge
+                    badgeContent={0}
+                    color="primary">
+                    <NotificationIcon />
+                </Badge>}
+            id="account-menu"
+            title="Notifications">
+            <MenuItem>You are up to date</MenuItem>
+        </DropdownMenu>;
+
diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx
new file mode 100644 (file)
index 0000000..ae86fe5
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Toolbar, IconButton, Tooltip, Grid } from "@material-ui/core";
+import { DetailsIcon } from "~/components/icon/icon";
+import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs";
+import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
+import { connect } from 'react-redux';
+
+interface MainContentBarProps {
+    onDetailsPanelToggle: () => void;
+}
+
+export const MainContentBar = connect(undefined, {
+    onDetailsPanelToggle: detailsPanelActions.TOGGLE_DETAILS_PANEL
+})((props: MainContentBarProps) =>
+    <Toolbar>
+        <Grid container>
+            <Grid container item xs alignItems="center">
+                <Breadcrumbs />
+            </Grid>
+            <Grid item>
+                <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+                    <Tooltip title="Additional Info">
+                        <DetailsIcon />
+                    </Tooltip>
+                </IconButton>
+            </Grid>
+        </Grid>
+    </Toolbar>);
index b81f39ef338a850ce22a158cb4dd57cd5ca51de2..70bc92b7162aef6acc157dac5a1dc15d26d295a7 100644 (file)
@@ -13,18 +13,16 @@ import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action'
 
 const DRAWER_WITDH = 240;
 
-type CssRules = 'drawerPaper' | 'toolbar';
+type CssRules = 'root';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    drawerPaper: {
-        position: 'relative',
+    root: {
+        background: theme.palette.background.paper,
+        borderRight: `1px solid ${theme.palette.divider}`,
+        height: '100%',
+        overflowX: 'auto',
         width: DRAWER_WITDH,
-        display: 'flex',
-        flexDirection: 'column',
-        paddingTop: 58,
-        overflow: 'auto',
-    },
-    toolbar: theme.mixins.toolbar
+    }
 });
 
 const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
@@ -37,9 +35,6 @@ export const SidePanel = compose(
     withStyles(styles),
     connect(undefined, mapDispatchToProps)
 )(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
-    <Drawer
-        variant="permanent"
-        classes={{ paper: classes.drawerPaper }}>
-        <div className={classes.toolbar} />
+    <div className={classes.root}>
         <SidePanelTree {...props} />
-    </Drawer>);
+    </div>);
index f202b6983359642fd7eadf1c667d880a1855fac6..4d231a0cc1134602d1ebdf56fe053e1c22f64039 100644 (file)
@@ -6,10 +6,9 @@ import * as React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { connect, DispatchProp } from "react-redux";
 import { Route, Switch } from "react-router";
-import { login, logout } from "~/store/auth/auth-action";
 import { User } from "~/models/user";
 import { RootState } from "~/store/store";
-import { MainAppBar, MainAppBarActionProps, MainAppBarMenuItem } from '~/views-components/main-app-bar/main-app-bar';
+import { MainAppBar } from '~/views-components/main-app-bar/main-app-bar';
 import { push } from 'react-router-redux';
 import { ProjectPanel } from "~/views/project-panel/project-panel";
 import { DetailsPanel } from '~/views-components/details-panel/details-panel';
@@ -28,7 +27,6 @@ import { Routes } from '~/routes/routes';
 import { SidePanel } from '~/views-components/side-panel/side-panel';
 import { ProcessPanel } from '~/views/process-panel/process-panel';
 import { ProcessLogPanel } from '~/views/process-log-panel/process-log-panel';
-import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
 import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
 import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
 import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
@@ -40,39 +38,30 @@ import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-colle
 import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
 import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
 import { TrashPanel } from "~/views/trash-panel/trash-panel";
+import { MainContentBar } from '../../views-components/main-content-bar/main-content-bar';
+import { Grid } from '@material-ui/core';
 
-const APP_BAR_HEIGHT = 100;
-
-type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
+type CssRules = 'root' | 'contentWrapper' | 'content' | 'appBar';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
-        flexGrow: 1,
-        zIndex: 1,
         overflow: 'hidden',
-        position: 'relative',
-        display: 'flex',
         width: '100vw',
         height: '100vh'
     },
-    appBar: {
-        zIndex: theme.zIndex.drawer + 1,
-        position: "absolute",
-        width: "100%"
-    },
     contentWrapper: {
-        backgroundColor: theme.palette.background.default,
-        display: "flex",
-        flexGrow: 1,
+        background: theme.palette.background.default,
         minWidth: 0,
-        paddingTop: APP_BAR_HEIGHT
     },
     content: {
-        padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`,
-        overflowY: "auto",
-        flexGrow: 1,
-        position: 'relative'
+        minWidth: 0,
+        overflow: 'auto',
+        paddingLeft: theme.spacing.unit * 3,
+        paddingRight: theme.spacing.unit * 3,
     },
+    appBar: {
+        zIndex: 1,
+    }
 });
 
 interface WorkbenchDataProps {
@@ -85,24 +74,10 @@ interface WorkbenchGeneralProps {
     buildInfo: string;
 }
 
-interface WorkbenchActionProps {
-}
-
-type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
-
-interface NavMenuItem extends MainAppBarMenuItem {
-    action: () => void;
-}
+type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & DispatchProp<any> & WithStyles<CssRules>;
 
 interface WorkbenchState {
-    isCurrentTokenDialogOpen: boolean;
-    anchorEl: any;
     searchText: string;
-    menuItems: {
-        accountMenu: NavMenuItem[],
-        helpMenu: NavMenuItem[],
-        anonymousMenu: NavMenuItem[]
-    };
 }
 
 export const Workbench = withStyles(styles)(
@@ -114,105 +89,87 @@ export const Workbench = withStyles(styles)(
     )(
         class extends React.Component<WorkbenchProps, WorkbenchState> {
             state = {
-                isCurrentTokenDialogOpen: false,
-                anchorEl: null,
                 searchText: "",
-                breadcrumbs: [],
-                menuItems: {
-                    accountMenu: [
-                        {
-                            label: 'Current token',
-                            action: () => this.toggleCurrentTokenModal()
-                        },
-                        {
-                            label: "Logout",
-                            action: () => this.props.dispatch(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(login())
-                        }
-                    ]
-                }
             };
 
             render() {
-                const { classes, user } = this.props;
-                return (
-                    <div className={classes.root}>
-                        <div className={classes.appBar}>
+                return <>
+                    <Grid
+                        container
+                        direction="column"
+                        className={this.props.classes.root}>
+                        <Grid className={this.props.classes.appBar}>
                             <MainAppBar
-                                breadcrumbs={Breadcrumbs}
                                 searchText={this.state.searchText}
                                 user={this.props.user}
-                                menuItems={this.state.menuItems}
-                                buildInfo={this.props.buildInfo}
-                                {...this.mainAppBarActions} />
-                        </div>
-                        {user && <SidePanel />}
-                        <main className={classes.contentWrapper}>
-                            <div className={classes.content}>
-                                <Switch>
-                                    <Route path={Routes.PROJECTS} component={ProjectPanel} />
-                                    <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
-                                    <Route path={Routes.FAVORITES} component={FavoritePanel} />
-                                    <Route path={Routes.PROCESSES} component={ProcessPanel} />
-                                    <Route path={Routes.TRASH} component={TrashPanel} />
-                                    <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
-                                </Switch>
-                            </div>
-                            {user && <DetailsPanel />}
-                        </main>
-                        <ContextMenu />
-                        <Snackbar />
-                        <CreateProjectDialog />
-                        <CreateCollectionDialog />
-                        <RenameFileDialog />
-                        <PartialCopyCollectionDialog />
-                        <FileRemoveDialog />
-                        <CopyCollectionDialog />
-                        <FileRemoveDialog />
-                        <MultipleFilesRemoveDialog />
-                        <UpdateCollectionDialog />
-                        <FilesUploadCollectionDialog />
-                        <UpdateProjectDialog />
-                        <MoveCollectionDialog />
-                        <MoveProcessDialog />
-                        <MoveProjectDialog />
-                        <CurrentTokenDialog
-                            currentToken={this.props.currentToken}
-                            open={this.state.isCurrentTokenDialogOpen}
-                            handleClose={this.toggleCurrentTokenModal} />
-                    </div>
-                );
+                                onSearch={this.onSearch}
+                                buildInfo={this.props.buildInfo} />
+                        </Grid>
+                        {this.props.user &&
+                            <Grid
+                                container
+                                item
+                                xs
+                                alignItems="stretch"
+                                wrap="nowrap">
+                                <Grid item>
+                                    <SidePanel />
+                                </Grid>
+                                <Grid
+                                    container
+                                    item
+                                    xs
+                                    component="main"
+                                    direction="column"
+                                    className={this.props.classes.contentWrapper}>
+                                    <Grid item>
+                                        <MainContentBar />
+                                    </Grid>
+                                    <Grid item xs className={this.props.classes.content}>
+                                        <Switch>
+                                            <Route path={Routes.PROJECTS} component={ProjectPanel} />
+                                            <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+                                            <Route path={Routes.FAVORITES} component={FavoritePanel} />
+                                            <Route path={Routes.PROCESSES} component={ProcessPanel} />
+                                            <Route path={Routes.TRASH} component={TrashPanel} />
+                                            <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
+                                        </Switch>
+                                    </Grid>
+                                </Grid>
+                                <Grid item>
+                                    <DetailsPanel />
+                                </Grid>
+                            </Grid>}
+                    </Grid>
+                    <ContextMenu />
+                    <Snackbar />
+                    <CreateProjectDialog />
+                    <CreateCollectionDialog />
+                    <RenameFileDialog />
+                    <PartialCopyCollectionDialog />
+                    <FileRemoveDialog />
+                    <CopyCollectionDialog />
+                    <FileRemoveDialog />
+                    <MultipleFilesRemoveDialog />
+                    <UpdateCollectionDialog />
+                    <FilesUploadCollectionDialog />
+                    <UpdateProjectDialog />
+                    <MoveCollectionDialog />
+                    <MoveProcessDialog />
+                    <MoveProjectDialog />
+                    <CurrentTokenDialog />
+                </>;
             }
 
-            mainAppBarActions: MainAppBarActionProps = {
-                onSearch: searchText => {
-                    this.setState({ searchText });
-                    this.props.dispatch(push(`/search?q=${searchText}`));
-                },
-                onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action(),
-                onDetailsPanelToggle: () => {
-                    this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
-                },
-            };
+            onSearch = (searchText: string) => {
+                this.setState({ searchText });
+                this.props.dispatch(push(`/search?q=${searchText}`));
+            }
 
-            toggleCurrentTokenModal = () => {
-                this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen });
+            toggleDetailsPanel = () => {
+                this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
             }
+
         }
     )
 );