merge conflicts
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 23 Jul 2018 12:14:16 +0000 (14:14 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 23 Jul 2018 12:14:16 +0000 (14:14 +0200)
Feature #13781

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

109 files changed:
.env
.licenseignore
.npmrc [new file with mode: 0644]
.yarnrc [new file with mode: 0644]
Makefile
README.md
etc/arvados/workbench2/workbench2.example.json [new file with mode: 0644]
package.json
src/common/api/common-resource-service.test.ts
src/common/api/common-resource-service.ts
src/common/api/filter-builder.test.ts
src/common/api/filter-builder.ts
src/common/api/order-builder.test.ts
src/common/api/order-builder.ts
src/common/api/server-api.ts
src/common/api/url-builder.ts
src/common/config.ts [new file with mode: 0644]
src/common/custom-theme.ts
src/common/formatters.ts
src/common/url.ts [new file with mode: 0644]
src/components/attribute/attribute.tsx [deleted file]
src/components/breadcrumbs/breadcrumbs.test.tsx
src/components/breadcrumbs/breadcrumbs.tsx
src/components/column-selector/column-selector.test.tsx
src/components/column-selector/column-selector.tsx
src/components/context-menu/context-menu.test.tsx
src/components/context-menu/context-menu.tsx
src/components/data-explorer/data-explorer.test.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table-filters/data-table-filters.test.tsx
src/components/data-table-filters/data-table-filters.tsx
src/components/data-table/data-table.test.tsx
src/components/data-table/data-table.tsx
src/components/details-attribute/details-attribute.tsx [new file with mode: 0644]
src/components/details-panel-factory/details-panel-factory.tsx [deleted file]
src/components/details-panel-factory/items/abstract-item.tsx [deleted file]
src/components/details-panel-factory/items/collection-item.tsx [deleted file]
src/components/details-panel-factory/items/empty-item.tsx [deleted file]
src/components/details-panel-factory/items/process-item.tsx [deleted file]
src/components/details-panel-factory/items/project-item.tsx [deleted file]
src/components/dropdown-menu/dropdown-menu.test.tsx
src/components/dropdown-menu/dropdown-menu.tsx
src/components/empty-state/empty-state.tsx [deleted file]
src/components/icon/icon.tsx
src/components/list-item-text-icon/list-item-text-icon.tsx [new file with mode: 0644]
src/components/popover/helpers.ts
src/components/popover/popover.test.tsx
src/components/popover/popover.tsx
src/components/search-bar/search-bar.test.tsx
src/components/search-bar/search-bar.tsx
src/components/search-input/search-input.test.tsx
src/components/search-input/search-input.tsx
src/components/side-panel/side-panel.tsx
src/components/tree/tree.test.tsx
src/components/tree/tree.tsx
src/index.tsx
src/models/container-request.ts
src/models/details.ts [new file with mode: 0644]
src/models/empty.ts
src/services/auth-service/auth-service.ts
src/services/groups-service/groups-service.test.ts
src/services/groups-service/groups-service.ts
src/services/project-service/project-service.test.ts
src/services/project-service/project-service.ts
src/services/services.ts
src/store/auth/auth-action.ts
src/store/auth/auth-reducer.test.ts
src/store/auth/auth-reducer.ts
src/store/context-menu/context-menu-actions.ts
src/store/context-menu/context-menu-reducer.ts
src/store/data-explorer/data-explorer-action.ts
src/store/data-explorer/data-explorer-reducer.test.tsx
src/store/data-explorer/data-explorer-reducer.ts
src/store/details-panel/details-panel-action.ts
src/store/details-panel/details-panel-reducer.ts
src/store/navigation/navigation-action.ts
src/store/project-panel/project-panel-middleware.ts
src/store/project/project-action.ts
src/store/project/project-reducer.test.ts
src/store/project/project-reducer.ts
src/store/side-panel/side-panel-action.ts
src/store/side-panel/side-panel-reducer.test.ts
src/store/side-panel/side-panel-reducer.ts
src/store/store.ts
src/utils/dialog-validator.tsx [new file with mode: 0644]
src/views-components/api-token/api-token.tsx
src/views-components/context-menu/action-sets/project-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/root-project-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/context-menu-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/context-menu.tsx [new file with mode: 0644]
src/views-components/create-project-dialog/create-project-dialog.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/details-panel/collection-details.tsx [new file with mode: 0644]
src/views-components/details-panel/details-data.tsx [new file with mode: 0644]
src/views-components/details-panel/details-panel.tsx
src/views-components/details-panel/empty-details.tsx [new file with mode: 0644]
src/views-components/details-panel/process-details.tsx [new file with mode: 0644]
src/views-components/details-panel/project-details.tsx [new file with mode: 0644]
src/views-components/dialog-create/dialog-project-create.tsx
src/views-components/main-app-bar/main-app-bar.test.tsx
src/views-components/main-app-bar/main-app-bar.tsx
src/views-components/project-list/project-list.tsx [deleted file]
src/views-components/project-tree/project-tree.test.tsx
src/views-components/project-tree/project-tree.tsx
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.test.tsx
src/views/workbench/workbench.tsx
tslint.json
yarn.lock

diff --git a/.env b/.env
index 13aaad5027763659d998bb48097d2c04c819fd4f..a523865a6ae43a5b4e8bc670cd28029bfee3870e 100644 (file)
--- a/.env
+++ b/.env
@@ -2,4 +2,5 @@
 # 
 # SPDX-License-Identifier: AGPL-3.0
 
+REACT_APP_ARVADOS_CONFIG_URL=/config.json
 REACT_APP_ARVADOS_API_HOST=https://qr1hi.arvadosapi.com
\ No newline at end of file
index c499a4f0ff38faa9702dd88448266081ed93dab9..09914c902bc81a844c2dca12dfc90bdc6266818d 100644 (file)
@@ -10,3 +10,5 @@ cc-by-sa-3.0.txt
 README.md
 public/*
 .licenseignore
+.yarnrc
+.npmrc
diff --git a/.npmrc b/.npmrc
new file mode 100644 (file)
index 0000000..cffe8cd
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+save-exact=true
diff --git a/.yarnrc b/.yarnrc
new file mode 100644 (file)
index 0000000..95b8581
--- /dev/null
+++ b/.yarnrc
@@ -0,0 +1 @@
+save-prefix false
index 30ab7bc4aca8e48159a96f214b5c756a16ae7f65..c28b4b86c360b0e4200af5f5eb6ef0513983da7b 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -84,7 +84,7 @@ $(DEB_FILE): build
         --iteration "$(ITERATION)" \
         --maintainer="$(MAINTAINER)" \
         --description="$(DESCRIPTION)" \
-        --deb-no-default-config-files \
+        --config-files="etc/arvados/workbench2/workbench2.example.json" \
        $(WORKSPACE)/build/=$(DEST_DIR)
 
 $(RPM_FILE): build
@@ -96,6 +96,7 @@ $(RPM_FILE): build
         --iteration "$(ITERATION)" \
         --maintainer="$(MAINTAINER)" \
         --description="$(DESCRIPTION)" \
+        --config-files="etc/arvados/workbench2/workbench2.example.json" \
         $(WORKSPACE)/build/=$(DEST_DIR)
 
 copy: $(DEB_FILE) $(RPM_FILE)
index 864a54fa89a122aab17afffa18d9828e3de0c050..998d424662ac4cb69fb75b89904d9955fe5bc25d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -26,12 +26,22 @@ yarn install
 yarn build
 </pre>
 
-### Configuration
+### Build time configuration
 You can customize project global variables using env variables. Default values are placed in the `.env` file.
 
 Example:
 ```
-REACT_APP_ARVADOS_API_HOST=localhost:8000 yarn start
+REACT_APP_ARVADOS_CONFIG_URL=config.json yarn build
+```
+
+### Run time configuration
+The app will fetch runtime configuration when starting. By default it will try to fetch `/config.json`. You can customize this url using build time configuration.
+
+Currently this configuration schema is supported:
+```
+{
+    "API_HOST": "string"
+}
 ```
 
 ### Licensing
diff --git a/etc/arvados/workbench2/workbench2.example.json b/etc/arvados/workbench2/workbench2.example.json
new file mode 100644 (file)
index 0000000..d790112
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "API_HOST": "CHANGE.TO.YOUR.ARVADOS.API.HOST" 
+}
\ No newline at end of file
index a8c56177bfeaf5e4dadcbb3c063709ab1a4d98e7..06fa893f97abe1dbcd18523df1deebf0d2660a3e 100644 (file)
@@ -3,12 +3,12 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
-    "@material-ui/core": "1.2.1",
+    "@material-ui/core": "1.4.0",
     "@material-ui/icons": "1.1.0",
-    "@types/lodash": "4.14.109",
+    "@types/lodash": "4.14.112",
     "@types/redux-form": "^7.4.1",
     "axios": "0.18.0",
-    "classnames": "^2.2.6",
+    "classnames": "2.2.6",
     "lodash": "4.17.10",
     "react": "16.4.1",
     "react-dom": "16.4.1",
   },
   "devDependencies": {
     "@types/classnames": "^2.2.4",
-    "@types/enzyme": "3.1.10",
+    "@types/enzyme": "3.1.12",
     "@types/enzyme-adapter-react-16": "1.0.2",
-    "@types/jest": "23.1.0",
-    "@types/node": "10.3.3",
-    "@types/react": "16.3",
+    "@types/jest": "23.3.0",
+    "@types/node": "10.5.2",
+    "@types/react": "16.4",
     "@types/react-dom": "16.0.6",
-    "@types/react-redux": "6.0.2",
-    "@types/react-router": "4.0.26",
+    "@types/react-redux": "6.0.4",
+    "@types/react-router": "4.0.29",
     "@types/react-router-dom": "4.2.7",
     "@types/react-router-redux": "5.0.15",
     "@types/redux-devtools": "3.0.44",
     "@types/redux-form": "^7.4.1",
-    "axios-mock-adapter": "^1.15.0",
-    "enzyme": "^3.3.0",
-    "enzyme-adapter-react-16": "^1.1.1",
+    "axios-mock-adapter": "1.15.0",
+    "enzyme": "3.3.0",
+    "enzyme-adapter-react-16": "1.1.1",
     "jest-localstorage-mock": "2.2.0",
     "redux-devtools": "3.4.1",
     "redux-form": "^7.4.2",
index 7093b59c555430953ed0e678ee56ff10566aa4bb..8346624550edc4af8149ab81c0838eb02405a640 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import CommonResourceService from "./common-resource-service";
+import { CommonResourceService } from "./common-resource-service";
 import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
 
index 39825c0e3eb8ae661212161325f7673df7cb095b..3956fb7390983824a402456abc2144850b85cda2 100644 (file)
@@ -3,8 +3,8 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as _ from "lodash";
-import FilterBuilder from "./filter-builder";
-import OrderBuilder from "./order-builder";
+import { FilterBuilder } from "./filter-builder";
+import { OrderBuilder } from "./order-builder";
 import { AxiosInstance, AxiosPromise } from "axios";
 import { Resource } from "../../models/resource";
 
@@ -31,7 +31,7 @@ export interface Errors {
     errorToken: string;
 }
 
-export default class CommonResourceService<T extends Resource> {
+export class CommonResourceService<T extends Resource> {
 
     static mapResponseKeys = (response: any): Promise<any> =>
         CommonResourceService.mapKeys(_.camelCase)(response.data)
@@ -103,6 +103,5 @@ export default class CommonResourceService<T extends Resource> {
     update(uuid: string) {
         throw new Error("Not implemented");
     }
-
 }
 
index 342439345f5210c55430f5218fad4ad40ee992f9..d129a806d0a2fd8937d05caed3cecad1b69f7d86 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import FilterBuilder from "./filter-builder";
+import { FilterBuilder } from "./filter-builder";
 
 describe("FilterBuilder", () => {
 
index 38c4fee8bf167a2bc29854eb9e8a0b4916e5f74f..28ad060f46b1e825c47ff0f0e04a6faf318c3603 100644 (file)
@@ -5,8 +5,7 @@
 import * as _ from "lodash";
 import { Resource } from "../../models/resource";
 
-export default class FilterBuilder<T extends Resource = Resource> {
-
+export class FilterBuilder<T extends Resource = Resource> {
     static create<T extends Resource = Resource>(resourcePrefix = "") {
         return new FilterBuilder<T>(resourcePrefix);
     }
@@ -61,5 +60,4 @@ export default class FilterBuilder<T extends Resource = Resource> {
         }
         return this;
     }
-
 }
index b80756d408ef55770727727f9f2f3b3095e4a03e..f53bddb5cc51e047540029564579041c4798c647 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import OrderBuilder from "./order-builder";
+import { OrderBuilder } from "./order-builder";
 
 describe("OrderBuilder", () => {
     it("should build correct order query", () => {
index b5a2e807a0b5cf2538ee91cdceaa5d08ab1beab4..ed990541c61bb960e4b1a5074a4530570d1617eb 100644 (file)
@@ -5,14 +5,14 @@
 import * as _ from "lodash";
 import { Resource } from "../../models/resource";
 
-export default class OrderBuilder<T extends Resource = Resource> {
+export class OrderBuilder<T extends Resource = Resource> {
 
     static create<T extends Resource = Resource>(prefix?: string){
         return new OrderBuilder<T>([], prefix);
     }
 
     private constructor(
-        private order: string[] = [], 
+        private order: string[] = [],
         private prefix = ""){}
 
     private addRule (direction: string, attribute: keyof T) {
index 330ce657e23bb5cb54a21ecf4a5e82d135446348..5beecd48ee7dafabfb34b5e2c1984af964f08498 100644 (file)
@@ -18,3 +18,7 @@ export function setServerApiAuthorizationHeader(token: string) {
 export function removeServerApiAuthorizationHeader() {
     delete serverApi.defaults.headers.common.Authorization;
 }
+
+export const setBaseUrl = (url: string) => {
+    serverApi.defaults.baseURL = url + "/arvados/v1";
+};
index e5786a239d5868cf43f0e868c976ae8f0e21a5fa..0587c837371dbe0ef242885f0bce6a4a5c2e9c4b 100644 (file)
@@ -2,25 +2,25 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-export default class UrlBuilder {
-       private url: string = "";
-       private query: string = "";
+export class UrlBuilder {
+    private readonly url: string = "";
+    private query: string = "";
 
-       constructor(host: string) {
-               this.url = host;
-       }
+    constructor(host: string) {
+        this.url = host;
+    }
 
-       public addParam(param: string, value: string) {
-               if (this.query.length === 0) {
-                       this.query += "?";
-               } else {
-                       this.query += "&";
-               }
-               this.query += `${param}=${value}`;
-               return this;
-       }
+    public addParam(param: string, value: string) {
+        if (this.query.length === 0) {
+            this.query += "?";
+        } else {
+            this.query += "&";
+        }
+        this.query += `${param}=${value}`;
+        return this;
+    }
 
-       public get() {
-               return this.url + this.query;
-       }
+    public get() {
+        return this.url + this.query;
+    }
 }
diff --git a/src/common/config.ts b/src/common/config.ts
new file mode 100644 (file)
index 0000000..4b4a52a
--- /dev/null
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import Axios from "../../node_modules/axios";
+
+export const CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json";
+
+export interface Config {
+    API_HOST: string;
+}
+
+const defaultConfig: Config = {
+    API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || ""
+};
+
+export const fetchConfig = () => {
+    return Axios
+        .get<Config>(CONFIG_URL + "?nocache=" + (new Date()).getTime())
+        .then(response => response.data)
+        .catch(() => Promise.resolve(defaultConfig));
+};
+
index 0850f8815a7c6bb712b4c8f935f3f0da7f38c9bf..c85acd9089b0011ac2a819068b55d1ce5fd35688 100644 (file)
@@ -8,6 +8,8 @@ import purple from '@material-ui/core/colors/purple';
 import blue from '@material-ui/core/colors/blue';
 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';
 
 interface ArvadosThemeOptions extends ThemeOptions {
     customs: any;
@@ -17,8 +19,16 @@ export interface ArvadosTheme extends Theme {
     customs: any;
 }
 
-const purple900 = purple["900"];
+const red900 = red["900"];
+const yellow700 = yellow["700"];
+const purple800 = purple["800"];
+const grey200 = grey["200"];
+const grey300 = grey["300"];
+const grey500 = grey["500"];
 const grey600 = grey["600"];
+const grey700 = grey["700"];
+const grey900 = grey["900"];
+
 const themeOptions: ArvadosThemeOptions = {
     customs: {
         colors: {
@@ -26,9 +36,14 @@ const themeOptions: ArvadosThemeOptions = {
         }
     },
     overrides: {
+        MuiTypography: {
+            body1: {
+                fontSize: '0.8125rem'
+            }
+        },
         MuiAppBar: {
             colorPrimary: {
-                backgroundColor: purple900
+                backgroundColor: purple800
             }
         },
         MuiTabs: {
@@ -36,13 +51,28 @@ const themeOptions: ArvadosThemeOptions = {
                 color: grey600
             },
             indicator: {
-                backgroundColor: purple900
+                backgroundColor: purple800
             }
         },
         MuiTab: {
             selected: {
                 fontWeight: 700,
-                color: purple900
+                color: purple800
+            }
+        },
+        MuiList: {
+            root: {
+                color: grey900
+            }
+        },
+        MuiListItemText: {
+            root: {
+                padding: 0
+            }
+        },
+        MuiListItemIcon: {
+            root: {
+                fontSize: '1.25rem'
             }
         }
     },
index fe7df14c9d12670a3a9c2f88480b6d6e519a902a..38ef0223e957f1b134d1dfed10c23341af145fbc 100644 (file)
@@ -10,7 +10,7 @@ export const formatDate = (isoDate: string) => {
 
 export const formatFileSize = (size?: number) => {
     if (typeof size === "number") {
-        for (const { base, unit } of fileSizes) {
+        for (const { base, unit } of FILE_SIZES) {
             if (size >= base) {
                 return `${(size / base).toFixed()} ${unit}`;
             }
@@ -19,7 +19,7 @@ export const formatFileSize = (size?: number) => {
     return "";
 };
 
-const fileSizes = [
+const FILE_SIZES = [
     {
         base: 1000000000000,
         unit: "TB"
diff --git a/src/common/url.ts b/src/common/url.ts
new file mode 100644 (file)
index 0000000..1824f26
--- /dev/null
@@ -0,0 +1,6 @@
+export function getUrlParameter(search: string, name: string) {
+    const safeName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
+    const regex = new RegExp('[\\?&]' + safeName + '=([^&#]*)');
+    const results = regex.exec(search);
+    return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
+}
diff --git a/src/components/attribute/attribute.tsx b/src/components/attribute/attribute.tsx
deleted file mode 100644 (file)
index ea35f5b..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import Typography from '@material-ui/core/Typography';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'src/common/custom-theme';
-
-interface AttributeDataProps {
-    label: string;
-    value?: string | number;
-    link?: string;
-}
-
-type AttributeProps = AttributeDataProps & WithStyles<CssRules>;
-
-class Attribute extends React.Component<AttributeProps> {
-
-    hasLink() {
-        return !!this.props.link;
-    }
-
-    render() {
-        const { label, link, value, children, classes } = this.props;
-        return <Typography component="div" className={classes.attribute}>
-                    <Typography component="span" className={classes.label}>{label}</Typography>
-                    { this.hasLink() ? (
-                        <a href='{link}' className={classes.link} target='_blank'>{value}</a>
-                    ) : (
-                        <Typography component="span" className={classes.value}>
-                            {value}
-                            {children}
-                        </Typography>
-                    )}
-                </Typography>;
-    }
-
-}
-
-type CssRules = 'attribute' | 'label' | 'value' | 'link';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    attribute: {
-        display: 'flex',
-        alignItems: 'flex-start',
-        marginBottom: theme.spacing.unit
-    },
-    label: {
-        color: theme.palette.grey["500"],
-        width: '40%'
-    },
-    value: {
-        width: '60%',
-        display: 'flex',
-        alignItems: 'flex-start',
-        textTransform: 'capitalize'
-    },
-    link: {
-        color: theme.palette.primary.main,
-        textDecoration: 'none'
-    }
-});
-
-export default withStyles(styles)(Attribute);
\ No newline at end of file
index ef3f8887976384922004830e2c5c6b2d8953edce..ea3d5ac21e678f28e8f2c890dadd5713e69821e6 100644 (file)
@@ -6,7 +6,7 @@ import * as React from "react";
 import { mount, configure } from "enzyme";
 
 import * as Adapter from "enzyme-adapter-react-16";
-import Breadcrumbs from "./breadcrumbs";
+import { Breadcrumbs } from "./breadcrumbs";
 import { Button } from "@material-ui/core";
 import ChevronRightIcon from '@material-ui/icons/ChevronRight';
 
@@ -50,4 +50,4 @@ describe("<Breadcrumbs />", () => {
     });
 
 
-});
\ No newline at end of file
+});
index 4868e137f9f6b11065bdd2dbfaf3d3ff4ac642da..da549dba46757a9932d7655f43c25e56431d4380 100644 (file)
@@ -11,59 +11,52 @@ export interface Breadcrumb {
     label: string;
 }
 
+type CssRules = "item" | "currentItem" | "label";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    item: {
+        opacity: 0.6
+    },
+    currentItem: {
+        opacity: 1
+    },
+    label: {
+        textTransform: "none"
+    }
+});
+
 interface BreadcrumbsProps {
     items: Breadcrumb[];
     onClick: (breadcrumb: Breadcrumb) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
 }
 
-const Breadcrumbs: React.SFC<BreadcrumbsProps & WithStyles<CssRules>> = ({ classes, onClick, onContextMenu, items }) => {
-    return <Grid container alignItems="center" wrap="nowrap">
-        {
-            items.map((item, index) => {
-                const isLastItem = index === items.length - 1;
-                return (
-                    <React.Fragment key={index}>
-                        <Tooltip title={item.label}>
-                            <Button
+export const Breadcrumbs = withStyles(styles)(
+    ({ classes, onClick, onContextMenu, items }: BreadcrumbsProps & WithStyles<CssRules>) =>
+    <Grid container alignItems="center" wrap="nowrap">
+    {
+        items.map((item, index) => {
+            const isLastItem = index === items.length - 1;
+            return (
+                <React.Fragment key={index}>
+                    <Tooltip title={item.label}>
+                        <Button
+                            color="inherit"
+                            className={isLastItem ? classes.currentItem : classes.item}
+                            onClick={() => onClick(item)}
+                            onContextMenu={event => onContextMenu(event, item)}>
+                            <Typography
+                                noWrap
                                 color="inherit"
-                                className={isLastItem ? classes.currentItem : classes.item}
-                                onClick={() => onClick(item)}
-                                onContextMenu={event => onContextMenu(event, item)}>
-                                <Typography
-                                    noWrap
-                                    color="inherit"
-                                    className={classes.label}>
-                                    {item.label}
-                                </Typography>
-                            </Button>
-                        </Tooltip>
-                        {
-                            !isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />
-                        }
-                    </React.Fragment>
-                );
-            })
-        }
-    </Grid>;
-};
-
-type CssRules = "item" | "currentItem" | "label";
-
-const styles: StyleRulesCallback<CssRules> = theme => {
-    const { unit } = theme.spacing;
-    return {
-        item: {
-            opacity: 0.6
-        },
-        currentItem: {
-            opacity: 1
-        },
-        label: {
-            textTransform: "none"
-        }
-    };
-};
-
-export default withStyles(styles)(Breadcrumbs);
-
+                                className={classes.label}>
+                                {item.label}
+                            </Typography>
+                        </Button>
+                    </Tooltip>
+                    {!isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />}
+                </React.Fragment>
+            );
+        })
+    }
+    </Grid>
+);
index c2835ad7e9819d8b95bb596eb5b50d99940319f2..01dba85c0621e1ac50718a340cb9059f8c147a2a 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from "react";
 import { mount, configure } from "enzyme";
 import * as Adapter from "enzyme-adapter-react-16";
-import ColumnSelector, { ColumnSelectorProps, ColumnSelectorTrigger } from "./column-selector";
+import { ColumnSelector, ColumnSelectorTrigger } from "./column-selector";
 import { ListItem, Checkbox } from "@material-ui/core";
 import { DataColumns } from "../data-table/data-table";
 
index b5dd43b85a43c1eeb298ce977726c9c784a111b3..0f496e25cd2c396f9fa20bcabf7e3c0915e49573 100644 (file)
@@ -3,19 +3,32 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { WithStyles, StyleRulesCallback, Theme, withStyles, IconButton, Paper, List, Checkbox, ListItemText, ListItem } from '@material-ui/core';
+import { WithStyles, StyleRulesCallback, withStyles, IconButton, Paper, List, Checkbox, ListItemText, ListItem } from '@material-ui/core';
 import MenuIcon from "@material-ui/icons/Menu";
 import { DataColumn, isColumnConfigurable } from '../data-table/data-column';
-import Popover from "../popover/popover";
+import { Popover } from "../popover/popover";
 import { IconButtonProps } from '@material-ui/core/IconButton';
 import { DataColumns } from '../data-table/data-table';
+import { ArvadosTheme } from "../../common/custom-theme";
 
-export interface ColumnSelectorProps {
+interface ColumnSelectorDataProps {
     columns: DataColumns<any>;
     onColumnToggle: (column: DataColumn<any>) => void;
 }
 
-const ColumnSelector: React.SFC<ColumnSelectorProps & WithStyles<CssRules>> = ({ columns, onColumnToggle, classes }) =>
+type CssRules = "checkbox";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    checkbox: {
+        width: 24,
+        height: 24
+    }
+});
+
+export type ColumnSelectorProps = ColumnSelectorDataProps & WithStyles<CssRules>;
+
+export const ColumnSelector = withStyles(styles)(
+    ({ columns, onColumnToggle, classes }: ColumnSelectorProps) =>
     <Popover triggerComponent={ColumnSelectorTrigger}>
         <Paper>
             <List dense>
@@ -38,20 +51,10 @@ const ColumnSelector: React.SFC<ColumnSelectorProps & WithStyles<CssRules>> = ({
                     ))}
             </List>
         </Paper>
-    </Popover>;
+    </Popover>
+);
 
-export const ColumnSelectorTrigger: React.SFC<IconButtonProps> = (props) =>
+export const ColumnSelectorTrigger = (props: IconButtonProps) =>
     <IconButton {...props}>
         <MenuIcon />
     </IconButton>;
-
-type CssRules = "checkbox";
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
-    checkbox: {
-        width: 24,
-        height: 24
-    }
-});
-
-export default withStyles(styles)(ColumnSelector);
index e4e2397da280ae7ddfa20d9a7a6ed816c5c83c80..a245253858c8f5e7a1cfe213f62debbe9c0c404b 100644 (file)
@@ -5,31 +5,32 @@
 import * as React from "react";
 import { mount, configure, shallow } from "enzyme";
 import * as Adapter from "enzyme-adapter-react-16";
-import ContextMenu from "./context-menu";
+import { ContextMenu } from "./context-menu";
 import { ListItem } from "@material-ui/core";
+import { ShareIcon } from "../icon/icon";
 
 configure({ adapter: new Adapter() });
 
 describe("<ContextMenu />", () => {
-    const actions = [[{
-        icon: "",
+    const items = [[{
+        icon: ShareIcon,
         name: "Action 1.1"
     }, {
-        icon: "",
+        icon: ShareIcon,
         name: "Action 1.2"
     },], [{
-        icon: "",
+        icon: ShareIcon,
         name: "Action 2.1"
     }]];
 
-    it("calls onActionClick with clicked action", () => {
-        const onActionClick = jest.fn();
+    it("calls onItemClick with clicked action", () => {
+        const onItemClick = jest.fn();
         const contextMenu = mount(<ContextMenu
             anchorEl={document.createElement("div")}
             onClose={jest.fn()}
-            onActionClick={onActionClick}
-            actions={actions} />);
+            onItemClick={onItemClick}
+            items={items} />);
         contextMenu.find(ListItem).at(2).simulate("click");
-        expect(onActionClick).toHaveBeenCalledWith(actions[1][0]);
+        expect(onItemClick).toHaveBeenCalledWith(items[1][0]);
     });
-});
\ No newline at end of file
+});
index c892ba2616dda6480de47d2c3767596636267917..2103a2a09c1211b1123ee8d93088fae8516cb9af 100644 (file)
@@ -4,25 +4,25 @@
 import * as React from "react";
 import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core";
 import { DefaultTransformOrigin } from "../popover/helpers";
+import { IconType } from "../icon/icon";
 
-export interface ContextMenuAction {
+export interface ContextMenuItem {
     name: string;
-    icon: string;
-    openCreateDialog?: boolean;
+    icon: IconType;
 }
 
-export type ContextMenuActionGroup = ContextMenuAction[];
+export type ContextMenuItemGroup = ContextMenuItem[];
 
-export interface ContextMenuProps<T> {
+export interface ContextMenuProps {
     anchorEl?: HTMLElement;
-    actions: ContextMenuActionGroup[];
-    onActionClick: (action: ContextMenuAction) => void;
+    items: ContextMenuItemGroup[];
+    onItemClick: (action: ContextMenuItem) => void;
     onClose: () => void;
 }
 
-export default class ContextMenu<T> extends React.PureComponent<ContextMenuProps<T>> {
+export class ContextMenu extends React.PureComponent<ContextMenuProps> {
     render() {
-        const { anchorEl, actions, onClose, onActionClick } = this.props;
+        const { anchorEl, items, onClose, onItemClick} = this.props;
         return <Popover
             anchorEl={anchorEl}
             open={!!anchorEl}
@@ -31,21 +31,21 @@ export default class ContextMenu<T> extends React.PureComponent<ContextMenuProps
             anchorOrigin={DefaultTransformOrigin}
             onContextMenu={this.handleContextMenu}>
             <List dense>
-                {actions.map((group, groupIndex) =>
+                {items.map((group, groupIndex) =>
                     <React.Fragment key={groupIndex}>
-                        {group.map((action, actionIndex) =>
+                        {group.map((item, actionIndex) =>
                             <ListItem
                                 button
                                 key={actionIndex}
-                                onClick={() => onActionClick(action)}>
+                                onClick={() => onItemClick(item)}>
                                 <ListItemIcon>
-                                    <i className={action.icon} />
+                                    <item.icon/>
                                 </ListItemIcon>
                                 <ListItemText>
-                                    {action.name}
+                                    {item.name}
                                 </ListItemText>
                             </ListItem>)}
-                        {groupIndex < actions.length - 1 && <Divider />}
+                        {groupIndex < items.length - 1 && <Divider />}
                     </React.Fragment>)}
             </List>
         </Popover>;
index 5d4877f2638dee125a874ce52868ff2b610fee62..616a9c122e751cdf72746b4e58838c6d6c3d41ca 100644 (file)
@@ -6,10 +6,10 @@ import * as React from "react";
 import { configure, mount } from "enzyme";
 import * as Adapter from 'enzyme-adapter-react-16';
 
-import DataExplorer from "./data-explorer";
-import ColumnSelector from "../column-selector/column-selector";
-import DataTable from "../data-table/data-table";
-import SearchInput from "../search-input/search-input";
+import { DataExplorer } from "./data-explorer";
+import { ColumnSelector } from "../column-selector/column-selector";
+import { DataTable } from "../data-table/data-table";
+import { SearchInput } from "../search-input/search-input";
 import { TablePagination } from "@material-ui/core";
 
 configure({ adapter: new Adapter() });
@@ -109,4 +109,4 @@ const mockDataExplorerProps = () => ({
     onChangePage: jest.fn(),
     onChangeRowsPerPage: jest.fn(),
     onContextMenu: jest.fn()
-});
\ No newline at end of file
+});
index e851ca992412257e8e036b7c5371f00d9411359f..4699fd6de0c143f4871febb38b421be8e4ee737f 100644 (file)
@@ -3,15 +3,27 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles, TablePagination, IconButton } from '@material-ui/core';
+import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton } from '@material-ui/core';
 import MoreVertIcon from "@material-ui/icons/MoreVert";
-import ColumnSelector from "../../components/column-selector/column-selector";
-import DataTable, { DataColumns } from "../../components/data-table/data-table";
-import { DataColumn } from "../../components/data-table/data-column";
-import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
-import SearchInput from '../search-input/search-input';
+import { ColumnSelector } from "../column-selector/column-selector";
+import { DataTable, DataColumns } from "../data-table/data-table";
+import { DataColumn } from "../data-table/data-column";
+import { DataTableFilterItem } from '../data-table-filters/data-table-filters';
+import { SearchInput } from '../search-input/search-input';
+import { ArvadosTheme } from "../../common/custom-theme";
 
-interface DataExplorerProps<T> {
+type CssRules = "searchBox" | "toolbar";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    searchBox: {
+        paddingBottom: theme.spacing.unit * 2
+    },
+    toolbar: {
+        paddingTop: theme.spacing.unit * 2
+    }
+});
+
+interface DataExplorerDataProps<T> {
     items: T[];
     itemsAvailable: number;
     columns: DataColumns<T>;
@@ -31,33 +43,35 @@ interface DataExplorerProps<T> {
     extractKey?: (item: T) => React.Key;
 }
 
-class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<CssRules>> {
+type DataExplorerProps<T> = DataExplorerDataProps<T> & WithStyles<CssRules>;
 
-    render() {
-        return <Paper>
-            <Toolbar className={this.props.classes.toolbar}>
-                <Grid container justify="space-between" wrap="nowrap" alignItems="center">
-                    <div className={this.props.classes.searchBox}>
-                        <SearchInput
-                            value={this.props.searchValue}
-                            onSearch={this.props.onSearch} />
-                    </div>
-                    <ColumnSelector
-                        columns={this.props.columns}
-                        onColumnToggle={this.props.onColumnToggle} />
-                </Grid>
-            </Toolbar>
-            <DataTable
-                columns={[...this.props.columns, this.contextMenuColumn]}
-                items={this.props.items}
-                onRowClick={(_, item: T) => this.props.onRowClick(item)}
-                onContextMenu={this.props.onContextMenu}
-                onRowDoubleClick={(_, item: T) => this.props.onRowDoubleClick(item)}
-                onFiltersChange={this.props.onFiltersChange}
-                onSortToggle={this.props.onSortToggle}
-                extractKey={this.props.extractKey} />
-            <Toolbar>
-                {this.props.items.length > 0 &&
+export const DataExplorer = withStyles(styles)(
+    class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+        render() {
+            return <Paper>
+                <Toolbar className={this.props.classes.toolbar}>
+                    <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+                        <div className={this.props.classes.searchBox}>
+                            <SearchInput
+                                value={this.props.searchValue}
+                                onSearch={this.props.onSearch}/>
+                        </div>
+                        <ColumnSelector
+                            columns={this.props.columns}
+                            onColumnToggle={this.props.onColumnToggle}/>
+                    </Grid>
+                </Toolbar>
+                <DataTable
+                    columns={[...this.props.columns, this.contextMenuColumn]}
+                    items={this.props.items}
+                    onRowClick={(_, item: T) => this.props.onRowClick(item)}
+                    onContextMenu={this.props.onContextMenu}
+                    onRowDoubleClick={(_, item: T) => this.props.onRowDoubleClick(item)}
+                    onFiltersChange={this.props.onFiltersChange}
+                    onSortToggle={this.props.onSortToggle}
+                    extractKey={this.props.extractKey}/>
+                <Toolbar>
+                    {this.props.items.length > 0 &&
                     <Grid container justify="flex-end">
                         <TablePagination
                             count={this.props.itemsAvailable}
@@ -69,45 +83,32 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<
                             component="div"
                         />
                     </Grid>}
-            </Toolbar>
-        </Paper>;
-    }
+                </Toolbar>
+            </Paper>;
+        }
 
-    changePage = (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
-        this.props.onChangePage(page);
-    }
+        changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
+            this.props.onChangePage(page);
+        }
 
-    changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
-        this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
-    }
+        changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
+            this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
+        }
 
-    renderContextMenuTrigger = (item: T) =>
-        <Grid container justify="flex-end">
-            <IconButton onClick={event => this.props.onContextMenu(event, item)}>
-                <MoreVertIcon />
-            </IconButton>
-        </Grid>
+        renderContextMenuTrigger = (item: T) =>
+            <Grid container justify="flex-end">
+                <IconButton onClick={event => this.props.onContextMenu(event, item)}>
+                    <MoreVertIcon/>
+                </IconButton>
+            </Grid>
 
-    contextMenuColumn = {
-        name: "Actions",
-        selected: true,
-        key: "context-actions",
-        renderHeader: () => null,
-        render: this.renderContextMenuTrigger,
-        width: "auto"
-    };
-
-}
-
-type CssRules = "searchBox" | "toolbar";
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
-    searchBox: {
-        paddingBottom: theme.spacing.unit * 2
-    },
-    toolbar: {
-        paddingTop: theme.spacing.unit * 2
+        contextMenuColumn = {
+            name: "Actions",
+            selected: true,
+            key: "context-actions",
+            renderHeader: () => null,
+            render: this.renderContextMenuTrigger,
+            width: "auto"
+        };
     }
-});
-
-export default withStyles(styles)(DataExplorer);
+);
index b2daebeff581c49a4b1916b5a4ad7a5d0c36c09f..b0a84b51760a50cef284c567bd24ddd1754a61ea 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as React from "react";
 import { mount, configure } from "enzyme";
-import DataTableFilter, { DataTableFilterItem } from "./data-table-filters";
+import { DataTableFilters, DataTableFilterItem } from "./data-table-filters";
 import * as Adapter from 'enzyme-adapter-react-16';
 import { Checkbox, ButtonBase, ListItem, Button, ListItemText } from "@material-ui/core";
 
@@ -19,12 +19,12 @@ describe("<DataTableFilter />", () => {
             name: "Filter 2",
             selected: false
         }];
-        const dataTableFilter = mount(<DataTableFilter name="" filters={filters} />);
+        const dataTableFilter = mount(<DataTableFilters name="" filters={filters} />);
         dataTableFilter.find(ButtonBase).simulate("click");
         expect(dataTableFilter.find(Checkbox).at(0).prop("checked")).toBeTruthy();
         expect(dataTableFilter.find(Checkbox).at(1).prop("checked")).toBeFalsy();
     });
-    
+
     it("updates filters after filters prop change", () => {
         const filters = [{
             name: "Filter 1",
@@ -34,7 +34,7 @@ describe("<DataTableFilter />", () => {
             name: "Filter 2",
             selected: true
         }];
-        const dataTableFilter = mount(<DataTableFilter name="" filters={filters} />);
+        const dataTableFilter = mount(<DataTableFilters name="" filters={filters} />);
         dataTableFilter.find(ButtonBase).simulate("click");
         expect(dataTableFilter.find(Checkbox).prop("checked")).toBeTruthy();
         dataTableFilter.find(ListItem).simulate("click");
@@ -53,7 +53,7 @@ describe("<DataTableFilter />", () => {
             selected: false
         }];
         const onChange = jest.fn();
-        const dataTableFilter = mount(<DataTableFilter name="" filters={filters} onChange={onChange} />);
+        const dataTableFilter = mount(<DataTableFilters name="" filters={filters} onChange={onChange} />);
         dataTableFilter.find(ButtonBase).simulate("click");
         dataTableFilter.find(ListItem).at(1).simulate("click");
         dataTableFilter.find(Button).at(0).simulate("click");
@@ -65,4 +65,4 @@ describe("<DataTableFilter />", () => {
             selected: true
         }]);
     });
-});
\ No newline at end of file
+});
index bede5aeaf9b10c102cfd8e949122299e56bc8acb..d288a5a3dbfc8997cee987598cfb767048e748f9 100644 (file)
@@ -23,133 +23,6 @@ import {
 import * as classnames from "classnames";
 import { DefaultTransformOrigin } from "../popover/helpers";
 
-export interface DataTableFilterItem {
-    name: string;
-    selected: boolean;
-}
-
-export interface DataTableFilterProps {
-    name: string;
-    filters: DataTableFilterItem[];
-    onChange?: (filters: DataTableFilterItem[]) => void;
-}
-
-interface DataTableFilterState {
-    anchorEl?: HTMLElement;
-    filters: DataTableFilterItem[];
-    prevFilters: DataTableFilterItem[];
-}
-
-class DataTableFilter extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
-    state: DataTableFilterState = {
-        anchorEl: undefined,
-        filters: [],
-        prevFilters: []
-    };
-    icon = React.createRef<HTMLElement>();
-
-    render() {
-        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>
-            <Popover
-                anchorEl={this.state.anchorEl}
-                open={!!this.state.anchorEl}
-                anchorOrigin={DefaultTransformOrigin}
-                transformOrigin={DefaultTransformOrigin}
-                onClose={this.cancel}>
-                <Card>
-                    <CardContent>
-                        <Typography variant="caption">
-                            {name}
-                        </Typography>
-                    </CardContent>
-                    <List dense>
-                        {this.state.filters.map((filter, index) =>
-                            <ListItem
-                                button
-                                key={index}
-                                onClick={this.toggleFilter(filter)}>
-                                <Checkbox
-                                    disableRipple
-                                    color="primary"
-                                    checked={filter.selected}
-                                    className={classes.checkbox} />
-                                <ListItemText>
-                                    {filter.name}
-                                </ListItemText>
-                            </ListItem>
-                        )}
-                    </List>
-                    <CardActions>
-                        <Button
-                            color="primary"
-                            variant="raised"
-                            size="small"
-                            onClick={this.submit}>
-                            Ok
-                        </Button>
-                        <Button
-                            color="primary"
-                            variant="outlined"
-                            size="small"
-                            onClick={this.cancel}>
-                            Cancel
-                        </Button>
-                    </CardActions >
-                </Card>
-            </Popover>
-        </>;
-    }
-
-    static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
-        return props.filters !== state.prevFilters
-            ? { ...state, filters: props.filters, prevFilters: props.filters }
-            : state;
-    }
-
-    open = () => {
-        this.setState({ anchorEl: this.icon.current || undefined });
-    }
-
-    submit = () => {
-        const { onChange } = this.props;
-        if (onChange) {
-            onChange(this.state.filters);
-        }
-        this.setState({ anchorEl: undefined });
-    }
-
-    cancel = () => {
-        this.setState(prev => ({
-            ...prev,
-            filters: prev.prevFilters,
-            anchorEl: undefined
-        }));
-    }
-
-    toggleFilter = (toggledFilter: DataTableFilterItem) => () => {
-        this.setState(prev => ({
-            ...prev,
-            filters: prev.filters.map(filter =>
-                filter === toggledFilter
-                    ? { ...filter, selected: !filter.selected }
-                    : filter)
-        }));
-    }
-}
-
-
 export type CssRules = "root" | "icon" | "active" | "checkbox";
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
@@ -185,4 +58,130 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     }
 });
 
-export default withStyles(styles)(DataTableFilter);
+export interface DataTableFilterItem {
+    name: string;
+    selected: boolean;
+}
+
+export interface DataTableFilterProps {
+    name: string;
+    filters: DataTableFilterItem[];
+    onChange?: (filters: DataTableFilterItem[]) => void;
+}
+
+interface DataTableFilterState {
+    anchorEl?: HTMLElement;
+    filters: DataTableFilterItem[];
+    prevFilters: DataTableFilterItem[];
+}
+
+export const DataTableFilters = withStyles(styles)(
+    class extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
+        state: DataTableFilterState = {
+            anchorEl: undefined,
+            filters: [],
+            prevFilters: []
+        };
+        icon = React.createRef<HTMLElement>();
+
+        render() {
+            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>
+                <Popover
+                    anchorEl={this.state.anchorEl}
+                    open={!!this.state.anchorEl}
+                    anchorOrigin={DefaultTransformOrigin}
+                    transformOrigin={DefaultTransformOrigin}
+                    onClose={this.cancel}>
+                    <Card>
+                        <CardContent>
+                            <Typography variant="caption">
+                                {name}
+                            </Typography>
+                        </CardContent>
+                        <List dense>
+                            {this.state.filters.map((filter, index) =>
+                                <ListItem
+                                    button
+                                    key={index}
+                                    onClick={this.toggleFilter(filter)}>
+                                    <Checkbox
+                                        disableRipple
+                                        color="primary"
+                                        checked={filter.selected}
+                                        className={classes.checkbox} />
+                                    <ListItemText>
+                                        {filter.name}
+                                    </ListItemText>
+                                </ListItem>
+                            )}
+                        </List>
+                        <CardActions>
+                            <Button
+                                color="primary"
+                                variant="raised"
+                                size="small"
+                                onClick={this.submit}>
+                                Ok
+                            </Button>
+                            <Button
+                                color="primary"
+                                variant="outlined"
+                                size="small"
+                                onClick={this.cancel}>
+                                Cancel
+                            </Button>
+                        </CardActions >
+                    </Card>
+                </Popover>
+            </>;
+        }
+
+        static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
+            return props.filters !== state.prevFilters
+                ? { ...state, filters: props.filters, prevFilters: props.filters }
+                : state;
+        }
+
+        open = () => {
+            this.setState({ anchorEl: this.icon.current || undefined });
+        }
+
+        submit = () => {
+            const { onChange } = this.props;
+            if (onChange) {
+                onChange(this.state.filters);
+            }
+            this.setState({ anchorEl: undefined });
+        }
+
+        cancel = () => {
+            this.setState(prev => ({
+                ...prev,
+                filters: prev.prevFilters,
+                anchorEl: undefined
+            }));
+        }
+
+        toggleFilter = (toggledFilter: DataTableFilterItem) => () => {
+            this.setState(prev => ({
+                ...prev,
+                filters: prev.filters.map(filter =>
+                    filter === toggledFilter
+                        ? { ...filter, selected: !filter.selected }
+                        : filter)
+            }));
+        }
+    }
+);
index ec84acac9cb1ba0346c3ab9c8a46830a43cf9f7c..7e460c892f1c76f60f6afae0fe5be7c792f1d2f8 100644 (file)
@@ -6,8 +6,8 @@ import * as React from "react";
 import { mount, configure } from "enzyme";
 import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
 import * as Adapter from "enzyme-adapter-react-16";
-import DataTable, { DataColumns } from "./data-table";
-import DataTableFilters from "../data-table-filters/data-table-filters";
+import { DataTable, DataColumns } from "./data-table";
+import { DataTableFilters } from "../data-table-filters/data-table-filters";
 import { SortDirection } from "./data-column";
 
 configure({ adapter: new Adapter() });
@@ -169,6 +169,4 @@ describe("<DataTable />", () => {
         dataTable.find(DataTableFilters).prop("onChange")([]);
         expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
     });
-
-
-});
\ No newline at end of file
+});
index e0e30480da607f3c86c892c2e6b24c839468a701..829bc84ebe86aa348c00f8a8f0c4abd2508403eb 100644 (file)
@@ -5,11 +5,11 @@
 import * as React from 'react';
 import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles, Typography } from '@material-ui/core';
 import { DataColumn, SortDirection } from './data-column';
-import DataTableFilters, { DataTableFilterItem } from "../data-table-filters/data-table-filters";
+import { DataTableFilters,  DataTableFilterItem } from "../data-table-filters/data-table-filters";
 
 export type DataColumns<T, F extends DataTableFilterItem = DataTableFilterItem> = Array<DataColumn<T, F>>;
 
-export interface DataTableProps<T> {
+export interface DataTableDataProps<T> {
     items: T[];
     columns: DataColumns<T>;
     onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
@@ -20,80 +20,6 @@ export interface DataTableProps<T> {
     extractKey?: (item: T) => React.Key;
 }
 
-class DataTable<T> extends React.Component<DataTableProps<T> & WithStyles<CssRules>> {
-    render() {
-        const { items, classes } = this.props;
-        return <div
-            className={classes.tableContainer}>
-            <Table>
-                <TableHead>
-                    <TableRow>
-                        {this.mapVisibleColumns(this.renderHeadCell)}
-                    </TableRow>
-                </TableHead>
-                <TableBody className={classes.tableBody}>
-                    {items.map(this.renderBodyRow)}
-                </TableBody>
-            </Table>
-        </div>;
-    }
-
-    renderHeadCell = (column: DataColumn<T>, index: number) => {
-        const { name, key, renderHeader, filters, sortDirection } = column;
-        const { onSortToggle, onFiltersChange } = this.props;
-        return <TableCell key={key || index} style={{ width: column.width, minWidth: column.width }}>
-            {renderHeader ?
-                renderHeader() :
-                filters
-                    ? <DataTableFilters
-                        name={`${name} filters`}
-                        onChange={filters =>
-                            onFiltersChange &&
-                            onFiltersChange(filters, column)}
-                        filters={filters}>
-                        {name}
-                    </DataTableFilters>
-                    : sortDirection
-                        ? <TableSortLabel
-                            active={sortDirection !== SortDirection.None}
-                            direction={sortDirection !== SortDirection.None ? sortDirection : undefined}
-                            onClick={() =>
-                                onSortToggle &&
-                                onSortToggle(column)}>
-                            {name}
-                        </TableSortLabel>
-                        : <span>
-                            {name}
-                        </span>}
-        </TableCell>;
-    }
-
-    renderBodyRow = (item: T, index: number) => {
-        const { onRowClick, onRowDoubleClick, extractKey } = this.props;
-        return <TableRow
-            hover
-            key={extractKey ? extractKey(item) : index}
-            onClick={event => onRowClick && onRowClick(event, item)}
-            onContextMenu={this.handleRowContextMenu(item)}
-            onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) }>
-            {this.mapVisibleColumns((column, index) => (
-                <TableCell key={column.key || index}>
-                    {column.render(item)}
-                </TableCell>
-            ))}
-        </TableRow>;
-    }
-
-    mapVisibleColumns = (fn: (column: DataColumn<T>, index: number) => React.ReactElement<any>) => {
-        return this.props.columns.filter(column => column.selected).map(fn);
-    }
-
-    handleRowContextMenu = (item: T) =>
-        (event: React.MouseEvent<HTMLElement>) =>
-            this.props.onContextMenu(event, item)
-
-}
-
 type CssRules = "tableBody" | "tableContainer" | "noItemsInfo";
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
@@ -110,4 +36,80 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     }
 });
 
-export default withStyles(styles)(DataTable);
+type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
+
+export const DataTable = withStyles(styles)(
+    class Component<T> extends React.Component<DataTableProps<T>> {
+        render() {
+            const { items, classes } = this.props;
+            return <div
+                className={classes.tableContainer}>
+                <Table>
+                    <TableHead>
+                        <TableRow>
+                            {this.mapVisibleColumns(this.renderHeadCell)}
+                        </TableRow>
+                    </TableHead>
+                    <TableBody className={classes.tableBody}>
+                        {items.map(this.renderBodyRow)}
+                    </TableBody>
+                </Table>
+            </div>;
+        }
+
+        renderHeadCell = (column: DataColumn<T>, index: number) => {
+            const { name, key, renderHeader, filters, sortDirection } = column;
+            const { onSortToggle, onFiltersChange } = this.props;
+            return <TableCell key={key || index} style={{ width: column.width, minWidth: column.width }}>
+                {renderHeader ?
+                    renderHeader() :
+                    filters
+                        ? <DataTableFilters
+                            name={`${name} filters`}
+                            onChange={filters =>
+                                onFiltersChange &&
+                                onFiltersChange(filters, column)}
+                            filters={filters}>
+                            {name}
+                        </DataTableFilters>
+                        : sortDirection
+                            ? <TableSortLabel
+                                active={sortDirection !== SortDirection.None}
+                                direction={sortDirection !== SortDirection.None ? sortDirection : undefined}
+                                onClick={() =>
+                                    onSortToggle &&
+                                    onSortToggle(column)}>
+                                {name}
+                            </TableSortLabel>
+                            : <span>
+                                {name}
+                            </span>}
+            </TableCell>;
+        }
+
+        renderBodyRow = (item: T, index: number) => {
+            const { onRowClick, onRowDoubleClick, extractKey } = this.props;
+            return <TableRow
+                hover
+                key={extractKey ? extractKey(item) : index}
+                onClick={event => onRowClick && onRowClick(event, item)}
+                onContextMenu={this.handleRowContextMenu(item)}
+                onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) }>
+                {this.mapVisibleColumns((column, index) => (
+                    <TableCell key={column.key || index}>
+                        {column.render(item)}
+                    </TableCell>
+                ))}
+            </TableRow>;
+        }
+
+        mapVisibleColumns = (fn: (column: DataColumn<T>, index: number) => React.ReactElement<any>) => {
+            return this.props.columns.filter(column => column.selected).map(fn);
+        }
+
+        handleRowContextMenu = (item: T) =>
+            (event: React.MouseEvent<HTMLElement>) =>
+                this.props.onContextMenu(event, item)
+
+    }
+);
diff --git a/src/components/details-attribute/details-attribute.tsx b/src/components/details-attribute/details-attribute.tsx
new file mode 100644 (file)
index 0000000..56da6c1
--- /dev/null
@@ -0,0 +1,55 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import Typography from '@material-ui/core/Typography';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '../../common/custom-theme';
+
+type CssRules = 'attribute' | 'label' | 'value' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    attribute: {
+        display: 'flex',
+        alignItems: 'flex-start',
+        marginBottom: theme.spacing.unit
+    },
+    label: {
+        color: theme.palette.grey["500"],
+        width: '40%'
+    },
+    value: {
+        width: '60%',
+        display: 'flex',
+        alignItems: 'flex-start',
+        textTransform: 'capitalize'
+    },
+    link: {
+        width: '60%',
+        color: theme.palette.primary.main,
+        textDecoration: 'none',
+        overflowWrap: 'break-word'
+    }
+});
+
+interface DetailsAttributeDataProps {
+    label: string;
+    value?: string | number;
+    link?: string;
+    children?: React.ReactNode;
+}
+
+type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
+
+export const DetailsAttribute = withStyles(styles)(({ label, link, value, children, classes }: DetailsAttributeProps) =>
+    <Typography component="div" className={classes.attribute}>
+        <Typography component="span" className={classes.label}>{label}</Typography>
+        { link
+            ? <a href={link} className={classes.link} target='_blank'>{value}</a>
+            : <Typography component="span" className={classes.value}>
+                {value}
+                {children}
+            </Typography> }
+    </Typography>
+);
diff --git a/src/components/details-panel-factory/details-panel-factory.tsx b/src/components/details-panel-factory/details-panel-factory.tsx
deleted file mode 100644 (file)
index bb7d855..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import ProjectItem from './items/project-item';
-import CollectionItem from './items/collection-item';
-import ProcessItem from './items/process-item';
-import AbstractItem from './items/abstract-item';
-import EmptyItem from './items/empty-item';
-import { DetailsPanelResource } from '../../views-components/details-panel/details-panel';
-import { EmptyResource } from '../../models/empty';
-import { ResourceKind } from '../../models/resource';
-
-export default class DetailsPanelFactory {
-    static createItem(res: DetailsPanelResource): AbstractItem {
-        switch (res.kind) {
-            case ResourceKind.Project:
-                return new ProjectItem(res);
-            case ResourceKind.Collection:
-                return new CollectionItem(res);
-            case ResourceKind.Process:
-                return new ProcessItem(res);
-            default:
-                return new EmptyItem(res as EmptyResource);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/components/details-panel-factory/items/abstract-item.tsx b/src/components/details-panel-factory/items/abstract-item.tsx
deleted file mode 100644 (file)
index a50c867..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { IconTypes } from '../../icon/icon';
-import { DetailsPanelResource } from '../../../views-components/details-panel/details-panel';
-
-export default abstract class AbstractItem<T extends DetailsPanelResource = DetailsPanelResource> {
-
-    constructor(protected item: T) {}
-
-    getTitle(): string {
-        return this.item.name;
-    }
-  
-    abstract getIcon(): IconTypes;
-    abstract buildDetails(): React.ReactElement<any>;
-    
-    buildActivity(): React.ReactElement<any> {
-        return <div/>;
-    }
-}
\ No newline at end of file
diff --git a/src/components/details-panel-factory/items/collection-item.tsx b/src/components/details-panel-factory/items/collection-item.tsx
deleted file mode 100644 (file)
index 1fa2891..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { IconTypes } from '../../icon/icon';
-import Attribute from '../../attribute/attribute';
-import AbstractItem from './abstract-item';
-import { CollectionResource } from '../../../models/collection';
-import { formatDate } from '../../../common/formatters';
-
-export default class CollectionItem extends AbstractItem<CollectionResource> {
-
-    getIcon(): IconTypes {
-        return IconTypes.COLLECTION;
-    }
-
-    buildDetails(): React.ReactElement<any> {
-        return <div>
-           <Attribute label='Type' value='Data Collection' />
-            <Attribute label='Size' value='---' />
-            <Attribute label='Owner' value={this.item.ownerUuid} />
-            <Attribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
-            <Attribute label='Created at' value={formatDate(this.item.createdAt)} />
-            {/* Links but we dont have view */}
-            <Attribute label='Collection UUID' link={this.item.uuid} value={this.item.uuid} />
-            <Attribute label='Content address' link={this.item.portableDataHash} value={this.item.portableDataHash} />
-            {/* Missing attrs */}
-            <Attribute label='Number of files' value='20' />
-            <Attribute label='Content size' value='54 MB' />
-        </div>;
-    }
-}
\ No newline at end of file
diff --git a/src/components/details-panel-factory/items/empty-item.tsx b/src/components/details-panel-factory/items/empty-item.tsx
deleted file mode 100644 (file)
index 16394c8..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { IconTypes } from '../../icon/icon';
-import AbstractItem from './abstract-item';
-import EmptyState from '../../empty-state/empty-state';
-import { EmptyResource } from '../../../models/empty';
-
-export default class EmptyItem extends AbstractItem<EmptyResource> {
-    
-    getIcon(): IconTypes {
-        return IconTypes.FOLDER;
-    }
-
-    buildDetails(): React.ReactElement<any> {
-        return <EmptyState icon={IconTypes.ANNOUNCEMENT}
-            message='Select a file or folder to view its details.' />;
-    }
-}
\ No newline at end of file
diff --git a/src/components/details-panel-factory/items/process-item.tsx b/src/components/details-panel-factory/items/process-item.tsx
deleted file mode 100644 (file)
index 1ea34de..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import IconBase, { IconTypes } from '../../icon/icon';
-import Attribute from '../../attribute/attribute';
-import AbstractItem from './abstract-item';
-import { ProcessResource } from '../../../models/process';
-import { formatDate } from '../../../common/formatters';
-
-export default class ProcessItem extends AbstractItem<ProcessResource> {
-
-    getIcon(): IconTypes {
-        return IconTypes.PROCESS;
-    }
-
-    buildDetails(): React.ReactElement<any> {
-        return <div>
-            <Attribute label='Type' value='Process' />
-            <Attribute label='Size' value='---' />
-            <Attribute label='Owner' value={this.item.ownerUuid} />
-
-            {/* Missing attr */}
-            <Attribute label='Status' value={this.item.state} />
-            <Attribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
-            
-            {/* Missing attrs */}
-            <Attribute label='Started at' value={formatDate(this.item.createdAt)} />
-            <Attribute label='Finished at' value={formatDate(this.item.expiresAt)} />
-
-            {/* Links but we dont have view */}
-            <Attribute label='Outputs' link={this.item.outputPath} value={this.item.outputPath} />
-            <Attribute label='UUID' link={this.item.uuid} value={this.item.uuid} />
-            <Attribute label='Container UUID' link={this.item.containerUuid} value={this.item.containerUuid} />
-            
-            <Attribute label='Priority' value={this.item.priority} />
-            <Attribute label='Runtime Constraints' value={this.item.runtimeConstraints} />
-            {/* Link but we dont have view */}
-            <Attribute label='Docker Image locator' link={this.item.containerImage} value={this.item.containerImage} />
-        </div>;
-    }
-}
\ No newline at end of file
diff --git a/src/components/details-panel-factory/items/project-item.tsx b/src/components/details-panel-factory/items/project-item.tsx
deleted file mode 100644 (file)
index 559816e..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { IconTypes } from '../../icon/icon';
-import Attribute from '../../attribute/attribute';
-import AbstractItem from './abstract-item';
-import { ProjectResource } from '../../../models/project';
-import { formatDate } from '../../../common/formatters';
-
-export default class ProjectItem extends AbstractItem<ProjectResource> {
-
-    getIcon(): IconTypes {
-        return IconTypes.FOLDER;
-    }
-
-    buildDetails(): React.ReactElement<any> {
-        return <div>
-            <Attribute label='Type' value={this.item.groupClass} />
-            {/* Missing attr */}
-            <Attribute label='Size' value='---' />
-            <Attribute label='Owner' value={this.item.ownerUuid} />
-            <Attribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
-            <Attribute label='Created at' value={formatDate(this.item.createdAt)} />
-            {/* Missing attr */}
-            <Attribute label='File size' value='1.4 GB' />
-            <Attribute label='Description' value={this.item.description} />
-        </div>;
-    }
-}
\ No newline at end of file
index 19924ad76c70496477329b139f47db1bcc7a5bbe..da232bd288499810c827f7ac602796348758f3fe 100644 (file)
@@ -4,23 +4,22 @@
 
 import * as React from "react";
 import { shallow, configure } from "enzyme";
-import DropdownMenu from "./dropdown-menu";
-import ChevronRightIcon from '@material-ui/icons/ChevronRight';
-
+import { DropdownMenu } from "./dropdown-menu";
 import * as Adapter from 'enzyme-adapter-react-16';
 import { MenuItem, IconButton, Menu } from "@material-ui/core";
+import { PaginationRightArrowIcon } from "../icon/icon";
 
 configure({ adapter: new Adapter() });
 
 describe("<DropdownMenu />", () => {
     it("renders menu icon", () => {
-        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
-        expect(dropdownMenu.find(ChevronRightIcon)).toHaveLength(1);
+        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />} />);
+        expect(dropdownMenu.find(PaginationRightArrowIcon)).toHaveLength(1);
     });
 
     it("render menu items", () => {
         const dropdownMenu = shallow(
-            <DropdownMenu id="test-menu" icon={ChevronRightIcon}>
+            <DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />}>
                 <MenuItem>Item 1</MenuItem>
                 <MenuItem>Item 2</MenuItem>
             </DropdownMenu>
@@ -29,15 +28,15 @@ describe("<DropdownMenu />", () => {
     });
 
     it("opens on menu icon click", () => {
-        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
+        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />} />);
         dropdownMenu.find(IconButton).simulate("click", {currentTarget: {}});
         expect(dropdownMenu.state().anchorEl).toBeDefined();
     });
-    
+
     it("closes on menu click", () => {
-        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={ChevronRightIcon} />);
+        const dropdownMenu = shallow(<DropdownMenu id="test-menu" icon={<PaginationRightArrowIcon />} />);
         dropdownMenu.find(Menu).simulate("click", {currentTarget: {}});
         expect(dropdownMenu.state().anchorEl).toBeUndefined();
     });
 
-});
\ No newline at end of file
+});
index 4f2b83af6591e6ab9d72d1e0d83914ff55bd4414..73b279b25e44886f2cb1639c425c55305e622fd3 100644 (file)
@@ -3,17 +3,20 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Menu, IconButton } from '@material-ui/core';
+import Menu from '@material-ui/core/Menu';
+import IconButton from '@material-ui/core/IconButton';
 import { PopoverOrigin } from '@material-ui/core/Popover';
 
-
 interface DropdownMenuProps {
     id: string;
-    icon: React.ComponentType;
+    icon: React.ReactElement<any>;
 }
 
-class DropdownMenu extends React.Component<DropdownMenuProps> {
+interface DropdownMenuState {
+    anchorEl: any;
+}
 
+export class DropdownMenu extends React.Component<DropdownMenuProps, DropdownMenuState> {
     state = {
         anchorEl: undefined
     };
@@ -24,7 +27,7 @@ class DropdownMenu extends React.Component<DropdownMenuProps> {
     };
 
     render() {
-        const { icon: Icon, id, children } = this.props;
+        const { icon, id, children } = this.props;
         const { anchorEl } = this.state;
         return (
             <div>
@@ -32,10 +35,8 @@ class DropdownMenu extends React.Component<DropdownMenuProps> {
                     aria-owns={anchorEl ? id : undefined}
                     aria-haspopup="true"
                     color="inherit"
-                    onClick={this.handleOpen}
-
-                >
-                    <Icon />
+                    onClick={this.handleOpen}>
+                    {icon}
                 </IconButton>
                 <Menu
                     id={id}
@@ -44,8 +45,7 @@ class DropdownMenu extends React.Component<DropdownMenuProps> {
                     onClose={this.handleClose}
                     onClick={this.handleClose}
                     anchorOrigin={this.transformOrigin}
-                    transformOrigin={this.transformOrigin}
-                >
+                    transformOrigin={this.transformOrigin}>
                     {children}
                 </Menu>
             </div>
@@ -60,6 +60,3 @@ class DropdownMenu extends React.Component<DropdownMenuProps> {
         this.setState({ anchorEl: event.currentTarget });
     }
 }
-
-
-export default DropdownMenu;
diff --git a/src/components/empty-state/empty-state.tsx b/src/components/empty-state/empty-state.tsx
deleted file mode 100644 (file)
index 205053b..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import Typography from '@material-ui/core/Typography';
-import { WithStyles, withStyles, StyleRulesCallback } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'src/common/custom-theme';
-import IconBase, { IconTypes } from '../icon/icon';
-
-export interface EmptyStateDataProps {
-    message: string;
-    icon: IconTypes;
-    details?: string;
-}
-
-type EmptyStateProps = EmptyStateDataProps & WithStyles<CssRules>;
-
-class EmptyState extends React.Component<EmptyStateProps, {}> {
-
-    render() {
-        const { classes, message, details, icon, children } = this.props;
-        return (
-            <Typography className={classes.container} component="div">
-                <IconBase icon={icon} className={classes.icon} />
-                <Typography variant="body1" gutterBottom>{message}</Typography>
-                { details && <Typography gutterBottom>{details}</Typography> }
-                { children && <Typography gutterBottom>{children}</Typography> }
-            </Typography>
-        );
-    }
-
-}
-
-type CssRules = 'container' | 'icon';
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    container: {
-        textAlign: 'center'
-    },
-    icon: {
-        color: theme.palette.grey["500"],
-        fontSize: '72px'
-    }
-});
-
-export default withStyles(styles)(EmptyState);
\ No newline at end of file
index c420a19c8d46397caa1934908528e5127e149b32..e80fee8e97db18107cc186cf2819c6dfc9f0ed9c 100644 (file)
@@ -3,48 +3,71 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import * as classnames from "classnames";
-import CloseAnnouncement from '@material-ui/icons/Announcement';
-import CloseIcon from '@material-ui/icons/Close';
-import FolderIcon from '@material-ui/icons/Folder';
+import AccessTime from '@material-ui/icons/AccessTime';
+import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
+import BubbleChart from '@material-ui/icons/BubbleChart';
+import Cached from '@material-ui/icons/Cached';
+import Code from '@material-ui/icons/Code';
+import ChevronLeft from '@material-ui/icons/ChevronLeft';
+import ChevronRight from '@material-ui/icons/ChevronRight';
+import Close from '@material-ui/icons/Close';
+import ContentCopy from '@material-ui/icons/ContentCopy';
+import CreateNewFolder from '@material-ui/icons/CreateNewFolder';
+import Delete from '@material-ui/icons/Delete';
+import Edit from '@material-ui/icons/Edit';
+import Folder from '@material-ui/icons/Folder';
+import GetApp from '@material-ui/icons/GetApp';
+import Help from '@material-ui/icons/Help';
+import Inbox from '@material-ui/icons/Inbox';
+import Info from '@material-ui/icons/Info';
+import Input from '@material-ui/icons/Input';
+import Menu from '@material-ui/icons/Menu';
+import MoreVert from '@material-ui/icons/MoreVert';
+import Notifications from '@material-ui/icons/Notifications';
+import People from '@material-ui/icons/People';
+import Person from '@material-ui/icons/Person';
+import PersonAdd from '@material-ui/icons/PersonAdd';
+import PlayArrow from '@material-ui/icons/PlayArrow';
+import RateReview from '@material-ui/icons/RateReview';
+import Search from '@material-ui/icons/Search';
+import Star from '@material-ui/icons/Star';
+import StarBorder from '@material-ui/icons/StarBorder';
 
-export enum IconTypes {
-    ANNOUNCEMENT = 'announcement',
-    FOLDER = 'folder',
-    CLOSE = 'close',
-    PROJECT  = 'project',
-    COLLECTION = 'collection',
-    PROCESS = 'process'
-}
+export type IconType = React.SFC<{ className?: string }>;
 
-interface IconBaseDataProps {
-    icon: IconTypes;
-    className?: string;
-}
-
-type IconBaseProps = IconBaseDataProps;
-
-interface IconBaseState {
-    icon: IconTypes;
-}
-
-const getSpecificIcon = (props: any) => ({
-    announcement: <CloseAnnouncement className={props.className} />,
-    folder: <FolderIcon className={props.className} />,
-    close: <CloseIcon className={props.className} />,
-    project: <i className={classnames([props.className, 'fas fa-folder fa-lg'])} />,
-    collection: <i className={classnames([props.className, 'fas fa-archive fa-lg'])} />,
-    process: <i className={classnames([props.className, 'fas fa-cogs fa-lg'])} />
-});
-
-class IconBase extends React.Component<IconBaseProps, IconBaseState> {
-    state = {
-        icon: IconTypes.FOLDER,
-    };
-
-    render() {
-        return getSpecificIcon(this.props)[this.props.icon];
-    }
-}
-
-export default IconBase;
\ No newline at end of file
+export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
+export const AdvancedIcon: IconType = (props) => <Folder {...props} />;
+export const CustomizeTableIcon: IconType = (props) => <Menu {...props} />;
+export const CopyIcon: IconType = (props) => <ContentCopy {...props} />;
+export const CollectionIcon: IconType = (props) => <Folder {...props} />;
+export const CloseIcon: IconType = (props) => <Close {...props} />;
+export const DefaultIcon: IconType = (props) => <RateReview {...props} />;
+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 LogIcon: IconType = (props) => <Folder {...props} />;
+export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
+export const MoveToIcon: IconType = (props) => <Input {...props} />;
+export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
+export const NotificationIcon: IconType = (props) => <Notifications {...props} />;
+export const PaginationDownIcon: IconType = (props) => <ArrowDropDown {...props} />;
+export const PaginationLeftArrowIcon: IconType = (props) => <ChevronLeft {...props} />;
+export const PaginationRightArrowIcon: IconType = (props) => <ChevronRight {...props} />;
+export const ProcessIcon: IconType = (props) => <BubbleChart {...props} />;
+export const ProjectIcon: IconType = (props) => <Folder {...props} />;
+export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
+export const ProvenanceGraphIcon: IconType = (props) => <Folder {...props} />;
+export const RecentIcon: IconType = (props) => <AccessTime {...props} />;
+export const RemoveIcon: IconType = (props) => <Delete {...props} />;
+export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
+export const RenameIcon: IconType = (props) => <Edit {...props} />;
+export const ReRunProcessIcon: IconType = (props) => <Cached {...props} />;
+export const SearchIcon: IconType = (props) => <Search {...props} />;
+export const ShareIcon: IconType = (props) => <PersonAdd {...props} />;
+export const ShareMeIcon: IconType = (props) => <People {...props} />;
+export const SidePanelRightArrowIcon: IconType = (props) => <PlayArrow {...props} />;
+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} />;
\ No newline at end of file
diff --git a/src/components/list-item-text-icon/list-item-text-icon.tsx b/src/components/list-item-text-icon/list-item-text-icon.tsx
new file mode 100644 (file)
index 0000000..8f9d474
--- /dev/null
@@ -0,0 +1,62 @@
+// 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/styles';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { ListItemIcon, ListItemText, Typography } from '@material-ui/core';
+import { IconType } from '../icon/icon';
+import * as classnames from "classnames";
+
+type CssRules = 'root' | 'listItemText' | 'hasMargin' | 'active';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        display: 'flex',
+        alignItems: 'center'
+    },
+    listItemText: {
+        fontWeight: 700
+    },
+    active: {
+        color: theme.palette.primary.main,
+    },
+    hasMargin: {
+        marginLeft: '18px',
+    },
+});
+
+export interface ListItemTextIconDataProps {
+    icon: IconType;
+    name: string;
+    isActive?: boolean;
+    hasMargin?: boolean;
+}
+
+type ListItemTextIconProps = ListItemTextIconDataProps & WithStyles<CssRules>;
+
+export const ListItemTextIcon = withStyles(styles)(
+    class extends React.Component<ListItemTextIconProps, {}> {
+        render() {
+            const { classes, isActive, hasMargin, name, icon: Icon } = this.props;
+            return (
+                <Typography component='span' className={classes.root}>
+                    <ListItemIcon className={classnames({
+                            [classes.hasMargin]: hasMargin,
+                            [classes.active]: isActive
+                        })}>
+                        <Icon />
+                    </ListItemIcon>
+                    <ListItemText primary={
+                        <Typography variant='body1' className={classnames(classes.listItemText, {
+                                [classes.active]: isActive
+                            })}>
+                            {name}
+                        </Typography>
+                    } />
+                </Typography>
+            );
+        }
+    }
+);
index 13f74a68254ab193f556d7da4f9b80d6ecba2493..f2be98cfdaf69bd3c7eaaae61ab8c76a4a6b723d 100644 (file)
@@ -4,13 +4,13 @@
 
 import { PopoverOrigin } from "@material-ui/core/Popover";
 
-export const mockAnchorFromMouseEvent = (event: React.MouseEvent<HTMLElement>) => {
+export const createAnchorAt = (position: {x: number, y: number}) => {
     const el = document.createElement('div');
     const clientRect = {
-        left: event.clientX,
-        right: event.clientX,
-        top: event.clientY,
-        bottom: event.clientY,
+        left: position.x,
+        right: position.x,
+        top: position.y,
+        bottom: position.y,
         width: 0,
         height: 0
     };
index fa24c0cd5568a44b7c96fd68b651acf7397fd197..37007ab41eb4fd397b1653e29b2d0281b10e1e01 100644 (file)
@@ -6,7 +6,7 @@ import * as React from "react";
 import { mount, configure } from "enzyme";
 import * as Adapter from "enzyme-adapter-react-16";
 
-import Popover, { DefaultTrigger } from "./popover";
+import { Popover, DefaultTrigger } from "./popover";
 import Button, { ButtonProps } from "@material-ui/core/Button";
 
 configure({ adapter: new Adapter() });
@@ -38,7 +38,7 @@ describe("<Popover />", () => {
         popover.find(DefaultTrigger).simulate("click");
         expect(popover.find(CustomTrigger)).toHaveLength(1);
     });
-    
+
     it("does not close if closeOnContentClick is not set", () => {
         const popover = mount(
             <Popover>
@@ -66,4 +66,4 @@ const CustomTrigger: React.SFC<ButtonProps> = (props) => (
     <Button {...props}>
         Open popover
     </Button>
-);
\ No newline at end of file
+);
index c8d40338cf8e1e782d40bfb54eb044432600efe5..9f3cd7809a1cd530c3bae410baacbb21b2e8958c 100644 (file)
@@ -13,9 +13,7 @@ export interface PopoverProps {
     closeOnContentClick?: boolean;
 }
 
-
-class Popover extends React.Component<PopoverProps> {
-
+export class Popover extends React.Component<PopoverProps> {
     state = {
         anchorEl: undefined
     };
@@ -57,7 +55,6 @@ class Popover extends React.Component<PopoverProps> {
             this.handleClose();
         }
     }
-
 }
 
 export const DefaultTrigger: React.SFC<IconButtonProps> = (props) => (
@@ -65,5 +62,3 @@ export const DefaultTrigger: React.SFC<IconButtonProps> = (props) => (
         <i className="fas" />
     </IconButton>
 );
-
-export default Popover;
index 2479e4011c3427bd296b5c2c712653fc50de3c62..07b5ebf6261dbab0849bad7e07df416ddaa49765 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as React from "react";
 import { mount, configure } from "enzyme";
-import SearchBar, { DEFAULT_SEARCH_DEBOUNCE } from "./search-bar";
+import { SearchBar, DEFAULT_SEARCH_DEBOUNCE } from "./search-bar";
 
 import * as Adapter from 'enzyme-adapter-react-16';
 
@@ -61,7 +61,7 @@ describe("<SearchBar />", () => {
             jest.advanceTimersByTime(DEFAULT_SEARCH_DEBOUNCE);
             expect(onSearch).toBeCalledWith("current value");
         });
-        
+
         it("calls onSearch after the time specified in props has passed", () => {
             const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={2000}/>);
             searchBar.find("input").simulate("change", { target: { value: "current value" } });
@@ -70,7 +70,7 @@ describe("<SearchBar />", () => {
             jest.advanceTimersByTime(1000);
             expect(onSearch).toBeCalledWith("current value");
         });
-        
+
         it("calls onSearch only once after no change happened during the specified time", () => {
             const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={1000}/>);
             searchBar.find("input").simulate("change", { target: { value: "current value" } });
@@ -79,7 +79,7 @@ describe("<SearchBar />", () => {
             jest.advanceTimersByTime(1000);
             expect(onSearch).toHaveBeenCalledTimes(1);
         });
-        
+
         it("calls onSearch again after the specified time has passed since previous call", () => {
             const searchBar = mount(<SearchBar value="" onSearch={onSearch} debounce={1000}/>);
             searchBar.find("input").simulate("change", { target: { value: "current value" } });
@@ -91,9 +91,7 @@ describe("<SearchBar />", () => {
             jest.advanceTimersByTime(1000);
             expect(onSearch).toBeCalledWith("latest value");
             expect(onSearch).toHaveBeenCalledTimes(2);
-            
-        });
 
+        });
     });
-
-});
\ No newline at end of file
+});
index 62c8cc3c510b9be4a25e0206afb350884811496c..de9e7de5261c299870cc16d4618971869dfc273b 100644 (file)
@@ -6,80 +6,6 @@ import * as React from 'react';
 import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
 import SearchIcon from '@material-ui/icons/Search';
 
-interface SearchBarDataProps {
-    value: string;
-}
-
-interface SearchBarActionProps {
-    onSearch: (value: string) => any;
-    debounce?: number;
-}
-
-type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
-
-interface SearchBarState {
-    value: string;
-}
-
-export const DEFAULT_SEARCH_DEBOUNCE = 1000;
-
-class SearchBar extends React.Component<SearchBarProps> {
-
-    state: SearchBarState = {
-        value: ""
-    };
-
-    timeout: number;
-
-    render() {
-        const { classes } = this.props;
-        return <Paper className={classes.container}>
-            <form onSubmit={this.handleSubmit}>
-                <input
-                    className={classes.input}
-                    onChange={this.handleChange}
-                    placeholder="Search"
-                    value={this.state.value}
-                />
-                <IconButton className={classes.button}>
-                    <SearchIcon />
-                </IconButton>
-            </form>
-        </Paper>;
-    }
-
-    componentDidMount() {
-        this.setState({value: this.props.value});
-    }
-
-    componentWillReceiveProps(nextProps: SearchBarProps) {
-        if (nextProps.value !== this.props.value) {
-            this.setState({ value: nextProps.value });
-        }
-    }
-
-    componentWillUnmount() {
-        clearTimeout(this.timeout);
-    }
-
-    handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
-        event.preventDefault();
-        clearTimeout(this.timeout);
-        this.props.onSearch(this.state.value);
-    }
-
-    handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-        clearTimeout(this.timeout);
-        this.setState({ value: event.target.value });
-        this.timeout = window.setTimeout(
-            () => this.props.onSearch(this.state.value),
-            this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
-        );
-
-    }
-
-}
-
 type CssRules = 'container' | 'input' | 'button';
 
 const styles: StyleRulesCallback<CssRules> = theme => {
@@ -106,4 +32,76 @@ const styles: StyleRulesCallback<CssRules> = theme => {
     };
 };
 
-export default withStyles(styles)(SearchBar);
\ No newline at end of file
+interface SearchBarDataProps {
+    value: string;
+}
+
+interface SearchBarActionProps {
+    onSearch: (value: string) => any;
+    debounce?: number;
+}
+
+type SearchBarProps = SearchBarDataProps & SearchBarActionProps & WithStyles<CssRules>;
+
+interface SearchBarState {
+    value: string;
+}
+
+export const DEFAULT_SEARCH_DEBOUNCE = 1000;
+
+export const SearchBar = withStyles(styles)(
+    class extends React.Component<SearchBarProps> {
+        state: SearchBarState = {
+            value: ""
+        };
+
+        timeout: number;
+
+        render() {
+            const {classes} = this.props;
+            return <Paper className={classes.container}>
+                <form onSubmit={this.handleSubmit}>
+                    <input
+                        className={classes.input}
+                        onChange={this.handleChange}
+                        placeholder="Search"
+                        value={this.state.value}
+                    />
+                    <IconButton className={classes.button}>
+                        <SearchIcon/>
+                    </IconButton>
+                </form>
+            </Paper>;
+        }
+
+        componentDidMount() {
+            this.setState({value: this.props.value});
+        }
+
+        componentWillReceiveProps(nextProps: SearchBarProps) {
+            if (nextProps.value !== this.props.value) {
+                this.setState({value: nextProps.value});
+            }
+        }
+
+        componentWillUnmount() {
+            clearTimeout(this.timeout);
+        }
+
+        handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+            event.preventDefault();
+            clearTimeout(this.timeout);
+            this.props.onSearch(this.state.value);
+        }
+
+        handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+            clearTimeout(this.timeout);
+            this.setState({value: event.target.value});
+            this.timeout = window.setTimeout(
+                () => this.props.onSearch(this.state.value),
+                this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
+            );
+
+        }
+    }
+);
index b07445a5857455f61879d62a39854de8c9219002..a91f9b17a6812408ed2764b6f45f8ee67ef61e32 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as React from "react";
 import { mount, configure } from "enzyme";
-import SearchInput, { DEFAULT_SEARCH_DEBOUNCE } from "./search-input";
+import { SearchInput, DEFAULT_SEARCH_DEBOUNCE } from "./search-input";
 
 import * as Adapter from 'enzyme-adapter-react-16';
 
@@ -61,7 +61,7 @@ describe("<SearchInput />", () => {
             jest.advanceTimersByTime(DEFAULT_SEARCH_DEBOUNCE);
             expect(onSearch).toBeCalledWith("current value");
         });
-        
+
         it("calls onSearch after the time specified in props has passed", () => {
             const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={2000}/>);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
@@ -70,7 +70,7 @@ describe("<SearchInput />", () => {
             jest.advanceTimersByTime(1000);
             expect(onSearch).toBeCalledWith("current value");
         });
-        
+
         it("calls onSearch only once after no change happened during the specified time", () => {
             const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
@@ -79,7 +79,7 @@ describe("<SearchInput />", () => {
             jest.advanceTimersByTime(1000);
             expect(onSearch).toHaveBeenCalledTimes(1);
         });
-        
+
         it("calls onSearch again after the specified time has passed since previous call", () => {
             const searchInput = mount(<SearchInput value="" onSearch={onSearch} debounce={1000}/>);
             searchInput.find("input").simulate("change", { target: { value: "current value" } });
@@ -91,9 +91,9 @@ describe("<SearchInput />", () => {
             jest.advanceTimersByTime(1000);
             expect(onSearch).toBeCalledWith("latest value");
             expect(onSearch).toHaveBeenCalledTimes(2);
-            
+
         });
 
     });
 
-});
\ No newline at end of file
+});
index edc82d55a419caa55744f39a4e184c66f55fcffc..dc02cd3d13ba13bedd50daf01963bb5a76d98f80 100644 (file)
@@ -3,87 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment, FormHelperText } from '@material-ui/core';
+import { IconButton, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment } from '@material-ui/core';
 import SearchIcon from '@material-ui/icons/Search';
 
-interface SearchInputDataProps {
-    value: string;
-}
-
-interface SearchInputActionProps {
-    onSearch: (value: string) => any;
-    debounce?: number;
-}
-
-type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyles<CssRules>;
-
-interface SearchInputState {
-    value: string;
-}
-
-export const DEFAULT_SEARCH_DEBOUNCE = 1000;
-
-class SearchInput extends React.Component<SearchInputProps> {
-
-    state: SearchInputState = {
-        value: ""
-    };
-
-    timeout: number;
-
-    render() {
-        const { classes } = this.props;
-        return <form onSubmit={this.handleSubmit}>
-            <FormControl>
-                <InputLabel>Search</InputLabel>
-                <Input
-                    type="text"
-                    value={this.state.value}
-                    onChange={this.handleChange}
-                    endAdornment={
-                        <InputAdornment position="end">
-                            <IconButton
-                                onClick={this.handleSubmit}>
-                                <SearchIcon />
-                            </IconButton>
-                        </InputAdornment>
-                    } />
-            </FormControl>
-        </form>;
-    }
-
-    componentDidMount() {
-        this.setState({ value: this.props.value });
-    }
-
-    componentWillReceiveProps(nextProps: SearchInputProps) {
-        if (nextProps.value !== this.props.value) {
-            this.setState({ value: nextProps.value });
-        }
-    }
-
-    componentWillUnmount() {
-        clearTimeout(this.timeout);
-    }
-
-    handleSubmit = (event: React.FormEvent<HTMLElement>) => {
-        event.preventDefault();
-        clearTimeout(this.timeout);
-        this.props.onSearch(this.state.value);
-    }
-
-    handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-        clearTimeout(this.timeout);
-        this.setState({ value: event.target.value });
-        this.timeout = window.setTimeout(
-            () => this.props.onSearch(this.state.value),
-            this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
-        );
-
-    }
-
-}
-
 type CssRules = 'container' | 'input' | 'button';
 
 const styles: StyleRulesCallback<CssRules> = theme => {
@@ -110,4 +32,80 @@ const styles: StyleRulesCallback<CssRules> = theme => {
     };
 };
 
-export default withStyles(styles)(SearchInput);
\ No newline at end of file
+interface SearchInputDataProps {
+    value: string;
+}
+
+interface SearchInputActionProps {
+    onSearch: (value: string) => any;
+    debounce?: number;
+}
+
+type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyles<CssRules>;
+
+interface SearchInputState {
+    value: string;
+}
+
+export const DEFAULT_SEARCH_DEBOUNCE = 1000;
+
+export const SearchInput = withStyles(styles)(
+    class extends React.Component<SearchInputProps> {
+        state: SearchInputState = {
+            value: ""
+        };
+
+        timeout: number;
+
+        render() {
+            const { classes } = this.props;
+            return <form onSubmit={this.handleSubmit}>
+                <FormControl>
+                    <InputLabel>Search</InputLabel>
+                    <Input
+                        type="text"
+                        value={this.state.value}
+                        onChange={this.handleChange}
+                        endAdornment={
+                            <InputAdornment position="end">
+                                <IconButton
+                                    onClick={this.handleSubmit}>
+                                    <SearchIcon/>
+                                </IconButton>
+                            </InputAdornment>
+                        }/>
+                </FormControl>
+            </form>;
+        }
+
+        componentDidMount() {
+            this.setState({ value: this.props.value });
+        }
+
+        componentWillReceiveProps(nextProps: SearchInputProps) {
+            if (nextProps.value !== this.props.value) {
+                this.setState({ value: nextProps.value });
+            }
+        }
+
+        componentWillUnmount() {
+            clearTimeout(this.timeout);
+        }
+
+        handleSubmit = (event: React.FormEvent<HTMLElement>) => {
+            event.preventDefault();
+            clearTimeout(this.timeout);
+            this.props.onSearch(this.state.value);
+        }
+
+        handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+            clearTimeout(this.timeout);
+            this.setState({ value: event.target.value });
+            this.timeout = window.setTimeout(
+                () => this.props.onSearch(this.state.value),
+                this.props.debounce || DEFAULT_SEARCH_DEBOUNCE
+            );
+
+        }
+    }
+);
index a7783fb256c611f99fac80803b781c9884b74f8c..4240b1bfd29708ec915555daffe3667587201006 100644 (file)
 
 import * as React from 'react';
 import { ReactElement } from 'react';
-import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
-import List from "@material-ui/core/List/List";
-import ListItem from "@material-ui/core/ListItem/ListItem";
-import ListItemText from "@material-ui/core/ListItemText/ListItemText";
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import Collapse from "@material-ui/core/Collapse/Collapse";
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
+import { SidePanelRightArrowIcon, IconType } from '../icon/icon';
+import * as classnames from "classnames";
+import { ListItemTextIcon } from '../list-item-text-icon/list-item-text-icon';
 
-import { Typography } from '@material-ui/core';
+type CssRules = 'active' | 'row' | 'root' | 'list' | 'iconClose' | 'iconOpen' | 'toggableIconContainer' | 'toggableIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        overflowY: 'auto',
+        minWidth: '240px',
+        whiteSpace: 'nowrap',
+        marginTop: '52px',
+        display: 'flex',
+        flexGrow: 1,
+    },
+    list: {
+        padding: '5px 0px 5px 14px',
+        minWidth: '240px',
+    },
+    row: {
+        display: 'flex',
+        alignItems: 'center',
+    },
+    toggableIconContainer: {
+        color: theme.palette.grey["700"],
+        height: '14px',
+        position: 'absolute'
+    },
+    toggableIcon: {
+        fontSize: '14px'
+    },
+    active: {
+        color: theme.palette.primary.main,
+    },
+    iconClose: {
+        transition: 'all 0.1s ease',
+    },
+    iconOpen: {
+        transition: 'all 0.1s ease',
+        transform: 'rotate(90deg)',
+    }
+});
 
 export interface SidePanelItem {
     id: string;
     name: string;
-    icon: string;
+    icon: IconType;
     active?: boolean;
     open?: boolean;
     margin?: boolean;
     openAble?: boolean;
 }
 
-interface SidePanelProps {
+interface SidePanelDataProps {
     toggleOpen: (id: string) => void;
     toggleActive: (id: string) => void;
     sidePanelItems: SidePanelItem[];
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
 }
 
-class SidePanel extends React.Component<SidePanelProps & WithStyles<CssRules>> {
-    render(): ReactElement<any> {
-        const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
-        const { listItemText, leftSidePanelContainer, row, list, icon, projectIconMargin, active, activeArrow, inactiveArrow, arrowTransition, arrowRotate } = classes;
-        return (
-            <div className={leftSidePanelContainer}>
-                <List>
-                    {sidePanelItems.map(it => (
-                        <span key={it.name}>
-                            <ListItem button className={list} onClick={() => toggleActive(it.id)} onContextMenu={this.handleRowContextMenu(it)}>
-                                <span className={row}>
-                                    {it.openAble ? <i onClick={() => toggleOpen(it.id)} className={`${it.active ? activeArrow : inactiveArrow} 
-                                        ${it.open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} /> : null}
-                                    <ListItemIcon className={it.active ? active : ''}>
-                                        <i className={`${it.icon} ${icon} ${it.margin ? projectIconMargin : ''}`} />
-                                    </ListItemIcon>
-                                    <ListItemText className={listItemText} primary={<Typography className={it.active ? active : ''}>{it.name}</Typography>} />
-                                </span>
-                            </ListItem>
-                            {it.openAble ? (
-                                <Collapse in={it.open} timeout="auto" unmountOnExit>
-                                    {children}
-                                </Collapse>) : null}
-                        </span>
-                    ))}
-                </List>
-            </div>
-        );
-    }
-
-    handleRowContextMenu = (item: SidePanelItem) =>
-        (event: React.MouseEvent<HTMLElement>) =>
-            item.openAble ? this.props.onContextMenu(event, item) : null
+type SidePanelProps = SidePanelDataProps & WithStyles<CssRules>;
 
-}
+export const SidePanel = withStyles(styles)(
+    class extends React.Component<SidePanelProps> {
+        render(): ReactElement<any> {
+            const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
+            const { root, row, list, toggableIconContainer } = classes;
+            return (
+                <div className={root}>
+                    <List>
+                        {sidePanelItems.map(it => (
+                            <span key={it.name}>
+                                <ListItem button className={list} onClick={() => toggleActive(it.id)}
+                                          onContextMenu={this.handleRowContextMenu(it)}>
+                                    <span className={row}>
+                                        {it.openAble ? (
+                                            <i onClick={() => toggleOpen(it.id)} className={toggableIconContainer}>
+                                                <ListItemIcon
+                                                    className={this.getToggableIconClassNames(it.open, it.active)}>
+                                                    < SidePanelRightArrowIcon/>
+                                                </ListItemIcon>
+                                            </i>
+                                        ) : null}
+                                        <ListItemTextIcon icon={it.icon} name={it.name} isActive={it.active}
+                                                          hasMargin={it.margin}/>
+                                    </span>
+                                </ListItem>
+                                {it.openAble ? (
+                                    <Collapse in={it.open} timeout="auto" unmountOnExit>
+                                        {children}
+                                    </Collapse>
+                                ) : null}
+                            </span>
+                        ))}
+                    </List>
+                </div>
+            );
+        }
 
-type CssRules = 'active' | 'listItemText' | 'row' | 'leftSidePanelContainer' | 'list' | 'icon' | 'projectIconMargin' |
-    'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition';
+        getToggableIconClassNames = (isOpen?: boolean, isActive ?: boolean) => {
+            const { classes } = this.props;
+            return classnames(classes.toggableIcon, {
+                [classes.iconOpen]: isOpen,
+                [classes.iconClose]: !isOpen,
+                [classes.active]: isActive
+            });
+        }
 
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
-    active: {
-        color: '#4285F6',
-    },
-    listItemText: {
-        padding: '0px',
-    },
-    row: {
-        display: 'flex',
-        alignItems: 'center',
-    },
-    activeArrow: {
-        color: '#4285F6',
-        position: 'absolute',
-    },
-    inactiveArrow: {
-        position: 'absolute',
-    },
-    arrowTransition: {
-        transition: 'all 0.1s ease',
-    },
-    arrowRotate: {
-        transition: 'all 0.1s ease',
-        transform: 'rotate(-90deg)',
-    },
-    leftSidePanelContainer: {
-        overflowY: 'auto',
-        minWidth: '240px',
-        whiteSpace: 'nowrap',
-        marginTop: '52px',
-        display: 'flex',
-        flexGrow: 1,
-    },
-    list: {
-        paddingBottom: '5px',
-        paddingTop: '5px',
-        paddingLeft: '14px',
-        minWidth: '240px',
-    },
-    icon: {
-        minWidth: '20px',
-    },
-    projectIconMargin: {
-        marginLeft: '17px',
+        handleRowContextMenu = (item: SidePanelItem) =>
+            (event: React.MouseEvent<HTMLElement>) =>
+                item.openAble ? this.props.onContextMenu(event, item) : null
     }
-});
-
-export default withStyles(styles)(SidePanel);
\ No newline at end of file
+);
index 9ac051130cc86f447f53cb537a15d7b01aaf1022..58484c37a2358a538cd6f8af2827506d0f809b92 100644 (file)
@@ -7,7 +7,7 @@ import * as Enzyme from 'enzyme';
 import * as Adapter from 'enzyme-adapter-react-16';
 import ListItem from "@material-ui/core/ListItem/ListItem";
 
-import Tree, { TreeItem } from './tree';
+import { Tree, TreeItem } from './tree';
 import { ProjectResource } from '../../models/project';
 import { mockProjectResource } from '../../models/test-utils';
 
index 8de9bda5970ffe7d495f94f24b51229aacf9c1df..e4d8c72ce685814711b3d336bcaaceac409192d6 100644 (file)
@@ -5,10 +5,43 @@
 import * as React from 'react';
 import List from "@material-ui/core/List/List";
 import ListItem from "@material-ui/core/ListItem/ListItem";
-import { StyleRulesCallback, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
+import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
 import { ReactElement } from "react";
 import Collapse from "@material-ui/core/Collapse/Collapse";
 import CircularProgress from '@material-ui/core/CircularProgress';
+import { ArvadosTheme } from '../../common/custom-theme';
+
+type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    list: {
+        paddingBottom: '3px',
+        paddingTop: '3px',
+    },
+    activeArrow: {
+        color: theme.palette.primary.main,
+        position: 'absolute',
+    },
+    inactiveArrow: {
+        color: theme.palette.grey["700"],
+        position: 'absolute',
+    },
+    arrowTransition: {
+        transition: 'all 0.1s ease',
+    },
+    arrowRotate: {
+        transition: 'all 0.1s ease',
+        transform: 'rotate(-90deg)',
+    },
+    arrowVisibility: {
+        opacity: 0,
+    },
+    loader: {
+        position: 'absolute',
+        transform: 'translate(0px)',
+        top: '3px'
+    }
+});
 
 export enum TreeItemStatus {
     Initial,
@@ -35,76 +68,48 @@ interface TreeProps<T> {
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
 }
 
-class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
-    render(): ReactElement<any> {
-        const level = this.props.level ? this.props.level : 0;
-        const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
-        const { list, inactiveArrow, activeArrow, loader } = classes;
-        return <List component="div" className={list}>
-            {items && items.map((it: TreeItem<T>, idx: number) =>
-                <div key={`item/${level}/${idx}`}>
-                    <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }} onClick={() => toggleItemActive(it.id, it.status)} onContextMenu={this.handleRowContextMenu(it)}>
-                        {it.status === TreeItemStatus.Pending ? <CircularProgress size={10} className={loader} /> : null}
-                        {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open, it.id)}
-                        {render(it, level)}
-                    </ListItem>
-                    {it.items && it.items.length > 0 &&
+export const Tree = withStyles(styles)(
+    class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
+        render(): ReactElement<any> {
+            const level = this.props.level ? this.props.level : 0;
+            const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
+            const { list, inactiveArrow, activeArrow, loader } = classes;
+            return <List component="div" className={list}>
+                {items && items.map((it: TreeItem<T>, idx: number) =>
+                    <div key={`item/${level}/${idx}`}>
+                        <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}
+                                  onClick={() => toggleItemActive(it.id, it.status)}
+                                  onContextMenu={this.handleRowContextMenu(it)}>
+                            {it.status === TreeItemStatus.Pending ?
+                                <CircularProgress size={10} className={loader}/> : null}
+                            {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open, it.id)}
+                            {render(it, level)}
+                        </ListItem>
+                        {it.items && it.items.length > 0 &&
                         <Collapse in={it.open} timeout="auto" unmountOnExit>
-                            <StyledTree
+                            <Tree
                                 items={it.items}
                                 render={render}
                                 toggleItemOpen={toggleItemOpen}
                                 toggleItemActive={toggleItemActive}
                                 level={level + 1}
-                                onContextMenu={onContextMenu} />
+                                onContextMenu={onContextMenu}/>
                         </Collapse>}
-                </div>)}
-        </List>;
-    }
-    renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) {
-        const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes;
-        return <i onClick={() => this.props.toggleItemOpen(id, status)}
-            className={`
-                    ${arrowClass} 
-                    ${status === TreeItemStatus.Pending ? arrowVisibility : ''} 
-                    ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} />;
-    }
-
-    handleRowContextMenu = (item: TreeItem<T>) =>
-        (event: React.MouseEvent<HTMLElement>) =>
-            this.props.onContextMenu(event, item)
-}
+                    </div>)}
+            </List>;
+        }
 
-type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
+        renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) {
+            const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes;
+            return <i onClick={() => this.props.toggleItemOpen(id, status)}
+                      className={`
+                        ${arrowClass}
+                        ${status === TreeItemStatus.Pending ? arrowVisibility : ''}
+                        ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`}/>;
+        }
 
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
-    list: {
-        paddingBottom: '3px',
-        paddingTop: '3px',
-    },
-    activeArrow: {
-        color: '#4285F6',
-        position: 'absolute',
-    },
-    inactiveArrow: {
-        position: 'absolute',
-    },
-    arrowTransition: {
-        transition: 'all 0.1s ease',
-    },
-    arrowRotate: {
-        transition: 'all 0.1s ease',
-        transform: 'rotate(-90deg)',
-    },
-    arrowVisibility: {
-        opacity: 0,
-    },
-    loader: {
-        position: 'absolute',
-        transform: 'translate(0px)',
-        top: '3px'
+        handleRowContextMenu = (item: TreeItem<T>) =>
+            (event: React.MouseEvent<HTMLElement>) =>
+                this.props.onContextMenu(event, item)
     }
-});
-
-const StyledTree = withStyles(styles)(Tree);
-export default StyledTree;
+);
index a06b4851a314d678f175bd8941ea11d14adf5ed4..6d53e0d439bf52f05d7d7b639adbd91dc06e2489 100644 (file)
@@ -5,51 +5,54 @@
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import { Provider } from "react-redux";
-import Workbench from './views/workbench/workbench';
+import { Workbench } from './views/workbench/workbench';
 import './index.css';
 import { Route } from "react-router";
 import createBrowserHistory from "history/createBrowserHistory";
-import configureStore from "./store/store";
+import { configureStore } from "./store/store";
 import { ConnectedRouter } from "react-router-redux";
-import ApiToken from "./views-components/api-token/api-token";
-import authActions from "./store/auth/auth-action";
+import { ApiToken } from "./views-components/api-token/api-token";
+import { authActions } from "./store/auth/auth-action";
 import { authService } from "./services/services";
 import { getProjectList } from "./store/project/project-action";
 import { MuiThemeProvider } from '@material-ui/core/styles';
 import { CustomTheme } from './common/custom-theme';
-import CommonResourceService from './common/api/common-resource-service';
-import { CollectionResource } from './models/collection';
-import { serverApi } from './common/api/server-api';
-import { ProcessResource } from './models/process';
-
-const history = createBrowserHistory();
-
-const store = configureStore(history);
-
-store.dispatch(authActions.INIT());
-store.dispatch<any>(getProjectList(authService.getUuid()));
-
-// const service = new CommonResourceService<CollectionResource>(serverApi, "collections");
-// service.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Collection 1 short title"});
-// service.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Collection 2 long long long title"});
-
-// const processService = new CommonResourceService<ProcessResource>(serverApi, "container_requests");
-// processService.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Process 1 short title"});
-// processService.create({ ownerUuid: "qr1hi-j7d0g-u55bcc7fa5w7v4p", name: "Process 2 long long long title" });
-
-const App = () =>
-    <MuiThemeProvider theme={CustomTheme}>
-        <Provider store={store}>
-            <ConnectedRouter history={history}>
-                <div>
-                    <Route path="/" component={Workbench} />
-                    <Route path="/token" component={ApiToken} />
-                </div>
-            </ConnectedRouter>
-        </Provider>
-    </MuiThemeProvider>;
-
-ReactDOM.render(
-    <App />,
-    document.getElementById('root') as HTMLElement
-);
+import { fetchConfig } from './common/config';
+import { setBaseUrl } from './common/api/server-api';
+import { addMenuActionSet, ContextMenuKind } from "./views-components/context-menu/context-menu";
+import { rootProjectActionSet } from "./views-components/context-menu/action-sets/root-project-action-set";
+import { projectActionSet } from "./views-components/context-menu/action-sets/project-action-set";
+
+addMenuActionSet(ContextMenuKind.RootProject, rootProjectActionSet);
+addMenuActionSet(ContextMenuKind.Project, projectActionSet);
+
+fetchConfig()
+    .then(config => {
+
+        setBaseUrl(config.API_HOST);
+
+        const history = createBrowserHistory();
+        const store = configureStore(history);
+
+        store.dispatch(authActions.INIT());
+        store.dispatch<any>(getProjectList(authService.getUuid()));
+
+        const App = () =>
+            <MuiThemeProvider theme={CustomTheme}>
+                <Provider store={store}>
+                    <ConnectedRouter history={history}>
+                        <div>
+                            <Route path="/" component={Workbench} />
+                            <Route path="/token" component={ApiToken} />
+                        </div>
+                    </ConnectedRouter>
+                </Provider>
+            </MuiThemeProvider>;
+
+        ReactDOM.render(
+            <App />,
+            document.getElementById('root') as HTMLElement
+        );
+    });
+
+
index 162e0f57e454879d693dc5882545643503625369..4e5481570e6844100791c8b27b712a9498622963 100644 (file)
@@ -35,5 +35,4 @@ export interface ContainerRequestResource extends Resource {
     logUuid: string;
     outputUuid: string;
     filters: string;
-
-}
\ No newline at end of file
+}
diff --git a/src/models/details.ts b/src/models/details.ts
new file mode 100644 (file)
index 0000000..42eb5c9
--- /dev/null
@@ -0,0 +1,10 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ProjectResource } from "./project";
+import { CollectionResource } from "./collection";
+import { ProcessResource } from "./process";
+import { EmptyResource } from "./empty";
+
+export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource;
index 9731207f0e725b3d979e046b606b39af8b95c57e..539f9f5719523eba59d5575077985660cb0379db 100644 (file)
@@ -2,9 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ResourceKind } from "./resource";
-
 export interface EmptyResource {
     name: string;
     kind: undefined;
-}
\ No newline at end of file
+}
index e953a75d14aabbcd52a2d61fcf32e260d83717f3..1879e6a0abd1b5e2fedb75e3690329407aea4280 100644 (file)
@@ -2,8 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { API_HOST, serverApi } from "../../common/api/server-api";
+import { API_HOST } from "../../common/api/server-api";
 import { User } from "../../models/user";
+import { AxiosInstance } from "../../../node_modules/axios";
 
 export const API_TOKEN_KEY = 'apiToken';
 export const USER_EMAIL_KEY = 'userEmail';
@@ -21,7 +22,9 @@ export interface UserDetailsResponse {
     is_admin: boolean;
 }
 
-export default class AuthService {
+export class AuthService {
+
+    constructor(protected serverApi: AxiosInstance) { }
 
     public saveApiToken(token: string) {
         localStorage.setItem(API_TOKEN_KEY, token);
@@ -82,7 +85,7 @@ export default class AuthService {
     }
 
     public getUserDetails = (): Promise<User> => {
-        return serverApi
+        return this.serverApi
             .get<UserDetailsResponse>('/users/current')
             .then(resp => ({
                 email: resp.data.email,
index 2562a595d02ff52b0c0af4954d10f5fb20e67e06..c3be8bdaa8a6ecfa58a1553b7de57483481ff86b 100644 (file)
@@ -4,7 +4,7 @@
 
 import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
-import GroupsService from "./groups-service";
+import { GroupsService } from "./groups-service";
 
 describe("GroupsService", () => {
 
index 1318dace83226f305cf01f1099fd41470a72cad8..dfaf11d6e82ccfbc77ad4ba484e7577fa66b4bdf 100644 (file)
@@ -3,9 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as _ from "lodash";
-import CommonResourceService, { ListResults } from "../../common/api/common-resource-service";
-import FilterBuilder from "../../common/api/filter-builder";
-import OrderBuilder from "../../common/api/order-builder";
+import { CommonResourceService, ListResults } from "../../common/api/common-resource-service";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { OrderBuilder } from "../../common/api/order-builder";
 import { AxiosInstance } from "axios";
 import { GroupResource } from "../../models/group";
 import { CollectionResource } from "../../models/collection";
@@ -25,7 +25,7 @@ export type GroupContentsResource =
     ProjectResource |
     ProcessResource;
 
-export default class GroupsService<T extends GroupResource = GroupResource> extends CommonResourceService<T> {
+export class GroupsService<T extends GroupResource = GroupResource> extends CommonResourceService<T> {
 
     constructor(serverApi: AxiosInstance) {
         super(serverApi, "groups");
@@ -50,4 +50,4 @@ export enum GroupContentsResourcePrefix {
     Collection = "collections",
     Project = "groups",
     Process = "container_requests"
-}
\ No newline at end of file
+}
index 68df2450a298ce971ee4483842590f2a0812fc0d..f915c2dfb7939653fba3ac54a3533fd340d4bb4f 100644 (file)
@@ -3,9 +3,8 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import axios from "axios";
-import MockAdapter from "axios-mock-adapter";
-import ProjectService from "./project-service";
-import FilterBuilder from "../../common/api/filter-builder";
+import { ProjectService } from "./project-service";
+import { FilterBuilder } from "../../common/api/filter-builder";
 import { ProjectResource } from "../../models/project";
 
 describe("CommonResourceService", () => {
@@ -35,5 +34,5 @@ describe("CommonResourceService", () => {
             }
         });
     });
-    
+
 });
index 9ce9e2131446f8ba28c7e3ecb8814f36ca28e1f1..f759547a2ffd1127b2e01b5734bac74d660abf57 100644 (file)
@@ -2,13 +2,13 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import GroupsService, { ContentsArguments } from "../groups-service/groups-service";
+import { GroupsService, ContentsArguments } from "../groups-service/groups-service";
 import { ProjectResource } from "../../models/project";
 import { GroupClass } from "../../models/group";
 import { ListArguments } from "../../common/api/common-resource-service";
-import FilterBuilder from "../../common/api/filter-builder";
+import { FilterBuilder } from "../../common/api/filter-builder";
 
-export default class ProjectService extends GroupsService<ProjectResource> {
+export class ProjectService extends GroupsService<ProjectResource> {
 
     create(data: Partial<ProjectResource>) {
         const projectData = { ...data, groupClass: GroupClass.Project };
@@ -33,4 +33,4 @@ export default class ProjectService extends GroupsService<ProjectResource> {
                 .addEqual("groupClass", GroupClass.Project));
     }
 
-}
\ No newline at end of file
+}
index 143e97bdaf2dbbeff82b3e2cb5b23a691a0505e0..57f07d6c6a579550de2ab141fdbace8e0afebe03 100644 (file)
@@ -2,11 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import AuthService from "./auth-service/auth-service";
-import GroupsService from "./groups-service/groups-service";
+import { AuthService } from "./auth-service/auth-service";
+import { GroupsService } from "./groups-service/groups-service";
 import { serverApi } from "../common/api/server-api";
-import ProjectService from "./project-service/project-service";
+import { ProjectService } from "./project-service/project-service";
 
-export const authService = new AuthService();
+export const authService = new AuthService(serverApi);
 export const groupsService = new GroupsService(serverApi);
 export const projectService = new ProjectService(serverApi);
index a6e6f79794db27bc64178b6f53626c6e688c3865..e9930a02836da5d66c7e062813a5fd292784f973 100644 (file)
@@ -7,7 +7,7 @@ import { Dispatch } from "redux";
 import { authService } from "../../services/services";
 import { User } from "../../models/user";
 
-const actions = unionize({
+export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
     LOGIN: {},
     LOGOUT: {},
@@ -20,13 +20,11 @@ const actions = unionize({
 });
 
 export const getUserDetails = () => (dispatch: Dispatch): Promise<User> => {
-    dispatch(actions.USER_DETAILS_REQUEST());
+    dispatch(authActions.USER_DETAILS_REQUEST());
     return authService.getUserDetails().then(details => {
-        dispatch(actions.USER_DETAILS_SUCCESS(details));
+        dispatch(authActions.USER_DETAILS_SUCCESS(details));
         return details;
     });
 };
 
-
-export type AuthAction = UnionOf<typeof actions>;
-export default actions;
+export type AuthAction = UnionOf<typeof authActions>;
index 2e7c1a248800f94cf5bfdbe03c07e9e2f980d093..ea08e589838414d6bb1b876a21ce45422041ac35 100644 (file)
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import authReducer from "./auth-reducer";
-import actions from "./auth-action";
+import { authReducer } from "./auth-reducer";
+import { authActions } from "./auth-action";
 import {
     API_TOKEN_KEY,
     USER_EMAIL_KEY,
@@ -23,7 +23,7 @@ describe('auth-reducer', () => {
 
     it('should return default state on initialisation', () => {
         const initialState = undefined;
-        const state = authReducer(initialState, actions.INIT());
+        const state = authReducer(initialState, authActions.INIT());
         expect(state).toEqual({
             apiToken: undefined,
             user: undefined
@@ -40,7 +40,7 @@ describe('auth-reducer', () => {
         localStorage.setItem(USER_UUID_KEY, "uuid");
         localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
 
-        const state = authReducer(initialState, actions.INIT());
+        const state = authReducer(initialState, authActions.INIT());
         expect(state).toEqual({
             apiToken: "token",
             user: {
@@ -56,7 +56,7 @@ describe('auth-reducer', () => {
     it('should store token in local storage', () => {
         const initialState = undefined;
 
-        const state = authReducer(initialState, actions.SAVE_API_TOKEN("token"));
+        const state = authReducer(initialState, authActions.SAVE_API_TOKEN("token"));
         expect(state).toEqual({
             apiToken: "token",
             user: undefined
@@ -76,7 +76,7 @@ describe('auth-reducer', () => {
             ownerUuid: "ownerUuid"
         };
 
-        const state = authReducer(initialState, actions.USER_DETAILS_SUCCESS(user));
+        const state = authReducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
         expect(state).toEqual({
             apiToken: undefined,
             user: {
@@ -94,7 +94,7 @@ describe('auth-reducer', () => {
     it('should fire external url to login', () => {
         const initialState = undefined;
         window.location.assign = jest.fn();
-        authReducer(initialState, actions.LOGIN());
+        authReducer(initialState, authActions.LOGIN());
         expect(window.location.assign).toBeCalledWith(
             `${API_HOST}/login?return_to=${window.location.protocol}//${window.location.host}/token`
         );
@@ -103,7 +103,7 @@ describe('auth-reducer', () => {
     it('should fire external url to logout', () => {
         const initialState = undefined;
         window.location.assign = jest.fn();
-        authReducer(initialState, actions.LOGOUT());
+        authReducer(initialState, authActions.LOGOUT());
         expect(window.location.assign).toBeCalledWith(
             `${API_HOST}/logout?return_to=${location.protocol}//${location.host}`
         );
index f6974fd2073be49a5c01fe6677bff22edbc4ae8d..366385d50b506de529139f3f75151762f6a6285a 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import actions, { AuthAction } from "./auth-action";
+import { authActions, AuthAction } from "./auth-action";
 import { User } from "../../models/user";
 import { authService } from "../../services/services";
 import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
@@ -12,8 +12,8 @@ export interface AuthState {
     apiToken?: string;
 }
 
-const authReducer = (state: AuthState = {}, action: AuthAction) => {
-    return actions.match(action, {
+export const authReducer = (state: AuthState = {}, action: AuthAction) => {
+    return authActions.match(action, {
         SAVE_API_TOKEN: (token: string) => {
             authService.saveApiToken(token);
             setServerApiAuthorizationHeader(token);
@@ -45,5 +45,3 @@ const authReducer = (state: AuthState = {}, action: AuthAction) => {
         default: () => state
     });
 };
-
-export default authReducer;
index 89d652443cb79d950622af04f015940e377f807c..8e5eb1e795791260474d92e0fffe6e596560081d 100644 (file)
@@ -2,14 +2,15 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-// import { default as unionize, ofType, UnionOf } from "unionize";
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
 
-// const actions = unionize({
-//     OPEN_CONTEXT_MENU: ofType<{position: {x: number, y: number}}>()
-// }, {
-//     tag: 'type',
-//     value: 'payload'
-// });
+export const contextMenuActions = unionize({
+    OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
+    CLOSE_CONTEXT_MENU: ofType<{}>()
+}, {
+        tag: 'type',
+        value: 'payload'
+    });
 
-// export type ContextMenuAction = UnionOf<typeof actions>;
-// export default actions;
\ No newline at end of file
+export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
index 9a825a5ff7626060b4372c2012f3618d080a4807..b20ad723f23d9aec615ab4700ca48db90be9f840 100644 (file)
@@ -2,31 +2,32 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-// import actions, { DetailsPanelAction } from "./details-panel-action";
-// import { Resource, ResourceKind } from "../../models/resource";
+import { ResourceKind } from "../../models/resource";
+import { contextMenuActions, ContextMenuAction } from "./context-menu-actions";
 
-// export interface ContextMenuState {
-//     position: {
-//         x: number;
-//         y: number;
-//     },
-//     resource: {
-//         uuid: string;
-//         kind: ResourceKind.
-//     }
-// }
+export interface ContextMenuState {
+    position: ContextMenuPosition;
+    resource?: ContextMenuResource;
+}
 
-// const initialState = {
-//     item: null,
-//     isOpened: false
-// };
+export interface ContextMenuPosition {
+    x: number;
+    y: number;
+}
 
-// const reducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
-//     actions.match(action, {
-//         default: () => state,
-//         LOAD_DETAILS: () => state,
-//         LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
-//         TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
-//     });
+export interface ContextMenuResource {
+    uuid: string;
+    kind: string;
+}
+
+const initialState = {
+    position: { x: 0, y: 0 }
+};
+
+export const contextMenuReducer = (state: ContextMenuState = initialState, action: ContextMenuAction) =>
+    contextMenuActions.match(action, {
+        default: () => state,
+        OPEN_CONTEXT_MENU: ({resource, position}) => ({ resource, position }),
+        CLOSE_CONTEXT_MENU: () => ({ position: state.position })
+    });
 
-// export default reducer;
index 3d7ba53440f902ef75c5060feb954459f0cce84e..053f4194ec8ddab68b3583bf346f21ea39f470ad 100644 (file)
@@ -6,7 +6,7 @@ import { default as unionize, ofType, UnionOf } from "unionize";
 import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
 import { DataColumns } from "../../components/data-table/data-table";
 
-const actions = unionize({
+export const dataExplorerActions = unionize({
     RESET_PAGINATION: ofType<{ id: string }>(),
     REQUEST_ITEMS: ofType<{ id: string }>(),
     SET_COLUMNS: ofType<{ id: string, columns: DataColumns<any> }>(),
@@ -19,6 +19,4 @@ const actions = unionize({
     SET_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(),
 }, { tag: "type", value: "payload" });
 
-export type DataExplorerAction = UnionOf<typeof actions>;
-
-export default actions;
+export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
index 0eb3c321a3e107e49fb43af4f2c267e16648f81c..5b9f68ffbef75bdaa610cd8836668b326fd45660 100644 (file)
@@ -2,10 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import dataExplorerReducer, { initialDataExplorer } from "./data-explorer-reducer";
-import actions from "./data-explorer-action";
+import { dataExplorerReducer, initialDataExplorer } from "./data-explorer-reducer";
+import { dataExplorerActions } from "./data-explorer-action";
 import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
 import { DataColumns } from "../../components/data-table/data-table";
+import { SortDirection } from "../../components/data-table/data-column";
 
 describe('data-explorer-reducer', () => {
     it('should set columns', () => {
@@ -15,7 +16,7 @@ describe('data-explorer-reducer', () => {
             selected: true
         }];
         const state = dataExplorerReducer(undefined,
-            actions.SET_COLUMNS({ id: "Data explorer", columns }));
+            dataExplorerActions.SET_COLUMNS({ id: "Data explorer", columns }));
         expect(state["Data explorer"].columns).toEqual(columns);
     });
 
@@ -24,15 +25,15 @@ describe('data-explorer-reducer', () => {
             name: "Column 1",
             render: jest.fn(),
             selected: true,
-            sortDirection: "asc"
+            sortDirection: SortDirection.Asc
         }, {
             name: "Column 2",
             render: jest.fn(),
             selected: true,
-            sortDirection: "none",
+            sortDirection: SortDirection.None,
         }];
         const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
-            actions.TOGGLE_SORT({ id: "Data explorer", columnName: "Column 2" }));
+            dataExplorerActions.TOGGLE_SORT({ id: "Data explorer", columnName: "Column 2" }));
         expect(state["Data explorer"].columns[0].sortDirection).toEqual("none");
         expect(state["Data explorer"].columns[1].sortDirection).toEqual("asc");
     });
@@ -49,25 +50,31 @@ describe('data-explorer-reducer', () => {
             selected: true
         }];
         const state = dataExplorerReducer({ "Data explorer": { ...initialDataExplorer, columns } },
-            actions.SET_FILTERS({ id: "Data explorer", columnName: "Column 1", filters }));
+            dataExplorerActions.SET_FILTERS({ id: "Data explorer", columnName: "Column 1", filters }));
         expect(state["Data explorer"].columns[0].filters).toEqual(filters);
     });
 
     it('should set items', () => {
         const state = dataExplorerReducer({ "Data explorer": undefined },
-            actions.SET_ITEMS({ id: "Data explorer", items: ["Item 1", "Item 2"] }));
+            dataExplorerActions.SET_ITEMS({
+                id: "Data explorer",
+                items: ["Item 1", "Item 2"],
+                page: 0,
+                rowsPerPage: 10,
+                itemsAvailable: 100
+            }));
         expect(state["Data explorer"].items).toEqual(["Item 1", "Item 2"]);
     });
 
     it('should set page', () => {
         const state = dataExplorerReducer({ "Data explorer": undefined },
-            actions.SET_PAGE({ id: "Data explorer", page: 2 }));
+            dataExplorerActions.SET_PAGE({ id: "Data explorer", page: 2 }));
         expect(state["Data explorer"].page).toEqual(2);
     });
-    
+
     it('should set rows per page', () => {
         const state = dataExplorerReducer({ "Data explorer": undefined },
-            actions.SET_ROWS_PER_PAGE({ id: "Data explorer", rowsPerPage: 5 }));
+            dataExplorerActions.SET_ROWS_PER_PAGE({ id: "Data explorer", rowsPerPage: 5 }));
         expect(state["Data explorer"].rowsPerPage).toEqual(5);
     });
 });
index 01126170720c7593d9110eb74caffa3052558fd3..c112454b94f31451100d047d1e5f46f489e28e30 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { DataColumn, toggleSortDirection, resetSortDirection } from "../../components/data-table/data-column";
-import actions, { DataExplorerAction } from "./data-explorer-action";
+import { dataExplorerActions, DataExplorerAction } from "./data-explorer-action";
 import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
 import { DataColumns } from "../../components/data-table/data-table";
 
@@ -29,8 +29,8 @@ export const initialDataExplorer: DataExplorer = {
 
 export type DataExplorerState = Record<string, DataExplorer | undefined>;
 
-const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
-    actions.match(action, {
+export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
+    dataExplorerActions.match(action, {
         RESET_PAGINATION: ({ id }) =>
             update(state, id, explorer => ({ ...explorer, page: 0 })),
 
@@ -61,8 +61,6 @@ const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorer
         default: () => state
     });
 
-export default dataExplorerReducer;
-
 export const getDataExplorer = (state: DataExplorerState, id: string) =>
     state[id] || initialDataExplorer;
 
index 630428cf2b6a10a0bb3dfa1595df87b944d9579f..ba330f2e417dde4b07bd64914591258e255f084d 100644 (file)
@@ -3,28 +3,26 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from "unionize";
-import CommonResourceService from "../../common/api/common-resource-service";
+import { CommonResourceService } from "../../common/api/common-resource-service";
 import { Dispatch } from "redux";
 import { serverApi } from "../../common/api/server-api";
 import { Resource, ResourceKind } from "../../models/resource";
 
-const actions = unionize({
+export const detailsPanelActions = unionize({
     TOGGLE_DETAILS_PANEL: ofType<{}>(),
     LOAD_DETAILS: ofType<{ uuid: string, kind: ResourceKind }>(),
     LOAD_DETAILS_SUCCESS: ofType<{ item: Resource }>(),
 }, { tag: 'type', value: 'payload' });
 
-export default actions;
-
-export type DetailsPanelAction = UnionOf<typeof actions>;
+export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
 
 export const loadDetails = (uuid: string, kind: ResourceKind) =>
     (dispatch: Dispatch) => {
-        dispatch(actions.LOAD_DETAILS({ uuid, kind }));
+        dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
         getService(kind)
             .get(uuid)
             .then(project => {
-                dispatch(actions.LOAD_DETAILS_SUCCESS({ item: project }));
+                dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item: project }));
             });
     };
 
index f57b9f1c709ba49bb77cb3cc036692bfcf2ad1bd..97de4a9a50b041b9583a2afa4231e446e1d3008b 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import actions, { DetailsPanelAction } from "./details-panel-action";
+import { detailsPanelActions, DetailsPanelAction } from "./details-panel-action";
 import { Resource } from "../../models/resource";
 
 export interface DetailsPanelState {
@@ -15,12 +15,10 @@ const initialState = {
     isOpened: false
 };
 
-const reducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
-    actions.match(action, {
+export const detailsPanelReducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
+    detailsPanelActions.match(action, {
         default: () => state,
         LOAD_DETAILS: () => state,
         LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
         TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
     });
-
-export default reducer;
index d7630d7a48bf6691c270af0248b51a948bce1171..3920b5a27c69bf17cfbeca27a7c0bda562662f47 100644 (file)
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import projectActions, { getProjectList } from "../project/project-action";
+import { projectActions, getProjectList } from "../project/project-action";
 import { push } from "react-router-redux";
 import { TreeItemStatus } from "../../components/tree/tree";
 import { findTreeItem } from "../project/project-reducer";
-import dataExplorerActions from "../data-explorer/data-explorer-action";
+import { dataExplorerActions } from "../data-explorer/data-explorer-action";
 import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
 import { RootState } from "../store";
 import { Resource, ResourceKind } from "../../models/resource";
index e72b6c1b905469e9a5f6ea5e2d9bbc7457b0de23..fbed1783e260c45ae906f48529c9c55ef040a784 100644 (file)
@@ -3,22 +3,21 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Middleware } from "redux";
-import actions from "../../store/data-explorer/data-explorer-action";
+import { dataExplorerActions } from "../data-explorer/data-explorer-action";
 import { PROJECT_PANEL_ID, columns, ProjectPanelFilter, ProjectPanelColumnNames } from "../../views/project-panel/project-panel";
 import { groupsService } from "../../services/services";
-import { RootState } from "../../store/store";
-import { getDataExplorer, DataExplorerState } from "../../store/data-explorer/data-explorer-reducer";
+import { RootState } from "../store";
+import { getDataExplorer } from "../data-explorer/data-explorer-reducer";
 import { resourceToDataItem, ProjectPanelItem } from "../../views/project-panel/project-panel-item";
-import FilterBuilder from "../../common/api/filter-builder";
+import { FilterBuilder } from "../../common/api/filter-builder";
 import { DataColumns } from "../../components/data-table/data-table";
 import { ProcessResource } from "../../models/process";
-import { CollectionResource } from "../../models/collection";
-import OrderBuilder from "../../common/api/order-builder";
+import { OrderBuilder } from "../../common/api/order-builder";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
 import { SortDirection } from "../../components/data-table/data-column";
 
 export const projectPanelMiddleware: Middleware = store => next => {
-    next(actions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
+    next(dataExplorerActions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
 
     return action => {
 
@@ -30,30 +29,30 @@ export const projectPanelMiddleware: Middleware = store => next => {
                 }
             };
 
-        actions.match(action, {
+        dataExplorerActions.match(action, {
             SET_PAGE: handleProjectPanelAction(() => {
-                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
             }),
             SET_ROWS_PER_PAGE: handleProjectPanelAction(() => {
-                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
             }),
             SET_FILTERS: handleProjectPanelAction(() => {
-                store.dispatch(actions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
-                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+                store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
+                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
             }),
             TOGGLE_SORT: handleProjectPanelAction(() => {
-                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
             }),
             SET_SEARCH_VALUE: handleProjectPanelAction(() => {
-                store.dispatch(actions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
-                store.dispatch(actions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+                store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
+                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
             }),
             REQUEST_ITEMS: handleProjectPanelAction(() => {
                 const state = store.getState() as RootState;
                 const dataExplorer = getDataExplorer(state.dataExplorer, PROJECT_PANEL_ID);
                 const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
-                const typeFilters = getColumnFilters(columns, ProjectPanelColumnNames.Type);
-                const statusFilters = getColumnFilters(columns, ProjectPanelColumnNames.Status);
+                const typeFilters = getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
+                const statusFilters = getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
                 const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
                 const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.Asc ? SortDirection.Asc : SortDirection.Desc;
                 if (typeFilters.length > 0) {
@@ -62,7 +61,7 @@ export const projectPanelMiddleware: Middleware = store => next => {
                             limit: dataExplorer.rowsPerPage,
                             offset: dataExplorer.page * dataExplorer.rowsPerPage,
                             order: sortColumn
-                                ? sortColumn.name === ProjectPanelColumnNames.Name
+                                ? sortColumn.name === ProjectPanelColumnNames.NAME
                                     ? getOrder("name", sortDirection)
                                     : getOrder("createdAt", sortDirection)
                                 : OrderBuilder.create(),
@@ -77,7 +76,7 @@ export const projectPanelMiddleware: Middleware = store => next => {
                                 .concat(getSearchFilter(dataExplorer.searchValue))
                         })
                         .then(response => {
-                            store.dispatch(actions.SET_ITEMS({
+                            store.dispatch(dataExplorerActions.SET_ITEMS({
                                 id: PROJECT_PANEL_ID,
                                 items: response.items.map(resourceToDataItem),
                                 itemsAvailable: response.itemsAvailable,
@@ -86,7 +85,7 @@ export const projectPanelMiddleware: Middleware = store => next => {
                             }));
                         });
                 } else {
-                    store.dispatch(actions.SET_ITEMS({
+                    store.dispatch(dataExplorerActions.SET_ITEMS({
                         id: PROJECT_PANEL_ID,
                         items: [],
                         itemsAvailable: 0,
index 3da60f65c9ae9c3d7a14ccbfe0806ba2d6990394..075e77d15483746a751d59553299546f01bd1460 100644 (file)
@@ -6,10 +6,10 @@ import { default as unionize, ofType, UnionOf } from "unionize";
 import { ProjectResource } from "../../models/project";
 import { projectService } from "../../services/services";
 import { Dispatch } from "redux";
-import FilterBuilder from "../../common/api/filter-builder";
+import { FilterBuilder } from "../../common/api/filter-builder";
 import { RootState } from "../store";
 
-const actions = unionize({
+export const projectActions = unionize({
     OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
     CLOSE_PROJECT_CREATOR: ofType<{}>(),
     CREATE_PROJECT: ofType<Partial<ProjectResource>>(),
@@ -26,13 +26,13 @@ const actions = unionize({
     });
 
 export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch) => {
-    dispatch(actions.PROJECTS_REQUEST(parentUuid));
+    dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
     return projectService.list({
         filters: FilterBuilder
             .create<ProjectResource>()
             .addEqual("ownerUuid", parentUuid)
     }).then(({ items: projects }) => {
-        dispatch(actions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
+        dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
         return projects;
     });
 };
@@ -41,11 +41,10 @@ export const createProject = (project: Partial<ProjectResource>) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const { ownerUuid } = getState().projects.creator;
         const projectData = { ownerUuid, ...project };
-        dispatch(actions.CREATE_PROJECT(projectData));
+        dispatch(projectActions.CREATE_PROJECT(projectData));
         return projectService
             .create(projectData)
-            .then(project => dispatch(actions.CREATE_PROJECT_SUCCESS(project)));
+            .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
     };
 
-export type ProjectAction = UnionOf<typeof actions>;
-export default actions;
+export type ProjectAction = UnionOf<typeof projectActions>;
index dbac4e923ae196c5e6f1e5b684e1555bdae56816..c8eed87c71a70bec3bdbcd7157b569d7fc660552 100644 (file)
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import projectsReducer, { getTreePath } from "./project-reducer";
-import actions from "./project-action";
+import { projectsReducer, getTreePath } from "./project-reducer";
+import { projectActions } from "./project-action";
 import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 import { mockProjectResource } from "../../models/test-utils";
 
@@ -13,7 +13,7 @@ describe('project-reducer', () => {
         const initialState = undefined;
 
         const projects = [mockProjectResource({ uuid: "1" }), mockProjectResource({ uuid: "2" })];
-        const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
+        const state = projectsReducer(initialState, projectActions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
         expect(state).toEqual({
             items: [{
                 active: false,
@@ -64,7 +64,7 @@ describe('project-reducer', () => {
             creator: { opened: false, pending: false, ownerUuid: "" },
         };
 
-        const state = projectsReducer(initialState, actions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
+        const state = projectsReducer(initialState, projectActions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
         expect(state).toEqual(project);
     });
 
@@ -93,7 +93,7 @@ describe('project-reducer', () => {
             creator: { opened: false, pending: false, ownerUuid: "" },
         };
 
-        const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
+        const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
         expect(state).toEqual(project);
     });
 
@@ -124,7 +124,7 @@ describe('project-reducer', () => {
             creator: { opened: false, pending: false, ownerUuid: "" },
         };
 
-        const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
+        const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
         expect(state).toEqual(project);
     });
 });
index a329e81242f4b8d7e4fd0ab37555281297a16c56..94a451a86574e70de24f6d143ede7217ce25cf9c 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as _ from "lodash";
 
-import actions, { ProjectAction } from "./project-action";
+import { projectActions, ProjectAction } from "./project-action";
 import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 import { ProjectResource } from "../../models/project";
 
@@ -112,9 +112,9 @@ const initialState: ProjectState = {
 };
 
 
-const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => {
-    return actions.match(action, {
-        OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
+export const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => {
+    return projectActions.match(action, {
+        OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true, pending: false }),
         CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }),
         CREATE_PROJECT: () => updateCreator(state, { error: undefined }),
         CREATE_PROJECT_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "" }),
@@ -174,5 +174,3 @@ const projectsReducer = (state: ProjectState = initialState, action: ProjectActi
         default: () => state
     });
 };
-
-export default projectsReducer;
index 6a83946cc8fcdf75e1ebbd7bb4abcce3951a3d41..0dd6aad19e3ca0ee2e2311cb333a72a83a889176 100644 (file)
@@ -4,7 +4,7 @@
 
 import { default as unionize, ofType, UnionOf } from "unionize";
 
-const actions = unionize({
+export const sidePanelActions = unionize({
     TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<string>(),
     TOGGLE_SIDE_PANEL_ITEM_ACTIVE: ofType<string>(),
     RESET_SIDE_PANEL_ACTIVITY: ofType<{}>(),
@@ -13,5 +13,4 @@ const actions = unionize({
     value: 'payload'
 });
 
-export type SidePanelAction = UnionOf<typeof actions>;
-export default actions;
\ No newline at end of file
+export type SidePanelAction = UnionOf<typeof sidePanelActions>;
index 942c16ebdcc7d1588f156c25345f6eda9a5617db..e517fc8509d4175859b6ed3381baf7868442c67d 100644 (file)
@@ -2,8 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import sidePanelReducer from "./side-panel-reducer";
-import actions from "./side-panel-action";
+import { sidePanelReducer } from "./side-panel-reducer";
+import { sidePanelActions } from "./side-panel-action";
+import { ProjectsIcon } from "../../components/icon/icon";
 
 describe('side-panel-reducer', () => {
 
@@ -12,7 +13,7 @@ describe('side-panel-reducer', () => {
             {
                 id: "1",
                 name: "Projects",
-                icon: "fas fa-th fa-fw",
+                icon: ProjectsIcon,
                 open: false,
                 active: false,
             }
@@ -21,13 +22,13 @@ describe('side-panel-reducer', () => {
             {
                 id: "1",
                 name: "Projects",
-                icon: "fas fa-th fa-fw",
+                icon: ProjectsIcon,
                 open: false,
                 active: true,
             }
         ];
 
-        const state = sidePanelReducer(initialState, actions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(initialState[0].id));
+        const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(initialState[0].id));
         expect(state).toEqual(project);
     });
 
@@ -36,7 +37,7 @@ describe('side-panel-reducer', () => {
             {
                 id: "1",
                 name: "Projects",
-                icon: "fas fa-th fa-fw",
+                icon: ProjectsIcon,
                 open: false,
                 active: false,
             }
@@ -45,13 +46,13 @@ describe('side-panel-reducer', () => {
             {
                 id: "1",
                 name: "Projects",
-                icon: "fas fa-th fa-fw",
+                icon: ProjectsIcon,
                 open: true,
                 active: false,
             }
         ];
 
-        const state = sidePanelReducer(initialState, actions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
+        const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
         expect(state).toEqual(project);
     });
 
@@ -60,7 +61,7 @@ describe('side-panel-reducer', () => {
             {
                 id: "1",
                 name: "Projects",
-                icon: "fas fa-th fa-fw",
+                icon: ProjectsIcon,
                 open: false,
                 active: true,
             }
@@ -69,13 +70,13 @@ describe('side-panel-reducer', () => {
             {
                 id: "1",
                 name: "Projects",
-                icon: "fas fa-th fa-fw",
+                icon: ProjectsIcon,
                 open: false,
                 active: false,
             }
         ];
 
-        const state = sidePanelReducer(initialState, actions.RESET_SIDE_PANEL_ACTIVITY(initialState[0].id));
+        const state = sidePanelReducer(initialState, sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(initialState[0].id));
         expect(state).toEqual(project);
     });
-});
\ No newline at end of file
+});
index ca26eeb6435b619af28faf2e3a98d68190a83461..2bbd6a1182007d4283d081bef8035d1ecc12c9d6 100644 (file)
@@ -3,17 +3,17 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as _ from "lodash";
-
-import actions, { SidePanelAction } from './side-panel-action';
+import { sidePanelActions, SidePanelAction } from './side-panel-action';
 import { SidePanelItem } from '../../components/side-panel/side-panel';
+import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "../../components/icon/icon";
 
 export type SidePanelState = SidePanelItem[];
 
-const sidePanelReducer = (state: SidePanelState = sidePanelData, action: SidePanelAction) => {
+export const sidePanelReducer = (state: SidePanelState = sidePanelData, action: SidePanelAction) => {
     if (state.length === 0) {
         return sidePanelData;
     } else {
-        return actions.match(action, {
+        return sidePanelActions.match(action, {
             TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId => state.map(it => itemId === it.id && it.open === false ? {...it, open: true} : {...it, open: false}),
             TOGGLE_SIDE_PANEL_ITEM_ACTIVE: itemId => {
                 const sidePanel = _.cloneDeep(state);
@@ -48,7 +48,7 @@ export const sidePanelData = [
     {
         id: SidePanelIdentifiers.Projects,
         name: "Projects",
-        icon: "fas fa-th fa-fw",
+        icon: ProjectsIcon,
         open: false,
         active: false,
         margin: true,
@@ -57,31 +57,31 @@ export const sidePanelData = [
     {
         id: SidePanelIdentifiers.SharedWithMe,
         name: "Shared with me",
-        icon: "fas fa-users fa-fw",
+        icon: ShareMeIcon,
         active: false,
     },
     {
         id: SidePanelIdentifiers.Workflows,
         name: "Workflows",
-        icon: "fas fa-cogs fa-fw",
+        icon: WorkflowIcon,
         active: false,
     },
     {
         id: SidePanelIdentifiers.RecentOpen,
         name: "Recent open",
-        icon: "icon-time fa-fw",
+        icon: RecentIcon,
         active: false,
     },
     {
         id: SidePanelIdentifiers.Favourites,
         name: "Favorites",
-        icon: "fas fa-star fa-fw",
+        icon: FavoriteIcon,
         active: false,
     },
     {
         id: SidePanelIdentifiers.Trash,
         name: "Trash",
-        icon: "fas fa-trash-alt fa-fw",
+        icon: TrashIcon,
         active: false,
     }
 ];
@@ -91,5 +91,3 @@ function resetSidePanelActivity(sidePanel: SidePanelItem[]) {
         t.active = false;
     }
 }
-
-export default sidePanelReducer;
index 956fb46011bf3d2fd95cd98fafc4b8637a476186..01b06b9528a727cd3cb9642a16bffeb0e17954ea 100644 (file)
@@ -7,12 +7,13 @@ import { routerMiddleware, routerReducer, RouterState } from "react-router-redux
 import thunkMiddleware from 'redux-thunk';
 import { History } from "history";
 
-import projectsReducer, { ProjectState } from "./project/project-reducer";
-import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer';
-import authReducer, { AuthState } from "./auth/auth-reducer";
-import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
-import { projectPanelMiddleware } from '../store/project-panel/project-panel-middleware';
-import detailsPanelReducer, { DetailsPanelState } from './details-panel/details-panel-reducer';
+import { projectsReducer, ProjectState } from "./project/project-reducer";
+import { sidePanelReducer, SidePanelState } from './side-panel/side-panel-reducer';
+import { authReducer, AuthState } from "./auth/auth-reducer";
+import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-explorer-reducer';
+import { projectPanelMiddleware } from './project-panel/project-panel-middleware';
+import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
+import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
 import { reducer as formReducer } from 'redux-form';
 
 const composeEnhancers =
@@ -27,6 +28,7 @@ export interface RootState {
     dataExplorer: DataExplorerState;
     sidePanel: SidePanelState;
     detailsPanel: DetailsPanelState;
+    contextMenu: ContextMenuState;
 }
 
 const rootReducer = combineReducers({
@@ -36,11 +38,12 @@ const rootReducer = combineReducers({
     dataExplorer: dataExplorerReducer,
     sidePanel: sidePanelReducer,
     detailsPanel: detailsPanelReducer,
+    contextMenu: contextMenuReducer,
     form: formReducer
 });
 
 
-export default function configureStore(history: History) {
+export function configureStore(history: History) {
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware,
diff --git a/src/utils/dialog-validator.tsx b/src/utils/dialog-validator.tsx
new file mode 100644 (file)
index 0000000..9697a86
--- /dev/null
@@ -0,0 +1,74 @@
+// 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';
+
+type CssRules = "formInputError";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    formInputError: {
+        color: "#ff0000",
+        marginLeft: "5px",
+        fontSize: "11px",
+    }
+});
+
+type ValidatorProps = {
+    value: string,
+    onChange: (isValid: boolean | string) => void;
+    render: (hasError: boolean) => React.ReactElement<any>;
+    isRequired: boolean;
+};
+
+interface ValidatorState {
+    isPatternValid: boolean;
+    isLengthValid: boolean;
+}
+
+const nameRegEx = /^[a-zA-Z0-9-_ ]+$/;
+const maxInputLength = 60;
+
+export const Validator = withStyles(styles)(
+    class extends React.Component<ValidatorProps & WithStyles<CssRules>> {
+        state: ValidatorState = {
+            isPatternValid: true,
+            isLengthValid: true
+        };
+
+        componentWillReceiveProps(nextProps: ValidatorProps) {
+            const { value } = nextProps;
+
+            if (this.props.value !== value) {
+                this.setState({
+                    isPatternValid: value.match(nameRegEx),
+                    isLengthValid: value.length < maxInputLength
+                }, () => this.onChange());
+            }
+        }
+
+        onChange() {
+            const { value, onChange, isRequired } = this.props;
+            const { isPatternValid, isLengthValid } = this.state;
+            const isValid = value && isPatternValid && isLengthValid && (isRequired || (!isRequired && value.length > 0));
+
+            onChange(isValid);
+        }
+
+        render() {
+            const { classes, isRequired, value } = this.props;
+            const { isPatternValid, isLengthValid } = this.state;
+
+            return (
+                <span>
+            {this.props.render(!(isPatternValid && isLengthValid) && (isRequired || (!isRequired && value.length > 0)))}
+                    {!isPatternValid && (isRequired || (!isRequired && value.length > 0)) ?
+                        <span className={classes.formInputError}>This field allow only alphanumeric characters, dashes, spaces and underscores.<br/></span> : null}
+                    {!isLengthValid ?
+                        <span className={classes.formInputError}>This field should have max 60 characters.</span> : null}
+          </span>
+            );
+        }
+    }
+);
index e4ba4914a3518e95c557f3f93cc28293717dbd63..1d017ccdffe754ab0fa7ca1dc2777b5fcd985c61 100644 (file)
@@ -5,33 +5,27 @@
 import { Redirect, RouteProps } from "react-router";
 import * as React from "react";
 import { connect, DispatchProp } from "react-redux";
-import authActions, { getUserDetails } from "../../store/auth/auth-action";
+import { authActions, getUserDetails } from "../../store/auth/auth-action";
 import { authService } from "../../services/services";
 import { getProjectList } from "../../store/project/project-action";
+import { getUrlParameter } from "../../common/url";
 
 interface ApiTokenProps {
 }
 
-class ApiToken extends React.Component<ApiTokenProps & RouteProps & DispatchProp<any>, {}> {
-    static getUrlParameter(search: string, name: string) {
-        const safeName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
-        const regex = new RegExp('[\\?&]' + safeName + '=([^&#]*)');
-        const results = regex.exec(search);
-        return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
+export const ApiToken = connect()(
+    class extends React.Component<ApiTokenProps & RouteProps & DispatchProp<any>, {}> {
+        componentDidMount() {
+            const search = this.props.location ? this.props.location.search : "";
+            const apiToken = getUrlParameter(search, 'api_token');
+            this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
+            this.props.dispatch<any>(getUserDetails()).then(() => {
+                const rootUuid = authService.getRootUuid();
+                this.props.dispatch(getProjectList(rootUuid));
+            });
+        }
+        render() {
+            return <Redirect to="/"/>;
+        }
     }
-
-    componentDidMount() {
-        const search = this.props.location ? this.props.location.search : "";
-        const apiToken = ApiToken.getUrlParameter(search, 'api_token');
-        this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
-        this.props.dispatch<any>(getUserDetails()).then(() => {
-            const rootUuid = authService.getRootUuid();
-            this.props.dispatch(getProjectList(rootUuid));
-        });
-    }
-    render() {
-        return <Redirect to="/"/>;
-    }
-}
-
-export default connect()(ApiToken);
+);
diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts
new file mode 100644 (file)
index 0000000..66dbd4d
--- /dev/null
@@ -0,0 +1,19 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { projectActions } from "../../../store/project/project-action";
+import { ShareIcon, NewProjectIcon  } from "../../../components/icon/icon";
+
+export const projectActionSet: ContextMenuActionSet = [[{
+    icon: NewProjectIcon,
+    name: "New project",
+    execute: (dispatch, resource) => {
+        dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+    }
+}, {
+    icon: ShareIcon,
+    name: "Share",
+    execute: () => { return; }
+}]];
diff --git a/src/views-components/context-menu/action-sets/root-project-action-set.ts b/src/views-components/context-menu/action-sets/root-project-action-set.ts
new file mode 100644 (file)
index 0000000..139bd26
--- /dev/null
@@ -0,0 +1,15 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { projectActions } from "../../../store/project/project-action";
+import { NewProjectIcon } from "../../../components/icon/icon";
+
+export const rootProjectActionSet: ContextMenuActionSet =  [[{
+    icon: NewProjectIcon,
+    name: "New project",
+    execute: (dispatch, resource) => {
+        dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+    }
+}]];
diff --git a/src/views-components/context-menu/context-menu-action-set.ts b/src/views-components/context-menu/context-menu-action-set.ts
new file mode 100644 (file)
index 0000000..089580c
--- /dev/null
@@ -0,0 +1,13 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { ContextMenuItem } from "../../components/context-menu/context-menu";
+import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer";
+
+export interface ContextMenuAction extends ContextMenuItem {
+    execute(dispatch: Dispatch, resource: ContextMenuResource): void;
+}
+
+export type ContextMenuActionSet = Array<Array<ContextMenuAction>>;
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
new file mode 100644 (file)
index 0000000..cc2fcb3
--- /dev/null
@@ -0,0 +1,60 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { RootState } from "../../store/store";
+import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
+import { ContextMenu as ContextMenuComponent, ContextMenuProps, ContextMenuItem } from "../../components/context-menu/context-menu";
+import { createAnchorAt } from "../../components/popover/helpers";
+import { ContextMenuResource } from "../../store/context-menu/context-menu-reducer";
+import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
+import { Dispatch } from "redux";
+
+type DataProps = Pick<ContextMenuProps, "anchorEl" | "items"> & { resource?: ContextMenuResource };
+const mapStateToProps = (state: RootState): DataProps => {
+    const { position, resource } = state.contextMenu;
+    return {
+        anchorEl: resource ? createAnchorAt(position) : undefined,
+        items: getMenuActionSet(resource),
+        resource
+    };
+};
+
+type ActionProps = Pick<ContextMenuProps, "onClose"> & { onItemClick: (item: ContextMenuItem, resource?: ContextMenuResource) => void };
+const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({
+    onClose: () => {
+        dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
+    },
+    onItemClick: (action: ContextMenuAction, resource?: ContextMenuResource) => {
+        dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
+        if (resource) {
+            action.execute(dispatch, resource);
+        }
+    }
+});
+
+const mergeProps = ({ resource, ...dataProps }: DataProps, actionProps: ActionProps): ContextMenuProps => ({
+    ...dataProps,
+    ...actionProps,
+    onItemClick: item => {
+        actionProps.onItemClick(item, resource);
+    }
+});
+
+export const ContextMenu = connect(mapStateToProps, mapDispatchToProps, mergeProps)(ContextMenuComponent);
+
+const menuActionSets = new Map<string, ContextMenuActionSet>();
+
+export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) => {
+    menuActionSets.set(name, itemSet);
+};
+
+const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => {
+    return resource ? menuActionSets.get(resource.kind) || [] : [];
+};
+
+export enum ContextMenuKind {
+    RootProject = "RootProject",
+    Project = "Project"
+}
index f75c459347500da68ea97b196d7be098691b8bbd..43621bf73c0739edcc3d99bcf3077477e1b70d4f 100644 (file)
@@ -7,9 +7,9 @@ import { Dispatch } from "redux";
 import { SubmissionError } from "redux-form";
 
 import { RootState } from "../../store/store";
-import DialogProjectCreate from "../dialog-create/dialog-project-create";
-import actions, { createProject, getProjectList } from "../../store/project/project-action";
-import dataExplorerActions from "../../store/data-explorer/data-explorer-action";
+import  DialogProjectCreate from "../dialog-create/dialog-project-create";
+import { projectActions, createProject, getProjectList } from "../../store/project/project-action";
+import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
 import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
 
 const mapStateToProps = (state: RootState) => ({
@@ -27,7 +27,7 @@ export const addProject = (data: { name: string, description: string }) =>
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     handleClose: () => {
-        dispatch(actions.CLOSE_PROJECT_CREATOR());
+        dispatch(projectActions.CLOSE_PROJECT_CREATOR());
     },
     onSubmit: (data: { name: string, description: string }) => {
         return dispatch<any>(addProject(data))
@@ -37,4 +37,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
     }
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate);
+export const CreateProjectDialog = connect(mapStateToProps, mapDispatchToProps)(DialogProjectCreate);
index b0e189f5224251bb93467177f8f1609823d9c092..2645504c612200b27dd3a2f7b778b85ba8f9174e 100644 (file)
@@ -4,10 +4,10 @@
 
 import { connect } from "react-redux";
 import { RootState } from "../../store/store";
-import DataExplorer from "../../components/data-explorer/data-explorer";
+import { DataExplorer as DataExplorerComponent } from "../../components/data-explorer/data-explorer";
 import { getDataExplorer } from "../../store/data-explorer/data-explorer-reducer";
 import { Dispatch } from "redux";
-import actions from "../../store/data-explorer/data-explorer-action";
+import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
 import { DataColumn } from "../../components/data-table/data-column";
 import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
 
@@ -24,35 +24,35 @@ const mapStateToProps = (state: RootState, { id }: Props) =>
 
 const mapDispatchToProps = (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
     onSearch: (searchValue: string) => {
-        dispatch(actions.SET_SEARCH_VALUE({ id, searchValue }));
+        dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
     },
 
     onColumnToggle: (column: DataColumn<any>) => {
-        dispatch(actions.TOGGLE_COLUMN({ id, columnName: column.name }));
+        dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
     },
 
     onSortToggle: (column: DataColumn<any>) => {
-        dispatch(actions.TOGGLE_SORT({ id, columnName: column.name }));
+        dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
     },
 
     onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
-        dispatch(actions.SET_FILTERS({ id, columnName: column.name, filters }));
+        dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
     },
 
     onChangePage: (page: number) => {
-        dispatch(actions.SET_PAGE({ id, page }));
+        dispatch(dataExplorerActions.SET_PAGE({ id, page }));
     },
 
     onChangeRowsPerPage: (rowsPerPage: number) => {
-        dispatch(actions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+        dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
     },
 
     onRowClick,
 
     onRowDoubleClick,
-    
+
     onContextMenu,
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(DataExplorer);
+export const DataExplorer = connect(mapStateToProps, mapDispatchToProps)(DataExplorerComponent);
 
diff --git a/src/views-components/details-panel/collection-details.tsx b/src/views-components/details-panel/collection-details.tsx
new file mode 100644 (file)
index 0000000..2881776
--- /dev/null
@@ -0,0 +1,35 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { CollectionIcon } from '../../components/icon/icon';
+import { CollectionResource } from '../../models/collection';
+import { formatDate } from '../../common/formatters';
+import { resourceLabel } from '../../common/labels';
+import { ResourceKind } from '../../models/resource';
+import { DetailsData } from "./details-data";
+import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+
+export class CollectionDetails extends DetailsData<CollectionResource> {
+
+    getIcon(className?: string) {
+        return <CollectionIcon className={className} />;
+    }
+
+    getDetails() {
+        return <div>
+            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.Collection)} />
+            <DetailsAttribute label='Size' value='---' />
+            <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+            <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
+            <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
+            {/* Links but we dont have view */}
+            <DetailsAttribute label='Collection UUID' link={this.item.uuid} value={this.item.uuid} />
+            <DetailsAttribute label='Content address' link={this.item.portableDataHash} value={this.item.portableDataHash} />
+            {/* Missing attrs */}
+            <DetailsAttribute label='Number of files' value='20' />
+            <DetailsAttribute label='Content size' value='54 MB' />
+        </div>;
+    }
+}
diff --git a/src/views-components/details-panel/details-data.tsx b/src/views-components/details-panel/details-data.tsx
new file mode 100644 (file)
index 0000000..d20269c
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DetailsResource } from "../../models/details";
+
+export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
+    constructor(protected item: T) {}
+
+    getTitle(): string {
+        return this.item.name;
+    }
+
+    abstract getIcon(className?: string): React.ReactElement<any>;
+    abstract getDetails(): React.ReactElement<any>;
+
+    getActivity(): React.ReactElement<any> {
+        return <div/>;
+    }
+}
index 341957849ede81e94c98627f9eb4180553e446b4..ea8f2e40f6288fc27e56275dad668180bbda9484 100644 (file)
@@ -3,89 +3,23 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import Drawer from '@material-ui/core/Drawer';
-import IconButton from "@material-ui/core/IconButton";
+import { Drawer, IconButton, Tabs, Tab, Typography, Grid } from '@material-ui/core';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { ArvadosTheme } from '../../common/custom-theme';
-import Tabs from '@material-ui/core/Tabs';
-import Tab from '@material-ui/core/Tab';
-import Typography from '@material-ui/core/Typography';
-import Grid from '@material-ui/core/Grid';
 import * as classnames from "classnames";
-import { connect, Dispatch } from 'react-redux';
+import { connect } from 'react-redux';
 import { RootState } from '../../store/store';
-import actions from "../../store/details-panel/details-panel-action";
-import { ProjectResource } from '../../models/project';
-import { CollectionResource } from '../../models/collection';
-import IconBase, { IconTypes } from '../../components/icon/icon';
-import { ProcessResource } from '../../models/process';
-import DetailsPanelFactory from '../../components/details-panel-factory/details-panel-factory';
-import AbstractItem from '../../components/details-panel-factory/items/abstract-item';
+import { detailsPanelActions } from "../../store/details-panel/details-panel-action";
+import { CloseIcon } from '../../components/icon/icon';
 import { EmptyResource } from '../../models/empty';
-
-export interface DetailsPanelDataProps {
-    onCloseDrawer: () => void;
-    isOpened: boolean;
-    item: AbstractItem;
-}
-
-type DetailsPanelProps = DetailsPanelDataProps & WithStyles<CssRules>;
-
-class DetailsPanel extends React.Component<DetailsPanelProps, {}> {
-    state = {
-        tabsValue: 0
-    };
-
-    handleChange = (event: any, value: boolean) => {
-        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;
-        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}>
-                                <IconBase className={classes.headerIcon} icon={item.getIcon()} />
-                            </Grid>
-                            <Grid item xs={8}>
-                                <Typography variant="title">
-                                    {item.getTitle()}
-                                </Typography>
-                            </Grid>
-                            <Grid item>
-                                <IconButton color="inherit" onClick={onCloseDrawer}>
-                                    <IconBase icon={IconTypes.CLOSE} />
-                                </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.buildDetails()}
-                        </Grid>
-                    )}
-                    {tabsValue === 1 && this.renderTabContainer(
-                        <Grid container direction="column" />
-                    )}
-                </Drawer>
-            </Typography>
-        );
-    }
-
-}
+import { Dispatch } from "redux";
+import { ResourceKind } from "../../models/resource";
+import { ProjectDetails } from "./project-details";
+import { CollectionDetails } from "./collection-details";
+import { ProcessDetails } from "./process-details";
+import { EmptyDetails } from "./empty-details";
+import { DetailsData } from "./details-data";
+import { DetailsResource } from "../../models/details";
 
 type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
 
@@ -118,31 +52,95 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
-
-export type DetailsPanelResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource;
-
-const getItem = (res: DetailsPanelResource) => {
-    return DetailsPanelFactory.createItem(res);
-};
-
-const getDefaultItem = () => {
-    return DetailsPanelFactory.createItem({ kind: undefined, name: 'Projects' });
+const getItem = (resource: DetailsResource): DetailsData => {
+    const res = resource || { kind: undefined, name: 'Projects' };
+    switch (res.kind) {
+        case ResourceKind.Project:
+            return new ProjectDetails(res);
+        case ResourceKind.Collection:
+            return new CollectionDetails(res);
+        case ResourceKind.Process:
+            return new ProcessDetails(res);
+        default:
+            return new EmptyDetails(res as EmptyResource);
+    }
 };
 
-const mapStateToProps = ({ detailsPanel }: RootState) => {
-    const { isOpened, item } = detailsPanel;
-    return {
-        isOpened,
-        item: item ? getItem(item as DetailsPanelResource) : getDefaultItem()
-    };
-};
+const mapStateToProps = ({ detailsPanel }: RootState) => ({
+    isOpened: detailsPanel.isOpened,
+    item: getItem(detailsPanel.item as DetailsResource)
+});
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     onCloseDrawer: () => {
-        dispatch(actions.TOGGLE_DETAILS_PANEL());
+        dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
     }
 });
 
-const DetailsPanelContainer = connect(mapStateToProps, mapDispatchToProps)(DetailsPanel);
+export interface DetailsPanelDataProps {
+    onCloseDrawer: () => void;
+    isOpened: boolean;
+    item: DetailsData;
+}
 
-export default withStyles(styles)(DetailsPanelContainer);
\ No newline at end of file
+type DetailsPanelProps = DetailsPanelDataProps & WithStyles<CssRules>;
+
+export const DetailsPanel = withStyles(styles)(
+    connect(mapStateToProps, mapDispatchToProps)(
+        class extends React.Component<DetailsPanelProps> {
+            state = {
+                tabsValue: 0
+            };
+
+            handleChange = (event: any, value: boolean) => {
+                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;
+                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">
+                                            {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>
+                );
+            }
+        }
+    )
+);
diff --git a/src/views-components/details-panel/empty-details.tsx b/src/views-components/details-panel/empty-details.tsx
new file mode 100644 (file)
index 0000000..51112ce
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DefaultIcon, IconType, ProjectsIcon } from '../../components/icon/icon';
+import { EmptyResource } from '../../models/empty';
+import { DetailsData } from "./details-data";
+import Typography from "@material-ui/core/Typography";
+import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
+import { ArvadosTheme } from "../../common/custom-theme";
+import Icon from "@material-ui/core/Icon/Icon";
+
+type CssRules = 'container' | 'icon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    container: {
+        textAlign: 'center'
+    },
+    icon: {
+        color: theme.palette.grey["500"],
+        fontSize: '72px'
+    }
+});
+
+export interface EmptyStateDataProps {
+    message: string;
+    icon: IconType;
+    details?: string;
+    children?: React.ReactNode;
+}
+
+type EmptyStateProps = EmptyStateDataProps & WithStyles<CssRules>;
+
+const EmptyState = withStyles(styles)(
+    ({ classes, details, message, children, icon: Icon }: EmptyStateProps) =>
+        <Typography className={classes.container} component="div">
+            <Icon className={classes.icon}/>
+            <Typography variant="body1" gutterBottom>{message}</Typography>
+            {details && <Typography gutterBottom>{details}</Typography>}
+            {children && <Typography gutterBottom>{children}</Typography>}
+        </Typography>
+);
+
+export class EmptyDetails extends DetailsData<EmptyResource> {
+    getIcon(className?: string) {
+        return <ProjectsIcon className={className}/>;
+    }
+
+    getDetails() {
+       return <EmptyState icon={DefaultIcon} message='Select a file or folder to view its details.'/>;
+    }
+}
diff --git a/src/views-components/details-panel/process-details.tsx b/src/views-components/details-panel/process-details.tsx
new file mode 100644 (file)
index 0000000..931ff7e
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { ProcessIcon } from '../../components/icon/icon';
+import { ProcessResource } from '../../models/process';
+import { formatDate } from '../../common/formatters';
+import { ResourceKind } from '../../models/resource';
+import { resourceLabel } from '../../common/labels';
+import { DetailsData } from "./details-data";
+import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+
+export class ProcessDetails extends DetailsData<ProcessResource> {
+
+    getIcon(className?: string){
+        return <ProcessIcon className={className} />;
+    }
+
+    getDetails() {
+        return <div>
+            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.Process)} />
+            <DetailsAttribute label='Size' value='---' />
+            <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+
+            {/* Missing attr */}
+            <DetailsAttribute label='Status' value={this.item.state} />
+            <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
+
+            {/* Missing attrs */}
+            <DetailsAttribute label='Started at' value={formatDate(this.item.createdAt)} />
+            <DetailsAttribute label='Finished at' value={formatDate(this.item.expiresAt)} />
+
+            {/* Links but we dont have view */}
+            <DetailsAttribute label='Outputs' link={this.item.outputPath} value={this.item.outputPath} />
+            <DetailsAttribute label='UUID' link={this.item.uuid} value={this.item.uuid} />
+            <DetailsAttribute label='Container UUID' link={this.item.containerUuid} value={this.item.containerUuid} />
+
+            <DetailsAttribute label='Priority' value={this.item.priority} />
+            <DetailsAttribute label='Runtime Constraints' value={this.item.runtimeConstraints} />
+            {/* Link but we dont have view */}
+            <DetailsAttribute label='Docker Image locator' link={this.item.containerImage} value={this.item.containerImage} />
+        </div>;
+    }
+}
diff --git a/src/views-components/details-panel/project-details.tsx b/src/views-components/details-panel/project-details.tsx
new file mode 100644 (file)
index 0000000..84b3706
--- /dev/null
@@ -0,0 +1,33 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { ProjectIcon } from '../../components/icon/icon';
+import { ProjectResource } from '../../models/project';
+import { formatDate } from '../../common/formatters';
+import { ResourceKind } from '../../models/resource';
+import { resourceLabel } from '../../common/labels';
+import { DetailsData } from "./details-data";
+import { DetailsAttribute } from "../../components/details-attribute/details-attribute";
+
+export class ProjectDetails extends DetailsData<ProjectResource> {
+
+    getIcon(className?: string) {
+        return <ProjectIcon className={className} />;
+    }
+
+    getDetails() {
+        return <div>
+            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.Project)} />
+            {/* Missing attr */}
+            <DetailsAttribute label='Size' value='---' />
+            <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+            <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
+            <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
+            {/* Missing attr */}
+            <DetailsAttribute label='File size' value='1.4 GB' />
+            <DetailsAttribute label='Description' value={this.item.description} />
+        </div>;
+    }
+}
index 6fb8a699df64e01f35dc5a60a49c35063cd827ff..34c655e2b9922ff9da8c338cac14e59785795c32 100644 (file)
@@ -125,4 +125,4 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
 export default compose(
     reduxForm({ form: 'projectCreateDialog' }),
     withStyles(styles)
-)(DialogProjectCreate);
\ No newline at end of file
+)(DialogProjectCreate);
index f58b26a0f1c27c0415844f852511a868c581cc8d..6d5c9de897162288c5936d028f07dcb1dfaee88b 100644 (file)
@@ -5,10 +5,10 @@
 import * as React from "react";
 import { mount, configure, ReactWrapper } from "enzyme";
 import * as Adapter from "enzyme-adapter-react-16";
-import MainAppBar 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 { MainAppBar } 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";
 
@@ -28,6 +28,7 @@ describe("<MainAppBar />", () => {
         const mainAppBar = mount(
             <MainAppBar
                 user={user}
+                onContextMenu={jest.fn()}
                 onDetailsPanelToggle={jest.fn()}
                 onContextMenu={jest.fn()}
                 {...{ searchText: "", breadcrumbs: [], menuItems: { accountMenu: [], helpMenu: [], anonymousMenu: [] }, onSearch: jest.fn(), onBreadcrumbClick: jest.fn(), onMenuItemClick: jest.fn() }}
@@ -60,6 +61,7 @@ describe("<MainAppBar />", () => {
             <MainAppBar
                 searchText="search text"
                 searchDebounce={2000}
+                onContextMenu={jest.fn()}
                 onSearch={onSearch}
                 onContextMenu={jest.fn()}
                 onDetailsPanelToggle={jest.fn()}
@@ -79,6 +81,7 @@ describe("<MainAppBar />", () => {
         const mainAppBar = mount(
             <MainAppBar
                 breadcrumbs={items}
+                onContextMenu={jest.fn()}
                 onBreadcrumbClick={onBreadcrumbClick}
                 onContextMenu={jest.fn()}
                 onDetailsPanelToggle={jest.fn()}
@@ -97,6 +100,7 @@ describe("<MainAppBar />", () => {
         const mainAppBar = mount(
             <MainAppBar
                 menuItems={menuItems}
+                onContextMenu={jest.fn()}
                 onMenuItemClick={onMenuItemClick}
                 onContextMenu={jest.fn()}
                 onDetailsPanelToggle={jest.fn()}
index d2082395125e740e1cff707293d4e7f8d081740b..9c031080a77d3a1c392fdf78383cb501d7c2934e 100644 (file)
@@ -4,14 +4,11 @@
 
 import * as React from "react";
 import { AppBar, Toolbar, Typography, Grid, IconButton, Badge, Button, MenuItem } from "@material-ui/core";
-import NotificationsIcon from "@material-ui/icons/Notifications";
-import PersonIcon from "@material-ui/icons/Person";
-import HelpIcon from "@material-ui/icons/Help";
-import InfoIcon from '@material-ui/icons/Info';
-import SearchBar from "../../components/search-bar/search-bar";
-import Breadcrumbs, { Breadcrumb } from "../../components/breadcrumbs/breadcrumbs";
-import DropdownMenu from "../../components/dropdown-menu/dropdown-menu";
 import { User, getUserFullname } from "../../models/user";
+import { SearchBar } from "../../components/search-bar/search-bar";
+import { Breadcrumbs, Breadcrumb } from "../../components/breadcrumbs/breadcrumbs";
+import { DropdownMenu } from "../../components/dropdown-menu/dropdown-menu";
+import { DetailsIcon, NotificationIcon, UserPanelIcon, HelpIcon } from "../../components/icon/icon";
 
 export interface MainAppBarMenuItem {
     label: string;
@@ -76,29 +73,29 @@ export const MainAppBar: React.SFC<MainAppBarProps> = (props) => {
                     onClick={props.onBreadcrumbClick}
                     onContextMenu={props.onContextMenu} />
             }
-            <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
-                <InfoIcon />
-            </IconButton>
+            { props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+                    <DetailsIcon />
+                </IconButton>
+            }
         </Toolbar>
     </AppBar>;
 };
 
-
 const renderMenuForUser = ({ user, menuItems, onMenuItemClick }: MainAppBarProps) => {
     return (
         <>
             <IconButton color="inherit">
                 <Badge badgeContent={3} color="primary">
-                    <NotificationsIcon />
+                    <NotificationIcon />
                 </Badge>
             </IconButton>
-            <DropdownMenu icon={PersonIcon} id="account-menu">
+            <DropdownMenu icon={<UserPanelIcon />} id="account-menu">
                 <MenuItem>
                     {getUserFullname(user)}
                 </MenuItem>
                 {renderMenuItems(menuItems.accountMenu, onMenuItemClick)}
             </DropdownMenu>
-            <DropdownMenu icon={HelpIcon} id="help-menu">
+            <DropdownMenu icon={<HelpIcon />} id="help-menu">
                 {renderMenuItems(menuItems.helpMenu, onMenuItemClick)}
             </DropdownMenu>
         </>
@@ -120,5 +117,3 @@ const renderMenuItems = (menuItems: MainAppBarMenuItem[], onMenuItemClick: (menu
         </MenuItem>
     ));
 };
-
-export default MainAppBar;
diff --git a/src/views-components/project-list/project-list.tsx b/src/views-components/project-list/project-list.tsx
deleted file mode 100644 (file)
index 88cd0f7..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { Theme } from "@material-ui/core";
-import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
-import Paper from "@material-ui/core/Paper/Paper";
-import Table from "@material-ui/core/Table/Table";
-import TableHead from "@material-ui/core/TableHead/TableHead";
-import TableRow from "@material-ui/core/TableRow/TableRow";
-import TableCell from "@material-ui/core/TableCell/TableCell";
-import TableBody from "@material-ui/core/TableBody/TableBody";
-
-type CssRules = 'root' | 'table';
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
-    root: {
-        width: '100%',
-        marginTop: theme.spacing.unit * 3,
-        overflowX: 'auto',
-    },
-    table: {
-        minWidth: 700,
-    },
-});
-
-interface ProjectListProps {
-}
-
-class ProjectList extends React.Component<ProjectListProps & WithStyles<CssRules>, {}> {
-    render() {
-        const {classes} = this.props;
-        return <Paper className={classes.root}>
-            <Table className={classes.table}>
-                <TableHead>
-                    <TableRow>
-                        <TableCell>Name</TableCell>
-                        <TableCell>Status</TableCell>
-                        <TableCell>Type</TableCell>
-                        <TableCell>Shared by</TableCell>
-                        <TableCell>File size</TableCell>
-                        <TableCell>Last modified</TableCell>
-                    </TableRow>
-                </TableHead>
-                <TableBody>
-                    <TableRow>
-                        <TableCell>Project 1</TableCell>
-                        <TableCell>Complete</TableCell>
-                        <TableCell>Project</TableCell>
-                        <TableCell>John Doe</TableCell>
-                        <TableCell>1.5 GB</TableCell>
-                        <TableCell>9:22 PM</TableCell>
-                    </TableRow>
-                </TableBody>
-            </Table>
-        </Paper>;
-    }
-}
-
-export default withStyles(styles)(ProjectList);
index d53f8a9e5a41871c80a1eeb80e2b25dcfe2bfada..56566da617998bcd658fff147e6dfd7ee2268a89 100644 (file)
@@ -10,7 +10,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
 import { Collapse } from '@material-ui/core';
 import CircularProgress from '@material-ui/core/CircularProgress';
 
-import ProjectTree from './project-tree';
+import { ProjectTree } from './project-tree';
 import { TreeItem } from '../../components/tree/tree';
 import { ProjectResource } from '../../models/project';
 import { mockProjectResource } from '../../models/test-utils';
@@ -81,9 +81,9 @@ describe("ProjectTree component", () => {
                 ]
             }
         ];
-        const wrapper = mount(<ProjectTree 
-            projects={project} 
-            toggleOpen={jest.fn()} 
+        const wrapper = mount(<ProjectTree
+            projects={project}
+            toggleOpen={jest.fn()}
             toggleActive={jest.fn()}
             onContextMenu={jest.fn()} />);
 
@@ -98,9 +98,9 @@ describe("ProjectTree component", () => {
             active: true,
             status: 1
         };
-        const wrapper = mount(<ProjectTree 
-            projects={[project]} 
-            toggleOpen={jest.fn()} 
+        const wrapper = mount(<ProjectTree
+            projects={[project]}
+            toggleOpen={jest.fn()}
             toggleActive={jest.fn()}
             onContextMenu={jest.fn()} />);
 
index 17592a7f36d9374e8c6bef603e132d9690567944..c9d4c3e3a5e47d1a7f5b2971689b7e7203886db1 100644 (file)
@@ -5,12 +5,19 @@
 import * as React from 'react';
 import { ReactElement } from 'react';
 import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
-import ListItemText from "@material-ui/core/ListItemText/ListItemText";
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import Typography from '@material-ui/core/Typography';
-
-import Tree, { TreeItem, TreeItemStatus } from '../../components/tree/tree';
+import { Tree, TreeItem, TreeItemStatus } from '../../components/tree/tree';
 import { ProjectResource } from '../../models/project';
+import { ProjectIcon } from '../../components/icon/icon';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { ListItemTextIcon } from '../../components/list-item-text-icon/list-item-text-icon';
+
+type CssRules = 'root';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        marginLeft: `${theme.spacing.unit * 1.5}px`,
+    }
+});
 
 export interface ProjectTreeProps {
     projects: Array<TreeItem<ProjectResource>>;
@@ -19,50 +26,26 @@ export interface ProjectTreeProps {
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectResource>) => void;
 }
 
-class ProjectTree<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
-    render(): ReactElement<any> {
-        const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props;
-        const { active, listItemText, row, treeContainer } = classes;
-        return (
-            <div className={treeContainer}>
-                <Tree items={projects}
-                    onContextMenu={onContextMenu}
-                    toggleItemOpen={toggleOpen}
-                    toggleItemActive={toggleActive}
-                    render={(project: TreeItem<ProjectResource>) =>
-                        <span className={row}>
-                            <ListItemIcon className={project.active ? active : ''}>
-                                <i className="fas fa-folder" />
-                            </ListItemIcon>
-                            <ListItemText className={listItemText} primary={
-                                <Typography className={project.active ? active : ''}>{project.data.name}</Typography>
-                            } />
-                        </span>
-                    } />
-            </div>
-        );
+export const ProjectTree = withStyles(styles)(
+    class ProjectTreeGeneric<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
+        render(): ReactElement<any> {
+            const { classes, projects, toggleOpen, toggleActive, onContextMenu } = this.props;
+            return (
+                <div className={classes.root}>
+                    <Tree items={projects}
+                        onContextMenu={onContextMenu}
+                        toggleItemOpen={toggleOpen}
+                        toggleItemActive={toggleActive}
+                        render={
+                            (project: TreeItem<ProjectResource>) =>
+                                <ListItemTextIcon
+                                    icon={ProjectIcon}
+                                    name={project.data.name}
+                                    isActive={project.active}
+                                    hasMargin={true}/>
+                        }/>
+                </div>
+            );
+        }
     }
-}
-
-type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer';
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
-    active: {
-        color: '#4285F6',
-    },
-    listItemText: {
-        padding: '0px',
-    },
-    row: {
-        display: 'flex',
-        alignItems: 'center',
-        marginLeft: '20px',
-    },
-    treeContainer: {
-        minWidth: '240px',
-        whiteSpace: 'nowrap',
-        marginLeft: '13px',
-    }
-});
-
-export default withStyles(styles)(ProjectTree);
+);
index e34ea1ecda6c72c6bc124ab4113376733d288872..daf22b11faca3b02e87441a625cf50f513f84191 100644 (file)
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { ProjectPanelItem } from './project-panel-item';
 import { Grid, Typography, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { formatDate, formatFileSize } from '../../common/formatters';
-import DataExplorer from "../../views-components/data-explorer/data-explorer";
+import { DataExplorer } from "../../views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
 import { DataColumns } from '../../components/data-table/data-table';
 import { RouteComponentProps } from 'react-router';
@@ -16,58 +16,12 @@ import { ContainerRequestState } from '../../models/container-request';
 import { SortDirection } from '../../components/data-table/data-column';
 import { ResourceKind } from '../../models/resource';
 import { resourceLabel } from '../../common/labels';
-
-export const PROJECT_PANEL_ID = "projectPanel";
-
-export interface ProjectPanelFilter extends DataTableFilterItem {
-    type: ResourceKind | ContainerRequestState;
-}
-
-type ProjectPanelProps = {
-    currentItemId: string,
-    onItemClick: (item: ProjectPanelItem) => void,
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
-    onDialogOpen: (ownerUuid: string) => void;
-    onItemDoubleClick: (item: ProjectPanelItem) => void,
-    onItemRouteChange: (itemId: string) => void
-}
-    & DispatchProp
-    & WithStyles<CssRules>
-    & RouteComponentProps<{ id: string }>;
-
-class ProjectPanel extends React.Component<ProjectPanelProps> {
-    render() {
-        return <div>
-            <div className={this.props.classes.toolbar}>
-                <Button color="primary" variant="raised" className={this.props.classes.button}>
-                    Create a collection
-                </Button>
-                <Button color="primary" variant="raised" className={this.props.classes.button}>
-                    Run a process
-                </Button>
-                <Button color="primary" onClick={() => this.props.onDialogOpen(this.props.currentItemId)} variant="raised" className={this.props.classes.button}>
-                    New project
-                </Button>
-            </div>
-            <DataExplorer
-                id={PROJECT_PANEL_ID}
-                onRowClick={this.props.onItemClick}
-                onRowDoubleClick={this.props.onItemDoubleClick}
-                onContextMenu={this.props.onContextMenu}
-                extractKey={(item: ProjectPanelItem) => item.uuid} />
-        </div>;
-    }
-
-    componentWillReceiveProps({ match, currentItemId }: ProjectPanelProps) {
-        if (match.params.id !== currentItemId) {
-            this.props.onItemRouteChange(match.params.id);
-        }
-    }
-}
+import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
+import { ArvadosTheme } from '../../common/custom-theme';
 
 type CssRules = "toolbar" | "button";
 
-const styles: StyleRulesCallback<CssRules> = theme => ({
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
         paddingBottom: theme.spacing.unit * 3,
         textAlign: "right"
@@ -78,11 +32,7 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
 });
 
 const renderName = (item: ProjectPanelItem) =>
-    <Grid
-        container
-        alignItems="center"
-        wrap="nowrap"
-        spacing={16}>
+    <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
             {renderIcon(item)}
         </Grid>
@@ -97,20 +47,19 @@ const renderName = (item: ProjectPanelItem) =>
 const renderIcon = (item: ProjectPanelItem) => {
     switch (item.kind) {
         case ResourceKind.Project:
-            return <i className="fas fa-folder fa-lg" />;
+            return <ProjectIcon />;
         case ResourceKind.Collection:
-            return <i className="fas fa-archive fa-lg" />;
+            return <CollectionIcon />;
         case ResourceKind.Process:
-            return <i className="fas fa-cogs fa-lg" />;
+            return <ProcessIcon />;
         default:
-            return <i />;
+            return <DefaultIcon />;
     }
 };
 
-const renderDate = (date: string) =>
-    <Typography noWrap>
-        {formatDate(date)}
-    </Typography>;
+const renderDate = (date: string) => {
+    return <Typography noWrap>{formatDate(date)}</Typography>;
+};
 
 const renderFileSize = (fileSize?: number) =>
     <Typography noWrap>
@@ -118,92 +67,159 @@ const renderFileSize = (fileSize?: number) =>
     </Typography>;
 
 const renderOwner = (owner: string) =>
-    <Typography noWrap color="primary">
+    <Typography noWrap color="primary" >
         {owner}
     </Typography>;
 
-const renderType = (type: string) => {
-    return <Typography noWrap>
+const renderType = (type: string) =>
+    <Typography noWrap>
         {resourceLabel(type)}
     </Typography>;
-};
 
 const renderStatus = (item: ProjectPanelItem) =>
-    <Typography noWrap align="center">
+    <Typography noWrap align="center" >
         {item.status || "-"}
     </Typography>;
 
 export enum ProjectPanelColumnNames {
-    Name = "Name",
-    Status = "Status",
-    Type = "Type",
-    Owner = "Owner",
-    FileSize = "File size",
-    LastModified = "Last modified"
+    NAME = "Name",
+    STATUS = "Status",
+    TYPE = "Type",
+    OWNER = "Owner",
+    FILE_SIZE = "File size",
+    LAST_MODIFIED = "Last modified"
+}
 
+export interface ProjectPanelFilter extends DataTableFilterItem {
+    type: ResourceKind | ContainerRequestState;
 }
 
-export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [{
-    name: ProjectPanelColumnNames.Name,
-    selected: true,
-    sortDirection: SortDirection.Asc,
-    render: renderName,
-    width: "450px"
-}, {
-    name: "Status",
-    selected: true,
-    filters: [{
-        name: ContainerRequestState.Committed,
+export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
+    {
+        name: ProjectPanelColumnNames.NAME,
         selected: true,
-        type: ContainerRequestState.Committed
-    }, {
-        name: ContainerRequestState.Final,
+        sortDirection: SortDirection.Asc,
+        render: renderName,
+        width: "450px"
+    },
+    {
+        name: "Status",
         selected: true,
-        type: ContainerRequestState.Final
-    }, {
-        name: ContainerRequestState.Uncommitted,
+        filters: [
+            {
+                name: ContainerRequestState.Committed,
+                selected: true,
+                type: ContainerRequestState.Committed
+            },
+            {
+                name: ContainerRequestState.Final,
+                selected: true,
+                type: ContainerRequestState.Final
+            },
+            {
+                name: ContainerRequestState.Uncommitted,
+                selected: true,
+                type: ContainerRequestState.Uncommitted
+            }
+        ],
+        render: renderStatus,
+        width: "75px"
+    },
+    {
+        name: ProjectPanelColumnNames.TYPE,
         selected: true,
-        type: ContainerRequestState.Uncommitted
-    }],
-    render: renderStatus,
-    width: "75px"
-}, {
-    name: ProjectPanelColumnNames.Type,
-    selected: true,
-    filters: [{
-        name: resourceLabel(ResourceKind.Collection),
+        filters: [
+            {
+                name: resourceLabel(ResourceKind.Collection),
+                selected: true,
+                type: ResourceKind.Collection
+            },
+            {
+                name: resourceLabel(ResourceKind.Process),
+                selected: true,
+                type: ResourceKind.Process
+            },
+            {
+                name: resourceLabel(ResourceKind.Project),
+                selected: true,
+                type: ResourceKind.Project
+            }
+        ],
+        render: item => renderType(item.kind),
+        width: "125px"
+    },
+    {
+        name: ProjectPanelColumnNames.OWNER,
         selected: true,
-        type: ResourceKind.Collection
-    }, {
-        name: resourceLabel(ResourceKind.Process),
+        render: item => renderOwner(item.owner),
+        width: "200px"
+    },
+    {
+        name: ProjectPanelColumnNames.FILE_SIZE,
         selected: true,
-        type: ResourceKind.Process
-    }, {
-        name: resourceLabel(ResourceKind.Project),
+        render: item => renderFileSize(item.fileSize),
+        width: "50px"
+    },
+    {
+        name: ProjectPanelColumnNames.LAST_MODIFIED,
         selected: true,
-        type: ResourceKind.Project
-    }],
-    render: item => renderType(item.kind),
-    width: "125px"
-}, {
-    name: ProjectPanelColumnNames.Owner,
-    selected: true,
-    render: item => renderOwner(item.owner),
-    width: "200px"
-}, {
-    name: ProjectPanelColumnNames.FileSize,
-    selected: true,
-    render: item => renderFileSize(item.fileSize),
-    width: "50px"
-}, {
-    name: ProjectPanelColumnNames.LastModified,
-    selected: true,
-    sortDirection: SortDirection.None,
-    render: item => renderDate(item.lastModified),
-    width: "150px"
-}];
-
-
-export default withStyles(styles)(
+        sortDirection: SortDirection.None,
+        render: item => renderDate(item.lastModified),
+        width: "150px"
+    }
+];
+
+export const PROJECT_PANEL_ID = "projectPanel";
+
+interface ProjectPanelDataProps {
+    currentItemId: string;
+}
+
+interface ProjectPanelActionProps {
+    onItemClick: (item: ProjectPanelItem) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
+    onDialogOpen: (ownerUuid: string) => void;
+    onItemDoubleClick: (item: ProjectPanelItem) => void;
+    onItemRouteChange: (itemId: string) => void;
+}
+
+type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp
+                        & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+
+export const ProjectPanel = withStyles(styles)(
     connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
-        ProjectPanel));
+        class extends React.Component<ProjectPanelProps> {
+            render() {
+                const { classes } = this.props;
+                return <div>
+                    <div className={classes.toolbar}>
+                        <Button color="primary" variant="raised" className={classes.button}>
+                            Create a 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>
+                    <DataExplorer
+                        id={PROJECT_PANEL_ID}
+                        onRowClick={this.props.onItemClick}
+                        onRowDoubleClick={this.props.onItemDoubleClick}
+                        onContextMenu={this.props.onContextMenu}
+                        extractKey={(item: ProjectPanelItem) => item.uuid} />
+                </div>;
+            }
+
+            handleNewProjectClick = () => {
+                this.props.onDialogOpen(this.props.currentItemId);
+            }
+            componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
+                if (match.params.id !== currentItemId) {
+                    onItemRouteChange(match.params.id);
+                }
+            }
+        }
+    )
+);
\ No newline at end of file
index 79a98ad60b172191a509bff635b3079eb7fc1ddb..538b8e780f352489a4c0e15e6bf5493e2f0ee96d 100644 (file)
@@ -4,9 +4,9 @@
 
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
-import Workbench from '../../views/workbench/workbench';
+import { Workbench } from '../../views/workbench/workbench';
 import { Provider } from "react-redux";
-import configureStore from "../../store/store";
+import { configureStore } from "../../store/store";
 import createBrowserHistory from "history/createBrowserHistory";
 import { ConnectedRouter } from "react-router-redux";
 import { MuiThemeProvider } from '@material-ui/core/styles';
index b2bdac802f7fd5bc1d173fcd1085f41db04fe2e7..b1e7cd78659efe4cfa88239adaa591d7cda4813b 100644 (file)
@@ -7,34 +7,31 @@ import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/st
 import Drawer from '@material-ui/core/Drawer';
 import { connect, DispatchProp } from "react-redux";
 import { Route, Switch, RouteComponentProps } from "react-router";
-import authActions from "../../store/auth/auth-action";
+import { authActions } 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, MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
 import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
 import { push } from 'react-router-redux';
-import ProjectTree from '../../views-components/project-tree/project-tree';
+import { ProjectTree } from '../../views-components/project-tree/project-tree';
 import { TreeItem } from "../../components/tree/tree";
 import { getTreePath } from '../../store/project/project-reducer';
-import sidePanelActions from '../../store/side-panel/side-panel-action';
-import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
+import { sidePanelActions } from '../../store/side-panel/side-panel-action';
+import { SidePanel, SidePanelItem } from '../../components/side-panel/side-panel';
 import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
-import projectActions from "../../store/project/project-action";
-import ProjectPanel from "../project-panel/project-panel";
-import DetailsPanel from '../../views-components/details-panel/details-panel';
+import { projectActions } from "../../store/project/project-action";
+import { ProjectPanel } from "../project-panel/project-panel";
+import { DetailsPanel } from '../../views-components/details-panel/details-panel';
 import { ArvadosTheme } from '../../common/custom-theme';
-import ContextMenu, { ContextMenuAction } from '../../components/context-menu/context-menu';
-import { mockAnchorFromMouseEvent } from '../../components/popover/helpers';
-import DialogProjectCreate from "../../views-components/create-project-dialog/create-project-dialog";
+import { CreateProjectDialog } from "../../views-components/create-project-dialog/create-project-dialog";
 import { authService } from '../../services/services';
 
-import detailsPanelActions, { loadDetails } from "../../store/details-panel/details-panel-action";
+import { detailsPanelActions, loadDetails } from "../../store/details-panel/details-panel-action";
+import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
 import { SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer';
 import { ProjectResource } from '../../models/project';
 import { ResourceKind } from '../../models/resource';
+import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
 
 const drawerWidth = 240;
 const appBarHeight = 100;
@@ -98,10 +95,6 @@ interface NavMenuItem extends MainAppBarMenuItem {
 }
 
 interface WorkbenchState {
-    contextMenu: {
-        anchorEl?: HTMLElement;
-        itemUuid?: string;
-    };
     anchorEl: any;
     searchText: string;
     menuItems: {
@@ -111,207 +104,157 @@ interface WorkbenchState {
     };
 }
 
-
-class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
-    state = {
-        contextMenu: {
-            anchorEl: undefined,
-            itemUuid: undefined
-        },
-        isCreationDialogOpen: false,
-        anchorEl: null,
-        searchText: "",
-        breadcrumbs: [],
-        menuItems: {
-            accountMenu: [
-                {
-                    label: "Logout",
-                    action: () => this.props.dispatch(authActions.LOGOUT())
-                },
-                {
-                    label: "My account",
-                    action: () => this.props.dispatch(push("/my-account"))
-                }
-            ],
-            helpMenu: [
-                {
-                    label: "Help",
-                    action: () => this.props.dispatch(push("/help"))
+export const Workbench = withStyles(styles)(
+    connect<WorkbenchDataProps>(
+        (state: RootState) => ({
+            projects: state.projects.items,
+            currentProjectId: state.projects.currentItemId,
+            user: state.auth.user,
+            sidePanelItems: state.sidePanel
+        })
+    )(
+        class extends React.Component<WorkbenchProps, WorkbenchState> {
+            state = {
+                isCreationDialogOpen: false,
+                anchorEl: null,
+                searchText: "",
+                breadcrumbs: [],
+                menuItems: {
+                    accountMenu: [
+                        {
+                            label: "Logout",
+                            action: () => this.props.dispatch(authActions.LOGOUT())
+                        },
+                        {
+                            label: "My account",
+                            action: () => this.props.dispatch(push("/my-account"))
+                        }
+                    ],
+                    helpMenu: [
+                        {
+                            label: "Help",
+                            action: () => this.props.dispatch(push("/help"))
+                        }
+                    ],
+                    anonymousMenu: [
+                        {
+                            label: "Sign in",
+                            action: () => this.props.dispatch(authActions.LOGIN())
+                        }
+                    ]
                 }
-            ],
-            anonymousMenu: [
-                {
-                    label: "Sign in",
-                    action: () => this.props.dispatch(authActions.LOGIN())
-                }
-            ]
-        }
-    };
+            };
 
-    mainAppBarActions: MainAppBarActionProps = {
-        onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
-            this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
-            this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
-        },
-        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());
-        },
-        onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
-            this.openContextMenu(event, breadcrumb.itemId);
-        }
-    };
+            render() {
+                const path = getTreePath(this.props.projects, this.props.currentProjectId);
+                const breadcrumbs = path.map(item => ({
+                    label: item.data.name,
+                    itemId: item.data.uuid,
+                    status: item.status
+                }));
 
-    toggleSidePanelOpen = (itemId: string) => {
-        this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
-    }
+                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}
+                                {...this.mainAppBarActions} />
+                        </div>
+                        {user &&
+                            <Drawer
+                                variant="permanent"
+                                classes={{
+                                    paper: classes.drawerPaper,
+                                }}>
+                                <div className={classes.toolbar} />
+                                <SidePanel
+                                    toggleOpen={this.toggleSidePanelOpen}
+                                    toggleActive={this.toggleSidePanelActive}
+                                    sidePanelItems={this.props.sidePanelItems}
+                                    onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "", ContextMenuKind.RootProject)}>
+                                    <ProjectTree
+                                        projects={this.props.projects}
+                                        toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
+                                        onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid, ContextMenuKind.Project)}
+                                        toggleActive={itemId => {
+                                            this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
+                                            this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
+                                            this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.Projects));
+                                        }} />
+                                </SidePanel>
+                            </Drawer>}
+                        <main className={classes.contentWrapper}>
+                            <div className={classes.content}>
+                                <Switch>
+                                    <Route path="/projects/:id" render={this.renderProjectPanel} />
+                                </Switch>
+                            </div>
+                            {user && <DetailsPanel />}
+                        </main>
+                        <ContextMenu />
+                        <CreateProjectDialog />
+                    </div>
+                );
+            }
 
-    toggleSidePanelActive = (itemId: string) => {
-        this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
-        this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
-        this.props.dispatch(push("/"));
-    }
+            renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
+                onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
+                onContextMenu={(event, item) => this.openContextMenu(event, item.uuid, ContextMenuKind.Project)}
+                onDialogOpen={this.handleCreationDialogOpen}
+                onItemClick={item => {
+                    this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+                }}
+                onItemDoubleClick={item => {
+                    this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
+                    this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.Project));
+                }}
+                {...props} />
 
-    handleCreationDialogOpen = (itemUuid: string) => {
-        this.closeContextMenu();
-        this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
-    }
+            mainAppBarActions: MainAppBarActionProps = {
+                onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
+                    this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
+                    this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
+                },
+                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());
+                },
+                onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
+                    this.openContextMenu(event, breadcrumb.itemId, ContextMenuKind.Project);
+                }
+            };
 
+            toggleSidePanelOpen = (itemId: string) => {
+                this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
+            }
 
-    openContextMenu = (event: React.MouseEvent<HTMLElement>, itemUuid: string) => {
-        event.preventDefault();
-        this.setState({
-            contextMenu: {
-                anchorEl: mockAnchorFromMouseEvent(event),
-                itemUuid
+            toggleSidePanelActive = (itemId: string) => {
+                this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
+                this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
+                this.props.dispatch(push("/"));
             }
-        });
-    }
 
-    closeContextMenu = () => {
-        this.setState({ contextMenu: {} });
-    }
+            handleCreationDialogOpen = (itemUuid: string) => {
+                this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
+            }
 
-    openCreateDialog = (item: ContextMenuAction) => {
-        const { itemUuid } = this.state.contextMenu;
-        if (item.openCreateDialog && itemUuid) {
-            this.handleCreationDialogOpen(itemUuid);
+            openContextMenu = (event: React.MouseEvent<HTMLElement>, itemUuid: string, kind: ContextMenuKind) => {
+                event.preventDefault();
+                this.props.dispatch(
+                    contextMenuActions.OPEN_CONTEXT_MENU({
+                        position: { x: event.clientX, y: event.clientY },
+                        resource: { uuid: itemUuid, kind }
+                    })
+                );
+            }
         }
-    }
-
-    render() {
-        const path = getTreePath(this.props.projects, this.props.currentProjectId);
-        const breadcrumbs = path.map(item => ({
-            label: item.data.name,
-            itemId: item.data.uuid,
-            status: item.status
-        }));
-
-        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}
-                        {...this.mainAppBarActions} />
-                </div>
-                {user &&
-                    <Drawer
-                        variant="permanent"
-                        classes={{
-                            paper: classes.drawerPaper,
-                        }}>
-                        <div className={classes.toolbar} />
-                        <SidePanel
-                            toggleOpen={this.toggleSidePanelOpen}
-                            toggleActive={this.toggleSidePanelActive}
-                            sidePanelItems={this.props.sidePanelItems}
-                            onContextMenu={(event) => this.openContextMenu(event, authService.getUuid() || "")}>
-                            <ProjectTree
-                                projects={this.props.projects}
-                                toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
-                                onContextMenu={(event, item) => this.openContextMenu(event, item.data.uuid)}
-                                toggleActive={itemId => {
-                                    this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE));
-                                    this.props.dispatch<any>(loadDetails(itemId, ResourceKind.Project));
-                                    this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.Projects));
-                                }} />
-                        </SidePanel>
-                    </Drawer>}
-                <main className={classes.contentWrapper}>
-                    <div className={classes.content}>
-                        <Switch>
-                            <Route path="/projects/:id" render={this.renderProjectPanel} />
-                        </Switch>
-                    </div>
-                    <DetailsPanel />
-                </main>
-                <ContextMenu
-                    anchorEl={this.state.contextMenu.anchorEl}
-                    actions={contextMenuActions}
-                    onActionClick={this.openCreateDialog}
-                    onClose={this.closeContextMenu} />
-                <DialogProjectCreate />
-            </div>
-        );
-    }
-
-    renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
-        onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
-        onContextMenu={(event, item) => this.openContextMenu(event, item.uuid)}
-        onDialogOpen={this.handleCreationDialogOpen}
-        onItemClick={item => {
-            this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
-        }}
-        onItemDoubleClick={item => {
-            this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
-            this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.Project));
-        }}
-        {...props} />
-}
-
-const contextMenuActions = [[{
-    icon: "fas fa-plus fa-fw",
-    name: "New project",
-    openCreateDialog: true
-}, {
-    icon: "fas fa-users fa-fw",
-    name: "Share"
-}, {
-    icon: "fas fa-sign-out-alt fa-fw",
-    name: "Move to"
-}, {
-    icon: "fas fa-star fa-fw",
-    name: "Add to favourite"
-}, {
-    icon: "fas fa-edit fa-fw",
-    name: "Rename"
-}, {
-    icon: "fas fa-copy fa-fw",
-    name: "Make a copy"
-}, {
-    icon: "fas fa-download fa-fw",
-    name: "Download"
-}], [{
-    icon: "fas fa-trash-alt fa-fw",
-    name: "Remove"
-}
-]];
-
-export default connect<WorkbenchDataProps>(
-    (state: RootState) => ({
-        projects: state.projects.items,
-        currentProjectId: state.projects.currentItemId,
-        user: state.auth.user,
-        sidePanelItems: state.sidePanel
-    })
-)(
-    withStyles(styles)(Workbench)
+    )
 );
index 0ddfc627b98e0fa2a637b4a0779d4e1101f6d9c7..85b43690d37e54ae7d2d4c3dd1f80dffb527abd9 100644 (file)
@@ -19,7 +19,8 @@
   "linterOptions": {
     "exclude": [
       "config/**/*.js",
-      "node_modules/**/*.ts"
+      "node_modules/**/*.ts",
+      "coverage/lcov-report/*.js"
     ]
   }
 }
index 0034169d75448de4aa4c61606da529be798bb14c..3557ebefe81e4c1e197f30077f9e8c6892ce28a9 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -3,29 +3,29 @@
 
 
 "@babel/code-frame@^7.0.0-beta.35":
-  version "7.0.0-beta.51"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.51.tgz#bd71d9b192af978df915829d39d4094456439a0c"
+  version "7.0.0-beta.54"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.54.tgz#0024f96fdf7028a21d68e273afd4e953214a1ead"
   dependencies:
-    "@babel/highlight" "7.0.0-beta.51"
+    "@babel/highlight" "7.0.0-beta.54"
 
-"@babel/highlight@7.0.0-beta.51":
-  version "7.0.0-beta.51"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.51.tgz#e8844ae25a1595ccfd42b89623b4376ca06d225d"
+"@babel/highlight@7.0.0-beta.54":
+  version "7.0.0-beta.54"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.54.tgz#155d507358329b8e7068970017c3fd74a9b08584"
   dependencies:
     chalk "^2.0.0"
     esutils "^2.0.2"
     js-tokens "^3.0.0"
 
 "@babel/runtime@^7.0.0-beta.42":
-  version "7.0.0-beta.51"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.51.tgz#48b8ed18307034c6620f643514650ca2ccc0165a"
+  version "7.0.0-beta.54"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf"
   dependencies:
     core-js "^2.5.7"
-    regenerator-runtime "^0.11.1"
+    regenerator-runtime "^0.12.0"
 
-"@material-ui/core@1.2.1":
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.2.1.tgz#f8c73da10b875762b37be7167ec2ac79b027499f"
+"@material-ui/core@1.4.0":
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.0.tgz#e535fef84576b096c46e1fb7d6c4c61895155fd3"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.42"
     "@types/jss" "^9.5.3"
@@ -37,6 +37,7 @@
     deepmerge "^2.0.1"
     dom-helpers "^3.2.1"
     hoist-non-react-statics "^2.5.0"
+    is-plain-object "^2.0.4"
     jss "^9.3.3"
     jss-camel-case "^6.0.0"
     jss-default-unit "^8.0.2"
     jss-vendor-prefixer "^7.0.0"
     keycode "^2.1.9"
     normalize-scroll-left "^0.1.2"
+    popper.js "^1.0.0"
     prop-types "^15.6.0"
     react-event-listener "^0.6.0"
     react-jss "^8.1.0"
-    react-popper "^0.10.0"
     react-transition-group "^2.2.1"
-    recompose "^0.26.0 || ^0.27.0"
+    recompose "^0.27.0"
     scroll "^2.0.3"
     warning "^4.0.1"
 
     recompose "^0.26.0 || ^0.27.0"
 
 "@types/cheerio@*":
-  version "0.22.7"
-  resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.7.tgz#4a92eafedfb2b9f4437d3a4410006d81114c66ce"
+  version "0.22.8"
+  resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.8.tgz#5702f74f78b73e13f1eb1bd435c2c9de61a250d4"
 
 "@types/classnames@^2.2.4":
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.4.tgz#d3ee9ebf714aa34006707b8f4a58fd46b642305a"
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.5.tgz#62945b24b48dc02fb32e89252bde3daf942c4235"
 
 "@types/enzyme-adapter-react-16@1.0.2":
   version "1.0.2"
@@ -75,9 +76,9 @@
   dependencies:
     "@types/enzyme" "*"
 
-"@types/enzyme@*", "@types/enzyme@3.1.10":
-  version "3.1.10"
-  resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.10.tgz#28108a9864e65699751469551a803a35d2e26160"
+"@types/enzyme@*", "@types/enzyme@3.1.12":
+  version "3.1.12"
+  resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c"
   dependencies:
     "@types/cheerio" "*"
     "@types/react" "*"
@@ -86,9 +87,9 @@
   version "4.6.2"
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
 
-"@types/jest@23.1.0":
-  version "23.1.0"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.1.0.tgz#8054dd838ba23dc331794d26456b86c7e50bf0f6"
+"@types/jest@23.3.0":
+  version "23.3.0"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.0.tgz#5dd70033b616a6228042244ebd992f6426808810"
 
 "@types/jss@^9.5.3":
   version "9.5.3"
     csstype "^2.0.0"
     indefinite-observable "^1.0.1"
 
-"@types/lodash@4.14.109":
-  version "4.14.109"
-  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.109.tgz#b1c4442239730bf35cabaf493c772b18c045886d"
+"@types/lodash@4.14.112":
+  version "4.14.112"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.112.tgz#4a8d8e5716b97a1ac01fe1931ad1e4cba719de5a"
 
-"@types/node@*", "@types/node@10.3.3":
-  version "10.3.3"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.3.tgz#8798d9e39af2fa604f715ee6a6b19796528e46c3"
+"@types/node@*", "@types/node@10.5.2":
+  version "10.5.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
 
 "@types/react-dom@16.0.6":
   version "16.0.6"
     "@types/node" "*"
     "@types/react" "*"
 
-"@types/react-redux@6.0.2":
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.2.tgz#10069b53db8e0920fd8656e068dcf10c53c9ad2a"
+"@types/react-redux@6.0.4":
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.4.tgz#c1cfce0a0bd88983c75dbf393576f8dc59181586"
   dependencies:
     "@types/react" "*"
     redux "^4.0.0"
     "@types/react-router" "*"
     redux ">= 3.7.2"
 
-"@types/react-router@*", "@types/react-router@4.0.26":
-  version "4.0.26"
-  resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.26.tgz#4489c873642baa633014294a6d0a290001ba9860"
+"@types/react-router@*", "@types/react-router@4.0.29":
+  version "4.0.29"
+  resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.29.tgz#1a906dd99abf21297a5b7cf003d1fd36e7a92069"
   dependencies:
     "@types/history" "*"
     "@types/react" "*"
   dependencies:
     "@types/react" "*"
 
-"@types/react@*", "@types/react@16.3":
-  version "16.3.18"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.18.tgz#bf195aed4d77dc86f06e4c9bb760214a3b822b8d"
+"@types/react@*", "@types/react@16.4":
+  version "16.4.6"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.6.tgz#5024957c6bcef4f02823accf5974faba2e54fada"
   dependencies:
     csstype "^2.2.0"
 
@@ -226,8 +227,8 @@ ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5:
     json-schema-traverse "^0.3.0"
 
 ajv@^6.1.0:
-  version "6.5.1"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.1.tgz#88ebc1263c7133937d108b80c5572e64e1d9322d"
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz#678495f9b82f7cca6be248dd92f59bff5e1f4360"
   dependencies:
     fast-deep-equal "^2.0.1"
     fast-json-stable-stringify "^2.0.0"
@@ -659,8 +660,8 @@ babel-jest@^22.1.0, babel-jest@^22.4.4:
     babel-preset-jest "^22.4.4"
 
 babel-loader@^7.1.2:
-  version "7.1.4"
-  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015"
+  version "7.1.5"
+  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68"
   dependencies:
     find-cache-dir "^1.0.0"
     loader-utils "^1.0.2"
@@ -1040,8 +1041,8 @@ babel-preset-jest@^22.0.1, babel-preset-jest@^22.4.4:
     babel-plugin-syntax-object-rest-spread "^6.13.0"
 
 babel-preset-react-app@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-3.1.1.tgz#d3f06a79742f0e89d7afcb72e282d9809c850920"
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-3.1.2.tgz#49ba3681b917c4e5c73a5249d3ef4c48fae064e2"
   dependencies:
     babel-plugin-dynamic-import-node "1.1.0"
     babel-plugin-syntax-dynamic-import "6.18.0"
@@ -1153,8 +1154,8 @@ batch@0.6.1:
   resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
 
 bcrypt-pbkdf@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
   dependencies:
     tweetnacl "^0.14.3"
 
@@ -1259,8 +1260,8 @@ browser-process-hrtime@^0.1.2:
   resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz#425d68a58d3447f02a04aa894187fce8af8b7b8e"
 
 browser-resolve@^1.11.2:
-  version "1.11.2"
-  resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce"
+  version "1.11.3"
+  resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
   dependencies:
     resolve "1.1.7"
 
@@ -1284,12 +1285,13 @@ browserify-cipher@^1.0.0:
     evp_bytestokey "^1.0.0"
 
 browserify-des@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c"
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
   dependencies:
     cipher-base "^1.0.1"
     des.js "^1.0.0"
     inherits "^2.0.1"
+    safe-buffer "^5.1.2"
 
 browserify-rsa@^4.0.0:
   version "4.0.1"
@@ -1444,12 +1446,12 @@ caniuse-api@^1.5.2:
     lodash.uniq "^4.5.0"
 
 caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
-  version "1.0.30000856"
-  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000856.tgz#fbebb99abe15a5654fc7747ebb5315bdfde3358f"
+  version "1.0.30000867"
+  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000867.tgz#b55a6ecfac3107988940c9c7dfe1866315312c97"
 
 caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000792:
-  version "1.0.30000856"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000856.tgz#ecc16978135a6f219b138991eb62009d25ee8daa"
+  version "1.0.30000865"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz#70026616e8afe6e1442f8bb4e1092987d81a2f25"
 
 capture-exit@^1.2.0:
   version "1.2.0"
@@ -1529,8 +1531,8 @@ chokidar@^1.6.0, chokidar@^1.7.0:
     fsevents "^1.0.0"
 
 chokidar@^2.0.2:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176"
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
   dependencies:
     anymatch "^2.0.0"
     async-each "^1.0.0"
@@ -1539,12 +1541,13 @@ chokidar@^2.0.2:
     inherits "^2.0.1"
     is-binary-path "^1.0.0"
     is-glob "^4.0.0"
+    lodash.debounce "^4.0.8"
     normalize-path "^2.1.1"
     path-is-absolute "^1.0.0"
     readdirp "^2.0.0"
-    upath "^1.0.0"
+    upath "^1.0.5"
   optionalDependencies:
-    fsevents "^1.1.2"
+    fsevents "^1.2.2"
 
 chownr@^1.0.1:
   version "1.0.1"
@@ -1699,9 +1702,9 @@ combined-stream@1.0.6, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@2.15.x, commander@^2.12.1, commander@~2.15.0:
-  version "2.15.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+commander@2.16.x, commander@^2.12.1, commander@~2.16.0:
+  version "2.16.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
 
 commander@~2.13.0:
   version "2.13.0"
@@ -1719,22 +1722,22 @@ component-emitter@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
 
-compressible@~2.0.13:
+compressible@~2.0.14:
   version "2.0.14"
   resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.14.tgz#326c5f507fbb055f54116782b969a81b67a29da7"
   dependencies:
     mime-db ">= 1.34.0 < 2"
 
 compression@^1.5.2:
-  version "1.7.2"
-  resolved "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69"
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db"
   dependencies:
-    accepts "~1.3.4"
+    accepts "~1.3.5"
     bytes "3.0.0"
-    compressible "~2.0.13"
+    compressible "~2.0.14"
     debug "2.6.9"
     on-headers "~1.0.1"
-    safe-buffer "5.1.1"
+    safe-buffer "5.1.2"
     vary "~1.1.2"
 
 concat-map@0.0.1:
@@ -2015,8 +2018,8 @@ csso@~2.3.1:
     source-map "^0.5.3"
 
 cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b"
+  version "0.3.4"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"
 
 "cssstyle@>= 0.3.1 < 0.4.0":
   version "0.3.1"
@@ -2024,11 +2027,7 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
   dependencies:
     cssom "0.3.x"
 
-csstype@^2.0.0, csstype@^2.5.2:
-  version "2.5.3"
-  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7"
-
-csstype@^2.2.0:
+csstype@^2.0.0, csstype@^2.2.0, csstype@^2.5.2:
   version "2.5.5"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.5.tgz#4125484a3d42189a863943f23b9e4b80fedfa106"
 
@@ -2369,8 +2368,8 @@ ee-first@1.1.1:
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
 
 electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30:
-  version "1.3.48"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz#d3b0d8593814044e092ece2108fc3ac9aea4b900"
+  version "1.3.52"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz#d2d9f1270ba4a3b967b831c40ef71fb4d9ab5ce0"
 
 elliptic@^6.0.0:
   version "6.4.0"
@@ -2430,11 +2429,10 @@ enzyme-adapter-react-16@^1.1.1:
     react-test-renderer "^16.0.0-0"
 
 enzyme-adapter-utils@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7"
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.4.0.tgz#c403b81e8eb9953658569e539780964bdc98de62"
   dependencies:
-    lodash "^4.17.4"
-    object.assign "^4.0.4"
+    object.assign "^4.1.0"
     prop-types "^15.6.0"
 
 enzyme@^3.3.0:
@@ -2465,8 +2463,8 @@ errno@^0.1.3, errno@~0.1.7:
     prr "~1.0.1"
 
 error-ex@^1.2.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
   dependencies:
     is-arrayish "^0.2.1"
 
@@ -2558,8 +2556,8 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 
 escodegen@^1.9.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.10.0.tgz#f647395de22519fbd0d928ffcf1d17e0dec2603e"
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589"
   dependencies:
     esprima "^3.1.3"
     estraverse "^4.2.0"
@@ -2586,8 +2584,8 @@ esprima@^3.1.3:
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
 
 esprima@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
 
 esrecurse@^4.1.0:
   version "4.2.1"
@@ -2636,10 +2634,10 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
     safe-buffer "^5.1.1"
 
 exec-sh@^0.2.0:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38"
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36"
   dependencies:
-    merge "^1.1.3"
+    merge "^1.2.0"
 
 execa@^0.7.0:
   version "0.7.0"
@@ -2939,8 +2937,8 @@ flush-write-stream@^1.0.0:
     readable-stream "^2.0.4"
 
 follow-redirects@^1.0.0, follow-redirects@^1.3.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.0.tgz#234f49cf770b7f35b40e790f636ceba0c3a0ab77"
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.1.tgz#67a8f14f5a1f67f962c2c46469c79eaec0a90291"
   dependencies:
     debug "^3.1.0"
 
@@ -3049,7 +3047,7 @@ fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 
-fsevents@^1.0.0, fsevents@^1.1.2, fsevents@^1.1.3, fsevents@^1.2.3:
+fsevents@^1.0.0, fsevents@^1.1.3, fsevents@^1.2.2, fsevents@^1.2.3:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
   dependencies:
@@ -3082,8 +3080,8 @@ gauge@~2.7.3:
     wide-align "^1.1.0"
 
 get-caller-file@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
 
 get-stdin@^4.0.1:
   version "4.0.1"
@@ -3318,11 +3316,11 @@ hash-base@^3.0.0:
     safe-buffer "^5.0.1"
 
 hash.js@^1.0.0, hash.js@^1.0.3:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.4.tgz#8b50e1f35d51bd01e5ed9ece4dbe3549ccfa0a3c"
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812"
   dependencies:
     inherits "^2.0.3"
-    minimalistic-assert "^1.0.0"
+    minimalistic-assert "^1.0.1"
 
 he@1.1.x:
   version "1.1.1"
@@ -3347,8 +3345,8 @@ hmac-drbg@^1.0.0:
     minimalistic-crypto-utils "^1.0.1"
 
 hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
-  version "2.5.4"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.4.tgz#fc3b1ac05d2ae3abedec84eba846511b0d4fcc4f"
+  version "2.5.5"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
 
 hoist-non-react-statics@^2.5.4:
   version "2.5.5"
@@ -3368,8 +3366,8 @@ homedir-polyfill@^1.0.1:
     parse-passwd "^1.0.0"
 
 hosted-git-info@^2.1.4:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
 
 hpack.js@^2.1.6:
   version "2.1.6"
@@ -3395,16 +3393,16 @@ html-entities@^1.2.0:
   resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
 
 html-minifier@^3.2.3:
-  version "3.5.16"
-  resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.16.tgz#39f5aabaf78bdfc057fe67334226efd7f3851175"
+  version "3.5.19"
+  resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.19.tgz#ed53c4b7326fe507bc3a1adbcc3bbb56660a2ebd"
   dependencies:
     camel-case "3.0.x"
     clean-css "4.1.x"
-    commander "2.15.x"
+    commander "2.16.x"
     he "1.1.x"
     param-case "2.1.x"
     relateurl "0.2.x"
-    uglify-js "3.3.x"
+    uglify-js "3.4.x"
 
 html-webpack-plugin@2.29.0:
   version "2.29.0"
@@ -3679,8 +3677,8 @@ is-builtin-module@^1.0.0:
     builtin-modules "^1.0.0"
 
 is-callable@^1.1.1, is-callable@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
 
 is-ci@^1.0.10:
   version "1.1.0"
@@ -3833,12 +3831,6 @@ is-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
 
-is-odd@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
-  dependencies:
-    is-number "^4.0.0"
-
 is-path-cwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
@@ -4304,13 +4296,17 @@ jest@22.4.2:
     jest-cli "^22.4.2"
 
 js-base64@^2.1.9:
-  version "2.4.5"
-  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.5.tgz#e293cd3c7c82f070d700fc7a1ca0a2e69f101f92"
+  version "2.4.8"
+  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.8.tgz#57a9b130888f956834aa40c5b165ba59c758f033"
 
 js-tokens@^3.0.0, js-tokens@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
 
+"js-tokens@^3.0.0 || ^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+
 js-yaml@^3.4.3, js-yaml@^3.7.0:
   version "3.12.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
@@ -4501,8 +4497,8 @@ jss-vendor-prefixer@^7.0.0:
     css-vendor "^0.3.8"
 
 jss@^9.3.3, jss@^9.7.0:
-  version "9.8.3"
-  resolved "https://registry.yarnpkg.com/jss/-/jss-9.8.3.tgz#399da571c4b2c8f4cf418ca7e8627e44fc287fc8"
+  version "9.8.7"
+  resolved "https://registry.yarnpkg.com/jss/-/jss-9.8.7.tgz#ed9763fc0f2f0260fc8260dac657af61e622ce05"
   dependencies:
     is-in-browser "^1.1.3"
     symbol-observable "^1.1.0"
@@ -4632,6 +4628,10 @@ lodash.camelcase@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
 
+lodash.debounce@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+
 lodash.defaults@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
@@ -4694,10 +4694,10 @@ longest@^1.0.1:
   resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
 
 loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
   dependencies:
-    js-tokens "^3.0.0"
+    js-tokens "^3.0.0 || ^4.0.0"
 
 loud-rejection@^1.0.0:
   version "1.6.0"
@@ -4804,7 +4804,7 @@ merge-stream@^1.0.1:
   dependencies:
     readable-stream "^2.0.1"
 
-merge@^1.1.3:
+merge@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
 
@@ -4855,19 +4855,15 @@ miller-rabin@^4.0.0:
     bn.js "^4.0.0"
     brorand "^1.0.1"
 
-"mime-db@>= 1.34.0 < 2":
-  version "1.34.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.34.0.tgz#452d0ecff5c30346a6dc1e64b1eaee0d3719ff9a"
-
-mime-db@~1.33.0:
-  version "1.33.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
+"mime-db@>= 1.34.0 < 2", mime-db@~1.35.0:
+  version "1.35.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47"
 
 mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18:
-  version "2.1.18"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
+  version "2.1.19"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.19.tgz#71e464537a7ef81c15f2db9d97e913fc0ff606f0"
   dependencies:
-    mime-db "~1.33.0"
+    mime-db "~1.35.0"
 
 mime@1.4.1:
   version "1.4.1"
@@ -4887,7 +4883,7 @@ min-document@^2.19.0:
   dependencies:
     dom-walk "^0.1.0"
 
-minimalistic-assert@^1.0.0:
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
 
@@ -4995,15 +4991,14 @@ nan@^2.9.2:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
 
 nanomatch@^1.2.9:
-  version "1.2.9"
-  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2"
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
   dependencies:
     arr-diff "^4.0.0"
     array-unique "^0.3.2"
     define-property "^2.0.2"
     extend-shallow "^3.0.2"
     fragment-cache "^0.2.1"
-    is-odd "^2.0.0"
     is-windows "^1.0.2"
     kind-of "^6.0.2"
     object.pick "^1.3.0"
@@ -5024,7 +5019,7 @@ nearley@^2.7.10:
     randexp "0.4.6"
     semver "^5.4.1"
 
-needle@^2.2.0:
+needle@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
   dependencies:
@@ -5103,16 +5098,16 @@ node-notifier@^5.2.1:
     which "^1.3.0"
 
 node-pre-gyp@^0.10.0:
-  version "0.10.0"
-  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46"
+  version "0.10.3"
+  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
   dependencies:
     detect-libc "^1.0.2"
     mkdirp "^0.5.1"
-    needle "^2.2.0"
+    needle "^2.2.1"
     nopt "^4.0.1"
     npm-packlist "^1.1.6"
     npmlog "^4.0.2"
-    rc "^1.1.7"
+    rc "^1.2.7"
     rimraf "^2.6.1"
     semver "^5.3.0"
     tar "^4"
@@ -5204,8 +5199,8 @@ number-is-nan@^1.0.0:
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
 
 nwsapi@^2.0.0:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.3.tgz#3f4010d6c943f34018d3dfb5f2fbc0de90476959"
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.6.tgz#5fb7f5b828b97fe1de47eb2a6f8703036b6cb71a"
 
 oauth-sign@~0.8.2:
   version "0.8.2"
@@ -5232,8 +5227,8 @@ object-is@^1.0.1:
   resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
 
 object-keys@^1.0.11, object-keys@^1.0.8:
-  version "1.0.11"
-  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
 
 object-visit@^1.0.0:
   version "1.0.1"
@@ -5581,7 +5576,7 @@ pn@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
 
-popper.js@^1.14.1:
+popper.js@^1.0.0:
   version "1.14.3"
   resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
 
@@ -5873,8 +5868,8 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
     supports-color "^3.2.3"
 
 postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.13:
-  version "6.0.22"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
+  version "6.0.23"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
   dependencies:
     chalk "^2.4.1"
     source-map "^0.6.1"
@@ -5942,11 +5937,10 @@ promise@^7.1.1:
   dependencies:
     asap "~2.0.3"
 
-prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1:
-  version "15.6.1"
-  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
+prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
+  version "15.6.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
   dependencies:
-    fbjs "^0.8.16"
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
@@ -6094,7 +6088,7 @@ raw-body@2.3.2:
     iconv-lite "0.4.19"
     unpipe "1.0.0"
 
-rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
+rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
   dependencies:
@@ -6152,8 +6146,8 @@ react-is@^16.4.1:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
 
 react-jss@^8.1.0:
-  version "8.5.1"
-  resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.5.1.tgz#f97c72f6a1c86aa6408932a2a2836ce40c0ab9fc"
+  version "8.6.1"
+  resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.6.1.tgz#a06e2e1d2c4d91b4d11befda865e6c07fbd75252"
   dependencies:
     hoist-non-react-statics "^2.5.0"
     jss "^9.7.0"
@@ -6165,13 +6159,6 @@ react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
 
-react-popper@^0.10.0:
-  version "0.10.4"
-  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.10.4.tgz#af2a415ea22291edd504678d7afda8a6ee3295aa"
-  dependencies:
-    popper.js "^1.14.1"
-    prop-types "^15.6.1"
-
 react-reconciler@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d"
@@ -6277,12 +6264,13 @@ react-test-renderer@^16.0.0-0:
     react-is "^16.4.1"
 
 react-transition-group@^2.2.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.3.1.tgz#31d611b33e143a5e0f2d94c348e026a0f3b474b6"
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"
   dependencies:
     dom-helpers "^3.3.1"
     loose-envify "^1.3.1"
-    prop-types "^15.6.1"
+    prop-types "^15.6.2"
+    react-lifecycles-compat "^3.0.4"
 
 react@16.4.1:
   version "16.4.1"
@@ -6354,12 +6342,12 @@ readdirp@^2.0.0:
     set-immediate-shim "^1.0.1"
 
 realpath-native@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.0.tgz#7885721a83b43bd5327609f0ddecb2482305fdf0"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.1.tgz#07f40a0cce8f8261e2e8b7ebebf5c95965d7b633"
   dependencies:
     util.promisify "^1.0.0"
 
-"recompose@^0.26.0 || ^0.27.0":
+"recompose@^0.26.0 || ^0.27.0", recompose@^0.27.0:
   version "0.27.1"
   resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
   dependencies:
@@ -6398,8 +6386,8 @@ reduce-function-call@^1.0.1:
     balanced-match "^0.4.2"
 
 redux-devtools-instrument@^1.0.1:
-  version "1.8.3"
-  resolved "https://registry.yarnpkg.com/redux-devtools-instrument/-/redux-devtools-instrument-1.8.3.tgz#c510d67ab4e5e4525acd6e410c25ab46b85aca7c"
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/redux-devtools-instrument/-/redux-devtools-instrument-1.9.0.tgz#2faed9ac3292c783284b21843edfaa0567764a0c"
   dependencies:
     lodash "^4.2.0"
     symbol-observable "^1.0.2"
@@ -6449,10 +6437,14 @@ regenerate@^1.2.1:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
 
-regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
+regenerator-runtime@^0.11.0:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
 
+regenerator-runtime@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.0.tgz#8052ac952d85b10f3425192cd0c53f45cf65c6cb"
+
 regenerator-transform@^0.10.0:
   version "0.10.1"
   resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
@@ -6636,8 +6628,8 @@ resolve@1.6.0:
     path-parse "^1.0.5"
 
 resolve@^1.1.7, resolve@^1.3.2:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.0.tgz#a7f2ac27b78480ecc09c83782741d9f26e4f0c3e"
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
   dependencies:
     path-parse "^1.0.5"
 
@@ -6708,7 +6700,7 @@ safe-buffer@5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
 
-safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
 
@@ -7418,8 +7410,8 @@ toposort@^1.0.0:
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
 
 tough-cookie@>=2.3.3, tough-cookie@^2.3.3:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.2.tgz#aa9133154518b494efab98a58247bfc38818c00c"
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
   dependencies:
     psl "^1.1.24"
     punycode "^1.4.1"
@@ -7476,8 +7468,8 @@ tsconfig-paths-webpack-plugin@^2.0.0:
     tsconfig-paths "^3.1.1"
 
 tsconfig-paths@^3.1.1:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.4.0.tgz#d19fe80c5b245f99d17363471971eab54e65a8a7"
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.4.2.tgz#4640bffaeee3fc0ab986607edae203859156a8c3"
   dependencies:
     deepmerge "^2.0.1"
     minimist "^1.2.0"
@@ -7485,8 +7477,8 @@ tsconfig-paths@^3.1.1:
     strip-json-comments "^2.0.1"
 
 tslib@^1.8.0, tslib@^1.8.1:
-  version "1.9.2"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.2.tgz#8be0cc9a1f6dc7727c38deb16c2ebd1a2892988e"
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
 
 tslint-config-prettier@^1.10.0:
   version "1.13.0"
@@ -7499,8 +7491,8 @@ tslint-react@^3.2.0:
     tsutils "^2.13.1"
 
 tslint@^5.7.0:
-  version "5.10.0"
-  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3"
+  version "5.11.0"
+  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed"
   dependencies:
     babel-code-frame "^6.22.0"
     builtin-modules "^1.1.1"
@@ -7513,11 +7505,11 @@ tslint@^5.7.0:
     resolve "^1.3.2"
     semver "^5.3.0"
     tslib "^1.8.0"
-    tsutils "^2.12.1"
+    tsutils "^2.27.2"
 
-tsutils@^2.12.1, tsutils@^2.13.1:
-  version "2.27.1"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.1.tgz#ab0276ac23664f36ce8fd4414daec4aebf4373ee"
+tsutils@^2.13.1, tsutils@^2.27.2:
+  version "2.28.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.28.0.tgz#6bd71e160828f9d019b6f4e844742228f85169a1"
   dependencies:
     tslib "^1.8.1"
 
@@ -7567,11 +7559,11 @@ uglify-es@^3.3.4:
     commander "~2.13.0"
     source-map "~0.6.1"
 
-uglify-js@3.3.x:
-  version "3.3.28"
-  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.28.tgz#0efb9a13850e11303361c1051f64d2ec68d9be06"
+uglify-js@3.4.x, uglify-js@^3.0.13:
+  version "3.4.5"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.5.tgz#650889c0766cf0f6fd5346cea09cd212f544be69"
   dependencies:
-    commander "~2.15.0"
+    commander "~2.16.0"
     source-map "~0.6.1"
 
 uglify-js@^2.6, uglify-js@^2.8.29:
@@ -7583,13 +7575,6 @@ uglify-js@^2.6, uglify-js@^2.8.29:
   optionalDependencies:
     uglify-to-browserify "~1.0.0"
 
-uglify-js@^3.0.13:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.0.tgz#796762282b5b5f0eafe7d5c8c708d1d7bd5ba11d"
-  dependencies:
-    commander "~2.15.0"
-    source-map "~0.6.1"
-
 uglify-to-browserify@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
@@ -7603,8 +7588,8 @@ uglifyjs-webpack-plugin@^0.4.6:
     webpack-sources "^1.0.1"
 
 uglifyjs-webpack-plugin@^1.1.8:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz#2ef8387c8f1a903ec5e44fa36f9f3cbdcea67641"
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz#57638dd99c853a1ebfe9d97b42160a8a507f9d00"
   dependencies:
     cacache "^10.0.4"
     find-cache-dir "^1.0.0"
@@ -7659,8 +7644,8 @@ unique-string@^1.0.0:
     crypto-random-string "^1.0.0"
 
 universalify@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
 
 unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
@@ -7677,7 +7662,7 @@ unzip-response@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
 
-upath@^1.0.0:
+upath@^1.0.5:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
 
@@ -7743,10 +7728,8 @@ url@^0.11.0:
     querystring "0.2.0"
 
 use@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"
-  dependencies:
-    kind-of "^6.0.2"
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
 
 util-deprecate@~1.0.1:
   version "1.0.2"
@@ -7788,8 +7771,8 @@ uuid@^2.0.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
 
 uuid@^3.1.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+  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"