Merge branch 'master' into 13751-shared-with-me-view
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 17 Sep 2018 07:13:24 +0000 (09:13 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 17 Sep 2018 07:13:24 +0000 (09:13 +0200)
refs #13751

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

src/common/custom-theme.ts
src/components/icon/icon.tsx
src/services/api/filter-builder.test.ts
src/services/api/filter-builder.ts
src/store/trash-panel/trash-panel-middleware-service.ts
src/views-components/side-panel-button/side-panel-button.tsx [new file with mode: 0644]
src/views-components/side-panel/side-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/trash-panel/trash-panel.tsx
src/views/workbench/workbench.tsx

index 20790100dcf2012f7f12ec6912aa30d0f56ca875..3d56f78b5c80c8adf285526e9dd1439fbe3937c9 100644 (file)
@@ -10,6 +10,7 @@ import grey from '@material-ui/core/colors/grey';
 import green from '@material-ui/core/colors/green';
 import yellow from '@material-ui/core/colors/yellow';
 import red from '@material-ui/core/colors/red';
+import teal from '@material-ui/core/colors/teal';
 
 export interface ArvadosThemeOptions extends ThemeOptions {
     customs: any;
@@ -30,10 +31,8 @@ interface Colors {
     grey700: string;
 }
 
-const red900 = red["900"];
+const arvadosPurple = '#361336';
 const purple800 = purple["800"];
-const grey200 = grey["200"];
-const grey300 = grey["300"];
 const grey500 = grey["500"];
 const grey600 = grey["600"];
 const grey700 = grey["700"];
@@ -59,7 +58,7 @@ export const themeOptions: ArvadosThemeOptions = {
         },
         MuiAppBar: {
             colorPrimary: {
-                backgroundColor: purple800
+                backgroundColor: arvadosPurple
             }
         },
         MuiTabs: {
@@ -67,13 +66,13 @@ export const themeOptions: ArvadosThemeOptions = {
                 color: grey600
             },
             indicator: {
-                backgroundColor: purple800
+                backgroundColor: arvadosPurple
             }
         },
         MuiTab: {
             selected: {
                 fontWeight: 700,
-                color: purple800
+                color: arvadosPurple
             }
         },
         MuiList: {
@@ -112,7 +111,7 @@ export const themeOptions: ArvadosThemeOptions = {
             },
             underline: {
                 '&:after': {
-                    borderBottomColor: purple800
+                    borderBottomColor: arvadosPurple
                 },
                 '&:hover:not($disabled):not($focused):not($error):before': {
                     borderBottom: '1px solid inherit'
@@ -125,7 +124,7 @@ export const themeOptions: ArvadosThemeOptions = {
             },
             focused: {
                 "&$focused:not($error)": {
-                    color: purple800
+                    color: arvadosPurple
                 }
             }
         }
@@ -137,8 +136,9 @@ export const themeOptions: ArvadosThemeOptions = {
     },
     palette: {
         primary: {
-            main: rocheBlue,
-            dark: blue.A100
+            main: teal.A700,
+            dark: blue.A100,
+            contrastText: '#fff'
         }
     }
 };
index afc0fed1adbc6c6aba4d4b8096d794d3f7e154fa..c0668b8291fa3e7b89010ef0638cb8f2c16bafdd 100644 (file)
@@ -4,6 +4,7 @@
 
 import * as React from 'react';
 import AccessTime from '@material-ui/icons/AccessTime';
+import Add from '@material-ui/icons/Add';
 import ArrowBack from '@material-ui/icons/ArrowBack';
 import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
 import BubbleChart from '@material-ui/icons/BubbleChart';
@@ -48,6 +49,7 @@ import HelpOutline from '@material-ui/icons/HelpOutline';
 
 export type IconType = React.SFC<{ className?: string }>;
 
+export const AddIcon: IconType = (props) => <Add {...props} />;
 export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
 export const AdvancedIcon: IconType = (props) => <SettingsApplications {...props} />;
 export const BackIcon: IconType = (props) => <ArrowBack {...props} />;
index de2ba4cba65bbd008f581d20c4db5c16b1cbdc05..5f646de5f72911af6708ae78b80e2155ffe8a1a1 100644 (file)
@@ -12,12 +12,18 @@ describe("FilterBuilder", () => {
         filters = new FilterBuilder();
     });
 
-    it("should add 'equal' rule", () => {
+    it("should add 'equal' rule (string)", () => {
         expect(
             filters.addEqual("etag", "etagValue").getFilters()
         ).toEqual(`["etag","=","etagValue"]`);
     });
 
+    it("should add 'equal' rule (boolean)", () => {
+        expect(
+            filters.addEqual("is_trashed", true).getFilters()
+        ).toEqual(`["is_trashed","=",true]`);
+    });
+
     it("should add 'like' rule", () => {
         expect(
             filters.addLike("etag", "etagValue").getFilters()
index b5558dbb16a291f8ecb81fdbd642f742bb99b9d3..06a040e3cc373f8206c83783c90e8cdf010d095a 100644 (file)
@@ -11,7 +11,7 @@ export function joinFilters(filters0?: string, filters1?: string) {
 export class FilterBuilder {
     constructor(private filters = "") { }
 
-    public addEqual(field: string, value?: string, resourcePrefix?: string) {
+    public addEqual(field: string, value?: string | boolean, resourcePrefix?: string) {
         return this.addCondition(field, "=", value, "", "", resourcePrefix );
     }
 
@@ -35,11 +35,15 @@ export class FilterBuilder {
         return this.filters;
     }
 
-    private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "", resourcePrefix?: string) {
+    private addCondition(field: string, cond: string, value?: string | string[] | boolean, prefix: string = "", postfix: string = "", resourcePrefix?: string) {
         if (value) {
-            value = typeof value === "string"
-                ? `"${prefix}${value}${postfix}"`
-                : `["${value.join(`","`)}"]`;
+            if (typeof value === "string") {
+                value = `"${prefix}${value}${postfix}"`;
+            } else if (Array.isArray(value)) {
+                value = `["${value.join(`","`)}"]`;
+            } else {
+                value = value ? "true" : "false";
+            }
 
             const resPrefix = resourcePrefix
                 ? _.snakeCase(resourcePrefix) + "."
index 19ed3be13194982a97527417adb3b4eff8438389..6e8fa542478766368968e0912fc636da5f4c9b6b 100644 (file)
@@ -19,7 +19,6 @@ import { TrashPanelColumnNames, TrashPanelFilter } from "~/views/trash-panel/tra
 import { ProjectResource } from "~/models/project";
 import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel";
 import { updateFavorites } from "~/store/favorites/favorites-actions";
-import { TrashableResource } from "~/models/resource";
 import { snackbarActions } from "~/store/snackbar/snackbar-actions";
 import { updateResources } from "~/store/resources/resources-actions";
 
@@ -57,14 +56,13 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
                         .addIsA("uuid", typeFilters.map(f => f.type))
                         .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
                         .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+                        .addEqual("is_trashed", true)
                         .getFilters(),
                     recursive: true,
                     includeTrash: true
                 });
 
-            const items = listResults.items
-                .filter(it => (it as TrashableResource).isTrashed)
-                .map(it => it.uuid);
+            const items = listResults.items.map(it => it.uuid);
 
             api.dispatch(trashPanelActions.SET_ITEMS({
                 ...listResultsToDataExplorerItemsMeta(listResults),
diff --git a/src/views-components/side-panel-button/side-panel-button.tsx b/src/views-components/side-panel-button/side-panel-button.tsx
new file mode 100644 (file)
index 0000000..e39b378
--- /dev/null
@@ -0,0 +1,120 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from '~/store/store';
+import { getProperty } from '~/store/properties/properties';
+import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { PopoverOrigin } from '@material-ui/core/Popover';
+import { StyleRulesCallback, WithStyles, withStyles, Toolbar, Grid, Button, MenuItem, Menu } from '@material-ui/core';
+import { AddIcon, CollectionIcon, ProcessIcon, ProjectIcon } from '~/components/icon/icon';
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
+import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
+import { matchProjectRoute } from '~/routes/routes';
+
+type CssRules = 'button' | 'menuItem' | 'icon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    button: {
+        boxShadow: 'none',
+        padding: '2px 10px 2px 5px',
+        fontSize: '0.75rem'
+    },
+    menuItem: {
+        fontSize: '0.875rem',
+        color: theme.palette.grey["700"]
+    },
+    icon: {
+        marginRight: theme.spacing.unit
+    }
+});
+
+interface SidePanelDataProps {
+    currentItemId: string;
+    buttonVisible: boolean;
+}
+
+interface SidePanelState {
+    anchorEl: any;
+}
+
+type SidePanelProps = SidePanelDataProps & DispatchProp & WithStyles<CssRules>;
+
+const transformOrigin: PopoverOrigin = {
+    vertical: -50,
+    horizontal: 45
+};
+
+const isButtonVisible = ({ router }: RootState) => {
+    const pathname = router.location ? router.location.pathname : '';
+    const match = matchProjectRoute(pathname);
+    return !!match;
+};
+
+export const SidePanelButton = withStyles(styles)(
+    connect((state: RootState) => ({
+        currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+        buttonVisible: isButtonVisible(state)
+    }))(
+        class extends React.Component<SidePanelProps> {
+
+            state: SidePanelState = {
+                anchorEl: undefined
+            };
+
+            render() {
+                const { classes, buttonVisible  } = this.props;
+                const { anchorEl } = this.state;
+                return <Toolbar>
+                    {buttonVisible  && <Grid container>
+                        <Grid container item xs alignItems="center" justify="center">
+                            <Button variant="contained" color="primary" size="small" className={classes.button}
+                                aria-owns={anchorEl ? 'aside-menu-list' : undefined}
+                                aria-haspopup="true"
+                                onClick={this.handleOpen}>
+                                <AddIcon />
+                                New
+                            </Button>
+                            <Menu
+                                id='aside-menu-list'
+                                anchorEl={anchorEl}
+                                open={Boolean(anchorEl)}
+                                onClose={this.handleClose}
+                                onClick={this.handleClose}
+                                transformOrigin={transformOrigin}>
+                                <MenuItem className={classes.menuItem} onClick={this.handleNewCollectionClick}>
+                                    <CollectionIcon className={classes.icon} /> New collection
+                                </MenuItem>
+                                <MenuItem className={classes.menuItem}>
+                                    <ProcessIcon className={classes.icon} /> Run a process
+                                </MenuItem>
+                                <MenuItem className={classes.menuItem} onClick={this.handleNewProjectClick}>
+                                    <ProjectIcon className={classes.icon} /> New project
+                                </MenuItem>
+                            </Menu>
+                        </Grid>
+                    </Grid> }
+                </Toolbar>;
+            }
+
+            handleNewProjectClick = () => {
+                this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
+            }
+
+            handleNewCollectionClick = () => {
+                this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
+            }
+
+            handleClose = () => {
+                this.setState({ anchorEl: undefined });
+            }
+
+            handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
+                this.setState({ anchorEl: event.currentTarget });
+            }
+        }
+    )
+);
\ No newline at end of file
index 70bc92b7162aef6acc157dac5a1dc15d26d295a7..fffe3344c9ce66dd94c3a8d79933f83047aaf7a9 100644 (file)
@@ -4,12 +4,13 @@
 
 import * as React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { SidePanelTree, SidePanelTreeProps } from '~/views-components/side-panel-tree/side-panel-tree';
 import { compose, Dispatch } from 'redux';
 import { connect } from 'react-redux';
 import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action';
+import { Grid } from '@material-ui/core';
+import { SidePanelButton } from '~/views-components/side-panel-button/side-panel-button';
 
 const DRAWER_WITDH = 240;
 
@@ -35,6 +36,7 @@ export const SidePanel = compose(
     withStyles(styles),
     connect(undefined, mapDispatchToProps)
 )(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
-    <div className={classes.root}>
+    <Grid item xs>
+        <SidePanelButton />
         <SidePanelTree {...props} />
-    </div>);
+    </Grid>);
\ No newline at end of file
index 63aedaddeb4e17a62cfd318a12cdea66114c87e2..2c962ef829a5158b743a52b0ab548221c02ce63a 100644 (file)
@@ -31,7 +31,7 @@ import { filterResources } from '~/store/resources/resources';
 import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 
-type CssRules = 'root' | "toolbar" | "button";
+type CssRules = 'root' | "button";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -39,10 +39,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         width: '100%',
         height: '100%'
     },
-    toolbar: {
-        paddingBottom: theme.spacing.unit * 3,
-        textAlign: "right"
-    },
     button: {
         marginLeft: theme.spacing.unit
     },
@@ -143,17 +139,6 @@ export const ProjectPanel = withStyles(styles)(
             render() {
                 const { classes } = this.props;
                 return <div className={classes.root}>
-                    <div className={classes.toolbar}>
-                        <Button color="primary" onClick={this.handleNewCollectionClick} variant="raised" className={classes.button}>
-                            New collection
-                        </Button>
-                        <Button color="primary" variant="raised" className={classes.button}>
-                            Run a process
-                        </Button>
-                        <Button color="primary" onClick={this.handleNewProjectClick} variant="raised" className={classes.button}>
-                            New project
-                        </Button>
-                    </div>
                     {this.hasAnyItems()
                         ? <DataExplorer
                             id={PROJECT_PANEL_ID}
@@ -179,14 +164,6 @@ export const ProjectPanel = withStyles(styles)(
                 return resource.ownerUuid === this.props.currentItemId;
             }
 
-            handleNewProjectClick = () => {
-                this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
-            }
-
-            handleNewCollectionClick = () => {
-                this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
-            }
-
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
                 const menuKind = resourceKindToContextMenuKind(resourceUuid);
                 const resource = getResource<ProjectResource>(resourceUuid)(this.props.resources);
index 4a1e197d540dc059a8cfe15c6d642c218a308a0d..db7d8f688096e34cb80ca4a1e7bc796b1e0d11bb 100644 (file)
@@ -99,11 +99,6 @@ export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
                 selected: true,
                 type: ResourceKind.COLLECTION
             },
-            {
-                name: resourceLabel(ResourceKind.PROCESS),
-                selected: true,
-                type: ResourceKind.PROCESS
-            },
             {
                 name: resourceLabel(ResourceKind.PROJECT),
                 selected: true,
index fee7652b0ae89fbd619037ecfa6fc81855cd75a3..9f1ab478ffc63f8f042af94eb6c3dd87dddd3978 100644 (file)
@@ -45,7 +45,7 @@ import { Grid } from '@material-ui/core';
 import { SharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel';
 import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
 
-type CssRules = 'root' | 'contentWrapper' | 'content' | 'appBar';
+type CssRules = 'root' | 'asidePanel' | 'contentWrapper' | 'content' | 'appBar';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -53,6 +53,10 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         width: '100vw',
         height: '100vh'
     },
+    asidePanel: {
+        maxWidth: '240px',
+        background: theme.palette.background.default
+    },
     contentWrapper: {
         background: theme.palette.background.default,
         minWidth: 0,
@@ -95,14 +99,11 @@ export const Workbench = withStyles(styles)(
             state = {
                 searchText: "",
             };
-
             render() {
+                const { classes } = this.props;
                 return <>
-                    <Grid
-                        container
-                        direction="column"
-                        className={this.props.classes.root}>
-                        <Grid className={this.props.classes.appBar}>
+                    <Grid container direction="column" className={classes.root}>
+                        <Grid className={classes.appBar}>
                             <MainAppBar
                                 searchText={this.state.searchText}
                                 user={this.props.user}
@@ -110,26 +111,15 @@ export const Workbench = withStyles(styles)(
                                 buildInfo={this.props.buildInfo} />
                         </Grid>
                         {this.props.user &&
-                            <Grid
-                                container
-                                item
-                                xs
-                                alignItems="stretch"
-                                wrap="nowrap">
-                                <Grid item>
+                            <Grid container item xs alignItems="stretch" wrap="nowrap">
+                                <Grid container item xs component='aside' direction='column' className={classes.asidePanel}>
                                     <SidePanel />
                                 </Grid>
-                                <Grid
-                                    container
-                                    item
-                                    xs
-                                    component="main"
-                                    direction="column"
-                                    className={this.props.classes.contentWrapper}>
+                                <Grid container item xs component="main" direction="column" className={classes.contentWrapper}>
                                     <Grid item>
                                         <MainContentBar />
                                     </Grid>
-                                    <Grid item xs className={this.props.classes.content}>
+                                    <Grid item xs className={classes.content}>
                                         <Switch>
                                             <Route path={Routes.PROJECTS} component={ProjectPanel} />
                                             <Route path={Routes.COLLECTIONS} component={CollectionPanel} />