Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 18 Jun 2018 11:03:22 +0000 (13:03 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 18 Jun 2018 11:03:22 +0000 (13:03 +0200)
Feature #13628

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

25 files changed:
package.json
src/common/formatters.ts [new file with mode: 0644]
src/components/data-explorer/data-explorer.tsx [new file with mode: 0644]
src/components/data-explorer/data-item.ts [new file with mode: 0644]
src/components/data-explorer/index.ts [new file with mode: 0644]
src/components/data-table/column-selector/column-selector.test.tsx [new file with mode: 0644]
src/components/data-table/column-selector/column-selector.tsx [new file with mode: 0644]
src/components/data-table/data-column.ts [new file with mode: 0644]
src/components/data-table/data-table.test.tsx [new file with mode: 0644]
src/components/data-table/data-table.tsx [new file with mode: 0644]
src/components/data-table/index.ts [new file with mode: 0644]
src/components/popover/popover.test.tsx [new file with mode: 0644]
src/components/popover/popover.tsx [new file with mode: 0644]
src/components/project-tree/project-tree.test.tsx
src/components/project-tree/project-tree.tsx
src/components/tree/tree.test.tsx
src/components/tree/tree.tsx
src/models/project.ts
src/services/project-service/project-service.ts
src/store/project/project-action.ts
src/store/project/project-reducer.test.ts
src/store/project/project-reducer.ts
src/views/data-explorer/data-explorer.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx
yarn.lock

index 967faf77823e761ce636b40c81b9726a0fd5c70a..fda2ead62ec4895668d94e3aa0e7410d4a570904 100644 (file)
@@ -3,16 +3,16 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
-    "@material-ui/core": "1.2.0",
-    "@material-ui/icons": "^1.1.0",
-    "@types/lodash": "^4.14.109",
+    "@material-ui/core": "1.2.1",
+    "@material-ui/icons": "1.1.0",
+    "@types/lodash": "4.14.109",
     "axios": "0.18.0",
     "lodash": "4.17.10",
-    "react": "16.4.0",
-    "react-dom": "16.4.0",
+    "react": "16.4.1",
+    "react-dom": "16.4.1",
     "react-redux": "5.0.7",
-    "react-router": "4.2.0",
-    "react-router-dom": "4.2.2",
+    "react-router": "4.3.1",
+    "react-router-dom": "4.3.1",
     "react-router-redux": "5.0.0-alpha.9",
     "react-scripts-ts": "2.16.0",
     "redux": "4.0.0",
     "lint": "tslint src/** -t verbose"
   },
   "devDependencies": {
-    "@types/enzyme": "^3.1.10",
-    "@types/enzyme-adapter-react-16": "^1.0.2",
-    "@types/jest": "23.0.0",
-    "@types/node": "10.3.0",
-    "@types/react": "16.3.16",
-    "@types/react-dom": "16.0.5",
-    "@types/react-redux": "6.0.1",
+    "@types/enzyme": "3.1.10",
+    "@types/enzyme-adapter-react-16": "1.0.2",
+    "@types/jest": "23.1.0",
+    "@types/node": "10.3.3",
+    "@types/react": "16.3.18",
+    "@types/react-dom": "16.0.6",
+    "@types/react-redux": "6.0.2",
     "@types/react-router": "4.0.26",
     "@types/react-router-dom": "4.2.7",
     "@types/react-router-redux": "5.0.15",
@@ -42,7 +42,7 @@
     "enzyme-adapter-react-16": "^1.1.1",
     "jest-localstorage-mock": "2.2.0",
     "redux-devtools": "3.4.1",
-    "typescript": "2.9.1"
+    "typescript": "2.9.2"
   },
   "moduleNameMapper": {
     "^~/(.*)$": "<rootDir>/src/$1"
diff --git a/src/common/formatters.ts b/src/common/formatters.ts
new file mode 100644 (file)
index 0000000..1d9a520
--- /dev/null
@@ -0,0 +1,42 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const formatDate = (isoDate: string) => {
+    const date = new Date(isoDate);
+    return date.toLocaleString();
+};
+
+export const formatFileSize = (size?: number) => {
+    if (typeof size === "number") {
+        for (const { base, unit } of fileSizes) {
+            if (size >= base) {
+                return `${(size / base).toFixed()} ${unit}`;
+            }
+        }
+    }
+    return "";
+};
+
+const fileSizes = [
+    {
+        base: 1000000000000,
+        unit: "TB"
+    },
+    {
+        base: 1000000000,
+        unit: "GB"
+    },
+    {
+        base: 1000000,
+        unit: "MB"
+    },
+    {
+        base: 1000,
+        unit: "KB"
+    },
+    {
+        base: 1,
+        unit: "B"
+    }
+];
\ No newline at end of file
diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx
new file mode 100644 (file)
index 0000000..9aeb28a
--- /dev/null
@@ -0,0 +1,191 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DataTable, DataTableProps, DataColumn, ColumnSelector } from "../../components/data-table";
+import { Typography, Grid, ListItem, Divider, List, ListItemIcon, ListItemText, Paper, Toolbar } from '@material-ui/core';
+import IconButton, { IconButtonProps } from '@material-ui/core/IconButton';
+import MoreVertIcon from "@material-ui/icons/MoreVert";
+import Popover from '../popover/popover';
+import { formatFileSize, formatDate } from '../../common/formatters';
+import { DataItem } from './data-item';
+
+interface DataExplorerProps {
+    items: DataItem[];
+    onItemClick: (item: DataItem) => void;
+}
+
+type DataExplorerState = Pick<DataTableProps<DataItem>, "columns">;
+
+class DataExplorer extends React.Component<DataExplorerProps, DataExplorerState> {
+    state: DataExplorerState = {
+        columns: [
+            {
+                name: "Name",
+                selected: true,
+                render: item => this.renderName(item)
+            },
+            {
+                name: "Status",
+                selected: true,
+                render: item => renderStatus(item.status)
+            },
+            {
+                name: "Type",
+                selected: true,
+                render: item => renderType(item.type)
+            },
+            {
+                name: "Owner",
+                selected: true,
+                render: item => renderOwner(item.owner)
+            },
+            {
+                name: "File size",
+                selected: true,
+                render: (item) => renderFileSize(item.fileSize)
+            },
+            {
+                name: "Last modified",
+                selected: true,
+                render: item => renderDate(item.lastModified)
+            },
+            {
+                name: "Actions",
+                selected: true,
+                configurable: false,
+                renderHeader: () => null,
+                render: renderItemActions
+            }
+        ]
+    };
+
+    render() {
+        return <Paper>
+            <Toolbar>
+                <Grid container justify="flex-end">
+                    <ColumnSelector
+                        columns={this.state.columns}
+                        onColumnToggle={this.toggleColumn} />
+                </Grid>
+            </Toolbar>
+            <DataTable
+                columns={this.state.columns}
+                items={this.props.items} />
+            <Toolbar />
+        </Paper>;
+    }
+
+    toggleColumn = (column: DataColumn<DataItem>) => {
+        const index = this.state.columns.indexOf(column);
+        const columns = this.state.columns.slice(0);
+        columns.splice(index, 1, { ...column, selected: !column.selected });
+        this.setState({ columns });
+    }
+
+    renderName = (item: DataItem) =>
+        <Grid
+            container
+            alignItems="center"
+            wrap="nowrap"
+            spacing={16}
+            onClick={() => this.props.onItemClick(item)}>
+            <Grid item>
+                {renderIcon(item)}
+            </Grid>
+            <Grid item>
+                <Typography color="primary">
+                    {item.name}
+                </Typography>
+            </Grid>
+        </Grid>
+
+}
+
+const renderIcon = (dataItem: DataItem) => {
+    switch (dataItem.type) {
+        case "arvados#group":
+            return <i className="fas fa-folder fa-lg" />;
+        case "arvados#groupList":
+            return <i className="fas fa-th fa-lg" />;
+        default:
+            return <i />;
+    }
+};
+
+const renderDate = (date: string) =>
+    <Typography noWrap>
+        {formatDate(date)}
+    </Typography>;
+
+const renderFileSize = (fileSize?: number) =>
+    <Typography noWrap>
+        {formatFileSize(fileSize)}
+    </Typography>;
+
+const renderOwner = (owner: string) =>
+    <Typography noWrap color="primary">
+        {owner}
+    </Typography>;
+
+const renderType = (type: string) =>
+    <Typography noWrap>
+        {type}
+    </Typography>;
+
+const renderStatus = (status?: string) =>
+    <Typography noWrap align="center">
+        {status || "-"}
+    </Typography>;
+
+const renderItemActions = () =>
+    <Grid container justify="flex-end">
+        <Popover triggerComponent={ItemActionsTrigger}>
+            <List dense>
+                {[{
+                    icon: "fas fa-users",
+                    label: "Share"
+                },
+                {
+                    icon: "fas fa-sign-out-alt",
+                    label: "Move to"
+                },
+                {
+                    icon: "fas fa-star",
+                    label: "Add to favourite"
+                },
+                {
+                    icon: "fas fa-edit",
+                    label: "Rename"
+                },
+                {
+                    icon: "fas fa-copy",
+                    label: "Make a copy"
+                },
+                {
+                    icon: "fas fa-download",
+                    label: "Download"
+                }].map(renderAction)}
+                < Divider />
+                {renderAction({ icon: "fas fa-trash-alt", label: "Remove" })}
+            </List>
+        </Popover>
+    </Grid>;
+
+const renderAction = (action: { label: string, icon: string }, index?: number) =>
+    <ListItem button key={index}>
+        <ListItemIcon>
+            <i className={action.icon} />
+        </ListItemIcon>
+        <ListItemText>
+            {action.label}
+        </ListItemText>
+    </ListItem>;
+
+const ItemActionsTrigger: React.SFC<IconButtonProps> = (props) =>
+    <IconButton {...props}>
+        <MoreVertIcon />
+    </IconButton>;
+
+export default DataExplorer;
diff --git a/src/components/data-explorer/data-item.ts b/src/components/data-explorer/data-item.ts
new file mode 100644 (file)
index 0000000..3a80992
--- /dev/null
@@ -0,0 +1,12 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface DataItem {
+    name: string;
+    type: string;
+    owner: string;
+    lastModified: string;
+    fileSize?: number;
+    status?: string;
+}
\ No newline at end of file
diff --git a/src/components/data-explorer/index.ts b/src/components/data-explorer/index.ts
new file mode 100644 (file)
index 0000000..bde402d
--- /dev/null
@@ -0,0 +1,6 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export { default as DataExplorer } from "./data-explorer";
+export * from "./data-item";
\ No newline at end of file
diff --git a/src/components/data-table/column-selector/column-selector.test.tsx b/src/components/data-table/column-selector/column-selector.test.tsx
new file mode 100644 (file)
index 0000000..26c16a1
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { mount, configure } from "enzyme";
+import * as Adapter from "enzyme-adapter-react-16";
+import ColumnSelector, { ColumnSelectorProps, ColumnSelectorTrigger } from "./column-selector";
+import { DataColumn } from "../data-column";
+import { ListItem, Checkbox } from "@material-ui/core";
+
+configure({ adapter: new Adapter() });
+
+describe("<ColumnSelector />", () => {
+    it("shows only configurable columns", () => {
+        const columns: Array<DataColumn<void>> = [
+            {
+                name: "Column 1",
+                render: () => <span />,
+                selected: true
+            },
+            {
+                name: "Column 2",
+                render: () => <span />,
+                selected: true,
+                configurable: true,
+            },
+            {
+                name: "Column 3",
+                render: () => <span />,
+                selected: true,
+                configurable: false
+            }
+        ];
+        const columnsConfigurator = mount(<ColumnSelector columns={columns} onColumnToggle={jest.fn()} />);
+        columnsConfigurator.find(ColumnSelectorTrigger).simulate("click");
+        expect(columnsConfigurator.find(ListItem)).toHaveLength(2);
+    });
+
+    it("renders checked checkboxes next to selected columns", () => {
+        const columns: Array<DataColumn<void>> = [
+            {
+                name: "Column 1",
+                render: () => <span />,
+                selected: true
+            },
+            {
+                name: "Column 2",
+                render: () => <span />,
+                selected: false
+            },
+            {
+                name: "Column 3",
+                render: () => <span />,
+                selected: true
+            }
+        ];
+        const columnsConfigurator = mount(<ColumnSelector columns={columns} onColumnToggle={jest.fn()} />);
+        columnsConfigurator.find(ColumnSelectorTrigger).simulate("click");
+        expect(columnsConfigurator.find(Checkbox).at(0).prop("checked")).toBe(true);
+        expect(columnsConfigurator.find(Checkbox).at(1).prop("checked")).toBe(false);
+        expect(columnsConfigurator.find(Checkbox).at(2).prop("checked")).toBe(true);
+    });
+
+    it("calls onColumnToggle with clicked column", () => {
+        const columns: Array<DataColumn<void>> = [
+            {
+                name: "Column 1",
+                render: () => <span />,
+                selected: true
+            }
+        ];
+        const onColumnToggle = jest.fn();
+        const columnsConfigurator = mount(<ColumnSelector columns={columns} onColumnToggle={onColumnToggle} />);
+        columnsConfigurator.find(ColumnSelectorTrigger).simulate("click");
+        columnsConfigurator.find(ListItem).simulate("click");
+        expect(onColumnToggle).toHaveBeenCalledWith(columns[0]);
+    });
+});
\ No newline at end of file
diff --git a/src/components/data-table/column-selector/column-selector.tsx b/src/components/data-table/column-selector/column-selector.tsx
new file mode 100644 (file)
index 0000000..87d3e8d
--- /dev/null
@@ -0,0 +1,56 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 MenuIcon from "@material-ui/icons/Menu";
+import { DataColumn, isColumnConfigurable } from '../data-column';
+import Popover from "../../popover/popover";
+import { IconButtonProps } from '@material-ui/core/IconButton';
+
+export interface ColumnSelectorProps {
+    columns: Array<DataColumn<any>>;
+    onColumnToggle: (column: DataColumn<any>) => void;
+}
+
+const ColumnSelector: React.SFC<ColumnSelectorProps & WithStyles<CssRules>> = ({ columns, onColumnToggle, classes }) =>
+    <Popover triggerComponent={ColumnSelectorTrigger}>
+        <Paper>
+            <List dense>
+                {columns
+                    .filter(isColumnConfigurable)
+                    .map((column, index) => (
+                        <ListItem
+                            button
+                            key={index}
+                            onClick={() => onColumnToggle(column)}>
+                            <Checkbox
+                                disableRipple
+                                color="primary"
+                                checked={column.selected}
+                                className={classes.checkbox} />
+                            <ListItemText>
+                                {column.name}
+                            </ListItemText>
+                        </ListItem>
+                    ))}
+            </List>
+        </Paper>
+    </Popover>;
+
+export const ColumnSelectorTrigger: React.SFC<IconButtonProps> = (props) =>
+    <IconButton {...props}>
+        <MenuIcon />
+    </IconButton>;
+
+type CssRules = "checkbox";
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+    checkbox: {
+        width: 24,
+        height: 24
+    }
+});
+
+export default withStyles(styles)(ColumnSelector);
diff --git a/src/components/data-table/data-column.ts b/src/components/data-table/data-column.ts
new file mode 100644 (file)
index 0000000..d3b1473
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface DataColumn<T> {
+    name: string;
+    selected: boolean;
+    configurable?: boolean;
+    key?: React.Key;
+    render: (item: T) => React.ReactElement<void>;
+    renderHeader?: () => React.ReactElement<void> | null;
+}
+
+export const isColumnConfigurable = <T>(column: DataColumn<T>) => {
+    return column.configurable === undefined || column.configurable;
+};
\ No newline at end of file
diff --git a/src/components/data-table/data-table.test.tsx b/src/components/data-table/data-table.test.tsx
new file mode 100644 (file)
index 0000000..4a34a6b
--- /dev/null
@@ -0,0 +1,107 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { mount, configure } from "enzyme";
+import * as Adapter from "enzyme-adapter-react-16";
+import DataTable from "./data-table";
+import { DataColumn } from "./data-column";
+import { TableHead, TableCell, Typography, TableBody, Button } from "@material-ui/core";
+
+configure({ adapter: new Adapter() });
+
+describe("<DataTable />", () => {
+    it("shows only selected columns", () => {
+        const columns: Array<DataColumn<string>> = [
+            {
+                name: "Column 1",
+                render: () => <span />,
+                selected: true
+            },
+            {
+                name: "Column 2",
+                render: () => <span />,
+                selected: true
+            },
+            {
+                name: "Column 3",
+                render: () => <span />,
+                selected: false
+            }
+        ];
+        const dataTable = mount(<DataTable columns={columns} items={["item 1"]}/>);
+        expect(dataTable.find(TableHead).find(TableCell)).toHaveLength(2);
+    });
+    
+    it("renders column name", () => {
+        const columns: Array<DataColumn<string>> = [
+            {
+                name: "Column 1",
+                render: () => <span />,
+                selected: true
+            }
+        ];
+        const dataTable = mount(<DataTable columns={columns} items={["item 1"]}/>);
+        expect(dataTable.find(TableHead).find(TableCell).text()).toBe("Column 1");
+    });
+    
+    it("uses renderHeader instead of name prop", () => {
+        const columns: Array<DataColumn<string>> = [
+            {
+                name: "Column 1",
+                renderHeader: () => <span>Column Header</span>,
+                render: () => <span />,
+                selected: true
+            }
+        ];
+        const dataTable = mount(<DataTable columns={columns} items={["item 1"]}/>);
+        expect(dataTable.find(TableHead).find(TableCell).text()).toBe("Column Header");
+    });
+    
+    it("passes column key prop to corresponding cells", () => {
+        const columns: Array<DataColumn<string>> = [
+            {
+                name: "Column 1",
+                key: "column-1-key",
+                render: () => <span />,
+                selected: true
+            }
+        ];
+        const dataTable = mount(<DataTable columns={columns} items={["item 1"]}/>);
+        expect(dataTable.find(TableHead).find(TableCell).key()).toBe("column-1-key");
+        expect(dataTable.find(TableBody).find(TableCell).key()).toBe("column-1-key");
+    });
+    
+    it("shows information that items array is empty", () => {
+        const columns: Array<DataColumn<string>> = [
+            {
+                name: "Column 1",
+                render: () => <span />,
+                selected: true
+            }
+        ];
+        const dataTable = mount(<DataTable columns={columns} items={[]}/>);
+        expect(dataTable.find(Typography).text()).toBe("No items");
+    });
+
+    it("renders items", () => {
+        const columns: Array<DataColumn<string>> = [
+            {
+                name: "Column 1",
+                render: (item) => <Typography>{item}</Typography>,
+                selected: true
+            },
+            {
+                name: "Column 2",
+                render: (item) => <Button>{item}</Button>,
+                selected: true
+            }
+        ];
+        const dataTable = mount(<DataTable columns={columns} items={["item 1"]}/>);
+        expect(dataTable.find(TableBody).find(Typography).text()).toBe("item 1");
+        expect(dataTable.find(TableBody).find(Button).text()).toBe("item 1");
+    });
+
+
+});
\ No newline at end of file
diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx
new file mode 100644 (file)
index 0000000..e7ce03a
--- /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 { Table, TableBody, TableRow, TableCell, TableHead, StyleRulesCallback, Theme, WithStyles, withStyles, Typography } from '@material-ui/core';
+import { DataColumn } from './data-column';
+
+export interface DataTableProps<T> {
+    items: T[];
+    columns: Array<DataColumn<T>>;
+    onItemClick?: (item: T) => void;
+}
+
+class DataTable<T> extends React.Component<DataTableProps<T> & WithStyles<CssRules>> {
+    render() {
+        const { items, columns, classes, onItemClick } = this.props;
+        return <div className={classes.tableContainer}>
+            {items.length > 0 ?
+                <Table>
+                    <TableHead>
+                        <TableRow>
+                            {columns
+                                .filter(column => column.selected)
+                                .map(({ name, renderHeader, key }, index) =>
+                                    <TableCell key={key || index}>
+                                        {renderHeader ? renderHeader() : name}
+                                    </TableCell>
+                                )}
+                        </TableRow>
+                    </TableHead>
+                    <TableBody className={classes.tableBody}>
+                        {items
+                            .map((item, index) =>
+                                <TableRow
+                                    hover
+                                    key={index}
+                                    onClick={() => onItemClick && onItemClick(item)}>
+                                    {columns
+                                        .filter(column => column.selected)
+                                        .map((column, index) => (
+                                            <TableCell key={column.key || index}>
+                                                {column.render(item)}
+                                            </TableCell>
+                                        ))}
+                                </TableRow>
+                            )}
+                    </TableBody>
+                </Table> : <Typography 
+                    className={classes.noItemsInfo}
+                    variant="body2"
+                    gutterBottom>
+                    No items
+                </Typography>}
+        </div>;
+    }
+}
+
+type CssRules = "tableBody" | "tableContainer" | "noItemsInfo";
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+    tableContainer: {
+        overflowX: 'auto'
+    },
+    tableBody: {
+        background: theme.palette.background.paper
+    },
+    noItemsInfo: {
+        textAlign: "center",
+        padding: theme.spacing.unit
+    }
+});
+
+export default withStyles(styles)(DataTable);
diff --git a/src/components/data-table/index.ts b/src/components/data-table/index.ts
new file mode 100644 (file)
index 0000000..f35754b
--- /dev/null
@@ -0,0 +1,9 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export * from "./data-column";
+export * from "./column-selector/column-selector";
+export { default as ColumnSelector } from "./column-selector/column-selector";
+export * from "./data-table";
+export { default as DataTable } from "./data-table";
\ No newline at end of file
diff --git a/src/components/popover/popover.test.tsx b/src/components/popover/popover.test.tsx
new file mode 100644 (file)
index 0000000..fa24c0c
--- /dev/null
@@ -0,0 +1,69 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { mount, configure } from "enzyme";
+import * as Adapter from "enzyme-adapter-react-16";
+
+import Popover, { DefaultTrigger } from "./popover";
+import Button, { ButtonProps } from "@material-ui/core/Button";
+
+configure({ adapter: new Adapter() });
+
+describe("<Popover />", () => {
+    it("opens on default trigger click", () => {
+        const popover = mount(<Popover />);
+        popover.find(DefaultTrigger).simulate("click");
+        expect(popover.state().anchorEl).toBeDefined();
+    });
+
+    it("renders custom trigger", () => {
+        const popover = mount(<Popover triggerComponent={CustomTrigger} />);
+        expect(popover.find(Button).text()).toBe("Open popover");
+    });
+
+    it("opens on custom trigger click", () => {
+        const popover = mount(<Popover triggerComponent={CustomTrigger} />);
+        popover.find(CustomTrigger).simulate("click");
+        expect(popover.state().anchorEl).toBeDefined();
+    });
+
+    it("renders children when opened", () => {
+        const popover = mount(
+            <Popover>
+                <CustomTrigger />
+            </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>
+                <CustomTrigger />
+            </Popover>
+        );
+        popover.find(DefaultTrigger).simulate("click");
+        popover.find(CustomTrigger).simulate("click");
+        expect(popover.state().anchorEl).toBeDefined();
+    });
+    it("closes on content click if closeOnContentClick is set", () => {
+        const popover = mount(
+            <Popover closeOnContentClick>
+                <CustomTrigger />
+            </Popover>
+        );
+        popover.find(DefaultTrigger).simulate("click");
+        popover.find(CustomTrigger).simulate("click");
+        expect(popover.state().anchorEl).toBeUndefined();
+    });
+
+});
+
+const CustomTrigger: React.SFC<ButtonProps> = (props) => (
+    <Button {...props}>
+        Open popover
+    </Button>
+);
\ No newline at end of file
diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx
new file mode 100644 (file)
index 0000000..c8d4033
--- /dev/null
@@ -0,0 +1,69 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Popover as MaterialPopover } from '@material-ui/core';
+
+import { PopoverOrigin } from '@material-ui/core/Popover';
+import IconButton, { IconButtonProps } from '@material-ui/core/IconButton';
+
+export interface PopoverProps {
+    triggerComponent?: React.ComponentType<{ onClick: (event: React.MouseEvent<any>) => void }>;
+    closeOnContentClick?: boolean;
+}
+
+
+class Popover extends React.Component<PopoverProps> {
+
+    state = {
+        anchorEl: undefined
+    };
+
+    transformOrigin: PopoverOrigin = {
+        vertical: "top",
+        horizontal: "right",
+    };
+
+    render() {
+        const Trigger = this.props.triggerComponent || DefaultTrigger;
+        return (
+            <>
+                <Trigger onClick={this.handleTriggerClick} />
+                <MaterialPopover
+                    anchorEl={this.state.anchorEl}
+                    open={Boolean(this.state.anchorEl)}
+                    onClose={this.handleClose}
+                    onClick={this.handleSelfClick}
+                    transformOrigin={this.transformOrigin}
+                    anchorOrigin={this.transformOrigin}
+                >
+                    {this.props.children}
+                </MaterialPopover>
+            </>
+        );
+    }
+
+    handleClose = () => {
+        this.setState({ anchorEl: undefined });
+    }
+
+    handleTriggerClick = (event: React.MouseEvent<any>) => {
+        this.setState({ anchorEl: event.currentTarget });
+    }
+
+    handleSelfClick = () => {
+        if (this.props.closeOnContentClick) {
+            this.handleClose();
+        }
+    }
+
+}
+
+export const DefaultTrigger: React.SFC<IconButtonProps> = (props) => (
+    <IconButton {...props}>
+        <i className="fas" />
+    </IconButton>
+);
+
+export default Popover;
index d42df0882e81551c0a4e4e477f389c16a72e09ba..932a29cc16793aede03e3dd035031cfa0a542d6b 100644 (file)
@@ -8,6 +8,7 @@ import * as Enzyme from 'enzyme';
 import * as Adapter from 'enzyme-adapter-react-16';
 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 { TreeItem } from '../tree/tree';
@@ -16,7 +17,7 @@ Enzyme.configure({ adapter: new Adapter() });
 
 describe("ProjectTree component", () => {
 
-    it("checks is there ListItemIcon in the ProjectTree component", () => {
+    it("should render ListItemIcon", () => {
         const project: TreeItem<Project> = {
             data: {
                 name: "sample name",
@@ -28,14 +29,15 @@ describe("ProjectTree component", () => {
             },
             id: "3",
             open: true,
-            active: true
+            active: true,
+            status: 1
         };
         const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
 
-        expect(wrapper.find(ListItemIcon).length).toEqual(1);
+        expect(wrapper.find(ListItemIcon)).toHaveLength(1);
     });
 
-    it("checks are there two ListItemIcon's in the ProjectTree component", () => {
+    it("should render 2 ListItemIcons", () => {
         const project: Array<TreeItem<Project>> = [
             {
                 data: {
@@ -48,7 +50,8 @@ describe("ProjectTree component", () => {
                 },
                 id: "3",
                 open: false,
-                active: true
+                active: true,
+                status: 1
             },
             {
                 data: {
@@ -61,15 +64,54 @@ describe("ProjectTree component", () => {
                 },
                 id: "3",
                 open: false,
-                active: true
+                active: true,
+                status: 1
             }
         ];
         const wrapper = mount(<ProjectTree projects={project} toggleProjectTreeItem={() => { }} />);
 
-        expect(wrapper.find(ListItemIcon).length).toEqual(2);
+        expect(wrapper.find(ListItemIcon)).toHaveLength(2);
     });
 
-    it("check ProjectTree, when open is changed", () => {
+    it("should render Collapse", () => {
+        const project: Array<TreeItem<Project>> = [
+            {
+                data: {
+                    name: "sample name",
+                    createdAt: "2018-06-12",
+                    modifiedAt: "2018-06-13",
+                    uuid: "uuid",
+                    ownerUuid: "ownerUuid",
+                    href: "href",
+                },
+                id: "3",
+                open: true,
+                active: true,
+                status: 2,
+                items: [
+                    {
+                        data: {
+                            name: "sample name",
+                            createdAt: "2018-06-12",
+                            modifiedAt: "2018-06-13",
+                            uuid: "uuid",
+                            ownerUuid: "ownerUuid",
+                            href: "href",
+                        },
+                        id: "3",
+                        open: true,
+                        active: true,
+                        status: 1
+                    }
+                ]
+            }
+        ];
+        const wrapper = mount(<ProjectTree projects={project} toggleProjectTreeItem={() => { }} />);
+
+        expect(wrapper.find(Collapse)).toHaveLength(1);
+    });
+
+    it("should render CircularProgress", () => {
         const project: TreeItem<Project> = {
             data: {
                 name: "sample name",
@@ -80,27 +122,12 @@ describe("ProjectTree component", () => {
                 href: "href",
             },
             id: "3",
-            open: true,
+            open: false,
             active: true,
-            items: [
-                {
-                    data: {
-                        name: "sample name",
-                        createdAt: "2018-06-12",
-                        modifiedAt: "2018-06-13",
-                        uuid: "uuid",
-                        ownerUuid: "ownerUuid",
-                        href: "href",
-                    },
-                    id: "4",
-                    open: false,
-                    active: true
-                }
-            ]
+            status: 1
         };
         const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
-        wrapper.setState({open: true });
 
-        expect(wrapper.find(Collapse).length).toEqual(1);
+        expect(wrapper.find(CircularProgress)).toHaveLength(1);
     });
 });
index 43ad312a940edb632ebcfa4e8eeacc28ac28a242..275805ffa9555f2d8f965311690c0bb76cdcf043 100644 (file)
@@ -9,7 +9,7 @@ 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 } from '../tree/tree';
+import Tree, { TreeItem, TreeItemStatus } from '../tree/tree';
 import { Project } from '../../models/project';
 
 type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer';
@@ -27,9 +27,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
         marginLeft: '20px',
     },
     treeContainer: {
-        position: 'absolute',
+        marginTop: '37px',
         overflowX: 'visible',
-        marginTop: '80px',
+        overflowY: 'auto',
         minWidth: '240px',
         whiteSpace: 'nowrap',
     }
@@ -37,7 +37,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
 
 export interface ProjectTreeProps {
     projects: Array<TreeItem<Project>>;
-    toggleProjectTreeItem: (id: string) => void;
+    toggleProjectTreeItem: (id: string, status: TreeItemStatus) => void;
 }
 
 class ProjectTree<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
index ffdc74f980167fadc5568006b956df4e696b6242..bb0499f8d18e2daf7a0a5d7330344966f012cc08 100644 (file)
@@ -1,7 +1,55 @@
 // Copyright (C) The Arvados Authors. All rights reserved.
 //
 // SPDX-License-Identifier: AGPL-3.0
+import * as React from 'react';
+import { mount } from 'enzyme';
+import * as Enzyme from 'enzyme';
+import * as Adapter from 'enzyme-adapter-react-16';
+import { Collapse } from '@material-ui/core';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import ListItem from "@material-ui/core/ListItem/ListItem";
 
-it("should render the tree", () => {
-       expect(true).toBe(true);
-});
\ No newline at end of file
+import Tree, {TreeItem} from './tree';
+import { Project } from '../../models/project';
+Enzyme.configure({ adapter: new Adapter() });
+
+describe("Tree component", () => {
+
+       it("should render ListItem", () => {
+               const project: TreeItem<Project> = {
+            data: {
+                name: "sample name",
+                createdAt: "2018-06-12",
+                modifiedAt: "2018-06-13",
+                uuid: "uuid",
+                ownerUuid: "ownerUuid",
+                href: "href",
+            },
+            id: "3",
+            open: true,
+                       active: true,
+                       status: 1,
+        };
+               const wrapper = mount(<Tree render={project => <div/>} toggleItem={() => { }} items={[project]}/>)
+               expect(wrapper.find(ListItem)).toHaveLength(1);
+       });
+    
+    it("should render arrow", () => {
+               const project: TreeItem<Project> = {
+            data: {
+                name: "sample name",
+                createdAt: "2018-06-12",
+                modifiedAt: "2018-06-13",
+                uuid: "uuid",
+                ownerUuid: "ownerUuid",
+                href: "href",
+            },
+            id: "3",
+            open: true,
+                       active: true,
+                       status: 1,
+        };
+               const wrapper = mount(<Tree render={project => <div/>} toggleItem={() => { }} items={[project]}/>)
+               expect(wrapper.find('i')).toHaveLength(1);
+       });
+});
index fb0df58e5d0a957955871db7c8a5f178c0e7f2c9..6731950c1adb8a78543cffa36000f2441e8144a4 100644 (file)
@@ -8,8 +8,10 @@ import ListItem from "@material-ui/core/ListItem/ListItem";
 import { StyleRulesCallback, Theme, 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 { inherits } from 'util';
 
-type CssRules = 'list' | 'activeArrow' | 'arrow' | 'arrowRotate';
+type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     list: {
@@ -20,53 +22,79 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
         color: '#4285F6',
         position: 'absolute',
     },
-    arrow: {
+    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'
     }
 });
 
+export enum TreeItemStatus {
+    Initial,
+    Pending,
+    Loaded
+}
+
 export interface TreeItem<T> {
     data: T;
     id: string;
     open: boolean;
     active: boolean;
+    status: TreeItemStatus;
+    toggled?: boolean;
     items?: Array<TreeItem<T>>;
 }
 
 interface TreeProps<T> {
     items?: Array<TreeItem<T>>;
     render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
-    toggleItem: (id: string) => any;
+    toggleItem: (id: string, status: TreeItemStatus) => any;
     level?: number;
 }
 
 class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
-    renderArrow (items: boolean, arrowClass: string, open: boolean){
-        return <i className={`${arrowClass} ${open ? "fas fa-caret-down" : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />;
+    renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) {
+        return <i
+            onClick={() => this.props.toggleItem(id, status)}
+            className={`
+                ${arrowClass} 
+                ${status === TreeItemStatus.Pending ? this.props.classes.arrowVisibility : ''} 
+                ${open ? `fas fa-caret-down ${this.props.classes.arrowTransition}` : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />;
     }
     render(): ReactElement<any> {
         const level = this.props.level ? this.props.level : 0;
-        const {classes, render, toggleItem, items} = this.props;
-        const {list, arrow, activeArrow} = classes;
+        const { classes, render, toggleItem, items } = 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 onClick={() => toggleItem(it.id)} className={list} style={{paddingLeft: (level + 1) * 20}}>
-                    {this.renderArrow(true, it.active ? activeArrow : arrow, it.open)}
-                    {render(it, level)}
-                </ListItem>
-                {it.items && it.items.length > 0 &&
-                <Collapse in={it.open} timeout="auto" unmountOnExit>
-                    <StyledTree
-                        items={it.items}
-                        render={render}
-                        toggleItem={toggleItem}
-                        level={level + 1}/>
-                </Collapse>}
-             </div>)}
+                <div key={`item/${level}/${idx}`}>
+                    <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}>
+                        {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
+                                items={it.items}
+                                render={render}
+                                toggleItem={toggleItem}
+                                level={level + 1} />
+                        </Collapse>}
+                </div>)}
         </List>;
     }
 }
index 83fb59bd3eb0b4f77854848a3637488ce9894eb2..830621b440d2e1ab1068eaa665ca39a4791d2328 100644 (file)
@@ -9,4 +9,5 @@ export interface Project {
     uuid: string;
     ownerUuid: string;
     href: string;
+    kind: string;
 }
index dbc0f9277e43cc270252be7dbd16e7887104878d..119cfece4b8fcb130fa69f4ec54304e2f7d270f5 100644 (file)
@@ -34,7 +34,7 @@ interface GroupsResponse {
 
 export default class ProjectService {
     public getProjectList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Project[]> => {
-        dispatch(actions.PROJECTS_REQUEST());
+        dispatch(actions.PROJECTS_REQUEST(parentUuid));
         if (parentUuid) {
             const fb = new FilterBuilder();
             fb.addLike(FilterField.OWNER_UUID, parentUuid);
@@ -47,7 +47,8 @@ export default class ProjectService {
                     modifiedAt: g.modified_at,
                     href: g.href,
                     uuid: g.uuid,
-                    ownerUuid: g.owner_uuid
+                    ownerUuid: g.owner_uuid,
+                    kind: g.kind
                 } as Project));
                 dispatch(actions.PROJECTS_SUCCESS({projects, parentItemId: parentUuid}));
                 return projects;
index 87ecbda9b56fbe1850d202d5cbb824373554d758..2856de66a09a675d122fdd8ea44ed39216d717a0 100644 (file)
@@ -8,7 +8,7 @@ import { default as unionize, ofType, UnionOf } from "unionize";
 const actions = unionize({
     CREATE_PROJECT: ofType<Project>(),
     REMOVE_PROJECT: ofType<string>(),
-    PROJECTS_REQUEST: {},
+    PROJECTS_REQUEST: ofType<any>(),
     PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(),
     TOGGLE_PROJECT_TREE_ITEM: ofType<string>()
 }, {
index 3e828830421ed713d0d006fa7c8f0e7e788822b6..311ec9d18346f2745337ecda29fb8f97028a2463 100644 (file)
@@ -4,7 +4,7 @@
 
 import projectsReducer, { findTreeBranch } from "./project-reducer";
 import actions from "./project-action";
-import { TreeItem } from "../../components/tree/tree";
+import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 
 describe('project-reducer', () => {
     it('should add new project to the list', () => {
@@ -15,7 +15,8 @@ describe('project-reducer', () => {
             createdAt: '2018-01-01',
             modifiedAt: '2018-01-01',
             ownerUuid: 'owner-test123',
-            uuid: 'test123'
+            uuid: 'test123',
+            kind: ""
         };
 
         const state = projectsReducer(initialState, actions.CREATE_PROJECT(project));
@@ -30,24 +31,27 @@ describe('project-reducer', () => {
             createdAt: '2018-01-01',
             modifiedAt: '2018-01-01',
             ownerUuid: 'owner-test123',
-            uuid: 'test123'
+            uuid: 'test123',
+            kind: ""
         };
 
         const projects = [project, project];
         const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
         expect(state).toEqual([{
-            active: false,
-            open: false,
-            id: "test123",
-            items: [],
-            data: project
-        }, {
-            active: false,
-            open: false,
-            id: "test123",
-            items: [],
-            data: project
-        }
+                active: false,
+                open: false,
+                id: "test123",
+                items: [],
+                data: project,
+                status: 0
+            }, {
+                active: false,
+                open: false,
+                id: "test123",
+                items: [],
+                data: project,
+                status: 0
+            }
         ]);
     });
 });
@@ -60,6 +64,7 @@ describe("findTreeBranch", () => {
         active: false,
         data: "",
         open: false,
+        status: TreeItemStatus.Initial
     });
 
     it("should return an array that matches path to the given item", () => {
index ac6d4b73ffd0fd5460ae07a7affe4a8953540803..8770391acbefe09eb6d8520e1b3ff21f0aa442e6 100644 (file)
@@ -4,12 +4,12 @@
 
 import { Project } from "../../models/project";
 import actions, { ProjectAction } from "./project-action";
-import { TreeItem } from "../../components/tree/tree";
+import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 import * as _ from "lodash";
 
 export type ProjectState = Array<TreeItem<Project>>;
 
-function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
+export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
     let item;
     for (const t of tree) {
         item = t.id === itemId
@@ -47,11 +47,15 @@ function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[],
     let treeItem;
     if (parentItemId) {
         treeItem = findTreeItem(tree, parentItemId);
+        if (treeItem) {
+            treeItem.status = TreeItemStatus.Loaded;
+        }
     }
     const items = projects.map((p, idx) => ({
         id: p.uuid,
         open: false,
         active: false,
+        status: TreeItemStatus.Initial,
         data: p,
         items: []
     } as TreeItem<Project>));
@@ -64,12 +68,18 @@ function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[],
     return items;
 }
 
-
 const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
     return actions.match(action, {
         CREATE_PROJECT: project => [...state, project],
         REMOVE_PROJECT: () => state,
-        PROJECTS_REQUEST: () => state,
+        PROJECTS_REQUEST: itemId => {
+            const tree = _.cloneDeep(state);
+            const item = findTreeItem(tree, itemId);
+            if (item) {
+                item.status = TreeItemStatus.Pending;
+            }
+            return tree;
+        },
         PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
             return updateProjectTree(state, projects, parentItemId);
         },
@@ -80,6 +90,7 @@ const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
             if (item) {
                 item.open = !item.open;
                 item.active = true;
+                item.toggled = true;
             }
             return tree;
         },
diff --git a/src/views/data-explorer/data-explorer.tsx b/src/views/data-explorer/data-explorer.tsx
new file mode 100644 (file)
index 0000000..5f17b63
--- /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 { DataTableProps } from "../../components/data-table";
+import { RouteComponentProps } from 'react-router';
+import { Project } from '../../models/project';
+import { ProjectState, findTreeItem } from '../../store/project/project-reducer';
+import { RootState } from '../../store/store';
+import { connect, DispatchProp } from 'react-redux';
+import { push } from 'react-router-redux';
+import projectActions from "../../store/project/project-action";
+import { DataExplorer, DataItem } from '../../components/data-explorer';
+import { TreeItem } from '../../components/tree/tree';
+
+interface DataExplorerViewDataProps {
+    projects: ProjectState;
+}
+
+type DataExplorerViewProps = DataExplorerViewDataProps & RouteComponentProps<{ name: string }> & DispatchProp;
+
+type DataExplorerViewState = Pick<DataTableProps<Project>, "columns">;
+
+interface MappedProjectItem extends DataItem {
+    uuid: string;
+}
+
+class DataExplorerView extends React.Component<DataExplorerViewProps, DataExplorerViewState> {
+
+    render() {
+        const project = findTreeItem(this.props.projects, this.props.match.params.name);
+        const projectItems = project && project.items || [];
+        return (
+            <DataExplorer
+                items={projectItems.map(mapTreeItem)}
+                onItemClick={this.goToProject}
+            />
+        );
+    }
+
+    goToProject = (project: MappedProjectItem) => {
+        this.props.dispatch(push(`/project/${project.uuid}`));
+        this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(project.uuid));
+    }
+
+}
+
+const mapTreeItem = (item: TreeItem<Project>): MappedProjectItem => ({
+    name: item.data.name,
+    type: item.data.kind,
+    owner: item.data.ownerUuid,
+    lastModified: item.data.modifiedAt,
+    uuid: item.data.uuid
+});
+
+
+export default connect(
+    (state: RootState) => ({
+        projects: state.projects
+    })
+)(DataExplorerView);
index cb691869b21c17ca9c406d251233ce3a1839023c..0aecc0d2b578e7045c4a6a37a892c5492cf81e65 100644 (file)
@@ -6,31 +6,21 @@ import * as React from 'react';
 
 import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
 import Drawer from '@material-ui/core/Drawer';
-import AppBar from '@material-ui/core/AppBar';
-import Toolbar from '@material-ui/core/Toolbar';
-import Typography from '@material-ui/core/Typography';
 import { connect, DispatchProp } from "react-redux";
-import ProjectList from "../../components/project-list/project-list";
 import { Route, Switch } from "react-router";
-import { Link } from "react-router-dom";
-import Button from "@material-ui/core/Button/Button";
 import authActions from "../../store/auth/auth-action";
-import IconButton from "@material-ui/core/IconButton/IconButton";
-import Menu from "@material-ui/core/Menu/Menu";
-import MenuItem from "@material-ui/core/MenuItem/MenuItem";
-import { AccountCircle } from "@material-ui/icons";
 import { User } from "../../models/user";
-import Grid from "@material-ui/core/Grid/Grid";
 import { RootState } from "../../store/store";
-import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItems, MainAppBarMenuItem } from '../../components/main-app-bar/main-app-bar';
+import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItem } from '../../components/main-app-bar/main-app-bar';
 import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
 import { push } from 'react-router-redux';
 import projectActions from "../../store/project/project-action";
 import ProjectTree from '../../components/project-tree/project-tree';
-import { TreeItem } from "../../components/tree/tree";
+import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 import { Project } from "../../models/project";
 import { projectService } from '../../services/services';
 import { findTreeBranch } from '../../store/project/project-reducer';
+import DataExplorer from '../data-explorer/data-explorer';
 
 const drawerWidth = 240;
 
@@ -78,6 +68,7 @@ type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp &
 
 interface NavBreadcrumb extends Breadcrumb {
     itemId: string;
+    status: TreeItemStatus;
 }
 
 interface NavMenuItem extends MainAppBarMenuItem {
@@ -128,8 +119,8 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
 
 
     mainAppBarActions: MainAppBarActionProps = {
-        onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
-            this.toggleProjectTreeItem(itemId);
+        onBreadcrumbClick: ({ itemId, status }: NavBreadcrumb) => {
+            this.toggleProjectTreeItem(itemId, status);
         },
         onSearch: searchText => {
             this.setState({ searchText });
@@ -138,17 +129,25 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
         onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action()
     };
 
-    toggleProjectTreeItem = (itemId: string) => {
+    toggleProjectTreeItem = (itemId: string, status: TreeItemStatus) => {
+        if (status === TreeItemStatus.Loaded) {
+            this.openProjectItem(itemId);
+        } else {
+            this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => this.openProjectItem(itemId));
+        }
+    }
+
+    openProjectItem = (itemId: string) => {
         const branch = findTreeBranch(this.props.projects, itemId);
         this.setState({
             breadcrumbs: branch.map(item => ({
                 label: item.data.name,
-                itemId: item.data.uuid
+                itemId: item.data.uuid,
+                status: item.status
             }))
         });
-        this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => {
-            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
-        });
+        this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
+        this.props.dispatch(push(`/project/${itemId}`));
     }
 
     render() {
@@ -179,7 +178,7 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                     <div className={classes.toolbar} />
                     <div className={classes.toolbar} />
                     <Switch>
-                        <Route path="/project/:name" component={ProjectList} />
+                        <Route path="/project/:name" component={DataExplorer} />
                     </Switch>
                 </main>
             </div>
index 309773f01a2af6e57d7aecc169a7b80d1e076620..eee6c8604b3bf2a54334135c3ef643abbb5037e8 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.49"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.49.tgz#becd805482734440c9d137e46d77340e64d7f51b"
+  version "7.0.0-beta.51"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.51.tgz#bd71d9b192af978df915829d39d4094456439a0c"
   dependencies:
-    "@babel/highlight" "7.0.0-beta.49"
+    "@babel/highlight" "7.0.0-beta.51"
 
-"@babel/highlight@7.0.0-beta.49":
-  version "7.0.0-beta.49"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.49.tgz#96bdc6b43e13482012ba6691b1018492d39622cc"
+"@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"
   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.49"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.49.tgz#03b3bf07eb982072c8e851dd2ddd5110282e61bf"
+  version "7.0.0-beta.51"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.51.tgz#48b8ed18307034c6620f643514650ca2ccc0165a"
   dependencies:
-    core-js "^2.5.6"
+    core-js "^2.5.7"
     regenerator-runtime "^0.11.1"
 
-"@material-ui/core@1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.2.0.tgz#ec372fd44f949faa604c4ccd4b7ee0bc5e08ac8c"
+"@material-ui/core@1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.2.1.tgz#f8c73da10b875762b37be7167ec2ac79b027499f"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.42"
     "@types/jss" "^9.5.3"
@@ -55,7 +55,7 @@
     scroll "^2.0.3"
     warning "^4.0.1"
 
-"@material-ui/icons@^1.1.0":
+"@material-ui/icons@1.1.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-1.1.0.tgz#4d025df7b0ba6ace8d6710079ed76013a4d26595"
   dependencies:
   version "0.22.7"
   resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.7.tgz#4a92eafedfb2b9f4437d3a4410006d81114c66ce"
 
-"@types/enzyme-adapter-react-16@^1.0.2":
+"@types/enzyme-adapter-react-16@1.0.2":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz#15ae37c64d6221a6f4b3a4aacc357cf773859de4"
   dependencies:
     "@types/enzyme" "*"
 
-"@types/enzyme@*", "@types/enzyme@^3.1.10":
+"@types/enzyme@*", "@types/enzyme@3.1.10":
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.10.tgz#28108a9864e65699751469551a803a35d2e26160"
   dependencies:
@@ -82,9 +82,9 @@
   version "4.6.2"
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
 
-"@types/jest@23.0.0":
-  version "23.0.0"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.0.0.tgz#760cac74f00bb9c3075587716d2b3b4435663bc0"
+"@types/jest@23.1.0":
+  version "23.1.0"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.1.0.tgz#8054dd838ba23dc331794d26456b86c7e50bf0f6"
 
 "@types/jss@^9.5.3":
   version "9.5.3"
     csstype "^2.0.0"
     indefinite-observable "^1.0.1"
 
-"@types/lodash@^4.14.109":
+"@types/lodash@4.14.109":
   version "4.14.109"
   resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.109.tgz#b1c4442239730bf35cabaf493c772b18c045886d"
 
-"@types/node@*", "@types/node@10.3.0":
-  version "10.3.0"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.0.tgz#078516315a84d56216b5d4fed8f75d59d3b16cac"
+"@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/react-dom@16.0.5":
-  version "16.0.5"
-  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.5.tgz#a757457662e3819409229e8f86795ff37b371f96"
+"@types/react-dom@16.0.6":
+  version "16.0.6"
+  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
   dependencies:
     "@types/node" "*"
     "@types/react" "*"
 
-"@types/react-redux@6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.1.tgz#bb8f6cc19d00a999f9d932ab796212ad3921994b"
+"@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"
   dependencies:
     "@types/react" "*"
     redux "^4.0.0"
   dependencies:
     "@types/react" "*"
 
-"@types/react@*", "@types/react@16.3.16":
-  version "16.3.16"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.16.tgz#78fc44a90b45701f50c8a7008f733680ba51fc86"
+"@types/react@*", "@types/react@16.3.18":
+  version "16.3.18"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.18.tgz#bf195aed4d77dc86f06e4c9bb760214a3b822b8d"
   dependencies:
     csstype "^2.2.0"
 
@@ -190,8 +190,8 @@ acorn@^4.0.3:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
 
 acorn@^5.0.0, acorn@^5.3.0:
-  version "5.6.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.1.tgz#c9e50c3e3717cf897f1b071ceadbb543bbc0a8d4"
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
 
 address@1.0.3, address@^1.0.1:
   version "1.0.3"
@@ -215,12 +215,12 @@ 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.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.0.tgz#4c8affdf80887d8f132c9c52ab8a2dc4d0b7b24c"
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.1.tgz#88ebc1263c7133937d108b80c5572e64e1d9322d"
   dependencies:
     fast-deep-equal "^2.0.1"
     fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.3.0"
+    json-schema-traverse "^0.4.1"
     uri-js "^4.2.1"
 
 align-text@^0.1.1, align-text@^0.1.3:
@@ -285,11 +285,11 @@ anymatch@^2.0.0:
     micromatch "^3.1.4"
     normalize-path "^2.1.1"
 
-append-transform@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+append-transform@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab"
   dependencies:
-    default-require-extensions "^1.0.0"
+    default-require-extensions "^2.0.0"
 
 aproba@^1.0.3, aproba@^1.1.1:
   version "1.2.0"
@@ -1427,12 +1427,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.30000848"
-  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000848.tgz#e149c981c72aa20439e3bc12c7cf8b3f7e1237c6"
+  version "1.0.30000856"
+  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000856.tgz#fbebb99abe15a5654fc7747ebb5315bdfde3358f"
 
 caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000792:
-  version "1.0.30000848"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000848.tgz#ec9c0a72ec8f9ef812e4f4b8628625af9c85ade0"
+  version "1.0.30000856"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000856.tgz#ecc16978135a6f219b138991eb62009d25ee8daa"
 
 capture-exit@^1.2.0:
   version "1.2.0"
@@ -1560,8 +1560,8 @@ class-utils@^0.3.5:
     static-extend "^0.1.1"
 
 classnames@^2.2.5:
-  version "2.2.5"
-  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
+  version "2.2.6"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
 
 clean-css@4.1.x:
   version "4.1.11"
@@ -1633,12 +1633,16 @@ collection-visit@^1.0.0:
     object-visit "^1.0.0"
 
 color-convert@^1.3.0, color-convert@^1.9.0:
-  version "1.9.1"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+  version "1.9.2"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.2.tgz#49881b8fba67df12a96bdf3f56c0aab9e7913147"
   dependencies:
-    color-name "^1.1.1"
+    color-name "1.1.1"
+
+color-name@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
 
-color-name@^1.0.0, color-name@^1.1.1:
+color-name@^1.0.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
 
@@ -1691,18 +1695,18 @@ commondir@^1.0.1:
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
 
 compare-versions@^3.1.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.2.1.tgz#a49eb7689d4caaf0b6db5220173fd279614000f7"
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.3.0.tgz#af93ea705a96943f622ab309578b9b90586f39c3"
 
 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:
-  version "2.0.13"
-  resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.13.tgz#0d1020ab924b2fdb4d6279875c7d6daba6baa7a9"
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.14.tgz#326c5f507fbb055f54116782b969a81b67a29da7"
   dependencies:
-    mime-db ">= 1.33.0 < 2"
+    mime-db ">= 1.34.0 < 2"
 
 compression@^1.5.2:
   version "1.7.2"
@@ -1797,7 +1801,7 @@ core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
 
-core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.6:
+core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7:
   version "2.5.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
 
@@ -2081,11 +2085,11 @@ deepmerge@^2.0.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.1.tgz#e862b4e45ea0555072bf51e7fd0d9845170ae768"
 
-default-require-extensions@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+default-require-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7"
   dependencies:
-    strip-bom "^2.0.0"
+    strip-bom "^3.0.0"
 
 define-properties@^1.1.2:
   version "1.1.2"
@@ -2529,8 +2533,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.9.1"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2"
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.10.0.tgz#f647395de22519fbd0d928ffcf1d17e0dec2603e"
   dependencies:
     esprima "^3.1.3"
     estraverse "^4.2.0"
@@ -2804,8 +2808,8 @@ fb-watchman@^2.0.0:
     bser "^2.0.0"
 
 fbjs@^0.8.1, fbjs@^0.8.16:
-  version "0.8.16"
-  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
+  version "0.8.17"
+  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
   dependencies:
     core-js "^1.0.0"
     isomorphic-fetch "^2.1.1"
@@ -2813,7 +2817,7 @@ fbjs@^0.8.1, fbjs@^0.8.16:
     object-assign "^4.1.0"
     promise "^7.1.1"
     setimmediate "^1.0.5"
-    ua-parser-js "^0.7.9"
+    ua-parser-js "^0.7.18"
 
 figures@^2.0.0:
   version "2.0.0"
@@ -3289,8 +3293,8 @@ hash-base@^3.0.0:
     safe-buffer "^5.0.1"
 
 hash.js@^1.0.0, hash.js@^1.0.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.4.tgz#8b50e1f35d51bd01e5ed9ece4dbe3549ccfa0a3c"
   dependencies:
     inherits "^2.0.3"
     minimalistic-assert "^1.0.0"
@@ -3317,9 +3321,9 @@ hmac-drbg@^1.0.0:
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.1"
 
-hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
+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"
 
 home-or-tmp@^2.0.0:
   version "2.0.0"
@@ -3484,8 +3488,8 @@ icss-utils@^2.1.0:
     postcss "^6.0.1"
 
 ieee754@^1.1.4:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
 
 iferr@^0.1.5:
   version "0.1.5"
@@ -3587,7 +3591,7 @@ interpret@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
 
-invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2:
+invariant@^2.0.0, invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
   dependencies:
@@ -3955,10 +3959,10 @@ istanbul-lib-coverage@^1.1.1, istanbul-lib-coverage@^1.1.2, istanbul-lib-coverag
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341"
 
 istanbul-lib-hook@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz#ae556fd5a41a6e8efa0b1002b1e416dfeaf9816c"
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz#f614ec45287b2a8fc4f07f5660af787575601805"
   dependencies:
-    append-transform "^0.4.0"
+    append-transform "^1.0.0"
 
 istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.8.0:
   version "1.10.1"
@@ -4343,6 +4347,10 @@ json-schema-traverse@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
 
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+
 json-schema@0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
@@ -4464,8 +4472,8 @@ jss-vendor-prefixer@^7.0.0:
     css-vendor "^0.3.8"
 
 jss@^9.3.3, jss@^9.7.0:
-  version "9.8.2"
-  resolved "https://registry.yarnpkg.com/jss/-/jss-9.8.2.tgz#09cabdfba831545bf094e399cfa45a1743daf4a6"
+  version "9.8.3"
+  resolved "https://registry.yarnpkg.com/jss/-/jss-9.8.3.tgz#399da571c4b2c8f4cf418ca7e8627e44fc287fc8"
   dependencies:
     is-in-browser "^1.1.3"
     symbol-observable "^1.1.0"
@@ -4818,7 +4826,7 @@ miller-rabin@^4.0.0:
     bn.js "^4.0.0"
     brorand "^1.0.1"
 
-"mime-db@>= 1.33.0 < 2":
+"mime-db@>= 1.34.0 < 2":
   version "1.34.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.34.0.tgz#452d0ecff5c30346a6dc1e64b1eaee0d3719ff9a"
 
@@ -5167,8 +5175,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.1"
-  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.1.tgz#a50d59a2dcb14b6931401171713ced2d0eb3468f"
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.3.tgz#3f4010d6c943f34018d3dfb5f2fbc0de90476959"
 
 oauth-sign@~0.8.2:
   version "0.8.2"
@@ -5351,8 +5359,8 @@ p-finally@^1.0.0:
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
 
 p-limit@^1.1.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
   dependencies:
     p-try "^1.0.0"
 
@@ -5905,7 +5913,7 @@ promise@^7.1.1:
   dependencies:
     asap "~2.0.3"
 
-prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1:
+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"
   dependencies:
@@ -5928,6 +5936,10 @@ pseudomap@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
 
+psl@^1.1.24:
+  version "1.1.28"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.28.tgz#4fb6ceb08a1e2214d4fd4de0ca22dae13740bc7b"
+
 public-encrypt@^4.0.0:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994"
@@ -6085,9 +6097,9 @@ react-dev-utils@^5.0.1:
     strip-ansi "3.0.1"
     text-table "0.2.0"
 
-react-dom@16.4.0:
-  version "16.4.0"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e"
+react-dom@16.4.1:
+  version "16.4.1"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.1.0"
@@ -6099,22 +6111,22 @@ react-error-overlay@^4.0.0:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
 
 react-event-listener@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.0.tgz#f8cf2821f5ca1844e0df1dac1c7b9a3ecb686fd7"
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.1.tgz#41c7a80a66b398c27dd511e22712b02f3d4eccca"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.42"
     prop-types "^15.6.0"
-    warning "^3.0.0"
+    warning "^4.0.1"
 
-react-is@^16.4.0:
-  version "16.4.0"
-  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf"
+react-is@^16.4.1:
+  version "16.4.1"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
 
 react-jss@^8.1.0:
-  version "8.4.0"
-  resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.4.0.tgz#7cb43d85dea56afafc8f0fd072ae27fcc0518950"
+  version "8.5.1"
+  resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.5.1.tgz#f97c72f6a1c86aa6408932a2a2836ce40c0ab9fc"
   dependencies:
-    hoist-non-react-statics "^2.3.1"
+    hoist-non-react-statics "^2.5.0"
     jss "^9.7.0"
     jss-preset-default "^4.3.0"
     prop-types "^15.6.0"
@@ -6151,16 +6163,16 @@ react-redux@5.0.7:
     loose-envify "^1.1.0"
     prop-types "^15.6.0"
 
-react-router-dom@4.2.2:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"
+react-router-dom@4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6"
   dependencies:
     history "^4.7.2"
-    invariant "^2.2.2"
+    invariant "^2.2.4"
     loose-envify "^1.3.1"
-    prop-types "^15.5.4"
-    react-router "^4.2.0"
-    warning "^3.0.0"
+    prop-types "^15.6.1"
+    react-router "^4.3.1"
+    warning "^4.0.1"
 
 react-router-redux@5.0.0-alpha.9:
   version "5.0.0-alpha.9"
@@ -6170,17 +6182,17 @@ react-router-redux@5.0.0-alpha.9:
     prop-types "^15.6.0"
     react-router "^4.2.0"
 
-react-router@4.2.0, react-router@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986"
+react-router@4.3.1, react-router@^4.2.0, react-router@^4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e"
   dependencies:
     history "^4.7.2"
-    hoist-non-react-statics "^2.3.0"
-    invariant "^2.2.2"
+    hoist-non-react-statics "^2.5.0"
+    invariant "^2.2.4"
     loose-envify "^1.3.1"
     path-to-regexp "^1.7.0"
-    prop-types "^15.5.4"
-    warning "^3.0.0"
+    prop-types "^15.6.1"
+    warning "^4.0.1"
 
 react-scripts-ts@2.16.0:
   version "2.16.0"
@@ -6227,13 +6239,13 @@ react-scripts-ts@2.16.0:
     fsevents "^1.1.3"
 
 react-test-renderer@^16.0.0-0:
-  version "16.4.0"
-  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.0.tgz#0dbe0e24263e94e1830c7afb1f403707fad313a3"
+  version "16.4.1"
+  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70"
   dependencies:
     fbjs "^0.8.16"
     object-assign "^4.1.1"
     prop-types "^15.6.0"
-    react-is "^16.4.0"
+    react-is "^16.4.1"
 
 react-transition-group@^2.2.1:
   version "2.3.1"
@@ -6243,9 +6255,9 @@ react-transition-group@^2.2.1:
     loose-envify "^1.3.1"
     prop-types "^15.6.1"
 
-react@16.4.0:
-  version "16.4.0"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585"
+react@16.4.1:
+  version "16.4.1"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.1.0"
@@ -6582,8 +6594,8 @@ resolve@1.6.0:
     path-parse "^1.0.5"
 
 resolve@^1.1.7, resolve@^1.3.2:
-  version "1.7.1"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.0.tgz#a7f2ac27b78480ecc09c83782741d9f26e4f0c3e"
   dependencies:
     path-parse "^1.0.5"
 
@@ -6664,7 +6676,7 @@ safe-regex@^1.1.0:
   dependencies:
     ret "~0.1.10"
 
-"safer-buffer@>= 2.1.2 < 3":
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
 
@@ -7009,13 +7021,14 @@ sprintf-js@~1.0.2:
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
 
 sshpk@^1.7.0:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb"
+  version "1.14.2"
+  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
   dependencies:
     asn1 "~0.2.3"
     assert-plus "^1.0.0"
     dashdash "^1.12.0"
     getpass "^0.1.1"
+    safer-buffer "^2.0.2"
   optionalDependencies:
     bcrypt-pbkdf "^1.0.0"
     ecc-jsbn "~0.1.1"
@@ -7362,7 +7375,14 @@ toposort@^1.0.0:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
 
-tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.3:
+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"
+  dependencies:
+    psl "^1.1.24"
+    punycode "^1.4.1"
+
+tough-cookie@~2.3.3:
   version "2.3.4"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
   dependencies:
@@ -7414,8 +7434,8 @@ tsconfig-paths-webpack-plugin@^2.0.0:
     tsconfig-paths "^3.1.1"
 
 tsconfig-paths@^3.1.1:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.3.2.tgz#bb48b845e221a44387be0f9968ee6c37c2a37c4d"
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.4.0.tgz#d19fe80c5b245f99d17363471971eab54e65a8a7"
   dependencies:
     deepmerge "^2.0.1"
     minimist "^1.2.0"
@@ -7490,11 +7510,11 @@ typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
-typescript@2.9.1:
-  version "2.9.1"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1.tgz#fdb19d2c67a15d11995fd15640e373e09ab09961"
+typescript@2.9.2:
+  version "2.9.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
 
-ua-parser-js@^0.7.9:
+ua-parser-js@^0.7.18:
   version "0.7.18"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
 
@@ -7667,8 +7687,8 @@ url-parse-lax@^1.0.0:
     prepend-http "^1.0.1"
 
 url-parse@^1.1.8, url-parse@~1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.0.tgz#6bfdaad60098c7fe06f623e42b22de62de0d3d75"
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.1.tgz#4dec9dad3dc8585f862fed461d2e19bbf623df30"
   dependencies:
     querystringify "^2.0.0"
     requires-port "^1.0.0"
@@ -7924,8 +7944,8 @@ whatwg-mimetype@^2.0.0, whatwg-mimetype@^2.1.0:
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f0f21d76cbba72362eb609dbed2a30cd17fcc7d4"
 
 whatwg-url@^6.4.0, whatwg-url@^6.4.1:
-  version "6.4.1"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.1.tgz#fdb94b440fd4ad836202c16e9737d511f012fd67"
+  version "6.5.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
   dependencies:
     lodash.sortby "^4.7.0"
     tr46 "^1.0.1"