refs #14161 Merge branch 'origin/14161-inputs-focus-enter-action'
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 18 Sep 2018 11:56:54 +0000 (13:56 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 18 Sep 2018 11:58:09 +0000 (13:58 +0200)
Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

126 files changed:
package.json
src/common/config.ts
src/common/custom-theme.ts
src/components/code-snippet/code-snippet.tsx
src/components/collection-panel-files/collection-panel-files.tsx
src/components/column-selector/column-selector.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table-filters/data-table-filters.tsx
src/components/data-table/data-table.tsx
src/components/default-code-snippet/default-code-snippet.tsx
src/components/dropdown-menu/dropdown-menu.tsx
src/components/file-tree/file-tree-item.tsx
src/components/icon/icon.tsx
src/components/search-bar/search-bar.tsx
src/components/search-input/search-input.tsx
src/index.tsx
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/services/ancestors-service/ancestors-service.ts
src/services/api/api-actions.ts [new file with mode: 0644]
src/services/api/filter-builder.test.ts
src/services/api/filter-builder.ts
src/services/api/url-builder.test.ts [new file with mode: 0644]
src/services/api/url-builder.ts
src/services/auth-service/auth-service.ts
src/services/collection-service/collection-service.ts
src/services/common-service/common-resource-service.test.ts
src/services/common-service/common-resource-service.ts
src/services/common-service/trashable-resource-service.ts
src/services/container-request-service/container-request-service.ts
src/services/container-service/container-service.ts
src/services/favorite-service/favorite-service.ts
src/services/groups-service/groups-service.test.ts
src/services/groups-service/groups-service.ts
src/services/keep-service/keep-service.ts
src/services/link-service/link-service.ts
src/services/log-service/log-service.ts
src/services/project-service/project-service.test.ts
src/services/services.ts
src/services/user-service/user-service.ts
src/store/auth/auth-actions.test.ts
src/store/auth/auth-reducer.test.ts
src/store/breadcrumbs/breadcrumbs-actions.ts
src/store/collections/collection-copy-actions.ts
src/store/collections/collection-create-actions.ts
src/store/collections/collection-move-actions.ts
src/store/collections/collection-partial-copy-actions.ts
src/store/collections/collection-update-actions.ts
src/store/collections/collection-upload-actions.ts
src/store/context-menu/context-menu-actions.ts
src/store/copy-dialog/copy-dialog.ts [new file with mode: 0644]
src/store/current-token-dialog/current-token-dialog-actions.tsx [new file with mode: 0644]
src/store/data-explorer/data-explorer-reducer.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/favorites/favorites-actions.ts
src/store/navigation/navigation-action.ts
src/store/processes/process-command-actions.ts [new file with mode: 0644]
src/store/processes/process-copy-actions.ts [new file with mode: 0644]
src/store/processes/process-move-actions.ts [new file with mode: 0644]
src/store/processes/process-update-actions.ts [new file with mode: 0644]
src/store/processes/process.ts
src/store/progress-indicator/progress-indicator-actions.ts [new file with mode: 0644]
src/store/progress-indicator/progress-indicator-reducer.ts [new file with mode: 0644]
src/store/progress-indicator/with-progress.ts [new file with mode: 0644]
src/store/project-panel/project-panel-middleware-service.ts
src/store/shared-with-me-panel/shared-with-me-middleware-service.ts [new file with mode: 0644]
src/store/shared-with-me-panel/shared-with-me-panel-actions.ts [new file with mode: 0644]
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/side-panel/side-panel-action.ts
src/store/snackbar/snackbar-actions.ts
src/store/snackbar/snackbar-reducer.ts
src/store/store.ts
src/store/trash-panel/trash-panel-middleware-service.ts
src/store/trash/trash-actions.ts
src/store/workbench/workbench-actions.ts
src/validators/validators.tsx
src/views-components/context-menu/action-sets/process-action-set.ts
src/views-components/context-menu/action-sets/process-resource-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/context-menu.tsx
src/views-components/current-token-dialog/current-token-dialog.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/details-panel/details-data.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
src/views-components/dialog-copy/dialog-copy.tsx [moved from src/views-components/dialog-copy/dialog-collection-copy.tsx with 78% similarity]
src/views-components/dialog-forms/copy-collection-dialog.ts
src/views-components/dialog-forms/copy-process-dialog.ts [new file with mode: 0644]
src/views-components/dialog-forms/move-process-dialog.ts [new file with mode: 0644]
src/views-components/dialog-forms/update-process-dialog.ts [new file with mode: 0644]
src/views-components/dialog-update/dialog-process-update.tsx [new file with mode: 0644]
src/views-components/form-fields/collection-form-fields.tsx
src/views-components/form-fields/process-form-fields.tsx [new file with mode: 0644]
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/process-command-dialog/process-command-dialog.tsx [new file with mode: 0644]
src/views-components/progress/content-progress.tsx [new file with mode: 0644]
src/views-components/progress/side-panel-progress.tsx [new file with mode: 0644]
src/views-components/progress/workbench-progress.tsx [new file with mode: 0644]
src/views-components/side-panel-button/side-panel-button.tsx [new file with mode: 0644]
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views-components/side-panel/side-panel.tsx
src/views-components/snackbar/snackbar.tsx
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/process-log-panel/process-log-code-snippet.tsx
src/views/process-log-panel/process-log-main-card.tsx
src/views/process-log-panel/process-log-panel-root.tsx
src/views/process-log-panel/process-log-panel.tsx
src/views/process-panel/process-information-card.tsx
src/views/process-panel/process-panel-root.tsx
src/views/process-panel/process-panel.tsx
src/views/process-panel/process-subprocesses-card.tsx
src/views/process-panel/process-subprocesses.tsx
src/views/process-panel/subprocesses-card.tsx
src/views/project-panel/project-panel.tsx
src/views/shared-with-me-panel/shared-with-me-panel.tsx [new file with mode: 0644]
src/views/trash-panel/trash-panel.tsx
src/views/workbench/workbench.tsx
tslint.json
typings/global.d.ts
yarn.lock

index 0e6435ebf536da29d695f224423f56780f4fca40..620ff5a6d03a6d7d616ab9d32b0ec2f47c0530ac 100644 (file)
     "react-router-dom": "4.3.1",
     "react-router-redux": "5.0.0-alpha.9",
     "react-scripts-ts": "2.17.0",
+    "react-splitter-layout": "3.0.1",
+    "react-transition-group": "2.4.0",
     "redux": "4.0.0",
     "redux-thunk": "2.3.0",
-    "unionize": "2.1.2"
+    "unionize": "2.1.2",
+    "uuid": "3.3.2"
   },
   "scripts": {
     "start": "react-scripts-ts start",
@@ -46,6 +49,7 @@
     "@types/react-router-redux": "5.0.15",
     "@types/redux-devtools": "3.0.44",
     "@types/redux-form": "7.4.5",
+    "@types/uuid": "3.4.4",
     "axios-mock-adapter": "1.15.0",
     "enzyme": "3.4.4",
     "enzyme-adapter-react-16": "1.2.0",
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 20790100dcf2012f7f12ec6912aa30d0f56ca875..ff0eb5e34ce7449f672464485b5879cc98f5d382 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;
@@ -26,14 +27,10 @@ interface Colors {
     yellow700: string;
     red900: string;
     blue500: string;
-    grey500: string;
-    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"];
@@ -47,8 +44,6 @@ export const themeOptions: ArvadosThemeOptions = {
             yellow700: yellow["700"],
             red900: red['900'],
             blue500: blue['500'],
-            grey500,
-            grey700
         }
     },
     overrides: {
@@ -59,7 +54,7 @@ export const themeOptions: ArvadosThemeOptions = {
         },
         MuiAppBar: {
             colorPrimary: {
-                backgroundColor: purple800
+                backgroundColor: arvadosPurple
             }
         },
         MuiTabs: {
@@ -67,13 +62,13 @@ export const themeOptions: ArvadosThemeOptions = {
                 color: grey600
             },
             indicator: {
-                backgroundColor: purple800
+                backgroundColor: arvadosPurple
             }
         },
         MuiTab: {
             selected: {
                 fontWeight: 700,
-                color: purple800
+                color: arvadosPurple
             }
         },
         MuiList: {
@@ -112,7 +107,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 +120,7 @@ export const themeOptions: ArvadosThemeOptions = {
             },
             focused: {
                 "&$focused:not($error)": {
-                    color: purple800
+                    color: arvadosPurple
                 }
             }
         }
@@ -137,8 +132,9 @@ export const themeOptions: ArvadosThemeOptions = {
     },
     palette: {
         primary: {
-            main: rocheBlue,
-            dark: blue.A100
+            main: teal.A700,
+            dark: blue.A100,
+            contrastText: '#fff'
         }
     }
 };
index b622210f008eb1c95c43897c6f94e99560fa5cc2..6cba299f1580a70d8076afc98606af1479ab5fb1 100644 (file)
@@ -5,29 +5,30 @@
 import * as React from 'react';
 import { StyleRulesCallback, WithStyles, Typography, withStyles, Theme } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
+import * as classNames from 'classnames';
 
 type CssRules = 'root';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         boxSizing: 'border-box',
-        width: '100%',
-        height: 'auto',
-        maxHeight: '550px',
-        overflow: 'scroll',
+        overflow: 'auto',
         padding: theme.spacing.unit
     }
 });
 
 export interface CodeSnippetDataProps {
     lines: string[];
+    className?: string;
 }
 
 type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
 
 export const CodeSnippet = withStyles(styles)(
-    ({ classes, lines }: CodeSnippetProps) =>
-        <Typography component="div" className={classes.root}>
+    ({ classes, lines, className }: CodeSnippetProps) =>
+        <Typography 
+        component="div" 
+        className={classNames(classes.root, className)}>
             {
                 lines.map((line: string, index: number) => {
                     return <Typography key={index} component="pre">{line}</Typography>;
index b037fcf41b6df2a28183c8b1d3c802765ba26715..341aa5ac062e6dab06694fd98b4207d32c2fd6a1 100644 (file)
@@ -53,11 +53,11 @@ export const CollectionPanelFiles =
                 <CardHeader
                     className={classes.cardSubheader}
                     action={
-                        <IconButton onClick={onOptionsMenuOpen}>
-                            <Tooltip title="More options">
+                        <Tooltip title="More options">
+                            <IconButton onClick={onOptionsMenuOpen}>
                                 <CustomizeTableIcon />
-                            </Tooltip>
-                        </IconButton>
+                            </IconButton>
+                        </Tooltip>
                     } />
                 <Grid container justify="space-between">
                     <Typography variant="caption" className={classes.nameHeader}>
index 210dc5aedda07bcf59f15cb7607c7f2b73662294..ccff61811b6303dacf9a8362e4c3853391d768b2 100644 (file)
@@ -29,34 +29,34 @@ export type ColumnSelectorProps = ColumnSelectorDataProps & WithStyles<CssRules>
 
 export const ColumnSelector = withStyles(styles)(
     ({ columns, onColumnToggle, classes }: ColumnSelectorProps) =>
-    <Popover triggerComponent={ColumnSelectorTrigger}>
-        <Paper>
-            <List dense>
-                {columns
-                    .filter(column => column.configurable)
-                    .map((column, index) =>
-                        <ListItem
-                            button
-                            key={index}
-                            onClick={() => onColumnToggle(column)}>
-                            <Checkbox
-                                disableRipple
-                                color="primary"
-                                checked={column.selected}
-                                className={classes.checkbox} />
-                            <ListItemText>
-                                {column.name}
-                            </ListItemText>
-                        </ListItem>
-                    )}
-            </List>
-        </Paper>
-    </Popover>
+        <Popover triggerComponent={ColumnSelectorTrigger}>
+            <Paper>
+                <List dense>
+                    {columns
+                        .filter(column => column.configurable)
+                        .map((column, index) =>
+                            <ListItem
+                                button
+                                key={index}
+                                onClick={() => onColumnToggle(column)}>
+                                <Checkbox
+                                    disableRipple
+                                    color="primary"
+                                    checked={column.selected}
+                                    className={classes.checkbox} />
+                                <ListItemText>
+                                    {column.name}
+                                </ListItemText>
+                            </ListItem>
+                        )}
+                </List>
+            </Paper>
+        </Popover>
 );
 
 export const ColumnSelectorTrigger = (props: IconButtonProps) =>
-    <IconButton {...props}>
-        <Tooltip title="Filters">
-            <MenuIcon />
-        </Tooltip>
-    </IconButton>;
+    <Tooltip title="Filters">
+        <IconButton {...props}>
+            <MenuIcon aria-label="Filters" />
+        </IconButton>
+    </Tooltip>;
index 58507fb07415c81b06189b93aa4ca714f1947122..59f4dbebb47832ff4ec5a827c44eb1a434db61ac 100644 (file)
@@ -33,6 +33,7 @@ interface DataExplorerDataProps<T> {
     page: number;
     contextMenuColumn: boolean;
     dataTableDefaultView?: React.ReactNode;
+    working?: boolean;
 }
 
 interface DataExplorerActionProps<T> {
@@ -60,7 +61,7 @@ export const DataExplorer = withStyles(styles)(
         }
         render() {
             const {
-                columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
+                columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView
@@ -87,6 +88,7 @@ export const DataExplorer = withStyles(styles)(
                     onFiltersChange={onFiltersChange}
                     onSortToggle={onSortToggle}
                     extractKey={extractKey}
+                    working={working}
                     defaultView={dataTableDefaultView}
                 />
                 <Toolbar>
@@ -114,11 +116,11 @@ export const DataExplorer = withStyles(styles)(
 
         renderContextMenuTrigger = (item: T) =>
             <Grid container justify="flex-end">
-                <IconButton onClick={event => this.props.onContextMenu(event, item)}>
-                    <Tooltip title="More options">
+                <Tooltip title="More options">
+                    <IconButton onClick={event => this.props.onContextMenu(event, item)}>
                         <MoreVertIcon />
-                    </Tooltip>
-                </IconButton>
+                    </IconButton>
+                </Tooltip>
             </Grid>
 
         contextMenuColumn: DataColumn<any> = {
index d288a5a3dbfc8997cee987598cfb767048e748f9..b8a6e834869eb5a3e59aa1125b5e8a867d8fe5e6 100644 (file)
@@ -18,7 +18,8 @@ import {
     Card,
     CardActions,
     Typography,
-    CardContent
+    CardContent,
+    Tooltip
 } from "@material-ui/core";
 import * as classnames from "classnames";
 import { DefaultTransformOrigin } from "../popover/helpers";
@@ -88,16 +89,18 @@ export const DataTableFilters = withStyles(styles)(
             const { name, classes, children } = this.props;
             const isActive = this.state.filters.some(f => f.selected);
             return <>
-                <ButtonBase
-                    className={classnames([classes.root, { [classes.active]: isActive }])}
-                    component="span"
-                    onClick={this.open}
-                    disableRipple>
-                    {children}
-                    <i className={classnames(["fas fa-filter", classes.icon])}
-                        data-fa-transform="shrink-3"
-                        ref={this.icon} />
-                </ButtonBase>
+                <Tooltip title='Filters'>
+                    <ButtonBase
+                        className={classnames([classes.root, { [classes.active]: isActive }])}
+                        component="span"
+                        onClick={this.open}
+                        disableRipple>
+                        {children}
+                        <i className={classnames(["fas fa-filter", classes.icon])}
+                            data-fa-transform="shrink-3"
+                            ref={this.icon} />
+                    </ButtonBase>
+                </Tooltip>
                 <Popover
                     anchorEl={this.state.anchorEl}
                     open={!!this.state.anchorEl}
index 65531a5b5543d1e95a0fb49f22514b96d53f49d8..25d81c62fa7dc89a8135c8a718c5dc02800d22a6 100644 (file)
@@ -19,6 +19,7 @@ export interface DataTableDataProps<T> {
     onSortToggle: (column: DataColumn<T>) => void;
     onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
     extractKey?: (item: T) => React.Key;
+    working?: boolean;
     defaultView?: React.ReactNode;
 }
 
@@ -63,7 +64,7 @@ export const DataTable = withStyles(styles)(
                             {items.map(this.renderBodyRow)}
                         </TableBody>
                     </Table>
-                    {items.length === 0 && this.renderNoItemsPlaceholder()}
+                    {items.length === 0 && this.props.working !== undefined && !this.props.working && this.renderNoItemsPlaceholder()}
                 </div>
             </div>;
         }
@@ -71,7 +72,7 @@ export const DataTable = withStyles(styles)(
         renderNoItemsPlaceholder = () => {
             return this.props.defaultView
                 ? this.props.defaultView
-                : <DataTableDefaultView />;
+                : <DataTableDefaultView/>;
         }
 
         renderHeadCell = (column: DataColumn<T>, index: number) => {
index 541f390616ef369d801a0067cddabcf89dbcb5ad..e8b89f321ca809302bcbdab1f09f4c13d09f6d38 100644 (file)
@@ -19,13 +19,11 @@ const theme = createMuiTheme({
         }
     },
     typography: {
-        fontFamily: 'VT323'
+        fontFamily: 'monospace'
     }
 });
 
-type DefaultCodeSnippet = CodeSnippetDataProps;
-
-export const DefaultCodeSnippet = (props: DefaultCodeSnippet) => 
+export const DefaultCodeSnippet = (props: CodeSnippetDataProps) => 
     <MuiThemeProvider theme={theme}>
-        <CodeSnippet lines={props.lines} />
+        <CodeSnippet {...props} />
     </MuiThemeProvider>;
\ No newline at end of file
index a00df75dd4e67709f2a40549f90199740781e396..cd68d5ba27a22298dd13a79b7c71e828eac77580 100644 (file)
@@ -33,15 +33,15 @@ export class DropdownMenu extends React.Component<DropdownMenuProps, DropdownMen
         const { anchorEl } = this.state;
         return (
             <div>
-                <IconButton
-                    aria-owns={anchorEl ? id : undefined}
-                    aria-haspopup="true"
-                    color="inherit"
-                    onClick={this.handleOpen}>
-                    <Tooltip title={title}>
+                <Tooltip title={title}>
+                    <IconButton
+                        aria-owns={anchorEl ? id : undefined}
+                        aria-haspopup="true"
+                        color="inherit"
+                        onClick={this.handleOpen}>
                         {icon}
-                    </Tooltip>
-                </IconButton>
+                    </IconButton>
+                </Tooltip>
                 <Menu
                     id={id}
                     anchorEl={anchorEl}
index 9b248e0c8b5f488bae4dca288501df8d724bc2e0..02dd04b4c84e3a4011fe8a9ac4ef5599a540300c 100644 (file)
@@ -47,13 +47,13 @@ export const FileTreeItem = withStyles(fileTreeItemStyle)(
                 <Typography
                     className={classes.sizeInfo}
                     variant="caption">{formatFileSize(item.data.size)}</Typography>
-                <IconButton
-                    className={classes.button}
-                    onClick={this.handleClick}>
-                    <Tooltip title="More options">
+                <Tooltip title="More options">
+                    <IconButton
+                        className={classes.button}
+                        onClick={this.handleClick}>
                         <MoreOptionsIcon />
-                    </Tooltip>
-                </IconButton>
+                    </IconButton>
+                </Tooltip>
             </div >;
         }
 
index afc0fed1adbc6c6aba4d4b8096d794d3f7e154fa..06a56172ffb52077d48e6c05734b5a627dd4b697 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} />;
@@ -62,8 +64,11 @@ export const DetailsIcon: IconType = (props) => <Info {...props} />;
 export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
 export const FavoriteIcon: IconType = (props) => <Star {...props} />;
 export const HelpIcon: IconType = (props) => <Help {...props} />;
+export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
+export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
 export const InputIcon: IconType = (props) => <InsertDriveFile {...props} />;
 export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
+export const MailIcon: IconType = (props) => <Mail {...props} />;
 export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
 export const MoveToIcon: IconType = (props) => <Input {...props} />;
 export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
@@ -90,6 +95,3 @@ export const TrashIcon: IconType = (props) => <Delete {...props} />;
 export const UserPanelIcon: IconType = (props) => <Person {...props} />;
 export const UsedByIcon: IconType = (props) => <Folder {...props} />;
 export const WorkflowIcon: IconType = (props) => <Code {...props} />;
-export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
-export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
-export const MailIcon: IconType = (props) => <Mail {...props} />;
index de9e7de5261c299870cc16d4618971869dfc273b..f01b5692f204ca3a72683f71cdecdbaf194a4108 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
+import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, Tooltip } from '@material-ui/core';
 import SearchIcon from '@material-ui/icons/Search';
 
 type CssRules = 'container' | 'input' | 'button';
@@ -58,7 +58,7 @@ export const SearchBar = withStyles(styles)(
         timeout: number;
 
         render() {
-            const {classes} = this.props;
+            const { classes } = this.props;
             return <Paper className={classes.container}>
                 <form onSubmit={this.handleSubmit}>
                     <input
@@ -67,20 +67,22 @@ export const SearchBar = withStyles(styles)(
                         placeholder="Search"
                         value={this.state.value}
                     />
-                    <IconButton className={classes.button}>
-                        <SearchIcon/>
-                    </IconButton>
+                    <Tooltip title='Search'>
+                        <IconButton className={classes.button}>
+                            <SearchIcon />
+                        </IconButton>
+                    </Tooltip>
                 </form>
             </Paper>;
         }
 
         componentDidMount() {
-            this.setState({value: this.props.value});
+            this.setState({ value: this.props.value });
         }
 
         componentWillReceiveProps(nextProps: SearchBarProps) {
             if (nextProps.value !== this.props.value) {
-                this.setState({value: nextProps.value});
+                this.setState({ value: nextProps.value });
             }
         }
 
@@ -96,7 +98,7 @@ export const SearchBar = withStyles(styles)(
 
         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
             clearTimeout(this.timeout);
-            this.setState({value: event.target.value});
+            this.setState({ value: event.target.value });
             this.timeout = window.setTimeout(
                 () => this.props.onSearch(this.state.value),
                 this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
index dc02cd3d13ba13bedd50daf01963bb5a76d98f80..f2ec2d6439024c066eebb2a0b8a18a0ec4fba901 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { IconButton, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment } from '@material-ui/core';
+import { IconButton, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment, Tooltip } from '@material-ui/core';
 import SearchIcon from '@material-ui/icons/Search';
 
 type CssRules = 'container' | 'input' | 'button';
@@ -68,12 +68,14 @@ export const SearchInput = withStyles(styles)(
                         onChange={this.handleChange}
                         endAdornment={
                             <InputAdornment position="end">
-                                <IconButton
-                                    onClick={this.handleSubmit}>
-                                    <SearchIcon/>
-                                </IconButton>
+                                <Tooltip title='Search'>
+                                    <IconButton
+                                        onClick={this.handleSubmit}>
+                                        <SearchIcon />
+                                    </IconButton>
+                                </Tooltip>
                             </InputAdornment>
-                        }/>
+                        } />
                 </FormControl>
             </form>;
         }
index a921b471383dbc399521b9298ee7f80bd3cb8a00..0d026f2389f2713372607cc718e681758e769725 100644 (file)
@@ -35,6 +35,10 @@ 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';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 
 const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
 const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
@@ -53,16 +57,26 @@ addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActio
 addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
+addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
 addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 
 fetchConfig()
-    .then((config) => {
+    .then(({ config, apiHost }) => {
         const history = createBrowserHistory();
-        const services = createServices(config);
+        const services = createServices(config, {
+            progressFn: (id, working) => {
+                store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
+            },
+            errorFn: (id, error) => {
+                console.error("Backend error:", error);
+                store.dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Backend error", kind: SnackbarKind.ERROR }));
+            }
+        });
         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} />;
@@ -83,8 +97,6 @@ fetchConfig()
             <App />,
             document.getElementById('root') as HTMLElement
         );
-
-
     });
 
 const initListener = (history: History, store: RootStore, services: ServiceRepository, config: Config) => {
index 00fb4bc05acbfcf8a4dc47e3c20f19516443cf19..33e0bef753c6f5535a18a02e94b7e8e3012052ab 100644 (file)
@@ -4,9 +4,11 @@
 
 import { History, Location } from 'history';
 import { RootStore } from '~/store/store';
-import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute } from './routes';
+import { matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute, matchTrashRoute, matchRootRoute, matchSharedWithMeRoute } from './routes';
 import { loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog } from '~/store/workbench/workbench-actions';
 import { navigateToRootProject } from '~/store/navigation/navigation-action';
+import { navigateToSharedWithMe } from '../store/navigation/navigation-action';
+import { loadSharedWithMe } from '../store/workbench/workbench-actions';
 
 export const addRouteChangeHandlers = (history: History, store: RootStore) => {
     const handler = handleLocationChange(store);
@@ -22,7 +24,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const trashMatch = matchTrashRoute(pathname);
     const processMatch = matchProcessRoute(pathname);
     const processLogMatch = matchProcessLogRoute(pathname);
-    
+    const sharedWithMeMatch = matchSharedWithMeRoute(pathname);
+
     if (projectMatch) {
         store.dispatch(loadProject(projectMatch.params.id));
     } else if (collectionMatch) {
@@ -37,5 +40,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(loadProcessLog(processLogMatch.params.id));
     } else if (rootMatch) {
         store.dispatch(navigateToRootProject);
+    } else if (sharedWithMeMatch) {
+        store.dispatch(loadSharedWithMe);
     }
 };
index f108e0b8be423592cb50578a8667e468c3251e55..fb28bd05bee5ff360c17cedd01bf2487d7cf4f22 100644 (file)
@@ -15,7 +15,8 @@ export const Routes = {
     PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
     FAVORITES: '/favorites',
     TRASH: '/trash',
-    PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`
+    PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`,
+    SHARED_WITH_ME: '/shared-with-me',
 };
 
 export const getResourceUrl = (uuid: string) => {
@@ -25,6 +26,8 @@ export const getResourceUrl = (uuid: string) => {
             return getProjectUrl(uuid);
         case ResourceKind.COLLECTION:
             return getCollectionUrl(uuid);
+        case ResourceKind.PROCESS:
+            return getProcessUrl(uuid);
         default:
             return undefined;
     }
@@ -58,3 +61,6 @@ export const matchProcessRoute = (route: string) =>
 
 export const matchProcessLogRoute = (route: string) =>
     matchPath<ResourceRouteParams>(route, { path: Routes.PROCESS_LOGS });
+
+export const matchSharedWithMeRoute = (route: string) =>
+    matchPath(route, { path: Routes.SHARED_WITH_ME });
index f90b4a3053ca1744c2228a51c4c45177bf644c87..44e4eef5c944b271eba16c558b43a4d700c4f886 100644 (file)
@@ -14,17 +14,21 @@ export class AncestorService {
         private userService: UserService
     ) { }
 
-    async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource | TrashableResource>> {
+    async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource>> {
         const service = this.getService(extractUuidObjectType(uuid));
         if (service) {
-            const resource = await service.get(uuid);
-            if (uuid === rootUuid) {
-                return [resource];
-            } else {
-                return [
-                    ...await this.ancestors(resource.ownerUuid, rootUuid),
-                    resource
-                ];
+            try {
+                const resource = await service.get(uuid);
+                if (uuid === rootUuid) {
+                    return [resource];
+                } else {
+                    return [
+                        ...await this.ancestors(resource.ownerUuid, rootUuid),
+                        resource
+                    ];
+                }
+            } catch (e) {
+                return [];
             }
         } else {
             return [];
diff --git a/src/services/api/api-actions.ts b/src/services/api/api-actions.ts
new file mode 100644 (file)
index 0000000..f986786
--- /dev/null
@@ -0,0 +1,11 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type ProgressFn = (id: string, working: boolean) => void;
+export type ErrorFn = (id: string, error: any) => void;
+
+export interface ApiActions {
+    progressFn: ProgressFn;
+    errorFn: ErrorFn;
+}
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) + "."
diff --git a/src/services/api/url-builder.test.ts b/src/services/api/url-builder.test.ts
new file mode 100644 (file)
index 0000000..2b48940
--- /dev/null
@@ -0,0 +1,36 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { OrderBuilder } from "./order-builder";
+import { joinUrls } from "~/services/api/url-builder";
+
+describe("UrlBuilder", () => {
+    it("should join urls properly 1", () => {
+        expect(joinUrls('http://localhost:3000', '/main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 2", () => {
+        expect(joinUrls('http://localhost:3000/', '/main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 3", () => {
+        expect(joinUrls('http://localhost:3000//', '/main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 4", () => {
+        expect(joinUrls('http://localhost:3000', '//main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 5", () => {
+        expect(joinUrls('http://localhost:3000///', 'main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 6", () => {
+        expect(joinUrls('http://localhost:3000///', '//main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 7", () => {
+        expect(joinUrls(undefined, '//main')).toEqual('/main');
+    });
+    it("should join urls properly 8", () => {
+        expect(joinUrls(undefined, 'main')).toEqual('/main');
+    });
+    it("should join urls properly 9", () => {
+        expect(joinUrls('http://localhost:3000///', undefined)).toEqual('http://localhost:3000');
+    });
+});
index 0587c837371dbe0ef242885f0bce6a4a5c2e9c4b..32039a50c23f2a12e1c2c7fdbfe690c4cceecee6 100644 (file)
@@ -24,3 +24,24 @@ export class UrlBuilder {
         return this.url + this.query;
     }
 }
+
+export function joinUrls(url0?: string, url1?: string) {
+    let u0 = "";
+    if (url0) {
+        let idx0 = url0.length - 1;
+        while (url0[idx0] === '/') { --idx0; }
+        u0 = url0.substr(0, idx0 + 1);
+    }
+    let u1 = "";
+    if (url1) {
+        let idx1 = 0;
+        while (url1[idx1] === '/') { ++idx1; }
+        u1 = url1.substr(idx1);
+    }
+    let url = u0;
+    if (u1.length > 0) {
+        url += '/';
+    }
+    url += u1;
+    return url;
+}
index 57915f70578f04be4afd19ef8d6de2543b1cdf3b..50760bb4d8493b5384b1564ca6e936c00001b40e 100644 (file)
@@ -4,6 +4,8 @@
 
 import { User } from "~/models/user";
 import { AxiosInstance } from "axios";
+import { ApiActions, ProgressFn } from "~/services/api/api-actions";
+import * as uuid from "uuid/v4";
 
 export const API_TOKEN_KEY = 'apiToken';
 export const USER_EMAIL_KEY = 'userEmail';
@@ -25,7 +27,8 @@ export class AuthService {
 
     constructor(
         protected apiClient: AxiosInstance,
-        protected baseUrl: string) { }
+        protected baseUrl: string,
+        protected actions: ApiActions) { }
 
     public saveApiToken(token: string) {
         localStorage.setItem(API_TOKEN_KEY, token);
@@ -86,15 +89,25 @@ export class AuthService {
     }
 
     public getUserDetails = (): Promise<User> => {
+        const reqId = uuid();
+        this.actions.progressFn(reqId, true);
         return this.apiClient
             .get<UserDetailsResponse>('/users/current')
-            .then(resp => ({
-                email: resp.data.email,
-                firstName: resp.data.first_name,
-                lastName: resp.data.last_name,
-                uuid: resp.data.uuid,
-                ownerUuid: resp.data.owner_uuid
-            }));
+            .then(resp => {
+                this.actions.progressFn(reqId, false);
+                return {
+                    email: resp.data.email,
+                    firstName: resp.data.first_name,
+                    lastName: resp.data.last_name,
+                    uuid: resp.data.uuid,
+                    ownerUuid: resp.data.owner_uuid
+                };
+            })
+            .catch(e => {
+                this.actions.progressFn(reqId, false);
+                this.actions.errorFn(reqId, e);
+                throw e;
+            });
     }
 
     public getRootUuid() {
index 6e6f2a97d439748a3fb96d9741cff45aa965e073..28de14f51595fcd067bacdd0a08c940dac8a6726 100644 (file)
@@ -11,12 +11,13 @@ import { mapTreeValues } from "~/models/tree";
 import { parseFilesResponse } from "./collection-service-files-response";
 import { fileToArrayBuffer } from "~/common/file";
 import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
 
 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
 
 export class CollectionService extends TrashableResourceService<CollectionResource> {
-    constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService) {
-        super(serverApi, "collections");
+    constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
+        super(serverApi, "collections", actions);
     }
 
     async files(uuid: string) {
index d67d5dbf403ab66ea59c6267dce0d0fa90dacd9c..5a3bae25fdf005d71245ef821b6cea7693d03a8d 100644 (file)
@@ -6,11 +6,18 @@ import { CommonResourceService } from "./common-resource-service";
 import axios, { AxiosInstance } from "axios";
 import MockAdapter from "axios-mock-adapter";
 import { Resource } from "src/models/resource";
+import { ApiActions } from "~/services/api/api-actions";
 
-export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(Service: new (client: AxiosInstance) => C) => {
+const actions: ApiActions = {
+    progressFn: (id: string, working: boolean) => {},
+    errorFn: (id: string, message: string) => {}
+};
+
+export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(
+    Service: new (client: AxiosInstance, actions: ApiActions) => C) => {
     const axiosInstance = axios.create();
     const axiosMock = new MockAdapter(axiosInstance);
-    const service = new Service(axiosInstance);
+    const service = new Service(axiosInstance, actions);
     Object.keys(service).map(key => service[key] = jest.fn());
     return service;
 };
@@ -28,14 +35,14 @@ describe("CommonResourceService", () => {
             .onPost("/resource/")
             .reply(200, { owner_uuid: "ownerUuidValue" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
         expect(resource).toEqual({ ownerUuid: "ownerUuidValue" });
     });
 
     it("#create maps request params to snake case", async () => {
         axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
         expect(axiosInstance.post).toHaveBeenCalledWith("/resource/", {owner_uuid: "ownerUuidValue"});
     });
@@ -45,7 +52,7 @@ describe("CommonResourceService", () => {
             .onDelete("/resource/uuid")
             .reply(200, { deleted_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.delete("uuid");
         expect(resource).toEqual({ deletedAt: "now" });
     });
@@ -55,7 +62,7 @@ describe("CommonResourceService", () => {
             .onGet("/resource/uuid")
             .reply(200, { modified_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.get("uuid");
         expect(resource).toEqual({ modifiedAt: "now" });
     });
@@ -73,7 +80,7 @@ describe("CommonResourceService", () => {
                 items_available: 20
             });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.list({ limit: 10, offset: 1 });
         expect(resource).toEqual({
             kind: "kind",
index 7b36b71cf42d7ce6ba289712eac1f2955b047ec4..f6810c0453b183a1db0847fe127a8dd607004d41 100644 (file)
@@ -5,6 +5,8 @@
 import * as _ from "lodash";
 import { AxiosInstance, AxiosPromise } from "axios";
 import { Resource } from "src/models/resource";
+import * as uuid from "uuid/v4";
+import { ApiActions } from "~/services/api/api-actions";
 
 export interface ListArguments {
     limit?: number;
@@ -32,13 +34,14 @@ export interface Errors {
 export enum CommonResourceServiceError {
     UNIQUE_VIOLATION = 'UniqueViolation',
     OWNERSHIP_CYCLE = 'OwnershipCycle',
+    MODIFYING_CONTAINER_REQUEST_FINAL_STATE = 'ModifyingContainerRequestFinalState',
     UNKNOWN = 'Unknown',
     NONE = 'None'
 }
 
 export class CommonResourceService<T extends Resource> {
 
-    static mapResponseKeys = (response: { data: any }): Promise<any> =>
+    static mapResponseKeys = (response: { data: any }) =>
         CommonResourceService.mapKeys(_.camelCase)(response.data)
 
     static mapKeys = (mapFn: (key: string) => string) =>
@@ -59,36 +62,55 @@ export class CommonResourceService<T extends Resource> {
             }
         }
 
-    static defaultResponse<R>(promise: AxiosPromise<R>): Promise<R> {
+    static defaultResponse<R>(promise: AxiosPromise<R>, actions: ApiActions): Promise<R> {
+        const reqId = uuid();
+        actions.progressFn(reqId, true);
         return promise
+            .then(data => {
+                actions.progressFn(reqId, false);
+                return data;
+            })
             .then(CommonResourceService.mapResponseKeys)
-            .catch(({ response }) => Promise.reject<Errors>(CommonResourceService.mapResponseKeys(response)));
+            .catch(({ response }) => {
+                actions.progressFn(reqId, false);
+                const errors = CommonResourceService.mapResponseKeys(response) as Errors;
+                actions.errorFn(reqId, errors);
+                throw errors;
+            });
     }
 
     protected serverApi: AxiosInstance;
     protected resourceType: string;
+    protected actions: ApiActions;
 
-    constructor(serverApi: AxiosInstance, resourceType: string) {
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
         this.serverApi = serverApi;
         this.resourceType = '/' + resourceType + '/';
+        this.actions = actions;
     }
 
     create(data?: Partial<T> | any) {
         return CommonResourceService.defaultResponse(
             this.serverApi
-                .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
+                .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
+            this.actions
+        );
     }
 
     delete(uuid: string): Promise<T> {
         return CommonResourceService.defaultResponse(
             this.serverApi
-                .delete(this.resourceType + uuid));
+                .delete(this.resourceType + uuid),
+            this.actions
+        );
     }
 
     get(uuid: string) {
         return CommonResourceService.defaultResponse(
             this.serverApi
-                .get<T>(this.resourceType + uuid));
+                .get<T>(this.resourceType + uuid),
+            this.actions
+        );
     }
 
     list(args: ListArguments = {}): Promise<ListResults<T>> {
@@ -102,14 +124,17 @@ export class CommonResourceService<T extends Resource> {
             this.serverApi
                 .get(this.resourceType, {
                     params: CommonResourceService.mapKeys(_.snakeCase)(params)
-                }));
+                }),
+            this.actions
+        );
     }
 
     update(uuid: string, data: Partial<T>) {
         return CommonResourceService.defaultResponse(
             this.serverApi
-                .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
-
+                .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
+            this.actions
+        );
     }
 }
 
@@ -121,6 +146,8 @@ export const getCommonResourceServiceError = (errorResponse: any) => {
                 return CommonResourceServiceError.UNIQUE_VIOLATION;
             case /ownership cycle/.test(error):
                 return CommonResourceServiceError.OWNERSHIP_CYCLE;
+            case /Mounts cannot be modified in state 'Final'/.test(error):
+                return CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE;
             default:
                 return CommonResourceServiceError.UNKNOWN;
         }
index 23e7366e9f69537aa0905e1a982c02943d3fb8bd..633b2fbd89cdf09041e4c93ee001916599763d62 100644 (file)
@@ -6,27 +6,32 @@ import * as _ from "lodash";
 import { AxiosInstance } from "axios";
 import { TrashableResource } from "src/models/resource";
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class TrashableResourceService<T extends TrashableResource> extends CommonResourceService<T> {
 
-    constructor(serverApi: AxiosInstance, resourceType: string) {
-        super(serverApi, resourceType);
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
+        super(serverApi, resourceType, actions);
     }
 
     trash(uuid: string): Promise<T> {
-        return this.serverApi
-            .post(this.resourceType + `${uuid}/trash`)
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .post(this.resourceType + `${uuid}/trash`),
+            this.actions
+        );
     }
 
     untrash(uuid: string): Promise<T> {
         const params = {
             ensure_unique_name: true
         };
-        return this.serverApi
-            .post(this.resourceType + `${uuid}/untrash`, {
-                params: CommonResourceService.mapKeys(_.snakeCase)(params)
-            })
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .post(this.resourceType + `${uuid}/untrash`, {
+                    params: CommonResourceService.mapKeys(_.snakeCase)(params)
+                }),
+            this.actions
+        );
     }
 }
index 01805ff903ee4396da0ae64dc283045da2119c98..e035ed5328fbecfef416212daa1183cb5d51b748 100644 (file)
@@ -4,10 +4,11 @@
 
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { AxiosInstance } from "axios";
-import { ContainerRequestResource } from '../../models/container-request';
+import { ContainerRequestResource } from '~/models/container-request';
+import { ApiActions } from "~/services/api/api-actions";
 
 export class ContainerRequestService extends CommonResourceService<ContainerRequestResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "container_requests");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "container_requests", actions);
     }
 }
index 0ace1f60af6a1e72fbf674727493209796a5f360..86b3d2dc8ca97c1a87bf6d0f14ef94235582569a 100644 (file)
@@ -4,10 +4,11 @@
 
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { AxiosInstance } from "axios";
-import { ContainerResource } from '../../models/container';
+import { ContainerResource } from '~/models/container';
+import { ApiActions } from "~/services/api/api-actions";
 
 export class ContainerService extends CommonResourceService<ContainerResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "containers");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "containers", actions);
     }
 }
index 4601054315fab0a196c1fa4de6c12056558e4a57..92b0713dbcc6ee2ce8b0eb0939d4cb928710676b 100644 (file)
@@ -19,7 +19,7 @@ export interface FavoriteListArguments {
 export class FavoriteService {
     constructor(
         private linkService: LinkService,
-        private groupsService: GroupsService
+        private groupsService: GroupsService,
     ) {}
 
     create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
index e1157f4b177e5ca18c9764c9bb249cf1467d7074..95355440e8e067a212d47a262b8a9c329f877b2d 100644 (file)
@@ -5,11 +5,17 @@
 import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
 import { GroupsService } from "./groups-service";
+import { ApiActions } from "~/services/api/api-actions";
 
 describe("GroupsService", () => {
 
     const axiosMock = new MockAdapter(axios);
 
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
+
     beforeEach(() => {
         axiosMock.reset();
     });
@@ -27,7 +33,7 @@ describe("GroupsService", () => {
                 items_available: 20
             });
 
-        const groupsService = new GroupsService(axios);
+        const groupsService = new GroupsService(axios, actions);
         const resource = await groupsService.contents("1", { limit: 10, offset: 1 });
         expect(resource).toEqual({
             kind: "kind",
index b285e9285518505cf6e459d357ba72ee58acec05..e705b6e5377541f5eaa245d2cf7afbc0b1c40dcf 100644 (file)
@@ -3,13 +3,15 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as _ from "lodash";
-import { CommonResourceService, ListResults } from "~/services/common-service/common-resource-service";
+import { CommonResourceService, ListResults, ListArguments } from '~/services/common-service/common-resource-service';
 import { AxiosInstance } from "axios";
 import { CollectionResource } from "~/models/collection";
 import { ProjectResource } from "~/models/project";
 import { ProcessResource } from "~/models/process";
 import { TrashableResource } from "~/models/resource";
 import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
+import { GroupResource } from "~/models/group";
 
 export interface ContentsArguments {
     limit?: number;
@@ -20,15 +22,19 @@ export interface ContentsArguments {
     includeTrash?: boolean;
 }
 
+export interface SharedArguments extends ListArguments {
+    include?: string;
+}
+
 export type GroupContentsResource =
     CollectionResource |
     ProjectResource |
     ProcessResource;
 
-export class GroupsService<T extends TrashableResource = TrashableResource> extends TrashableResourceService<T> {
+export class GroupsService<T extends GroupResource = GroupResource> extends TrashableResourceService<T> {
 
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "groups");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "groups", actions);
     }
 
     contents(uuid: string, args: ContentsArguments = {}): Promise<ListResults<GroupContentsResource>> {
@@ -38,11 +44,21 @@ export class GroupsService<T extends TrashableResource = TrashableResource> exte
             filters: filters ? `[${filters}]` : undefined,
             order: order ? order : undefined
         };
-        return this.serverApi
-            .get(this.resourceType + `${uuid}/contents`, {
-                params: CommonResourceService.mapKeys(_.snakeCase)(params)
-            })
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .get(this.resourceType + `${uuid}/contents`, {
+                    params: CommonResourceService.mapKeys(_.snakeCase)(params)
+                }),
+            this.actions
+        );
+    }
+
+    shared(params: SharedArguments = {}): Promise<ListResults<GroupContentsResource>> {
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .get(this.resourceType + 'shared', { params }),
+            this.actions
+        );
     }
 }
 
index 77d06d933d37ba334e5205a70adde87d127d83d4..17ee522e4dea00d7c6ecea813a3111a43f64fb4a 100644 (file)
@@ -5,9 +5,10 @@
 import { CommonResourceService } from "~/services/common-service/common-resource-service";\r
 import { AxiosInstance } from "axios";\r
 import { KeepResource } from "~/models/keep";\r
+import { ApiActions } from "~/services/api/api-actions";\r
 \r
 export class KeepService extends CommonResourceService<KeepResource> {\r
-    constructor(serverApi: AxiosInstance) {\r
-        super(serverApi, "keep_services");\r
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {\r
+        super(serverApi, "keep_services", actions);\r
     }\r
 }\r
index c77def5f8c2eb461b8533ded52b6ae522cdc03e4..2701279e7c5cee5dfabc6f8bf2d52babea51fc06 100644 (file)
@@ -5,9 +5,10 @@
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { LinkResource } from "~/models/link";
 import { AxiosInstance } from "axios";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class LinkService extends CommonResourceService<LinkResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "links");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "links", actions);
     }
 }
index 8f6c66c8a2ddbf24f9e02fb0fe84874036b23f6c..3a049a60b8b48f341b0c459961b8429effb7f0f0 100644 (file)
@@ -5,9 +5,10 @@
 import { AxiosInstance } from "axios";
 import { LogResource } from '~/models/log';
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class LogService extends CommonResourceService<LogResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "logs");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "logs", actions);
     }
 }
index 11c2f61f3d87f5d9f7190726fc22d658d078f73d..9052360627c6f68ff0d95253efa6b0f4ae88bc55 100644 (file)
@@ -5,13 +5,18 @@
 import axios from "axios";
 import { ProjectService } from "./project-service";
 import { FilterBuilder } from "~/services/api/filter-builder";
+import { ApiActions } from "~/services/api/api-actions";
 
 describe("CommonResourceService", () => {
     const axiosInstance = axios.create();
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
 
     it(`#create has groupClass set to "project"`, async () => {
         axiosInstance.post = jest.fn(() => Promise.resolve({ data: {} }));
-        const projectService = new ProjectService(axiosInstance);
+        const projectService = new ProjectService(axiosInstance, actions);
         const resource = await projectService.create({ name: "nameValue" });
         expect(axiosInstance.post).toHaveBeenCalledWith("/groups/", {
             name: "nameValue",
@@ -21,7 +26,7 @@ describe("CommonResourceService", () => {
 
     it("#list has groupClass filter set by default", async () => {
         axiosInstance.get = jest.fn(() => Promise.resolve({ data: {} }));
-        const projectService = new ProjectService(axiosInstance);
+        const projectService = new ProjectService(axiosInstance, actions);
         const resource = await projectService.list();
         expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
             params: {
index 53721dd301b64399d77775c3ec09b65240a6c564..9c764b0910a2b2338347951b56f427cfcfe9cfa3 100644 (file)
@@ -12,36 +12,37 @@ import { CollectionService } from "./collection-service/collection-service";
 import { TagService } from "./tag-service/tag-service";
 import { CollectionFilesService } from "./collection-files-service/collection-files-service";
 import { KeepService } from "./keep-service/keep-service";
-import { WebDAV } from "../common/webdav";
-import { Config } from "../common/config";
+import { WebDAV } from "~/common/webdav";
+import { Config } from "~/common/config";
 import { UserService } from './user-service/user-service';
 import { AncestorService } from "~/services/ancestors-service/ancestors-service";
 import { ResourceKind } from "~/models/resource";
 import { ContainerRequestService } from './container-request-service/container-request-service';
 import { ContainerService } from './container-service/container-service';
 import { LogService } from './log-service/log-service';
+import { ApiActions } from "~/services/api/api-actions";
 
 export type ServiceRepository = ReturnType<typeof createServices>;
 
-export const createServices = (config: Config) => {
+export const createServices = (config: Config, actions: ApiActions) => {
     const apiClient = Axios.create();
     apiClient.defaults.baseURL = config.baseUrl;
 
     const webdavClient = new WebDAV();
     webdavClient.defaults.baseURL = config.keepWebServiceUrl;
 
-    const containerRequestService = new ContainerRequestService(apiClient);
-    const containerService = new ContainerService(apiClient);
-    const groupsService = new GroupsService(apiClient);
-    const keepService = new KeepService(apiClient);
-    const linkService = new LinkService(apiClient);
-    const logService = new LogService(apiClient);
-    const projectService = new ProjectService(apiClient);
-    const userService = new UserService(apiClient);
-    
+    const containerRequestService = new ContainerRequestService(apiClient, actions);
+    const containerService = new ContainerService(apiClient, actions);
+    const groupsService = new GroupsService(apiClient, actions);
+    const keepService = new KeepService(apiClient, actions);
+    const linkService = new LinkService(apiClient, actions);
+    const logService = new LogService(apiClient, actions);
+    const projectService = new ProjectService(apiClient, actions);
+    const userService = new UserService(apiClient, actions);
+
     const ancestorsService = new AncestorService(groupsService, userService);
-    const authService = new AuthService(apiClient, config.rootUrl);
-    const collectionService = new CollectionService(apiClient, webdavClient, authService);
+    const authService = new AuthService(apiClient, config.rootUrl, actions);
+    const collectionService = new CollectionService(apiClient, webdavClient, authService, actions);
     const collectionFilesService = new CollectionFilesService(collectionService);
     const favoriteService = new FavoriteService(linkService, groupsService);
     const tagService = new TagService(linkService);
@@ -77,4 +78,4 @@ export const getResourceService = (kind?: ResourceKind) => (serviceRepository: S
         default:
             return undefined;
     }
-};
\ No newline at end of file
+};
index 31cc4bbbbce8820b357dcab978a2efc2f8fb381f..a69203dc5bece0c4c3e1f29aceba92c3de998849 100644 (file)
@@ -5,9 +5,10 @@
 import { AxiosInstance } from "axios";
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { UserResource } from "~/models/user";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class UserService extends CommonResourceService<UserResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "users");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "users", actions);
     }
 }
index 4ac48a0be2afa021ab220f553847eccde259871d..a1cd7f4f776776831956deae615b0ae0ed30878c 100644 (file)
@@ -18,15 +18,20 @@ import { createServices } from "~/services/services";
 import { configureStore, RootStore } from "../store";
 import createBrowserHistory from "history/createBrowserHistory";
 import { mockConfig } from '~/common/config';
+import { ApiActions } from "~/services/api/api-actions";
 
 describe('auth-actions', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
     let store: RootStore;
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
 
     beforeEach(() => {
-        store = configureStore(createBrowserHistory(), createServices(mockConfig({})));
+        store = configureStore(createBrowserHistory(), createServices(mockConfig({}), actions));
         localStorage.clear();
-        reducer = authReducer(createServices(mockConfig({})));
+        reducer = authReducer(createServices(mockConfig({}), actions));
     });
 
     it('should initialise state with user and api token from local storage', () => {
index 2b1920a61db9fc47e32fb543341a2010ccd59d63..1202bacb125b5e1afc61908cb64f9953946b41f6 100644 (file)
@@ -8,13 +8,18 @@ import { AuthAction, authActions } from "./auth-action";
 import 'jest-localstorage-mock';
 import { createServices } from "~/services/services";
 import { mockConfig } from '~/common/config';
+import { ApiActions } from "~/services/api/api-actions";
 
 describe('auth-reducer', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
 
     beforeAll(() => {
         localStorage.clear();
-        reducer = authReducer(createServices(mockConfig({})));
+        reducer = authReducer(createServices(mockConfig({}), actions));
     });
 
     it('should correctly initialise state', () => {
index cc7bb1dd2d2d6f9237e448dfe0c1fbca4692a6f3..4ac07b3bd5bbb39643e87d20fbba068999f4f03f 100644 (file)
@@ -7,9 +7,13 @@ import { RootState } from '~/store/store';
 import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
 import { getResource } from '~/store/resources/resources';
 import { TreePicker } from '../tree-picker/tree-picker';
-import { getSidePanelTreeBranch } from '../side-panel-tree/side-panel-tree-actions';
+import { getSidePanelTreeBranch, getSidePanelTreeNodeAncestorsIds } from '../side-panel-tree/side-panel-tree-actions';
 import { propertiesActions } from '../properties/properties-actions';
 import { getProcess } from '~/store/processes/process';
+import { ServiceRepository } from '~/services/services';
+import { SidePanelTreeCategory, activateSidePanelTreeItem } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { updateResources } from '../resources/resources-actions';
+import { ResourceKind } from '~/models/resource';
 
 export const BREADCRUMBS = 'breadcrumbs';
 
@@ -35,7 +39,33 @@ export const setSidePanelBreadcrumbs = (uuid: string) =>
         dispatch(setBreadcrumbs(breadcrumbs));
     };
 
-export const setProjectBreadcrumbs = setSidePanelBreadcrumbs;
+export const setSharedWithMeBreadcrumbs = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const ancestors = await services.ancestorsService.ancestors(uuid, '');
+        dispatch(updateResources(ancestors));
+        const initialBreadcrumbs: ResourceBreadcrumb[] = [
+            { label: SidePanelTreeCategory.SHARED_WITH_ME, uuid: SidePanelTreeCategory.SHARED_WITH_ME }
+        ];
+        const breadrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
+            ancestor.kind === ResourceKind.GROUP
+                ? [...breadcrumbs, { label: ancestor.name, uuid: ancestor.uuid }]
+                : breadcrumbs,
+            initialBreadcrumbs);
+
+        dispatch(setBreadcrumbs(breadrumbs));
+    };
+
+export const setProjectBreadcrumbs = (uuid: string) =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const ancestors = getSidePanelTreeNodeAncestorsIds(uuid)(getState().treePicker);
+        const rootUuid = services.authService.getUuid();
+        if (uuid === rootUuid ||ancestors.find(uuid => uuid === rootUuid)) {
+            dispatch(setSidePanelBreadcrumbs(uuid));
+        } else {
+            dispatch(setSharedWithMeBreadcrumbs(uuid));
+            dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
+        }
+    };
 
 export const setCollectionBreadcrumbs = (collectionUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
index 87ba0424be5df6bba68f31dbade878b57559077c..058d2dd457147495920945498fb79d29a40b09c9 100644 (file)
@@ -9,32 +9,30 @@ import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
 
-export interface CollectionCopyFormDialogData {
-    name: string;
-    ownerUuid: string;
-    uuid: string;
-}
-
 export const openCollectionCopyDialog = (resource: { name: string, uuid: string }) =>
     (dispatch: Dispatch) => {
         dispatch<any>(resetPickerProjectTree());
-        const initialData: CollectionCopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
+        const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
         dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
         dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
     };
 
-export const copyCollection = (resource: CollectionCopyFormDialogData) =>
+export const copyCollection = (resource: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
         try {
+            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_COPY_FORM_NAME));
             const collection = await services.collectionService.get(resource.uuid);
             const uuidKey = 'uuid';
             delete collection[uuidKey];
             await services.collectionService.create({ ...collection, ownerUuid: resource.ownerUuid, name: resource.name });
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
             return collection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
@@ -44,6 +42,7 @@ export const copyCollection = (resource: CollectionCopyFormDialogData) =>
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
                 throw new Error('Could not copy the collection.');
             }
-            return ;
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
+            return;
         }
     };
index 254d6a8aa6850cb318057f5bbbccd228e397653e..7f21887db6d8bd84d40af48522ddd5d44724d743 100644 (file)
@@ -10,6 +10,7 @@ import { ServiceRepository } from '~/services/services';
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { uploadCollectionFiles } from './collection-upload-actions';
 import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export interface CollectionCreateFormDialogData {
     ownerUuid: string;
@@ -30,16 +31,19 @@ export const createCollection = (data: CollectionCreateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_CREATE_FORM_NAME));
         try {
+            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_CREATE_FORM_NAME));
             const newCollection = await services.collectionService.create(data);
             await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
             dispatch(reset(COLLECTION_CREATE_FORM_NAME));
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
             return newCollection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' }));
             }
-            return ;
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
+            return;
         }
     };
index 420bcd01f1d90ed3d505414291d65b8f0bb89193..9bdc5523793a789a2df2b8329abe2b188117be5c 100644 (file)
@@ -8,10 +8,11 @@ import { startSubmit, stopSubmit, initialize } from 'redux-form';
 import { ServiceRepository } from '~/services/services';
 import { RootState } from '~/store/store';
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { projectPanelActions } from '~/store/project-panel/project-panel-action';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
 
@@ -26,11 +27,17 @@ export const moveCollection = (resource: MoveToFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_MOVE_FORM_NAME));
         try {
+            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_MOVE_FORM_NAME));
             const collection = await services.collectionService.get(resource.uuid);
             await services.collectionService.update(resource.uuid, { ...collection, ownerUuid: resource.ownerUuid });
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved', hideDuration: 2000 }));
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: 'Collection has been moved',
+                hideDuration: 2000,
+                kind: SnackbarKind.SUCCESS
+            }));
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
             return collection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
@@ -40,6 +47,7 @@ export const moveCollection = (resource: MoveToFormDialogData) =>
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the collection.', hideDuration: 2000 }));
             }
-            return ;
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
+            return;
         }
     };
index dedf75e16cda4d972720b4a685d2fecefe04d2d1..4dac9c7d7e5ce55d4246c885dcb7a707b54e7957 100644 (file)
@@ -9,8 +9,9 @@ import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { ServiceRepository } from '~/services/services';
 import { filterCollectionFilesBySelection } from '../collection-panel/collection-panel-files/collection-panel-files-state';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
 
@@ -42,6 +43,7 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
         const currentCollection = state.collectionPanel.item;
         if (currentCollection) {
             try {
+                dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
                 const collection = await services.collectionService.get(currentCollection.uuid);
                 const collectionCopy = {
                     ...collection,
@@ -54,7 +56,12 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
                 const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, false).map(file => file.id);
                 await services.collectionService.deleteFiles(newCollection.uuid, paths);
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'New collection created.', hideDuration: 2000 }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'New collection created.',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
             } catch (e) {
                 const error = getCommonResourceServiceError(e);
                 if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
@@ -66,6 +73,7 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
                     dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied but may contain incorrect files.', hideDuration: 2000 }));
                 }
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
             }
         }
     };
index bf05d4ddc5a3a02075e42c402d22f22090061f1f..9c859234f3a3911e755e9840fa6b554a6e7d83a0 100644 (file)
@@ -11,6 +11,7 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from "~/ser
 import { ServiceRepository } from "~/services/services";
 import { CollectionResource } from '~/models/collection';
 import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export interface CollectionUpdateFormDialogData {
     uuid: string;
@@ -31,15 +32,18 @@ export const updateCollection = (collection: Partial<CollectionResource>) =>
         const uuid = collection.uuid || '';
         dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
         try {
+            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
             const updatedCollection = await services.collectionService.update(uuid, collection);
             dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
             return updatedCollection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' }));
             }
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
             return;
         }
     };
index 4a5aff35009becbbdb6e018b4735fce4618fb842..ef241a7c33f03e2ddfcac24fe6cc9de67b417eb7 100644 (file)
@@ -7,9 +7,10 @@ import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { loadCollectionFiles } from '../collection-panel/collection-panel-files/collection-panel-files-actions';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
 import { reset, startSubmit } from 'redux-form';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const uploadCollectionFiles = (collectionUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
@@ -31,11 +32,21 @@ export const submitCollectionFiles = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
-            dispatch(startSubmit(COLLECTION_UPLOAD_FILES_DIALOG));
-            await dispatch<any>(uploadCollectionFiles(currentCollection.uuid));
-            dispatch<any>(loadCollectionFiles(currentCollection.uuid));
-            dispatch(closeUploadCollectionFilesDialog());
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Data has been uploaded.', hideDuration: 2000 }));
+            try {
+                dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+                dispatch(startSubmit(COLLECTION_UPLOAD_FILES_DIALOG));
+                await dispatch<any>(uploadCollectionFiles(currentCollection.uuid));
+                dispatch<any>(loadCollectionFiles(currentCollection.uuid));
+                dispatch(closeUploadCollectionFilesDialog());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'Data has been uploaded.',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+            } catch (e) {
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+            }
         }
     };
 
@@ -43,4 +54,4 @@ export const closeUploadCollectionFilesDialog = () => dialogActions.CLOSE_DIALOG
 
 const handleUploadProgress = (dispatch: Dispatch) => (fileId: number, loaded: number, total: number, currentTime: number) => {
     dispatch(fileUploaderActions.SET_UPLOAD_PROGRESS({ fileId, loaded, total, currentTime }));
-};
\ No newline at end of file
+};
index bb404b88963b9fbd358db6cdba1aa82a6e7bbf20..d85059d6e1e25a82dfb20c051a55c1c63ffcab0d 100644 (file)
@@ -13,6 +13,7 @@ import { UserResource } from '~/models/user';
 import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
 import { extractUuidKind, ResourceKind } from '~/models/resource';
 import { matchProcessRoute } from '~/routes/routes';
+import { Process } from '~/store/processes/process';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -84,14 +85,10 @@ export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, i
         }
     };
 
-export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
+export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const { location } = getState().router;
-        const pathname = location ? location.pathname : '';
-        const match = matchProcessRoute(pathname); 
-        const uuid = match ? match.params.id : '';
         const resource = {
-            uuid,
+            uuid: process.containerRequest.uuid,
             ownerUuid: '',
             kind: ResourceKind.PROCESS,
             name: '',
@@ -108,6 +105,8 @@ export const resourceKindToContextMenuKind = (uuid: string) => {
             return ContextMenuKind.PROJECT;
         case ResourceKind.COLLECTION:
             return ContextMenuKind.COLLECTION_RESOURCE;
+        case ResourceKind.PROCESS:
+            return ContextMenuKind.PROCESS_RESOURCE;
         case ResourceKind.USER:
             return ContextMenuKind.ROOT_PROJECT;
         default:
diff --git a/src/store/copy-dialog/copy-dialog.ts b/src/store/copy-dialog/copy-dialog.ts
new file mode 100644 (file)
index 0000000..4450cfc
--- /dev/null
@@ -0,0 +1,9 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface CopyFormDialogData {
+    name: string;
+    uuid: string;
+    ownerUuid: string;
+}
\ No newline at end of file
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 cc800244abc1d0d9c1673bd03743d1eff7024f14..d059d37af4639110170f848cf1badf4be411154a 100644 (file)
@@ -15,6 +15,7 @@ export interface DataExplorer {
     rowsPerPage: number;
     rowsPerPageOptions: number[];
     searchValue: string;
+    working?: boolean;
 }
 
 export const initialDataExplorer: DataExplorer = {
index c385309f71f28d3295e23b718089fab3e334b7f2..acdc12b4d3c6b90e112bb5fc1e384f274fbd9f6f 100644 (file)
@@ -16,7 +16,8 @@ import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
 import { LinkResource } from "~/models/link";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
 import { resourcesActions } from "~/store/resources/resources-actions";
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
 import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
 import { loadMissingProcessesInformation } from "~/store/project-panel/project-panel-middleware-service";
 
@@ -30,7 +31,6 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
         if (!dataExplorer) {
             api.dispatch(favoritesPanelDataExplorerIsNotSet());
         } else {
-
             const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
             const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
             const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
@@ -50,6 +50,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                     .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
             }
             try {
+                api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
                 const response = await this.services.favoriteService
                     .list(this.services.authService.getUuid()!, {
                         limit: dataExplorer.rowsPerPage,
@@ -61,6 +62,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                             .addILike("name", dataExplorer.searchValue)
                             .getFilters()
                     });
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(resourcesActions.SET_RESOURCES(response.items));
                 await api.dispatch<any>(loadMissingProcessesInformation(response.items));
                 api.dispatch(favoritePanelActions.SET_ITEMS({
@@ -71,12 +73,14 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                 }));
                 api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
             } catch (e) {
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(favoritePanelActions.SET_ITEMS({
                     items: [],
                     itemsAvailable: 0,
                     page: 0,
                     rowsPerPage: dataExplorer.rowsPerPage
                 }));
+                api.dispatch(couldNotFetchFavoritesContents());
             }
         }
     }
@@ -86,3 +90,9 @@ const favoritesPanelDataExplorerIsNotSet = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Favorites panel is not ready.'
     });
+
+const couldNotFetchFavoritesContents = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch favorites contents.',
+        kind: SnackbarKind.ERROR
+    });
index e5a8e591d20d1527b0137fffc3a4c35c8cd4b1ff..5a3001fbc0d352f6992421d9b7fa5c6354b0cfc1 100644 (file)
@@ -6,8 +6,9 @@ import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { Dispatch } from "redux";
 import { RootState } from "../store";
 import { checkFavorite } from "./favorites-reducer";
-import { snackbarActions } from "../snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import { ServiceRepository } from "~/services/services";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const favoritesActions = unionize({
     TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
@@ -19,10 +20,16 @@ export type FavoritesAction = UnionOf<typeof favoritesActions>;
 
 export const toggleFavorite = (resource: { uuid: string; name: string }) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        dispatch(progressIndicatorActions.START_WORKING("toggleFavorite"));
         const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
-        dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
         const isFavorite = checkFavorite(resource.uuid, getState().favorites);
+        dispatch(snackbarActions.OPEN_SNACKBAR({
+            message: isFavorite
+                ? "Removing from favorites..."
+                : "Adding to favorites..."
+        }));
+
         const promise: any = isFavorite
             ? services.favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
             : services.favoriteService.create({ userUuid, resource });
@@ -35,8 +42,14 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
                     message: isFavorite
                         ? "Removed from favorites"
                         : "Added to favorites",
-                    hideDuration: 2000
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
                 }));
+                dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
+            })
+            .catch((e: any) => {
+                dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
+                throw e;
             });
     };
 
index 0e87769e24a57095f4e74fd578f5c46ed1358c59..c68c5398754e13c6a439af6176cf53a36def01d7 100644 (file)
@@ -24,6 +24,8 @@ export const navigateTo = (uuid: string) =>
         }
         if (uuid === SidePanelTreeCategory.FAVORITES) {
             dispatch<any>(navigateToFavorites);
+        } else if(uuid === SidePanelTreeCategory.SHARED_WITH_ME){
+            dispatch(navigateToSharedWithMe);
         }
     };
 
@@ -44,4 +46,6 @@ export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootSt
     if (rootProjectUuid) {
         dispatch(navigateToProject(rootProjectUuid));
     }
-};
\ No newline at end of file
+};
+
+export const navigateToSharedWithMe = push(Routes.SHARED_WITH_ME);
diff --git a/src/store/processes/process-command-actions.ts b/src/store/processes/process-command-actions.ts
new file mode 100644 (file)
index 0000000..de55a2c
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { RootState } from '../store';
+import { Dispatch } from 'redux';
+import { getProcess } from '~/store/processes/process';
+
+export const PROCESS_COMMAND_DIALOG_NAME = 'processCommandDialog';
+
+export interface ProcessCommandDialogData {
+    command: string;
+    processName: string;
+}
+
+export const openProcessCommandDialog = (processUuid: string) =>
+    (dispatch: Dispatch<any>, getState: () => RootState) => {
+        const process = getProcess(processUuid)(getState().resources);
+        if (process) {
+            const data: ProcessCommandDialogData = {
+                command: process.containerRequest.command.join(' '),
+                processName: process.containerRequest.name,
+            };
+            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
+        }
+    };
diff --git a/src/store/processes/process-copy-actions.ts b/src/store/processes/process-copy-actions.ts
new file mode 100644 (file)
index 0000000..bb8d8f5
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { initialize, startSubmit } from 'redux-form';
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { getProcess, ProcessStatus, getProcessStatus } from '~/store/processes/process';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
+
+export const openCopyProcessDialog = (resource: { name: string, uuid: string }) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const process = getProcess(resource.uuid)(getState().resources);
+        if (process) {
+            const processStatus = getProcessStatus(process);
+            if (processStatus === ProcessStatus.DRAFT) {
+                dispatch<any>(resetPickerProjectTree());
+                const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
+                dispatch<any>(initialize(PROCESS_COPY_FORM_NAME, initialData));
+                dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} }));
+            } else {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can copy only draft processes.', hideDuration: 2000 }));
+            }
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+        }
+    };
+
+export const copyProcess = (resource: CopyFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(startSubmit(PROCESS_COPY_FORM_NAME));
+        try {
+            const process = await services.containerRequestService.get(resource.uuid);
+            const uuidKey = 'uuid';
+            delete process[uuidKey];
+            await services.containerRequestService.create({ ...process, ownerUuid: resource.ownerUuid, name: resource.name });
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+            return process;
+        } catch (e) {
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+            throw new Error('Could not copy the process.');
+        }
+    };
\ No newline at end of file
diff --git a/src/store/processes/process-move-actions.ts b/src/store/processes/process-move-actions.ts
new file mode 100644 (file)
index 0000000..6df8269
--- /dev/null
@@ -0,0 +1,57 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { startSubmit, stopSubmit, initialize } from 'redux-form';
+import { ServiceRepository } from '~/services/services';
+import { RootState } from '~/store/store';
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { getProcess, getProcessStatus, ProcessStatus } from '~/store/processes/process';
+
+export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName';
+
+export const openMoveProcessDialog = (resource: { name: string, uuid: string }) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const process = getProcess(resource.uuid)(getState().resources);
+        if (process) {
+            const processStatus = getProcessStatus(process);
+            if (processStatus === ProcessStatus.DRAFT) {
+                dispatch<any>(resetPickerProjectTree());
+                dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource));
+                dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} }));
+            } else {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You can move only draft processes.', hideDuration: 2000 }));
+            }
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+        }
+    };
+
+export const moveProcess = (resource: MoveToFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(startSubmit(PROCESS_MOVE_FORM_NAME));
+        try {
+            const process = await services.containerRequestService.get(resource.uuid);
+            await services.containerRequestService.update(resource.uuid, { ...process, ownerUuid: resource.ownerUuid });
+            dispatch(projectPanelActions.REQUEST_ITEMS());
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
+            return process;
+        } catch (e) {
+            const error = getCommonResourceServiceError(e);
+            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+                dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' }));
+            } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) {
+                dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'You can move only draft processes.' }));
+            } else {
+                dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the process.', hideDuration: 2000 }));
+            }
+            return;
+        }
+    };
\ No newline at end of file
diff --git a/src/store/processes/process-update-actions.ts b/src/store/processes/process-update-actions.ts
new file mode 100644 (file)
index 0000000..92cf032
--- /dev/null
@@ -0,0 +1,54 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { initialize, startSubmit, stopSubmit } from 'redux-form';
+import { RootState } from "~/store/store";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
+import { ServiceRepository } from "~/services/services";
+import { getProcess } from '~/store/processes/process';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export interface ProcessUpdateFormDialogData {
+    uuid: string;
+    name: string;
+}
+
+export const PROCESS_UPDATE_FORM_NAME = 'processUpdateFormName';
+
+export const openProcessUpdateDialog = (resource: ProcessUpdateFormDialogData) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const process = getProcess(resource.uuid)(getState().resources);
+        if(process) {
+            dispatch(initialize(PROCESS_UPDATE_FORM_NAME, { ...resource, name: process.containerRequest.name }));
+            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_UPDATE_FORM_NAME, data: {} }));
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+        }
+    };
+
+export const updateProcess = (resource: ProcessUpdateFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(startSubmit(PROCESS_UPDATE_FORM_NAME));
+        try {
+            const process = await services.containerRequestService.get(resource.uuid);
+            const updatedProcess = await services.containerRequestService.update(resource.uuid, { ...process, name: resource.name });
+            dispatch(projectPanelActions.REQUEST_ITEMS());
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
+            return updatedProcess;
+        } catch (e) {
+            const error = getCommonResourceServiceError(e);
+            if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+                dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' }));
+            } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) {
+                dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'You cannot modified in "Final" state.' }));
+            } else {
+                dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not update the process.', hideDuration: 2000 }));
+            }
+            return;
+        }
+    };
\ No newline at end of file
index c9e62f943455a2ebaae9b8d8b45c16ff8084a62f..ab8093b856c4c259b1bab8992e98293444f7ac78 100644 (file)
@@ -61,7 +61,7 @@ export const getProcessRuntime = ({ container }: Process) =>
         ? getTimeDiff(container.finishedAt || '', container.startedAt || '')
         : 0;
 
-export const getProcessStatusColor = (status: string, { customs }: ArvadosTheme) => {
+export const getProcessStatusColor = (status: string, { customs, palette }: ArvadosTheme) => {
     switch (status) {
         case ProcessStatus.RUNNING:
             return customs.colors.blue500;
@@ -71,7 +71,7 @@ export const getProcessStatusColor = (status: string, { customs }: ArvadosTheme)
         case ProcessStatus.FAILED:
             return customs.colors.red900;
         default:
-            return customs.colors.grey500;
+            return palette.grey["500"];
     }
 };
 
diff --git a/src/store/progress-indicator/progress-indicator-actions.ts b/src/store/progress-indicator/progress-indicator-actions.ts
new file mode 100644 (file)
index 0000000..34a43d8
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+
+export const progressIndicatorActions = unionize({
+    START_WORKING: ofType<string>(),
+    STOP_WORKING: ofType<string>(),
+    PERSIST_STOP_WORKING: ofType<string>(),
+    TOGGLE_WORKING: ofType<{ id: string, working: boolean }>()
+});
+
+export type ProgressIndicatorAction = UnionOf<typeof progressIndicatorActions>;
diff --git a/src/store/progress-indicator/progress-indicator-reducer.ts b/src/store/progress-indicator/progress-indicator-reducer.ts
new file mode 100644 (file)
index 0000000..849906b
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ProgressIndicatorAction, progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+
+export type ProgressIndicatorState = { id: string, working: boolean }[];
+
+const initialState: ProgressIndicatorState = [];
+
+export const progressIndicatorReducer = (state: ProgressIndicatorState = initialState, action: ProgressIndicatorAction) => {
+    const startWorking = (id: string) => state.find(p => p.id === id) ? state : state.concat({ id, working: true });
+    const stopWorking = (id: string) => state.filter(p => p.id !== id);
+
+    return progressIndicatorActions.match(action, {
+        START_WORKING: id => startWorking(id),
+        STOP_WORKING: id => stopWorking(id),
+        PERSIST_STOP_WORKING: id => state.map(p => ({
+            ...p,
+            working: p.id === id ? false : p.working
+        })),
+        TOGGLE_WORKING: ({ id, working }) => working ? startWorking(id) : stopWorking(id),
+        default: () => state,
+    });
+};
+
+export function isSystemWorking(state: ProgressIndicatorState): boolean {
+    return state.length > 0 && state.reduce((working, p) => working ? true : p.working, false);
+}
diff --git a/src/store/progress-indicator/with-progress.ts b/src/store/progress-indicator/with-progress.ts
new file mode 100644 (file)
index 0000000..24f7e32
--- /dev/null
@@ -0,0 +1,20 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+
+export type WithProgressStateProps = {
+    working: boolean;
+};
+
+export const withProgress = (id: string) =>
+    (component: React.ComponentType<WithProgressStateProps>) =>
+        connect(mapStateToProps(id))(component);
+
+export const mapStateToProps = (id: string) => (state: RootState): WithProgressStateProps => {
+    const progress = state.progressIndicator.find(p => p.id === id);
+    return { working: progress ? progress.working : false };
+};
index 4b4ec2697989831e734f963eb8c872b144b1ebee..09e76ae28b99ef30697228df98f97b78531559f7 100644 (file)
@@ -17,7 +17,8 @@ import { Dispatch, MiddlewareAPI } from "redux";
 import { ProjectResource } from "~/models/project";
 import { updateResources } from "~/store/resources/resources-actions";
 import { getProperty } from "~/store/properties/properties";
-import { snackbarActions } from '../snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '../snackbar/snackbar-actions';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
 import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
 import { ListResults } from '~/services/common-service/common-resource-service';
 import { loadContainers } from '../processes/processes-actions';
@@ -38,12 +39,21 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
             api.dispatch(projectPanelDataExplorerIsNotSet());
         } else {
             try {
+                api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
                 const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer));
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
                 api.dispatch(updateResources(response.items));
                 await api.dispatch<any>(loadMissingProcessesInformation(response.items));
                 api.dispatch(setItems(response));
             } catch (e) {
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+                api.dispatch(projectPanelActions.SET_ITEMS({
+                    items: [],
+                    itemsAvailable: 0,
+                    page: 0,
+                    rowsPerPage: dataExplorer.rowsPerPage
+                }));
                 api.dispatch(couldNotFetchProjectContents());
             }
         }
@@ -66,19 +76,19 @@ export const loadMissingProcessesInformation = (resources: GroupContentsResource
         }
     };
 
-const setItems = (listResults: ListResults<GroupContentsResource>) =>
+export const setItems = (listResults: ListResults<GroupContentsResource>) =>
     projectPanelActions.SET_ITEMS({
         ...listResultsToDataExplorerItemsMeta(listResults),
         items: listResults.items.map(resource => resource.uuid),
     });
 
-const getParams = (dataExplorer: DataExplorer) => ({
+export const getParams = (dataExplorer: DataExplorer) => ({
     ...dataExplorerToListParams(dataExplorer),
     order: getOrder(dataExplorer),
     filters: getFilters(dataExplorer),
 });
 
-const getFilters = (dataExplorer: DataExplorer) => {
+export const getFilters = (dataExplorer: DataExplorer) => {
     const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
     const typeFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE);
     const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS);
@@ -90,7 +100,7 @@ const getFilters = (dataExplorer: DataExplorer) => {
         .getFilters();
 };
 
-const getOrder = (dataExplorer: DataExplorer) => {
+export const getOrder = (dataExplorer: DataExplorer) => {
     const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
     const order = new OrderBuilder<ProjectResource>();
     if (sortColumn) {
@@ -116,7 +126,8 @@ const projectPanelCurrentUuidIsNotSet = () =>
 
 const couldNotFetchProjectContents = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch project contents.'
+        message: 'Could not fetch project contents.',
+        kind: SnackbarKind.ERROR
     });
 
 const projectPanelDataExplorerIsNotSet = () =>
diff --git a/src/store/shared-with-me-panel/shared-with-me-middleware-service.ts b/src/store/shared-with-me-panel/shared-with-me-middleware-service.ts
new file mode 100644 (file)
index 0000000..1ebb13e
--- /dev/null
@@ -0,0 +1,82 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataExplorerMiddlewareService, listResultsToDataExplorerItemsMeta, dataExplorerToListParams } from '../data-explorer/data-explorer-middleware-service';
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { getDataExplorer, DataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateFavorites } from '~/store/favorites/favorites-actions';
+import { updateResources } from '~/store/resources/resources-actions';
+import { loadMissingProcessesInformation } from '~/store/project-panel/project-panel-middleware-service';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { sharedWithMePanelActions } from './shared-with-me-panel-actions';
+import { ListResults } from '~/services/common-service/common-resource-service';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { SortDirection } from '~/components/data-table/data-column';
+import { OrderBuilder, OrderDirection } from '~/services/api/order-builder';
+import { ProjectResource } from '~/models/project';
+import { ProjectPanelColumnNames } from '~/views/project-panel/project-panel';
+import { FilterBuilder } from '~/services/api/filter-builder';
+
+export class SharedWithMeMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const state = api.getState();
+        const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+        try {
+            const response = await this.services.groupsService.shared(getParams(dataExplorer));
+            api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+            api.dispatch(updateResources(response.items));
+            await api.dispatch<any>(loadMissingProcessesInformation(response.items));
+            api.dispatch(setItems(response));
+        } catch (e) {
+            api.dispatch(couldNotFetchSharedItems());
+        }
+
+    }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    order: getOrder(dataExplorer),
+    filters: getFilters(dataExplorer),
+});
+
+export const getFilters = (dataExplorer: DataExplorer) => {
+    const filters = new FilterBuilder()
+        .addILike("name", dataExplorer.searchValue)
+        .getFilters();
+    return `[${filters}]`;
+};
+
+export const getOrder = (dataExplorer: DataExplorer) => {
+    const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+    const order = new OrderBuilder<ProjectResource>();
+    if (sortColumn) {
+        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+            ? OrderDirection.ASC
+            : OrderDirection.DESC;
+        const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+        return order
+            .addOrder(sortDirection, columnName)
+            .getOrder();
+    } else {
+        return order.getOrder();
+    }
+};
+
+export const setItems = (listResults: ListResults<GroupContentsResource>) =>
+    sharedWithMePanelActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const couldNotFetchSharedItems = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch shared items.'
+    });
diff --git a/src/store/shared-with-me-panel/shared-with-me-panel-actions.ts b/src/store/shared-with-me-panel/shared-with-me-panel-actions.ts
new file mode 100644 (file)
index 0000000..2553086
--- /dev/null
@@ -0,0 +1,18 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
+import { Dispatch } from 'redux';
+import { ServiceRepository } from "~/services/services";
+import { RootState } from '~/store/store';
+
+export const SHARED_WITH_ME_PANEL_ID = "sharedWithMePanel";
+export const sharedWithMePanelActions = bindDataExplorerActions(SHARED_WITH_ME_PANEL_ID);
+
+export const loadSharedWithMePanel = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(sharedWithMePanelActions.REQUEST_ITEMS());
+    };
+
+
index 95e409f5c5bfcd2ca6edfc08df4377dedb24dfae..23c5ea2217972d07765cc720b0e3253f4ac50b2a 100644 (file)
@@ -13,6 +13,7 @@ import { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
 import { TreeItemStatus } from "~/components/tree/tree";
 import { getNodeAncestors, getNodeValue, getNodeAncestorsIds, getNode } from '~/models/tree';
 import { ProjectResource } from '~/models/project';
+import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
 
 export enum SidePanelTreeCategory {
     PROJECTS = 'Projects',
@@ -147,14 +148,14 @@ export const expandSidePanelTreeItem = (nodeId: string) =>
         }
     };
 
-const getSidePanelTreeNode = (nodeId: string) => (treePicker: TreePicker) => {
+export const getSidePanelTreeNode = (nodeId: string) => (treePicker: TreePicker) => {
     const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
     return sidePanelTree
         ? getNodeValue(nodeId)(sidePanelTree)
         : undefined;
 };
 
-const getSidePanelTreeNodeAncestorsIds = (nodeId: string) => (treePicker: TreePicker) => {
+export const getSidePanelTreeNodeAncestorsIds = (nodeId: string) => (treePicker: TreePicker) => {
     const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
     return sidePanelTree
         ? getNodeAncestorsIds(nodeId)(sidePanelTree)
index 3b66157dcd83262b1edfc35e5e3b131e7d885cd0..2a5fdd0c2d17c1aae6c3455034fd303ddb523a36 100644 (file)
@@ -4,7 +4,7 @@
 
 import { Dispatch } from 'redux';
 import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { navigateToFavorites, navigateTo, navigateToTrash } from '../navigation/navigation-action';
+import { navigateToFavorites, navigateTo, navigateToTrash, navigateToSharedWithMe } from '../navigation/navigation-action';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 
 export const navigateFromSidePanel = (id: string) =>
@@ -22,6 +22,8 @@ const getSidePanelTreeCategoryAction = (id: string) => {
             return navigateToFavorites;
         case SidePanelTreeCategory.TRASH:
             return navigateToTrash;
+        case SidePanelTreeCategory.SHARED_WITH_ME:
+            return navigateToSharedWithMe;
         default:
             return sidePanelTreeCategoryNotAvailable(id);
     }
index 55d9f3a8651b86afecc516aa48dc1af0abc412e1..d6d7128e85547cb313f283127b039abbacd340fd 100644 (file)
@@ -4,9 +4,23 @@
 
 import { unionize, ofType, UnionOf } from "~/common/unionize";
 
+export interface SnackbarMessage {
+    message: string;
+    hideDuration: number;
+    kind: SnackbarKind;
+}
+
+export enum SnackbarKind {
+    SUCCESS = 1,
+    ERROR = 2,
+    INFO = 3,
+    WARNING = 4
+}
+
 export const snackbarActions = unionize({
-    OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number}>(),
-    CLOSE_SNACKBAR: ofType<{}>()
+    OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number, kind?: SnackbarKind}>(),
+    CLOSE_SNACKBAR: ofType<{}>(),
+    SHIFT_MESSAGES: ofType<{}>()
 });
 
 export type SnackbarAction = UnionOf<typeof snackbarActions>;
index fc2f4a1964e27627ff5d02e63150713bbe78eb3b..73c566fc8cb95945e7b9b95abea9dfa4e00e6684 100644 (file)
@@ -2,26 +2,43 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { SnackbarAction, snackbarActions } from "./snackbar-actions";
+import { SnackbarAction, snackbarActions, SnackbarKind, SnackbarMessage } from "./snackbar-actions";
 
 export interface SnackbarState {
-    message: string;
+    messages: SnackbarMessage[];
     open: boolean;
-    hideDuration: number;
 }
 
 const DEFAULT_HIDE_DURATION = 3000;
 
 const initialState: SnackbarState = {
-    message: "",
-    open: false,
-    hideDuration: DEFAULT_HIDE_DURATION
+    messages: [],
+    open: false
 };
 
 export const snackbarReducer = (state = initialState, action: SnackbarAction) => {
     return snackbarActions.match(action, {
-        OPEN_SNACKBAR: data => ({ ...initialState, ...data, open: true }),
-        CLOSE_SNACKBAR: () => initialState,
+        OPEN_SNACKBAR: data => {
+            return {
+                open: true,
+                messages: state.messages.concat({
+                    message: data.message,
+                    hideDuration: data.hideDuration ? data.hideDuration : DEFAULT_HIDE_DURATION,
+                    kind: data.kind ? data.kind : SnackbarKind.INFO
+                })
+            };
+        },
+        CLOSE_SNACKBAR: () => ({
+            ...state,
+            open: false
+        }),
+        SHIFT_MESSAGES: () => {
+            const messages = state.messages.filter((m, idx) => idx > 0);
+            return {
+                open: messages.length > 0,
+                messages
+            };
+        },
         default: () => state,
     });
 };
index 9b4f42b84fc81d8d64b2177c0187a6ee94fb60c9..012b747425b72e714472a5b2a0cfe89d03dc2546 100644 (file)
@@ -32,6 +32,9 @@ import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-mid
 import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
 import { processLogsPanelReducer } from './process-logs-panel/process-logs-panel-reducer';
 import { processPanelReducer } from '~/store/process-panel/process-panel-reducer';
+import { SHARED_WITH_ME_PANEL_ID } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+import { SharedWithMeMiddlewareService } from './shared-with-me-panel/shared-with-me-middleware-service';
+import { progressIndicatorReducer } from './progress-indicator/progress-indicator-reducer';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -54,13 +57,17 @@ export function configureStore(history: History, services: ServiceRepository): R
     const trashPanelMiddleware = dataExplorerMiddleware(
         new TrashPanelMiddlewareService(services, TRASH_PANEL_ID)
     );
+    const sharedWithMePanelMiddleware = dataExplorerMiddleware(
+        new SharedWithMeMiddlewareService(services, SHARED_WITH_ME_PANEL_ID)
+    );
 
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
         projectPanelMiddleware,
         favoritePanelMiddleware,
-        trashPanelMiddleware
+        trashPanelMiddleware,
+        sharedWithMePanelMiddleware,
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
@@ -83,5 +90,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     snackbar: snackbarReducer,
     treePicker: treePickerReducer,
     fileUploader: fileUploaderReducer,
-    processPanel: processPanelReducer
+    processPanel: processPanelReducer,
+    progressIndicator: progressIndicatorReducer
 });
index 19ed3be13194982a97527417adb3b4eff8438389..90838b207a559292ac538fbe62a02571233cbd30 100644 (file)
@@ -19,9 +19,9 @@ 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 { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { updateResources } from "~/store/resources/resources-actions";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -48,6 +48,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
         }
 
         try {
+            api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
             const userUuid = this.services.authService.getUuid()!;
             const listResults = await this.services.groupsService
                 .contents(userUuid, {
@@ -57,14 +58,14 @@ 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
                 });
+            api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
 
-            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),
@@ -73,6 +74,13 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
             api.dispatch<any>(updateFavorites(items));
             api.dispatch(updateResources(listResults.items));
         } catch (e) {
+            api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+            api.dispatch(trashPanelActions.SET_ITEMS({
+                items: [],
+                itemsAvailable: 0,
+                page: 0,
+                rowsPerPage: dataExplorer.rowsPerPage
+            }));
             api.dispatch(couldNotFetchTrashContents());
         }
     }
@@ -80,5 +88,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
 
 const couldNotFetchTrashContents = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch trash contents.'
+        message: 'Could not fetch trash contents.',
+        kind: SnackbarKind.ERROR
     });
+
index cd6df55670d322044c5d386a3c597cc19f0db3e0..5cf952eb1fa98a5a53857faf2f9da6224039fcfb 100644 (file)
@@ -5,53 +5,69 @@
 import { Dispatch } from "redux";
 import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "~/store/side-panel-tree/side-panel-tree-actions";
 import { projectPanelActions } from "~/store/project-panel/project-panel-action";
-import { ResourceKind, TrashableResource } from "~/models/resource";
+import { ResourceKind } from "~/models/resource";
 
 export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
-        if (isTrashed) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
-            await services.groupsService.untrash(uuid);
-            dispatch<any>(activateSidePanelTreeItem(uuid));
-            dispatch(trashPanelActions.REQUEST_ITEMS());
-            dispatch(snackbarActions.CLOSE_SNACKBAR());
+        try {
+            if (isTrashed) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+                await services.groupsService.untrash(uuid);
+                dispatch<any>(activateSidePanelTreeItem(uuid));
+                dispatch(trashPanelActions.REQUEST_ITEMS());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Restored from trash",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+            } else {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+                await services.groupsService.trash(uuid);
+                dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Added to trash",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+            }
+        } catch (e) {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Restored from trash",
-                hideDuration: 2000
-            }));
-        } else {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
-            await services.groupsService.trash(uuid);
-            dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
-            dispatch(snackbarActions.CLOSE_SNACKBAR());
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Added to trash",
-                hideDuration: 2000
+                message: "Could not move project to trash",
+                kind: SnackbarKind.ERROR
             }));
         }
     };
 
 export const toggleCollectionTrashed = (uuid: string, isTrashed: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
-        if (isTrashed) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
-            await services.collectionService.untrash(uuid);
-            dispatch(trashPanelActions.REQUEST_ITEMS());
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Restored from trash",
-                hideDuration: 2000
-            }));
-        } else {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
-            await services.collectionService.trash(uuid);
-            dispatch(projectPanelActions.REQUEST_ITEMS());
+        try {
+            if (isTrashed) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+                await services.collectionService.untrash(uuid);
+                dispatch(trashPanelActions.REQUEST_ITEMS());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Restored from trash",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+            } else {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+                await services.collectionService.trash(uuid);
+                dispatch(projectPanelActions.REQUEST_ITEMS());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Added to trash",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+            }
+        } catch (e) {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Added to trash",
-                hideDuration: 2000
+                message: "Could not move collection to trash",
+                kind: SnackbarKind.ERROR
             }));
         }
     };
index 036f4a3469448fd64f2b04151907bc6309ebe901..c79dc48f9f58e8c0bf81b659dcb3b00493001702 100644 (file)
@@ -9,13 +9,13 @@ import { loadCollectionPanel } from '~/store/collection-panel/collection-panel-a
 import { snackbarActions } from '../snackbar/snackbar-actions';
 import { loadFavoritePanel } from '../favorite-panel/favorite-panel-action';
 import { openProjectPanel, projectPanelActions } from '~/store/project-panel/project-panel-action';
-import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions';
+import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects, getSidePanelTreeNodeAncestorsIds } from '../side-panel-tree/side-panel-tree-actions';
 import { loadResource, updateResources } from '../resources/resources-actions';
 import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
 import { projectPanelColumns } from '~/views/project-panel/project-panel';
 import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
 import { matchRootRoute } from '~/routes/routes';
-import { setCollectionBreadcrumbs, setProjectBreadcrumbs, setSidePanelBreadcrumbs, setProcessBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
+import { setCollectionBreadcrumbs, setProjectBreadcrumbs, setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
 import { navigateToProject } from '../navigation/navigation-action';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { ServiceRepository } from '~/services/services';
@@ -29,11 +29,17 @@ import * as collectionCopyActions from '~/store/collections/collection-copy-acti
 import * as collectionUpdateActions from '~/store/collections/collection-update-actions';
 import * as collectionMoveActions from '~/store/collections/collection-move-actions';
 import * as processesActions from '../processes/processes-actions';
+import * as processMoveActions from '~/store/processes/process-move-actions';
+import * as processUpdateActions from '~/store/processes/process-update-actions';
+import * as processCopyActions from '~/store/processes/process-copy-actions';
 import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
 import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions';
 import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
+import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+import { loadSharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel-actions';
 
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
 export const loadWorkbench = () =>
     async (dispatch: Dispatch, getState: () => RootState) => {
@@ -45,6 +51,7 @@ export const loadWorkbench = () =>
                 dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
                 dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
                 dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
+                dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
                 dispatch<any>(initSidePanelTree());
                 if (router.location) {
                     const match = matchRootRoute(router.location.pathname);
@@ -75,10 +82,10 @@ export const loadTrash = () =>
     };
 
 export const loadProject = (uuid: string) =>
-    async (dispatch: Dispatch) => {
-        await dispatch<any>(activateSidePanelTreeItem(uuid));
-        dispatch<any>(setProjectBreadcrumbs(uuid));
-        dispatch<any>(openProjectPanel(uuid));
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(openProjectPanel(uuid));
+        await dispatch(activateSidePanelTreeItem(uuid));
+        dispatch(setProjectBreadcrumbs(uuid));
         dispatch(loadDetailsPanel(uuid));
     };
 
@@ -160,7 +167,7 @@ export const updateCollection = (data: collectionUpdateActions.CollectionUpdateF
         }
     };
 
-export const copyCollection = (data: collectionCopyActions.CollectionCopyFormDialogData) =>
+export const copyCollection = (data: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
             const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
@@ -191,7 +198,48 @@ export const loadProcess = (uuid: string) =>
         await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
         dispatch<any>(setProcessBreadcrumbs(uuid));
         dispatch(loadDetailsPanel(uuid));
-        
+
+    };
+
+export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) =>
+    async (dispatch: Dispatch) => {
+        try {
+            const process = await dispatch<any>(processUpdateActions.updateProcess(data));
+            if (process) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Process has been successfully updated.",
+                    hideDuration: 2000
+                }));
+                dispatch<any>(updateResources([process]));
+                dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+            }
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+        }
+    };
+
+export const moveProcess = (data: MoveToFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            const process = await dispatch<any>(processMoveActions.moveProcess(data));
+            dispatch<any>(updateResources([process]));
+            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been moved.', hideDuration: 2000 }));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+        }
+    };
+
+export const copyProcess = (data: CopyFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            const process = await dispatch<any>(processCopyActions.copyProcess(data));
+            dispatch<any>(updateResources([process]));
+            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000 }));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+        }
     };
 
 export const loadProcessLog = (uuid: string) =>
@@ -222,3 +270,9 @@ export const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
             dispatch<any>(loadProject(currentProjectPanelUuid));
         }
     };
+
+export const loadSharedWithMe = (dispatch: Dispatch) => {
+    dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
+    dispatch<any>(loadSharedWithMePanel());
+    dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
+};
index 95edadfdd1809b23e83aff5ec47043f761b9e30f..755cd7f7b4c7be4b7a1650653c3b7783406c1179 100644 (file)
@@ -19,3 +19,5 @@ export const COPY_NAME_VALIDATION = [require, maxLength(255)];
 export const COPY_FILE_VALIDATION = [require];
 
 export const MOVE_TO_VALIDATION = [require];
+
+export const PROCESS_NAME_VALIDATION = [require, maxLength(255)];
\ No newline at end of file
index 2897455bc4f2b2479e55f52c91729ab13cd5b995..107f1828c609f1d25f65073c4fbdba581282740e 100644 (file)
@@ -11,14 +11,16 @@ import {
 } from "~/components/icon/icon";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { navigateToProcessLogs } from '~/store/navigation/navigation-action';
+import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
+import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
+import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
+import { openProcessCommandDialog } from '../../../store/processes/process-command-actions';
 
 export const processActionSet: ContextMenuActionSet = [[
     {
         icon: RenameIcon,
         name: "Edit process",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: (dispatch, resource) => dispatch<any>(openProcessUpdateDialog(resource))
     },
     {
         icon: ShareIcon,
@@ -30,9 +32,7 @@ export const processActionSet: ContextMenuActionSet = [[
     {
         icon: MoveToIcon,
         name: "Move to",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: (dispatch, resource) => dispatch<any>(openMoveProcessDialog(resource))
     },
     {
         component: ToggleFavoriteAction,
@@ -45,9 +45,7 @@ export const processActionSet: ContextMenuActionSet = [[
     {
         icon: CopyIcon,
         name: "Copy to project",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: (dispatch, resource) => dispatch<any>(openCopyProcessDialog(resource))
     },
     {
         icon: ReRunProcessIcon,
@@ -74,7 +72,7 @@ export const processActionSet: ContextMenuActionSet = [[
         icon: CommandIcon,
         name: "Command",
         execute: (dispatch, resource) => {
-            // add code
+            dispatch<any>(openProcessCommandDialog(resource.uuid));
         }
     },
     {
diff --git a/src/views-components/context-menu/action-sets/process-resource-action-set.ts b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
new file mode 100644 (file)
index 0000000..b198523
--- /dev/null
@@ -0,0 +1,59 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ToggleFavoriteAction } from "../actions/favorite-action";
+import { toggleFavorite } from "~/store/favorites/favorites-actions";
+import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
+import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
+import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
+
+export const processResourceActionSet: ContextMenuActionSet = [[
+    {
+        icon: RenameIcon,
+        name: "Edit process",
+        execute: (dispatch, resource) => dispatch<any>(openProcessUpdateDialog(resource))
+    },
+    {
+        icon: ShareIcon,
+        name: "Share",
+        execute: (dispatch, resource) => {
+            // add code
+        }
+    },
+    {
+        icon: MoveToIcon,
+        name: "Move to",
+        execute: (dispatch, resource) => dispatch<any>(openMoveProcessDialog(resource))
+    },
+    {
+        component: ToggleFavoriteAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleFavorite(resource)).then(() => {
+                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+            });
+        }
+    },
+    {
+        icon: CopyIcon,
+        name: "Copy to project",
+        execute: (dispatch, resource) => dispatch<any>(openCopyProcessDialog(resource))
+    },
+    {
+        icon: DetailsIcon,
+        name: "View details",
+        execute: (dispatch, resource) => {
+            // add code
+        }
+    },
+    {
+        icon: RemoveIcon,
+        name: "Remove",
+        execute: (dispatch, resource) => {
+            // add code
+        }
+    }
+]];
index a545b2bdddfb447ebe5bf43ee768a841d52d36e8..d5c82be2d7f0736243f5e97661e4f7c7ec51d7a4 100644 (file)
@@ -65,5 +65,6 @@ export enum ContextMenuKind {
     COLLECTION = 'Collection',
     COLLECTION_RESOURCE = 'CollectionResource',
     PROCESS = "Process",
+    PROCESS_RESOURCE = 'ProcessResource',
     PROCESS_LOGS = "ProcessLogs"
 }
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 16dd59933411f5d394b31a58d5262fbff0418cda..74c3e64aaacf1139d5a90cff0c5ada69f40b14b2 100644 (file)
@@ -21,7 +21,9 @@ interface Props {
 }
 
 const mapStateToProps = (state: RootState, { id }: Props) => {
-    return getDataExplorer(state.dataExplorer, id);
+    const progress = state.progressIndicator.find(p => p.id === id);
+    const working = progress && progress.working;
+    return { ...getDataExplorer(state.dataExplorer, id), working };
 };
 
 const mapDispatchToProps = () => {
index 5c061883f9e0b6d57179cdc57a155f2a6fa90a3c..b5ebc36e0eab508f048595bd092b397eb59421c9 100644 (file)
@@ -9,7 +9,7 @@ export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
     constructor(protected item: T) {}
 
     getTitle(): string {
-        return this.item.name;
+        return this.item.name || 'Projects';
     }
 
     abstract getIcon(className?: string): React.ReactElement<any>;
index 7aae7860ac39999df6262fdb886198f49400a4e2..f0075558dfe20549a808a5ebf47840139381dece 100644 (file)
@@ -3,8 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Drawer, IconButton, Tabs, Tab, Typography, Grid } from '@material-ui/core';
+import { IconButton, Tabs, Tab, Typography, Grid, Tooltip } from '@material-ui/core';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { Transition } from 'react-transition-group';
 import { ArvadosTheme } from '~/common/custom-theme';
 import * as classnames from "classnames";
 import { connect } from 'react-redux';
@@ -20,41 +21,40 @@ import { ProcessDetails } from "./process-details";
 import { EmptyDetails } from "./empty-details";
 import { DetailsData } from "./details-data";
 import { DetailsResource } from "~/models/details";
-import { getResource } from '../../store/resources/resources';
+import { getResource } from '~/store/resources/resources';
 
-type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
+type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
 
-const drawerWidth = 320;
+const DRAWER_WIDTH = 320;
+const SLIDE_TIMEOUT = 500;
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    container: {
+    root: {
+        background: theme.palette.background.paper,
+        borderLeft: `1px solid ${theme.palette.divider}`,
+        height: '100%',
+        overflow: 'hidden',
+        transition: `width ${SLIDE_TIMEOUT}ms ease`,
         width: 0,
-        position: 'relative',
-        height: 'auto',
-        transition: 'width 0.5s ease',
-        '&$opened': {
-            width: drawerWidth
-        }
     },
-    opened: {},
-    drawerPaper: {
-        position: 'relative',
-        width: drawerWidth
+    opened: {
+        width: DRAWER_WIDTH,
+    },
+    container: {
+        maxWidth: 'none',
+        width: DRAWER_WIDTH,
     },
     headerContainer: {
         color: theme.palette.grey["600"],
         margin: `${theme.spacing.unit}px 0`,
-        textAlign: 'center'
+        textAlign: 'center',
     },
     headerIcon: {
-        fontSize: '2.125rem'
-    },
-    headerTitle: {
-        overflowWrap: 'break-word',
-        wordWrap: 'break-word'
+        fontSize: '2.125rem',
     },
     tabContainer: {
-        padding: theme.spacing.unit * 3
-    }
+        overflow: 'auto',
+        padding: theme.spacing.unit * 3,
+    },
 });
 
 const getItem = (resource: DetailsResource): DetailsData => {
@@ -104,51 +104,68 @@ export const DetailsPanel = withStyles(styles)(
                 this.setState({ tabsValue: value });
             }
 
-            renderTabContainer = (children: React.ReactElement<any>) =>
-                <Typography className={this.props.classes.tabContainer} component="div">
-                    {children}
-                </Typography>
-
             render() {
-                const { classes, onCloseDrawer, isOpened, item } = this.props;
-                const { tabsValue } = this.state;
+                const { classes, isOpened } = this.props;
                 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}>
-                                <Grid container alignItems='center' justify='space-around'>
-                                    <Grid item xs={2}>
-                                        {item.getIcon(classes.headerIcon)}
-                                    </Grid>
-                                    <Grid item xs={8}>
-                                        <Typography variant="title" className={classes.headerTitle}>
-                                            {item.getTitle()}
-                                        </Typography>
-                                    </Grid>
-                                    <Grid item>
-                                        <IconButton color="inherit" onClick={onCloseDrawer}>
-                                            {<CloseIcon />}
-                                        </IconButton>
-                                    </Grid>
-                                </Grid>
-                            </Typography>
-                            <Tabs value={tabsValue} onChange={this.handleChange}>
-                                <Tab disableRipple label="Details" />
-                                <Tab disableRipple label="Activity" disabled />
-                            </Tabs>
-                            {tabsValue === 0 && this.renderTabContainer(
-                                <Grid container direction="column">
-                                    {item.getDetails()}
-                                </Grid>
-                            )}
-                            {tabsValue === 1 && this.renderTabContainer(
-                                <Grid container direction="column" />
-                            )}
-                        </Drawer>
-                    </Typography>
+                    <Grid
+                        container
+                        direction="column"
+                        className={classnames([classes.root, { [classes.opened]: isOpened }])}>
+                        <Transition
+                            in={isOpened}
+                            timeout={SLIDE_TIMEOUT}
+                            unmountOnExit>
+                            {this.renderContent()}
+                        </Transition>
+                    </Grid>
                 );
             }
+
+            renderContent() {
+                const { classes, onCloseDrawer, item } = this.props;
+                const { tabsValue } = this.state;
+                return <Grid
+                    container
+                    direction="column"
+                    item
+                    xs
+                    className={classes.container} >
+                    <Grid
+                        item
+                        className={classes.headerContainer}
+                        container
+                        alignItems='center'
+                        justify='space-around'
+                        wrap="nowrap">
+                        <Grid item xs={2}>
+                            {item.getIcon(classes.headerIcon)}
+                        </Grid>
+                        <Grid item xs={8}>
+                            <Tooltip title={item.getTitle()}>
+                                <Typography variant="title" noWrap>
+                                    {item.getTitle()}
+                                </Typography>
+                            </Tooltip>
+                        </Grid>
+                        <Grid item>
+                            <IconButton color="inherit" onClick={onCloseDrawer}>
+                                <CloseIcon />
+                            </IconButton>
+                        </Grid>
+                    </Grid>
+                    <Grid item>
+                        <Tabs value={tabsValue} onChange={this.handleChange}>
+                            <Tab disableRipple label="Details" />
+                            <Tab disableRipple label="Activity" disabled />
+                        </Tabs>
+                    </Grid>
+                    <Grid item xs className={this.props.classes.tabContainer} >
+                        {tabsValue === 0
+                            ? item.getDetails()
+                            : null}
+                    </Grid>
+                </Grid >;
+            }
         }
     )
 );
index 7fc301fa6c1d44a92e4b38385ddde7683a2920a1..7c335a358c9048cff8af1b136143252a009aad3b 100644 (file)
@@ -19,10 +19,8 @@ export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyPr
         {...props}
     />;
 
-export const CollectionPartialCopyFields = () => <div style={{ display: 'flex' }}>
-    <div>
-        <CollectionNameField />
-        <CollectionDescriptionField />
-    </div>
+export const CollectionPartialCopyFields = () => <div>
+    <CollectionNameField />
+    <CollectionDescriptionField />
     <CollectionProjectPickerField />
 </div>;
similarity index 78%
rename from src/views-components/dialog-copy/dialog-collection-copy.tsx
rename to src/views-components/dialog-copy/dialog-copy.tsx
index 029db578520e197ed37e1f86511e8d20ae69dbbe..415541595c564ff1d3b672062852af92aaf25461 100644 (file)
@@ -9,19 +9,19 @@ import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
 import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from '~/validators/validators';
 import { TextField } from "~/components/text-field/text-field";
-import { CollectionCopyFormDialogData } from "~/store/collections/collection-copy-actions";
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
-type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CollectionCopyFormDialogData>;
+type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
 
-export const DialogCollectionCopy = (props: CopyFormDialogProps) =>
+export const DialogCopy = (props: CopyFormDialogProps) =>
     <FormDialog
         dialogTitle='Make a copy'
-        formFields={CollectionCopyFields}
+        formFields={CopyDialogFields}
         submitLabel='Copy'
         {...props}
     />;
 
-const CollectionCopyFields = () => <span>
+const CopyDialogFields = () => <span>
     <Field
         name='name'
         component={TextField}
index 245465fa55edfc2bcb32fab615122ec42c62a90f..41309fdff6952762ed9ae9d9c2922f53dc3e5082 100644 (file)
@@ -5,16 +5,17 @@
 import { compose } from "redux";
 import { withDialog } from "~/store/dialog/with-dialog";
 import { reduxForm } from 'redux-form';
-import { COLLECTION_COPY_FORM_NAME, CollectionCopyFormDialogData } from '~/store/collections/collection-copy-actions';
-import { DialogCollectionCopy } from "~/views-components/dialog-copy/dialog-collection-copy";
+import { COLLECTION_COPY_FORM_NAME } from '~/store/collections/collection-copy-actions';
+import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
 import { copyCollection } from '~/store/workbench/workbench-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 
 export const CopyCollectionDialog = compose(
     withDialog(COLLECTION_COPY_FORM_NAME),
-    reduxForm<CollectionCopyFormDialogData>({
+    reduxForm<CopyFormDialogData>({
         form: COLLECTION_COPY_FORM_NAME,
         onSubmit: (data, dispatch) => {
             dispatch(copyCollection(data));
         }
     })
-)(DialogCollectionCopy);
\ No newline at end of file
+)(DialogCopy);
\ No newline at end of file
diff --git a/src/views-components/dialog-forms/copy-process-dialog.ts b/src/views-components/dialog-forms/copy-process-dialog.ts
new file mode 100644 (file)
index 0000000..4ec17c6
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { reduxForm } from 'redux-form';
+import { PROCESS_COPY_FORM_NAME } from '~/store/processes/process-copy-actions';
+import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
+import { copyProcess } from '~/store/workbench/workbench-actions';
+import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+
+export const CopyProcessDialog = compose(
+    withDialog(PROCESS_COPY_FORM_NAME),
+    reduxForm<CopyFormDialogData>({
+        form: PROCESS_COPY_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(copyProcess(data));
+        }
+    })
+)(DialogCopy);
\ No newline at end of file
diff --git a/src/views-components/dialog-forms/move-process-dialog.ts b/src/views-components/dialog-forms/move-process-dialog.ts
new file mode 100644 (file)
index 0000000..baea34b
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from 'redux';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { reduxForm } from 'redux-form';
+import { PROCESS_MOVE_FORM_NAME } from '~/store/processes/process-move-actions';
+import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
+import { moveProcess } from '~/store/workbench/workbench-actions';
+
+export const MoveProcessDialog = compose(
+    withDialog(PROCESS_MOVE_FORM_NAME),
+    reduxForm<MoveToFormDialogData>({
+        form: PROCESS_MOVE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(moveProcess(data));
+        }
+    })
+)(DialogMoveTo);
\ No newline at end of file
diff --git a/src/views-components/dialog-forms/update-process-dialog.ts b/src/views-components/dialog-forms/update-process-dialog.ts
new file mode 100644 (file)
index 0000000..12d896d
--- /dev/null
@@ -0,0 +1,20 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { reduxForm } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { DialogProcessUpdate } from '~/views-components/dialog-update/dialog-process-update';
+import { PROCESS_UPDATE_FORM_NAME, ProcessUpdateFormDialogData } from '~/store/processes/process-update-actions';
+import { updateProcess } from "~/store/workbench/workbench-actions";
+
+export const UpdateProcessDialog = compose(
+    withDialog(PROCESS_UPDATE_FORM_NAME),
+    reduxForm<ProcessUpdateFormDialogData>({
+        form: PROCESS_UPDATE_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(updateProcess(data));
+        }
+    })
+)(DialogProcessUpdate);
\ No newline at end of file
diff --git a/src/views-components/dialog-update/dialog-process-update.tsx b/src/views-components/dialog-update/dialog-process-update.tsx
new file mode 100644 (file)
index 0000000..d5bbce6
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProcessUpdateFormDialogData } from '~/store/processes/process-update-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProcessNameField } from '~/views-components/form-fields/process-form-fields';
+
+type DialogProcessProps = WithDialogProps<{}> & InjectedFormProps<ProcessUpdateFormDialogData>;
+
+export const DialogProcessUpdate = (props: DialogProcessProps) =>
+    <FormDialog
+        dialogTitle='Edit Process'
+        formFields={ProcessEditFields}
+        submitLabel='Save'
+        {...props}
+    />;
+
+const ProcessEditFields = () => <span>
+    <ProcessNameField />
+</span>;
index 1771c0e72b81bab3f5c6b7e84b60dcfa15426b54..be5f93df6a52b401177f0fa12f27594e46abe2c4 100644 (file)
@@ -30,6 +30,6 @@ export const CollectionProjectPickerField = () =>
         validate={COLLECTION_PROJECT_VALIDATION} />;
 
 const ProjectPicker = (props: WrappedFieldProps) =>
-    <div style={{ width: '400px', height: '144px', display: 'flex', flexDirection: 'column' }}>
+    <div style={{ height: '144px', display: 'flex', flexDirection: 'column' }}>
         <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
     </div>;
diff --git a/src/views-components/form-fields/process-form-fields.tsx b/src/views-components/form-fields/process-form-fields.tsx
new file mode 100644 (file)
index 0000000..cf471b6
--- /dev/null
@@ -0,0 +1,15 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Field, WrappedFieldProps } from "redux-form";
+import { TextField } from "~/components/text-field/text-field";
+import { PROCESS_NAME_VALIDATION } from "~/validators/validators";
+
+export const ProcessNameField = () =>
+    <Field
+        name='name'
+        component={TextField}
+        validate={PROCESS_NAME_VALIDATION}
+        label="Process Name" />;
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..93cf4968e99e5bd1475258fa23b5f3ed35fe8003 100644 (file)
 // 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';
+import { ReactNode } from "react";
 
-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;
+    children?: ReactNode;
 }
 
 export interface MainAppBarActionProps {
     onSearch: (searchText: string) => void;
-    onMenuItemClick: (menuItem: MainAppBarMenuItem) => void;
-    onDetailsPanelToggle: () => void;
 }
 
 export type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps & WithStyles<CssRules>;
 
 export const MainAppBar = withStyles(styles)(
     (props: MainAppBarProps) => {
-        return <AppBar position="static">
-            <Toolbar>
+        return <AppBar position="absolute">
+            <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>
+            {props.children}
         </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..071b986
--- /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>
+                <Tooltip title="Additional Info">
+                    <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+                        <DetailsIcon />
+                    </IconButton>
+                </Tooltip>
+            </Grid>
+        </Grid>
+    </Toolbar>);
diff --git a/src/views-components/process-command-dialog/process-command-dialog.tsx b/src/views-components/process-command-dialog/process-command-dialog.tsx
new file mode 100644 (file)
index 0000000..4bde68d
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { PROCESS_COMMAND_DIALOG_NAME } from '~/store/processes/process-command-actions';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProcessCommandDialogData } from '~/store/processes/process-command-actions';
+import { DefaultCodeSnippet } from "~/components/default-code-snippet/default-code-snippet";
+import { compose } from 'redux';
+
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    codeSnippet: {
+        marginLeft: theme.spacing.unit * 3,
+        marginRight: theme.spacing.unit * 3,
+    }
+});
+
+export const ProcessCommandDialog = compose(
+    withDialog(PROCESS_COMMAND_DIALOG_NAME),
+    withStyles(styles),
+)(
+    (props: WithDialogProps<ProcessCommandDialogData> & WithStyles<CssRules>) =>
+        <Dialog
+            open={props.open}
+            maxWidth="md"
+            onClose={props.closeDialog}
+            style={{ alignSelf: 'stretch' }}>
+            <DialogTitle>{`Command - ${props.data.processName}`}</DialogTitle>
+            <DefaultCodeSnippet
+                className={props.classes.codeSnippet}
+                lines={[props.data.command]} />
+            <DialogActions>
+                <Button
+                    variant='flat'
+                    color='primary'
+                    onClick={props.closeDialog}>
+                    Close
+                </Button>
+            </DialogActions>
+        </Dialog>
+);
\ No newline at end of file
diff --git a/src/views-components/progress/content-progress.tsx b/src/views-components/progress/content-progress.tsx
new file mode 100644 (file)
index 0000000..fa2cad5
--- /dev/null
@@ -0,0 +1,13 @@
+// // Copyright (C) The Arvados Authors. All rights reserved.
+// //
+// // SPDX-License-Identifier: AGPL-3.0
+//
+// import * as React from 'react';
+// import { CircularProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+//
+// export const ContentProgress = withProgress(ProgressIndicatorData.CONTENT_PROGRESS)((props: WithProgressStateProps) =>
+//     props.started ? <CircularProgress /> : null
+// );
diff --git a/src/views-components/progress/side-panel-progress.tsx b/src/views-components/progress/side-panel-progress.tsx
new file mode 100644 (file)
index 0000000..2d832a5
--- /dev/null
@@ -0,0 +1,13 @@
+// // Copyright (C) The Arvados Authors. All rights reserved.
+// //
+// // SPDX-License-Identifier: AGPL-3.0
+//
+// import * as React from 'react';
+// import { CircularProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+//
+// export const SidePanelProgress = withProgress(ProgressIndicatorData.SIDE_PANEL_PROGRESS)((props: WithProgressStateProps) =>
+//     props.started ? <span style={{ display: 'flex', justifyContent: 'center', marginTop: "40px" }}><CircularProgress /></span> : null
+// );
diff --git a/src/views-components/progress/workbench-progress.tsx b/src/views-components/progress/workbench-progress.tsx
new file mode 100644 (file)
index 0000000..1fdd57c
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// import * as React from 'react';
+// import { LinearProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+
+// export const WorkbenchProgress = withProgress(ProgressIndicatorData.WORKBENCH_PROGRESS)(
+//     (props: WithProgressStateProps) =>
+//         props.started ? <LinearProgress color="secondary" /> : null
+// );
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..89c3400
--- /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: 0
+};
+
+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="flex-start">
+                            <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 d0b00d6fb50c4576a9e8190311ec056c826c32bc..4d4760fac37abf583bebde4342c26dc4f570cf75 100644 (file)
@@ -16,6 +16,7 @@ import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-acti
 
 export interface SidePanelTreeProps {
     onItemActivation: (id: string) => void;
+    sidePanelProgress?: boolean;
 }
 
 type SidePanelTreeActionProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
index b81f39ef338a850ce22a158cb4dd57cd5ca51de2..739e9eac1139ee62b3b38eeb3b8988cea5cc3bb2 100644 (file)
@@ -4,27 +4,27 @@
 
 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';
+import { RootState } from '~/store/store';
 
 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 => ({
@@ -33,13 +33,14 @@ const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
     }
 });
 
+const mapStateToProps = (state: RootState) => ({
+});
+
 export const SidePanel = compose(
     withStyles(styles),
-    connect(undefined, mapDispatchToProps)
+    connect(mapStateToProps, mapDispatchToProps)
 )(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
-    <Drawer
-        variant="permanent"
-        classes={{ paper: classes.drawerPaper }}>
-        <div className={classes.toolbar} />
+    <Grid item xs>
+        <SidePanelButton />
         <SidePanelTree {...props} />
-    </Drawer>);
+    </Grid>);
index 535777e1bd4fd0765f1ac88914e592165f969258..7449e1e2f82027afeb870a834031dc584df6390d 100644 (file)
@@ -7,21 +7,129 @@ import { connect } from "react-redux";
 import { RootState } from "~/store/store";
 import MaterialSnackbar, { SnackbarProps } from "@material-ui/core/Snackbar";
 import { Dispatch } from "redux";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
+import IconButton from '@material-ui/core/IconButton';
+import SnackbarContent from '@material-ui/core/SnackbarContent';
+import WarningIcon from '@material-ui/icons/Warning';
+import CheckCircleIcon from '@material-ui/icons/CheckCircle';
+import ErrorIcon from '@material-ui/icons/Error';
+import InfoIcon from '@material-ui/icons/Info';
+import CloseIcon from '@material-ui/icons/Close';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from "~/common/custom-theme";
+import { amber, green } from "@material-ui/core/colors";
+import * as classNames from 'classnames';
 
-const mapStateToProps = (state: RootState): SnackbarProps => ({
-    anchorOrigin: { vertical: "bottom", horizontal: "center" },
-    open: state.snackbar.open,
-    message: <span>{state.snackbar.message}</span>,
-    autoHideDuration: state.snackbar.hideDuration
-});
+const mapStateToProps = (state: RootState): SnackbarProps & ArvadosSnackbarProps => {
+    const messages = state.snackbar.messages;
+    return {
+        anchorOrigin: { vertical: "bottom", horizontal: "right" },
+        open: state.snackbar.open,
+        message: <span>{messages.length > 0 ? messages[0].message : ""}</span>,
+        autoHideDuration: messages.length > 0 ? messages[0].hideDuration : 0,
+        kind: messages.length > 0 ? messages[0].kind : SnackbarKind.INFO
+    };
+};
 
-const mapDispatchToProps = (dispatch: Dispatch): Pick<SnackbarProps, "onClose"> => ({
+const mapDispatchToProps = (dispatch: Dispatch) => ({
     onClose: (event: any, reason: string) => {
         if (reason !== "clickaway") {
             dispatch(snackbarActions.CLOSE_SNACKBAR());
         }
+    },
+    onExited: () => {
+        dispatch(snackbarActions.SHIFT_MESSAGES());
     }
 });
 
-export const Snackbar = connect(mapStateToProps, mapDispatchToProps)(MaterialSnackbar);
+const ArvadosSnackbar = (props: any) => <MaterialSnackbar {...props}>
+    <ArvadosSnackbarContent {...props}/>
+</MaterialSnackbar>;
+
+type CssRules = "success" | "error" | "info" | "warning" | "icon" | "iconVariant" | "message";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    success: {
+        backgroundColor: green[600]
+    },
+    error: {
+        backgroundColor: theme.palette.error.dark
+    },
+    info: {
+        backgroundColor: theme.palette.primary.dark
+    },
+    warning: {
+        backgroundColor: amber[700]
+    },
+    icon: {
+        fontSize: 20
+    },
+    iconVariant: {
+        opacity: 0.9,
+        marginRight: theme.spacing.unit
+    },
+    message: {
+        display: 'flex',
+        alignItems: 'center'
+    },
+});
+
+interface ArvadosSnackbarProps {
+    kind: SnackbarKind;
+}
+
+const ArvadosSnackbarContent = (props: SnackbarProps & ArvadosSnackbarProps & WithStyles<CssRules>) => {
+    const { classes, className, message, onClose, kind } = props;
+
+    let Icon = InfoIcon;
+    let cssClass = classes.info;
+
+    switch (kind) {
+        case SnackbarKind.INFO:
+            Icon = InfoIcon;
+            cssClass = classes.info;
+            break;
+        case SnackbarKind.WARNING:
+            Icon = WarningIcon;
+            cssClass = classes.warning;
+            break;
+        case SnackbarKind.SUCCESS:
+            Icon = CheckCircleIcon;
+            cssClass = classes.success;
+            break;
+        case SnackbarKind.ERROR:
+            Icon = ErrorIcon;
+            cssClass = classes.error;
+            break;
+    }
+
+    return (
+        <SnackbarContent
+            className={classNames(cssClass, className)}
+            aria-describedby="client-snackbar"
+            message={
+                <span id="client-snackbar" className={classes.message}>
+                    <Icon className={classNames(classes.icon, classes.iconVariant)}/>
+                    {message}
+                </span>
+            }
+            action={
+                <IconButton
+                    key="close"
+                    aria-label="Close"
+                    color="inherit"
+                    onClick={e => {
+                        if (onClose) {
+                            onClose(e, '');
+                        }
+                    }}>
+                    <CloseIcon className={classes.icon}/>
+                </IconButton>
+            }
+        />
+    );
+};
+
+export const Snackbar = connect(mapStateToProps, mapDispatchToProps)(
+    withStyles(styles)(ArvadosSnackbar)
+);
index 9672f30628de1b3930968588a643a9d844cda57c..8e46385cc9cfa694c938c6b1ce5183e32d7f858c 100644 (file)
@@ -78,13 +78,13 @@ export const CollectionPanel = withStyles(styles)(
                         <CardHeader
                             avatar={<CollectionIcon className={classes.iconHeader} />}
                             action={
-                                <IconButton
-                                    aria-label="More options"
-                                    onClick={this.handleContextMenu}>
-                                    <Tooltip title="More options">
+                                <Tooltip title="More options">
+                                    <IconButton
+                                        aria-label="More options"
+                                        onClick={this.handleContextMenu}>
                                         <MoreOptionsIcon />
-                                    </Tooltip>
-                                </IconButton>
+                                    </IconButton>
+                                </Tooltip>
                             }
                             title={item && item.name}
                             subheader={item && item.description} />
index 73849562dad12f34c4153188f0d9e6cd5ce8180c..4ba967c0529a37b5142f13bc6648e1b3e3b14a01 100644 (file)
@@ -164,23 +164,18 @@ export const FavoritePanel = withStyles(styles)(
     connect(mapStateToProps, mapDispatchToProps)(
         class extends React.Component<FavoritePanelProps> {
             render() {
-                return this.hasAnyFavorites()
-                    ? <DataExplorer
-                        id={FAVORITE_PANEL_ID}
-                        onRowClick={this.props.onItemClick}
-                        onRowDoubleClick={this.props.onItemDoubleClick}
-                        onContextMenu={this.props.onContextMenu}
-                        contextMenuColumn={true}
-                        dataTableDefaultView={<DataTableDefaultView icon={FavoriteIcon}/>} />
-                    : <PanelDefaultView
-                        icon={FavoriteIcon}
-                        messages={['Your favorites list is empty.']} />;
-            }
-
-            hasAnyFavorites = () => {
-                return Object
-                    .keys(this.props.favorites)
-                    .find(uuid => this.props.favorites[uuid]);
+                return <DataExplorer
+                    id={FAVORITE_PANEL_ID}
+                    onRowClick={this.props.onItemClick}
+                    onRowDoubleClick={this.props.onItemDoubleClick}
+                    onContextMenu={this.props.onContextMenu}
+                    contextMenuColumn={true}
+                    dataTableDefaultView={
+                        <DataTableDefaultView
+                            icon={FavoriteIcon}
+                            messages={['Your favorites list is empty.']}
+                            />
+                    } />;
             }
         }
     )
index ff6320eeaf03f76c711044b3bfc819a0c5c6e837..d02fc02ce1431c74a2aefdaf4137991d0552ba61 100644 (file)
@@ -3,10 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
-import { CodeSnippet, CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { MuiThemeProvider, createMuiTheme, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
+import { CodeSnippet } from '~/components/code-snippet/code-snippet';
 import grey from '@material-ui/core/colors/grey';
 
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = () => ({
+    codeSnippet: {
+        maxHeight: '550px',
+    }
+});
+
 const theme = createMuiTheme({
     overrides: {
         MuiTypography: {
@@ -23,9 +31,12 @@ const theme = createMuiTheme({
     }
 });
 
-type ProcessLogCodeSnippet = CodeSnippetDataProps;
+interface ProcessLogCodeSnippetProps {
+    lines: string[];
+}
 
-export const ProcessLogCodeSnippet = (props: ProcessLogCodeSnippet) => 
-    <MuiThemeProvider theme={theme}>
-        <CodeSnippet lines={props.lines} />
-    </MuiThemeProvider>;
\ No newline at end of file
+export const ProcessLogCodeSnippet = withStyles(styles)(
+    (props: ProcessLogCodeSnippetProps & WithStyles<CssRules>) =>
+        <MuiThemeProvider theme={theme}>
+            <CodeSnippet lines={props.lines} className={props.classes.codeSnippet} />
+        </MuiThemeProvider>);
\ No newline at end of file
index 66811f47b89209191c7de02d329efa4d2259da49..397f0378224df4c3827d5f64b36a29179f8df759 100644 (file)
@@ -53,10 +53,18 @@ interface ProcessLogMainCardDataProps {
     process: Process;
 }
 
-export type ProcessLogMainCardProps = ProcessLogMainCardDataProps & CodeSnippetDataProps & ProcessLogFormDataProps & ProcessLogFormActionProps;
+export interface ProcessLogMainCardActionProps {
+    onContextMenu: (event: React.MouseEvent<any>, process: Process) => void;
+}
+
+export type ProcessLogMainCardProps = ProcessLogMainCardDataProps
+    & ProcessLogMainCardActionProps
+    & CodeSnippetDataProps
+    & ProcessLogFormDataProps
+    & ProcessLogFormActionProps;
 
 export const ProcessLogMainCard = withStyles(styles)(
-    ({ classes, process, selectedFilter, filters, onChange, lines }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
+    ({ classes, process, selectedFilter, filters, onChange, lines, onContextMenu }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
         <Grid item xs={12}>
             <Link to={`/processes/${process.containerRequest.uuid}`} className={classes.backLink}>
                 <BackIcon className={classes.backIcon} /> Back
@@ -65,34 +73,35 @@ export const ProcessLogMainCard = withStyles(styles)(
                 <CardHeader
                     avatar={<ProcessIcon className={classes.iconHeader} />}
                     action={
-                        <div>
-                            <IconButton aria-label="More options">
-                                <Tooltip title="More options">
-                                    <MoreOptionsIcon />
-                                </Tooltip>
+                        <Tooltip title="More options">
+                            <IconButton onClick={event => onContextMenu(event, process)} aria-label="More options">
+                                <MoreOptionsIcon />
                             </IconButton>
-                        </div>
-                    }
+                        </Tooltip>}
                     title={
                         <Tooltip title={process.containerRequest.name} placement="bottom-start">
                             <Typography noWrap variant="title" className={classes.title}>
                                 {process.containerRequest.name}
                             </Typography>
-                        </Tooltip>
-                    }
+                        </Tooltip>}
                     subheader={process.containerRequest.description} />
                 <CardContent>
                     {lines.length > 0
-                        ? < Grid container spacing={24} alignItems='center'>
-                            <Grid item xs={6}>
-                                <ProcessLogForm selectedFilter={selectedFilter} filters={filters} onChange={onChange} />
-                            </Grid>
-                            <Grid item xs={6} className={classes.link}>
-                                <Typography component='div'>
-                                    Go to Log collection
+                        ? < Grid
+                            container
+                            spacing={24}
+                            direction='column'>
+                            <Grid container item>
+                                <Grid item xs={6}>
+                                    <ProcessLogForm selectedFilter={selectedFilter} filters={filters} onChange={onChange} />
+                                </Grid>
+                                <Grid item xs={6} className={classes.link}>
+                                    <Typography component='div'>
+                                        Go to Log collection
                                 </Typography>
+                                </Grid>
                             </Grid>
-                            <Grid item xs={12}>
+                            <Grid item xs>
                                 <ProcessLogCodeSnippet lines={lines} />
                             </Grid>
                         </Grid>
index 0845a4109780d1f8995e5d47d80eca99eead0b43..38870c402759814d3e9b4a0d5abaed08b5d4991c 100644 (file)
@@ -10,14 +10,13 @@ import { ProcessLogFormDataProps, ProcessLogFormActionProps } from '~/views/proc
 import { DefaultView } from '~/components/default-view/default-view';
 import { ProcessIcon } from '~/components/icon/icon';
 import { CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { ProcessLogMainCardActionProps } from './process-log-main-card';
 
 export type ProcessLogPanelRootDataProps = {
     process?: Process;
 } & ProcessLogFormDataProps & CodeSnippetDataProps;
 
-export type ProcessLogPanelRootActionProps = {
-    onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
-} & ProcessLogFormActionProps;
+export type ProcessLogPanelRootActionProps = ProcessLogMainCardActionProps & ProcessLogFormActionProps;
 
 export type ProcessLogPanelRootProps = ProcessLogPanelRootDataProps & ProcessLogPanelRootActionProps;
 
index 2b2d6842774fdc168a200864df22dc7df15bfe48..d578e784566b5d1c1d5bdd3caeb9672998c5e95f 100644 (file)
@@ -2,13 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from 'react';
 import { RootState } from '~/store/store';
 import { connect } from 'react-redux';
 import { getProcess } from '~/store/processes/process';
 import { Dispatch } from 'redux';
 import { openProcessContextMenu } from '~/store/context-menu/context-menu-actions';
-import { matchProcessLogRoute } from '~/routes/routes';
 import { ProcessLogPanelRootDataProps, ProcessLogPanelRootActionProps, ProcessLogPanelRoot } from './process-log-panel-root';
 import { getProcessPanelLogs } from '~/store/process-logs-panel/process-logs-panel';
 import { setProcessLogsPanelFilter } from '~/store/process-logs-panel/process-logs-panel-actions';
@@ -39,10 +37,10 @@ const mapStateToProps = (state: RootState): ProcessLogPanelRootDataProps => {
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessLogPanelRootActionProps => ({
-    onContextMenu: (event: React.MouseEvent<HTMLElement>) => {
-        dispatch<any>(openProcessContextMenu(event));
+    onContextMenu: (event, process) => {
+        dispatch<any>(openProcessContextMenu(event, process));
     },
-    onChange: (filter: FilterOption) => {
+    onChange: filter => {
         dispatch(setProcessLogsPanelFilter(filter.value));
     }
 });
index fea94b904341958f2b3edb0a07754a823a97a349..40fd8b229681ff38abd14125450e812e1767d564 100644 (file)
@@ -86,13 +86,13 @@ export const ProcessInformationCard = withStyles(styles, { withTheme: true })(
                         <Chip label={getProcessStatus(process)}
                             className={classes.chip}
                             style={{ backgroundColor: getProcessStatusColor(getProcessStatus(process), theme as ArvadosTheme) }} />
-                        <IconButton
-                            aria-label="More options"
-                            onClick={event => onContextMenu(event)}>
-                            <Tooltip title="More options">
+                        <Tooltip title="More options">
+                            <IconButton
+                                aria-label="More options"
+                                onClick={event => onContextMenu(event)}>
                                 <MoreOptionsIcon />
-                            </Tooltip>
-                        </IconButton>
+                            </IconButton>
+                        </Tooltip>
                     </div>
                 }
                 title={
index d12704bcf4b69c87ec59283dc824feba3e1dd308..ab8af36ffbb5e619e69fa22e2fbbb62b07d0a634 100644 (file)
@@ -20,19 +20,19 @@ export interface ProcessPanelRootDataProps {
 }
 
 export interface ProcessPanelRootActionProps {
-    onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
     onToggle: (status: string) => void;
 }
 
 export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps;
 
-export const ProcessPanelRoot = (props: ProcessPanelRootProps) =>
-    props.process
+export const ProcessPanelRoot = ({process, ...props}: ProcessPanelRootProps) =>
+    process
         ? <Grid container spacing={16} alignItems="stretch">
             <Grid item sm={12} md={7}>
                 <ProcessInformationCard
-                    process={props.process}
-                    onContextMenu={props.onContextMenu} />
+                    process={process}
+                    onContextMenu={event => props.onContextMenu(event, process)} />
             </Grid>
             <Grid item sm={12} md={5}>
                 <SubprocessesCard
index b3e36ab77c1742c646db51d2672b76d198a2c0b7..4f283a6c9cadb56826b74b47d09a19501a0253df 100644 (file)
@@ -27,8 +27,8 @@ const mapStateToProps = ({ router, resources, processPanel }: RootState): Proces
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
-    onContextMenu: event => {
-        dispatch<any>(openProcessContextMenu(event));
+    onContextMenu: (event, process) => {
+        dispatch<any>(openProcessContextMenu(event, process));
     },
     onToggle: status => {
         dispatch<any>(toggleProcessPanelFilter(status));
index 4226fefc8cb9bca9fe1e153cddb3a43194dc793b..54e0206e3624c8b512b8b730e318ffe9bd04228b 100644 (file)
@@ -83,14 +83,14 @@ export const ProcessSubprocessesCard = withStyles(styles, { withTheme: true })(
                         <Typography noWrap variant="body2" className={classes.status}>
                             {getProcessStatus(subprocess)}
                         </Typography>
-                        <IconButton
-                            className={classes.options}
-                            aria-label="More options"
-                            onClick={onContextMenu}>
-                            <Tooltip title="More options">
+                        <Tooltip title="More options">
+                            <IconButton
+                                className={classes.options}
+                                aria-label="More options"
+                                onClick={onContextMenu}>
                                 <MoreOptionsIcon />
-                            </Tooltip>
-                        </IconButton>
+                            </IconButton>
+                        </Tooltip>
                     </div>
                 }
                 title={
index 6e00deb0f1a0963ce8f69280ec84ed86c0c072dd..d3f87701fc73724799a8968df0b5da29d394d4bb 100644 (file)
@@ -9,14 +9,16 @@ import { Process } from '~/store/processes/process';
 
 export interface ProcessSubprocessesDataProps {
     subprocesses: Array<Process>;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
 }
 
 export const ProcessSubprocesses = ({ onContextMenu, subprocesses }: ProcessSubprocessesDataProps) => {
     return <Grid container spacing={16}>
         {subprocesses.map(subprocess =>
             <Grid item xs={12} sm={6} md={4} lg={2} key={subprocess.containerRequest.uuid}>
-                <ProcessSubprocessesCard onContextMenu={onContextMenu} subprocess={subprocess} />
+                <ProcessSubprocessesCard
+                    onContextMenu={event => onContextMenu(event, subprocess)}
+                    subprocess={subprocess} />
             </Grid>
         )}
     </Grid>;
index 0607c4711e2238ea26dcefff22663ca66568d258..9cff1e981630ed6a72e8a2868bb8c70c413b22cd 100644 (file)
@@ -16,7 +16,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         height: '100%'
     },
     title: {
-        color: theme.customs.colors.grey700
+        color: theme.palette.grey["700"]
     },
     gridFilter: {
         height: '20px',
index 63aedaddeb4e17a62cfd318a12cdea66114c87e2..2b2be2e8905ec5d111faf8c121da72dddba94371 100644 (file)
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
 import { DataColumns } from '~/components/data-table/data-table';
@@ -14,7 +13,6 @@ import { ContainerRequestState } from '~/models/container-request';
 import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind, Resource } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
-import { ArvadosTheme } from '~/common/custom-theme';
 import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
 import { ProjectIcon } from '~/components/icon/icon';
 import { ResourceName } from '~/views-components/data-explorer/renderers';
@@ -25,13 +23,12 @@ import { ProjectResource } from '~/models/project';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { getProperty } from '~/store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
-import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
-import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
-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';
+import { StyleRulesCallback, WithStyles } from "@material-ui/core";
+import { ArvadosTheme } from "~/common/custom-theme";
+import withStyles from "@material-ui/core/styles/withStyles";
 
-type CssRules = 'root' | "toolbar" | "button";
+type CssRules = 'root' | "button";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -39,10 +36,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,50 +136,27 @@ 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}
-                            onRowClick={this.handleRowClick}
-                            onRowDoubleClick={this.handleRowDoubleClick}
-                            onContextMenu={this.handleContextMenu}
-                            contextMenuColumn={true}
-                            dataTableDefaultView={<DataTableDefaultView icon={ProjectIcon}/>} />
-                        : <PanelDefaultView
-                            icon={ProjectIcon}
-                            messages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
-                    }
-
+                    <DataExplorer
+                        id={PROJECT_PANEL_ID}
+                        onRowClick={this.handleRowClick}
+                        onRowDoubleClick={this.handleRowDoubleClick}
+                        onContextMenu={this.handleContextMenu}
+                        contextMenuColumn={true}
+                        dataTableDefaultView={
+                            <DataTableDefaultView
+                                icon={ProjectIcon}
+                                messages={[
+                                    'Your project is empty.',
+                                    'Please create a project or create a collection and upload a data.'
+                                ]}/>
+                        }/>
                 </div>;
             }
 
-            hasAnyItems = () => {
-                const resources = filterResources(this.isCurrentItemChild)(this.props.resources);
-                return resources.length > 0;
-            }
-
             isCurrentItemChild = (resource: Resource) => {
                 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);
diff --git a/src/views/shared-with-me-panel/shared-with-me-panel.tsx b/src/views/shared-with-me-panel/shared-with-me-panel.tsx
new file mode 100644 (file)
index 0000000..9106f87
--- /dev/null
@@ -0,0 +1,77 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from '~/store/store';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { ShareMeIcon } from '~/components/icon/icon';
+import { ResourcesState, getResource } from '~/store/resources/resources';
+import { navigateTo } from "~/store/navigation/navigation-action";
+import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { SHARED_WITH_ME_PANEL_ID } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { GroupResource } from '~/models/group';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+
+type CssRules = "toolbar" | "button";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    toolbar: {
+        paddingBottom: theme.spacing.unit * 3,
+        textAlign: "right"
+    },
+    button: {
+        marginLeft: theme.spacing.unit
+    },
+});
+
+interface SharedWithMePanelDataProps {
+    resources: ResourcesState;
+}
+
+type SharedWithMePanelProps = SharedWithMePanelDataProps & DispatchProp & WithStyles<CssRules>;
+
+export const SharedWithMePanel = withStyles(styles)(
+    connect((state: RootState) => ({
+        resources: state.resources
+    }))(
+        class extends React.Component<SharedWithMePanelProps> {
+            render() {
+                return <DataExplorer
+                    id={SHARED_WITH_ME_PANEL_ID}
+                    onRowClick={this.handleRowClick}
+                    onRowDoubleClick={this.handleRowDoubleClick}
+                    onContextMenu={this.handleContextMenu}
+                    contextMenuColumn={false}
+                    dataTableDefaultView={<DataTableDefaultView icon={ShareMeIcon} />} />;
+            }
+
+            handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+                const resource = getResource<GroupResource>(resourceUuid)(this.props.resources);
+                if (resource) {
+                    this.props.dispatch<any>(openContextMenu(event, {
+                        name: '',
+                        uuid: resource.uuid,
+                        ownerUuid: resource.ownerUuid,
+                        isTrashed: resource.isTrashed,
+                        kind: resource.kind,
+                        menuKind: ContextMenuKind.PROJECT,
+                    }));
+                }
+            }
+
+            handleRowDoubleClick = (uuid: string) => {
+                this.props.dispatch<any>(navigateTo(uuid));
+            }
+
+            handleRowClick = (uuid: string) => {
+                this.props.dispatch(loadDetailsPanel(uuid));
+            }
+        }
+    )
+);
index 4a1e197d540dc059a8cfe15c6d642c218a308a0d..92febd8acf55467f960eb234ac1ecee99c6ea28a 100644 (file)
@@ -63,20 +63,20 @@ export const ResourceRestore =
         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
         return { resource, dispatch: props.dispatch };
     })((props: { resource?: TrashableResource, dispatch?: Dispatch<any> }) =>
-        <IconButton onClick={() => {
-            if (props.resource && props.dispatch) {
-                props.dispatch(toggleTrashed(
-                    props.resource.kind,
-                    props.resource.uuid,
-                    props.resource.ownerUuid,
-                    props.resource.isTrashed
-                ));
-            }
-        }}>
-            <Tooltip title="Restore">
+        <Tooltip title="Restore">
+            <IconButton onClick={() => {
+                if (props.resource && props.dispatch) {
+                    props.dispatch(toggleTrashed(
+                        props.resource.kind,
+                        props.resource.uuid,
+                        props.resource.ownerUuid,
+                        props.resource.isTrashed
+                    ));
+                }
+            }}>
                 <RestoreFromTrashIcon />
-            </Tooltip>
-        </IconButton>
+            </IconButton>
+        </Tooltip>
     );
 
 export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
@@ -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,
@@ -160,23 +155,17 @@ export const TrashPanel = withStyles(styles)(
     }))(
         class extends React.Component<TrashPanelProps> {
             render() {
-                return this.hasAnyTrashedResources()
-                    ? <DataExplorer
-                        id={TRASH_PANEL_ID}
-                        onRowClick={this.handleRowClick}
-                        onRowDoubleClick={this.handleRowDoubleClick}
-                        onContextMenu={this.handleContextMenu}
-                        contextMenuColumn={false}
-                        dataTableDefaultView={<DataTableDefaultView icon={TrashIcon} />} />
-                    : <PanelDefaultView
-                        icon={TrashIcon}
-                        messages={['Your trash list is empty.']} />;
-            }
-
-            hasAnyTrashedResources = () => {
-                // TODO: implement check if there is anything in the trash,
-                //       without taking pagination into the account
-                return true;
+                return <DataExplorer
+                    id={TRASH_PANEL_ID}
+                    onRowClick={this.handleRowClick}
+                    onRowDoubleClick={this.handleRowDoubleClick}
+                    onContextMenu={this.handleContextMenu}
+                    contextMenuColumn={false}
+                    dataTableDefaultView={
+                        <DataTableDefaultView
+                            icon={TrashIcon}
+                            messages={['Your trash list is empty.']}/>
+                    } />;
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
index 3c281087c2addf20ad8f3f6e7e31500673a28782..ad1a266881993b8f5e38eb568ac0f8f2c175ef3d 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,56 +27,65 @@ 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';
+import { CopyProcessDialog } from '~/views-components/dialog-forms/copy-process-dialog';
 import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
+import { UpdateProcessDialog } from '~/views-components/dialog-forms/update-process-dialog';
 import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
+import { MoveProcessDialog } from '~/views-components/dialog-forms/move-process-dialog';
 import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
 import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-collection-dialog';
 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, LinearProgress } from '@material-ui/core';
+import { SharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel';
+import SplitterLayout from 'react-splitter-layout';
+import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
+import { isSystemWorking } from "~/store/progress-indicator/progress-indicator-reducer";
 
-const APP_BAR_HEIGHT = 100;
-
-type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
+type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content' | 'appBar';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
-        flexGrow: 1,
-        zIndex: 1,
         overflow: 'hidden',
-        position: 'relative',
-        display: 'flex',
         width: '100vw',
-        height: '100vh'
+        height: '100vh',
+        paddingTop: theme.spacing.unit * 8
     },
-    appBar: {
-        zIndex: theme.zIndex.drawer + 1,
-        position: "absolute",
-        width: "100%"
+    container: {
+        position: 'relative'
+    },
+    splitter: {
+        '& > .layout-splitter': {
+            width: '2px'
+        }
+    },
+    asidePanel: {
+        height: '100%',
+        background: theme.palette.background.default
     },
     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,
+        paddingLeft: theme.spacing.unit * 3,
+        paddingRight: theme.spacing.unit * 3,
     },
+    appBar: {
+        zIndex: 1,
+    }
 });
 
 interface WorkbenchDataProps {
     user?: User;
     currentToken?: string;
+    working: boolean;
 }
 
 interface WorkbenchGeneralProps {
@@ -85,24 +93,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)(
@@ -110,108 +104,88 @@ export const Workbench = withStyles(styles)(
         (state: RootState) => ({
             user: state.auth.user,
             currentToken: state.auth.apiToken,
+            working: isSystemWorking(state.progressIndicator)
         })
     )(
         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}>
-                            <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 />
-                        <MoveProjectDialog />
-                        <CurrentTokenDialog
-                            currentToken={this.props.currentToken}
-                            open={this.state.isCurrentTokenDialogOpen}
-                            handleClose={this.toggleCurrentTokenModal} />
-                    </div>
-                );
+                const { classes } = this.props;
+                return <>
+                    <MainAppBar
+                        searchText={this.state.searchText}
+                        user={this.props.user}
+                        onSearch={this.onSearch}
+                        buildInfo={this.props.buildInfo}>
+                        {this.props.working ? <LinearProgress color="secondary" /> : null}
+                    </MainAppBar>
+                    <Grid container direction="column" className={classes.root}>
+                        {this.props.user &&
+                            <Grid container item xs alignItems="stretch" wrap="nowrap">
+                                <Grid container item className={classes.container}>
+                                <SplitterLayout customClassName={classes.splitter} percentage={true}
+                                    primaryIndex={0} primaryMinSize={20} secondaryInitialSize={80} secondaryMinSize={40}>
+                                        <Grid container item xs component='aside' direction='column' className={classes.asidePanel}>
+                                            <SidePanel />
+                                        </Grid>
+                                        <Grid container item xs component="main" direction="column" className={classes.contentWrapper}>
+                                            <Grid item>
+                                                <MainContentBar />
+                                            </Grid>
+                                            <Grid item xs 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} />
+                                                    <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
+                                                </Switch>
+                                            </Grid>
+                                        </Grid>
+                                    </SplitterLayout>
+                                </Grid>
+                                <Grid item>
+                                    <DetailsPanel />
+                                </Grid>
+                            </Grid>
+                        }
+                    </Grid>
+                    <ContextMenu />
+                    <CopyCollectionDialog />
+                    <CopyProcessDialog />
+                    <CreateCollectionDialog />
+                    <CreateProjectDialog />
+                    <CurrentTokenDialog />
+                    <FileRemoveDialog />
+                    <FileRemoveDialog />
+                    <FilesUploadCollectionDialog />
+                    <MoveCollectionDialog />
+                    <MoveProcessDialog />
+                    <MoveProjectDialog />
+                    <MultipleFilesRemoveDialog />
+                    <PartialCopyCollectionDialog />
+                    <ProcessCommandDialog />
+                    <RenameFileDialog />
+                    <Snackbar />
+                    <UpdateCollectionDialog />
+                    <UpdateProcessDialog />
+                    <UpdateProjectDialog />
+                </>;
             }
 
-            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());
             }
+
         }
     )
 );
index 85b43690d37e54ae7d2d4c3dd1f80dffb527abd9..f9b81ca95bf0e83a0f2d7b19866624d9eef5887d 100644 (file)
@@ -14,7 +14,8 @@
     "no-shadowed-variable": false,
     "semicolon": true,
     "array-type": false,
-    "interface-over-type-literal": false
+    "interface-over-type-literal": false,
+    "no-empty": false
   },
   "linterOptions": {
     "exclude": [
index 70a3fe53e1704c60bf533392163daa297200ef9d..da8e4415d1f6c2d863eb47cb8586ab9214a5ba32 100644 (file)
@@ -11,3 +11,5 @@ declare interface System {
   import<T = any>(module: string): Promise<T>
 }
 declare var System: System;
+
+declare module 'react-splitter-layout';
\ No newline at end of file
index 67c12647b4625337eae116a1f6feea7f9322da8e..30e94bdefb4c9be9d37fc394908c15653130d55f 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   dependencies:
     "@types/enzyme" "*"
 
-"@types/enzyme@*":
-  version "3.1.12"
-  resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c"
-  dependencies:
-    "@types/cheerio" "*"
-    "@types/react" "*"
-
-"@types/enzyme@3.1.13":
+"@types/enzyme@*", "@types/enzyme@3.1.13":
   version "3.1.13"
   resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.13.tgz#4bbc5c81fa40c9fc7efee25c4a23cb37119a33ea"
   dependencies:
   version "4.14.116"
   resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
 
-"@types/node@*":
-  version "10.5.2"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
-
-"@types/node@10.7.1":
+"@types/node@*", "@types/node@10.7.1":
   version "10.7.1"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
 
     "@types/react" "*"
     redux "^3.6.0 || ^4.0.0"
 
+"@types/uuid@3.4.4":
+  version "3.4.4"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
+  dependencies:
+    "@types/node" "*"
+
 abab@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
@@ -1701,14 +1696,10 @@ color-convert@^1.3.0, color-convert@^1.9.0:
   dependencies:
     color-name "1.1.1"
 
-color-name@1.1.1:
+color-name@1.1.1, color-name@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
 
-color-name@^1.0.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
-
 color-string@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
@@ -2357,20 +2348,13 @@ domutils@1.1:
   dependencies:
     domelementtype "1"
 
-domutils@1.5.1:
+domutils@1.5.1, domutils@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
   dependencies:
     dom-serializer "0"
     domelementtype "1"
 
-domutils@^1.5.1:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
-  dependencies:
-    dom-serializer "0"
-    domelementtype "1"
-
 dot-prop@^4.1.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
@@ -2832,14 +2816,10 @@ extract-text-webpack-plugin@3.0.2:
     schema-utils "^0.3.0"
     webpack-sources "^1.0.1"
 
-extsprintf@1.3.0:
+extsprintf@1.3.0, extsprintf@^1.2.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
 
-extsprintf@^1.2.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
-
 fast-deep-equal@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@@ -4934,7 +4914,7 @@ minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist@0.0.8:
+minimist@0.0.8, minimist@~0.0.1:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 
@@ -4942,10 +4922,6 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
-minimist@~0.0.1:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
-
 minipass@^2.2.1, minipass@^2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
@@ -5345,18 +5321,12 @@ onetime@^2.0.0:
   dependencies:
     mimic-fn "^1.0.0"
 
-opn@5.2.0:
+opn@5.2.0, opn@^5.1.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225"
   dependencies:
     is-wsl "^1.1.0"
 
-opn@^5.1.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
-  dependencies:
-    is-wsl "^1.1.0"
-
 optimist@^0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@@ -6036,14 +6006,10 @@ q@^1.1.2:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
 
-qs@6.5.1:
+qs@6.5.1, qs@~6.5.1:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
 
-qs@~6.5.1:
-  version "6.5.2"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
-
 query-string@^4.1.0:
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
@@ -6181,11 +6147,7 @@ react-event-listener@^0.6.2:
     prop-types "^15.6.0"
     warning "^4.0.1"
 
-react-is@^16.4.1:
-  version "16.4.1"
-  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
-
-react-is@^16.4.2:
+react-is@^16.4.1, react-is@^16.4.2:
   version "16.4.2"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88"
 
@@ -6298,6 +6260,10 @@ react-scripts-ts@2.17.0:
   optionalDependencies:
     fsevents "^1.1.3"
 
+react-splitter-layout@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/react-splitter-layout/-/react-splitter-layout-3.0.1.tgz#c2e00e69b35d240ab7a44f395d41803c5f4b70ef"
+
 react-test-renderer@^16.0.0-0:
   version "16.4.1"
   resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70"
@@ -6307,7 +6273,7 @@ react-test-renderer@^16.0.0-0:
     prop-types "^15.6.0"
     react-is "^16.4.1"
 
-react-transition-group@^2.2.1:
+react-transition-group@2.4.0, react-transition-group@^2.2.1:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
   dependencies:
@@ -6665,18 +6631,12 @@ resolve@1.1.7:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
 
-resolve@1.6.0:
+resolve@1.6.0, resolve@^1.1.7, resolve@^1.3.2:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.6.0.tgz#0fbd21278b27b4004481c395349e7aba60a9ff5c"
   dependencies:
     path-parse "^1.0.5"
 
-resolve@^1.1.7, resolve@^1.3.2:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
-  dependencies:
-    path-parse "^1.0.5"
-
 restore-cursor@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@@ -7124,11 +7084,7 @@ static-extend@^0.1.1:
     define-property "^0.2.5"
     object-copy "^0.1.0"
 
-"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
-
-statuses@~1.4.0:
+"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2", statuses@~1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
 
@@ -7808,14 +7764,14 @@ utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
 
+uuid@3.3.2, uuid@^3.1.0:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+
 uuid@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
 
-uuid@^3.1.0:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
-
 validate-npm-package-license@^3.0.1:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"
@@ -7998,14 +7954,10 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
   dependencies:
     iconv-lite "0.4.19"
 
-whatwg-fetch@2.0.3:
+whatwg-fetch@2.0.3, whatwg-fetch@>=0.10.0:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
 
-whatwg-fetch@>=0.10.0:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
-
 whatwg-mimetype@^2.0.0, whatwg-mimetype@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f0f21d76cbba72362eb609dbed2a30cd17fcc7d4"
@@ -8052,14 +8004,10 @@ window-size@0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
 
-wordwrap@0.0.2:
+wordwrap@0.0.2, wordwrap@~0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
 
-wordwrap@~0.0.2:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
-
 wordwrap@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"