Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 26 Jun 2018 16:42:24 +0000 (18:42 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 26 Jun 2018 16:42:24 +0000 (18:42 +0200)
Feature #13678

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

24 files changed:
src/common/formatters.ts
src/components/data-explorer/data-explorer.test.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-column.ts
src/components/data-table/data-table.test.tsx
src/components/data-table/data-table.tsx
src/models/resource.ts
src/services/auth-service/auth-service.ts
src/services/collection-service/collection-service.ts
src/services/project-service/project-service.ts
src/store/collection/collection-reducer.test.ts
src/store/collection/collection-reducer.ts
src/store/navigation/navigation-action.ts [new file with mode: 0644]
src/store/project/project-reducer.test.ts
src/store/project/project-reducer.ts
src/store/store.ts
src/views-components/project-explorer/project-explorer-item.ts [deleted file]
src/views-components/project-tree/project-tree.test.tsx
src/views/project-panel/project-panel-item.ts [new file with mode: 0644]
src/views/project-panel/project-panel-selectors.ts [new file with mode: 0644]
src/views/project-panel/project-panel.tsx [moved from src/views-components/project-explorer/project-explorer.tsx with 64% similarity]
src/views/workbench/workbench.test.tsx
src/views/workbench/workbench.tsx
tslint.json

index 1d9a52012445b1478c9dc3b4d37197dd9237bc74..fe7df14c9d12670a3a9c2f88480b6d6e519a902a 100644 (file)
@@ -4,7 +4,8 @@
 
 export const formatDate = (isoDate: string) => {
     const date = new Date(isoDate);
-    return date.toLocaleString();
+    const text = date.toLocaleString();
+    return text === 'Invalid Date' ? "" : text;
 };
 
 export const formatFileSize = (size?: number) => {
index 1ce564141c1832625f41ac8e3f4c167696d570c6..94c7be6dab933ff6991edda0a56ebf4b1edc0d2d 100644 (file)
@@ -27,7 +27,8 @@ describe("<DataExplorer />", () => {
             columns={[{ name: "Column 1", render: jest.fn(), selected: true }]} />);
         expect(dataExplorer.find(ContextMenu).prop("actions")).toEqual([]);
         dataExplorer.find(DataTable).prop("onRowContextMenu")({
-            preventDefault: jest.fn()
+            preventDefault: jest.fn(),
+            stopPropagation: jest.fn()
         }, "Item 1");
         dataExplorer.find(ContextMenu).prop("onActionClick")({ name: "Action 1", icon: "" });
         expect(onContextAction).toHaveBeenCalledWith({ name: "Action 1", icon: "" }, "Item 1");
@@ -88,7 +89,6 @@ describe("<DataExplorer />", () => {
             items={[]}
         />);
         expect(dataExplorer.find(SearchInput)).toHaveLength(0);
-        expect(dataExplorer.find(ColumnSelector)).toHaveLength(0);
         expect(dataExplorer.find(TablePagination)).toHaveLength(0);
     });
 
index 98f6f868c6f19defe2a3da55d4bfb46344328ce6..de9cb45d47235baccc41d92cdbe0dad9da48a4e5 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles, TablePagination, Table, IconButton } from '@material-ui/core';
+import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, Theme, WithStyles, TablePagination, IconButton } from '@material-ui/core';
 import MoreVertIcon from "@material-ui/icons/MoreVert";
 import ContextMenu, { ContextMenuActionGroup, ContextMenuAction } from "../../components/context-menu/context-menu";
 import ColumnSelector from "../../components/column-selector/column-selector";
@@ -51,18 +51,16 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<
                 onActionClick={this.callAction}
                 onClose={this.closeContextMenu} />
             <Toolbar className={this.props.classes.toolbar}>
-                {this.props.items.length > 0 &&
-                    <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>}
-
+                <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+                    <div className={this.props.classes.searchBox}>
+                        {this.props.items.length > 0 && <SearchInput
+                            value={this.props.searchValue}
+                            onSearch={this.props.onSearch} />}
+                    </div>
+                    <ColumnSelector
+                        columns={this.props.columns}
+                        onColumnToggle={this.props.onColumnToggle} />
+                </Grid>
             </Toolbar>
             <DataTable
                 columns={[
@@ -92,6 +90,7 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<
 
     openContextMenu = (event: React.MouseEvent<HTMLElement>, item: T) => {
         event.preventDefault();
+        event.stopPropagation();
         this.setState({
             contextMenu: {
                 anchorEl: mockAnchorFromMouseEvent(event),
@@ -142,7 +141,8 @@ class DataExplorer<T> extends React.Component<DataExplorerProps<T> & WithStyles<
         selected: true,
         key: "context-actions",
         renderHeader: () => null,
-        render: this.renderContextMenuTrigger
+        render: this.renderContextMenuTrigger,
+        width: "auto"
     };
 
 }
index 1ef7d98fbe5b77dfada2fc45fa261874f6868d4c..bbfea7acd5c6db1e43d224e68d455e5b4e52c491 100644 (file)
@@ -13,6 +13,7 @@ export interface DataColumn<T> {
     filters?: DataTableFilterItem[];
     render: (item: T) => React.ReactElement<void>;
     renderHeader?: () => React.ReactElement<void> | null;
+    width?: string;
 }
 
 export type SortDirection = "asc" | "desc" | "none";
index 77979af6901ea19b3c6d3ce7b702f5289b09dd4f..b9d112520acc33ec0083d05a6e022161711c08fb 100644 (file)
@@ -97,24 +97,6 @@ describe("<DataTable />", () => {
         expect(dataTable.find(TableBody).find(TableCell).key()).toBe("column-1-key");
     });
 
-    it("shows information that items array is empty", () => {
-        const columns: DataColumns<string> = [
-            {
-                name: "Column 1",
-                render: () => <span />,
-                selected: true
-            }
-        ];
-        const dataTable = mount(<DataTable
-            columns={columns}
-            items={[]}
-            onFiltersChange={jest.fn()}
-            onRowClick={jest.fn()}
-            onRowContextMenu={jest.fn()}
-            onSortToggle={jest.fn()} />);
-        expect(dataTable.find(Typography).text()).toBe("No items");
-    });
-
     it("renders items", () => {
         const columns: DataColumns<string> = [
             {
index e86113efdeac14a6e0f1928265b622875f582f19..e8a5b24e02c2fe22488236f1ae703f36ef482c8d 100644 (file)
@@ -22,29 +22,23 @@ class DataTable<T> extends React.Component<DataTableProps<T> & WithStyles<CssRul
     render() {
         const { items, classes } = this.props;
         return <div className={classes.tableContainer}>
-            {items.length > 0 ?
-                <Table>
-                    <TableHead>
-                        <TableRow>
-                            {this.mapVisibleColumns(this.renderHeadCell)}
-                        </TableRow>
-                    </TableHead>
-                    <TableBody className={classes.tableBody}>
-                        {items.map(this.renderBodyRow)}
-                    </TableBody>
-                </Table> : <Typography
-                    className={classes.noItemsInfo}
-                    variant="body2"
-                    gutterBottom>
-                    No items
-                </Typography>}
+            <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}>
+        return <TableCell key={key || index} style={{width: column.width, minWidth: column.width}}>
             {renderHeader ?
                 renderHeader() :
                 filters
@@ -96,7 +90,8 @@ type CssRules = "tableBody" | "tableContainer" | "noItemsInfo";
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     tableContainer: {
-        overflowX: 'auto'
+        overflowX: 'auto',
+        overflowY: 'hidden'
     },
     tableBody: {
         background: theme.palette.background.paper
index 39b4e915cad26f9b4919e38a8c25aadd6e077512..0f5fbc28f68cbd151857c31a59d9b634626a59a2 100644 (file)
@@ -1,3 +1,7 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
 export interface Resource {
     name: string;
     createdAt: string;
@@ -5,5 +9,23 @@ export interface Resource {
     uuid: string;
     ownerUuid: string;
     href: string;
-    kind: string;
+    kind: ResourceKind;
+}
+
+export enum ResourceKind {
+    PROJECT = "project",
+    COLLECTION = "collection",
+    PIPELINE = "pipeline",
+    LEVEL_UP = "",
+    UNKNOWN = "unknown"
+}
+
+export function getResourceKind(itemKind: string) {
+    switch (itemKind) {
+        case "arvados#project": return ResourceKind.PROJECT;
+        case "arvados#collection": return ResourceKind.COLLECTION;
+        case "arvados#pipeline": return ResourceKind.PIPELINE;
+        default:
+            return ResourceKind.UNKNOWN;
+    }
 }
index d71f0299aa6cd866d30da96dd59fd69e17b94f03..e953a75d14aabbcd52a2d61fcf32e260d83717f3 100644 (file)
@@ -35,6 +35,10 @@ export default class AuthService {
         return localStorage.getItem(API_TOKEN_KEY) || undefined;
     }
 
+    public getUuid() {
+        return localStorage.getItem(USER_UUID_KEY) || undefined;
+    }
+
     public getOwnerUuid() {
         return localStorage.getItem(USER_OWNER_UUID_KEY) || undefined;
     }
index 171cd8565ffc503a2137f56476997356fc1e4b11..bc9128171254eebe3b9a42c5cfad5ea86ec56856 100644 (file)
@@ -6,6 +6,7 @@ import { serverApi } from "../../common/api/server-api";
 import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
 import { ArvadosResource } from "../response";
 import { Collection } from "../../models/collection";
+import { getResourceKind } from "../../models/resource";
 
 interface CollectionResource extends ArvadosResource {
     name: string;
@@ -42,7 +43,7 @@ export default class CollectionService {
                     href: g.href,
                     uuid: g.uuid,
                     ownerUuid: g.owner_uuid,
-                    kind: g.kind
+                    kind: getResourceKind(g.kind)
                 } as Collection));
                 return collections;
             });
index bc34081811fdbbdd6aaa14437087ab82549a08fa..5bfa544940434009ec28432966d260c2d62beefb 100644 (file)
@@ -7,6 +7,7 @@ import { Dispatch } from "redux";
 import { Project } from "../../models/project";
 import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
 import { ArvadosResource } from "../response";
+import { getResourceKind } from "../../models/resource";
 
 interface GroupResource extends ArvadosResource {
     name: string;
@@ -39,7 +40,7 @@ export default class ProjectService {
                     href: g.href,
                     uuid: g.uuid,
                     ownerUuid: g.owner_uuid,
-                    kind: g.kind
+                    kind: getResourceKind(g.kind)
                 } as Project));
                 return projects;
             });
index 7b57ba7264430e50c9ec29a527f780157ccea2ce..7bc1aec994eb3271690e5a665f0772e9df901418 100644 (file)
@@ -4,6 +4,7 @@
 
 import collectionsReducer from "./collection-reducer";
 import actions from "./collection-action";
+import { ResourceKind } from "../../models/resource";
 
 describe('collection-reducer', () => {
     it('should add new collection to the list', () => {
@@ -15,7 +16,7 @@ describe('collection-reducer', () => {
             modifiedAt: '2018-01-01',
             ownerUuid: 'owner-test123',
             uuid: 'test123',
-            kind: ""
+            kind: ResourceKind.COLLECTION
         };
 
         const state = collectionsReducer(initialState, actions.CREATE_COLLECTION(collection));
@@ -31,7 +32,7 @@ describe('collection-reducer', () => {
             modifiedAt: '2018-01-01',
             ownerUuid: 'owner-test123',
             uuid: 'test123',
-            kind: ""
+            kind: ResourceKind.COLLECTION
         };
 
         const collections = [collection, collection];
index 939ca625bf5627f84e1bb509a6f9049725dd8eb4..5c257ea43bf0cb89dbbb05f3c71dc3131cfb3096 100644 (file)
@@ -13,7 +13,7 @@ const collectionsReducer = (state: CollectionState = [], action: CollectionActio
         CREATE_COLLECTION: collection => [...state, collection],
         REMOVE_COLLECTION: () => state,
         COLLECTIONS_REQUEST: () => {
-            return state;
+            return [];
         },
         COLLECTIONS_SUCCESS: ({ collections }) => {
             return collections;
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
new file mode 100644 (file)
index 0000000..b811f9a
--- /dev/null
@@ -0,0 +1,80 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import projectActions, { getProjectList } from "../project/project-action";
+import { push } from "react-router-redux";
+import { TreeItemStatus } from "../../components/tree/tree";
+import { getCollectionList } from "../collection/collection-action";
+import { findTreeItem } from "../project/project-reducer";
+import { Resource, ResourceKind } from "../../models/resource";
+import sidePanelActions from "../side-panel/side-panel-action";
+import dataExplorerActions from "../data-explorer/data-explorer-action";
+import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { projectPanelItems } from "../../views/project-panel/project-panel-selectors";
+import { RootState } from "../store";
+
+export const getResourceUrl = (resource: Resource): string => {
+    switch (resource.kind) {
+        case ResourceKind.LEVEL_UP: return `/projects/${resource.ownerUuid}`;
+        case ResourceKind.PROJECT: return `/projects/${resource.uuid}`;
+        case ResourceKind.COLLECTION: return `/collections/${resource.uuid}`;
+        default:
+            return "#";
+    }
+};
+
+export enum ItemMode {
+    BOTH,
+    OPEN,
+    ACTIVE
+}
+
+export const setProjectItem = (itemId: string, itemKind = ResourceKind.PROJECT, itemMode = ItemMode.OPEN) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const { projects } = getState();
+
+        let treeItem = findTreeItem(projects.items, itemId);
+        if (treeItem && itemKind === ResourceKind.LEVEL_UP) {
+            treeItem = findTreeItem(projects.items, treeItem.data.ownerUuid);
+        }
+
+        if (treeItem) {
+            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
+
+            if (treeItem.status === TreeItemStatus.Loaded) {
+                dispatch<any>(openProjectItem(treeItem.data, itemKind, itemMode));
+            } else {
+                dispatch<any>(getProjectList(itemId))
+                    .then(() => dispatch<any>(openProjectItem(treeItem!.data, itemKind, itemMode)));
+            }
+            if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
+                dispatch<any>(getCollectionList(itemId));
+            }
+        }
+    };
+
+const openProjectItem = (resource: Resource, itemKind: ResourceKind, itemMode: ItemMode) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+
+        const { collections, projects } = getState();
+
+        if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
+            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid));
+        }
+
+        if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
+            dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid));
+        }
+
+        dispatch(push(getResourceUrl({ ...resource, kind: itemKind })));
+        dispatch(dataExplorerActions.SET_ITEMS({
+            id: PROJECT_PANEL_ID,
+            items: projectPanelItems(
+                projects.items,
+                resource.uuid,
+                collections
+            )
+        }));
+    };
index e8d6afc6154dd004af9729042bbd9d48f7265bff..f4301040d5b165b709092892873245e9c6c3ca1c 100644 (file)
@@ -5,6 +5,7 @@
 import projectsReducer, { getTreePath } from "./project-reducer";
 import actions from "./project-action";
 import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
+import { ResourceKind } from "../../models/resource";
 
 describe('project-reducer', () => {
     it('should add new project to the list', () => {
@@ -16,11 +17,11 @@ describe('project-reducer', () => {
             modifiedAt: '2018-01-01',
             ownerUuid: 'owner-test123',
             uuid: 'test123',
-            kind: ""
+            kind: ResourceKind.PROJECT
         };
 
         const state = projectsReducer(initialState, actions.CREATE_PROJECT(project));
-        expect(state).toEqual([project]);
+        expect(state.items[0].data).toEqual(project);
     });
 
     it('should load projects', () => {
@@ -32,32 +33,35 @@ describe('project-reducer', () => {
             modifiedAt: '2018-01-01',
             ownerUuid: 'owner-test123',
             uuid: 'test123',
-            kind: ""
+            kind: ResourceKind.PROJECT
         };
 
         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,
-            status: 0
-        }, {
-            active: false,
-            open: false,
-            id: "test123",
-            items: [],
-            data: project,
-            status: 0
-        }
-        ]);
+        expect(state).toEqual({
+            items: [{
+                active: false,
+                open: false,
+                id: "test123",
+                items: [],
+                data: project,
+                status: 0
+            }, {
+                active: false,
+                open: false,
+                id: "test123",
+                items: [],
+                data: project,
+                status: 0
+            }
+            ],
+            currentItemId: ""
+        });
     });
 
     it('should remove activity on projects list', () => {
-        const initialState = [
-            {
+        const initialState = {
+            items: [{
                 data: {
                     name: 'test',
                     href: 'href',
@@ -65,16 +69,17 @@ describe('project-reducer', () => {
                     modifiedAt: '2018-01-01',
                     ownerUuid: 'owner-test123',
                     uuid: 'test123',
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "1",
                 open: true,
                 active: true,
                 status: 1
-            }
-        ];
-        const project = [
-            {
+            }],
+            currentItemId: "1"
+        };
+        const project = {
+            items: [{
                 data: {
                     name: 'test',
                     href: 'href',
@@ -82,22 +87,23 @@ describe('project-reducer', () => {
                     modifiedAt: '2018-01-01',
                     ownerUuid: 'owner-test123',
                     uuid: 'test123',
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "1",
                 open: true,
                 active: false,
                 status: 1
-            }
-        ];
+            }],
+            currentItemId: ""
+        };
 
-        const state = projectsReducer(initialState, actions.RESET_PROJECT_TREE_ACTIVITY(initialState[0].id));
+        const state = projectsReducer(initialState, actions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
         expect(state).toEqual(project);
     });
 
     it('should toggle project tree item activity', () => {
-        const initialState = [
-            {
+        const initialState = {
+            items: [{
                 data: {
                     name: 'test',
                     href: 'href',
@@ -105,16 +111,17 @@ describe('project-reducer', () => {
                     modifiedAt: '2018-01-01',
                     ownerUuid: 'owner-test123',
                     uuid: 'test123',
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "1",
                 open: true,
                 active: false,
                 status: 1
-            }
-        ];
-        const project = [
-            {
+            }],
+            currentItemId: "1"
+        };
+        const project = {
+            items: [{
                 data: {
                     name: 'test',
                     href: 'href',
@@ -122,23 +129,24 @@ describe('project-reducer', () => {
                     modifiedAt: '2018-01-01',
                     ownerUuid: 'owner-test123',
                     uuid: 'test123',
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "1",
                 open: true,
                 active: true,
                 status: 1
-            }
-        ];
+            }],
+            currentItemId: "1"
+        };
 
-        const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState[0].id));
+        const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
         expect(state).toEqual(project);
     });
 
 
     it('should close project tree item ', () => {
-        const initialState = [
-            {
+        const initialState = {
+            items: [{
                 data: {
                     name: 'test',
                     href: 'href',
@@ -146,17 +154,18 @@ describe('project-reducer', () => {
                     modifiedAt: '2018-01-01',
                     ownerUuid: 'owner-test123',
                     uuid: 'test123',
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "1",
                 open: true,
                 active: false,
                 status: 1,
                 toggled: false,
-            }
-        ];
-        const project = [
-            {
+            }],
+            currentItemId: "1"
+        };
+        const project = {
+            items: [{
                 data: {
                     name: 'test',
                     href: 'href',
@@ -164,23 +173,23 @@ describe('project-reducer', () => {
                     modifiedAt: '2018-01-01',
                     ownerUuid: 'owner-test123',
                     uuid: 'test123',
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "1",
                 open: false,
                 active: false,
                 status: 1,
                 toggled: true
-            }
-        ];
+            }],
+            currentItemId: "1"
+        };
 
-        const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState[0].id));
+        const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
         expect(state).toEqual(project);
     });
 });
 
 describe("findTreeBranch", () => {
-
     const createTreeItem = (id: string, items?: Array<TreeItem<string>>): TreeItem<string> => ({
         id,
         items,
index 48db05df77eb6051fcc5c84509fe317777ad7f26..0e2018b41c3830784c24b9615ad8b08db40cf70c 100644 (file)
@@ -8,7 +8,10 @@ import { Project } from "../../models/project";
 import actions, { ProjectAction } from "./project-action";
 import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 
-export type ProjectState = Array<TreeItem<Project>>;
+export type ProjectState = {
+    items: Array<TreeItem<Project>>,
+    currentItemId: string
+};
 
 export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
     let item;
@@ -23,8 +26,21 @@ export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeI
     return item;
 }
 
+export function getActiveTreeItem<T>(tree: Array<TreeItem<T>>): TreeItem<T> | undefined {
+    let item;
+    for (const t of tree) {
+        item = t.active
+            ? t
+            : getActiveTreeItem(t.items ? t.items : []);
+        if (item) {
+            break;
+        }
+    }
+    return item;
+}
+
 export function getTreePath<T>(tree: Array<TreeItem<T>>, itemId: string): Array<TreeItem<T>> {
-    for(const item of tree){
+    for (const item of tree){
         if(item.id === itemId){
             return [item];
         } else {
@@ -52,7 +68,7 @@ function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[],
             treeItem.status = TreeItemStatus.Loaded;
         }
     }
-    const items = projects.map((p, idx) => ({
+    const items = projects.map(p => ({
         id: p.uuid,
         open: false,
         active: false,
@@ -69,43 +85,67 @@ function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[],
     return items;
 }
 
-const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
+const projectsReducer = (state: ProjectState = { items: [], currentItemId: "" }, action: ProjectAction) => {
     return actions.match(action, {
-        CREATE_PROJECT: project => [...state, project],
+        CREATE_PROJECT: project => ({
+            ...state,
+            items: state.items.concat({
+                id: project.uuid,
+                open: false,
+                active: false,
+                status: TreeItemStatus.Loaded,
+                toggled: false,
+                items: [],
+                data: project
+            })
+        }),
         REMOVE_PROJECT: () => state,
         PROJECTS_REQUEST: itemId => {
-            const tree = _.cloneDeep(state);
-            const item = findTreeItem(tree, itemId);
+            const items = _.cloneDeep(state.items);
+            const item = findTreeItem(items, itemId);
             if (item) {
                 item.status = TreeItemStatus.Pending;
+                state.items = items;
             }
-            return tree;
+            return state;
         },
         PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
-            return updateProjectTree(state, projects, parentItemId);
+            return {
+                ...state,
+                items: updateProjectTree(state.items, projects, parentItemId)
+            };
         },
         TOGGLE_PROJECT_TREE_ITEM_OPEN: itemId => {
-            const tree = _.cloneDeep(state);
-            const item = findTreeItem(tree, itemId);
+            const items = _.cloneDeep(state.items);
+            const item = findTreeItem(items, itemId);
             if (item) {
                 item.toggled = true;
                 item.open = !item.open;
             }
-            return tree;
+            return {
+                items,
+                currentItemId: itemId
+            };
         },
         TOGGLE_PROJECT_TREE_ITEM_ACTIVE: itemId => {
-            const tree = _.cloneDeep(state);
-            resetTreeActivity(tree);
-            const item = findTreeItem(tree, itemId);
+            const items = _.cloneDeep(state.items);
+            resetTreeActivity(items);
+            const item = findTreeItem(items, itemId);
             if (item) {
                 item.active = true;
             }
-            return tree;
+            return {
+                items,
+                currentItemId: itemId
+            };
         },
         RESET_PROJECT_TREE_ACTIVITY: () => {
-            const tree = _.cloneDeep(state);
-            resetTreeActivity(tree);
-            return tree;
+            const items = _.cloneDeep(state.items);
+            resetTreeActivity(items);
+            return {
+                items,
+                currentItemId: ""
+            };
         },
         default: () => state
     });
index 7092c1d9e80c9740d22f73f308e21b999ae894f9..68c5d8238c74894857e08f7d34f8e0df90bcfb9b 100644 (file)
@@ -10,8 +10,8 @@ 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 collectionsReducer from "./collection/collection-reducer";
 import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer';
+import collectionsReducer, { CollectionState } from "./collection/collection-reducer";
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -21,6 +21,7 @@ const composeEnhancers =
 export interface RootState {
     auth: AuthState;
     projects: ProjectState;
+    collections: CollectionState;
     router: RouterState;
     dataExplorer: DataExplorerState;
     sidePanel: SidePanelState;
diff --git a/src/views-components/project-explorer/project-explorer-item.ts b/src/views-components/project-explorer/project-explorer-item.ts
deleted file mode 100644 (file)
index b7c8707..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { TreeItem } from "../../components/tree/tree";
-import { Project } from "../../models/project";
-
-export interface ProjectExplorerItem {
-    uuid: string;
-    name: string;
-    type: string;
-    owner: string;
-    lastModified: string;
-    fileSize?: number;
-    status?: string;
-}
-
-export const mapProjectTreeItem = (item: TreeItem<Project>): ProjectExplorerItem => ({
-    name: item.data.name,
-    type: item.data.kind,
-    owner: item.data.ownerUuid,
-    lastModified: item.data.modifiedAt,
-    uuid: item.data.uuid
-});
\ No newline at end of file
index 1ba3abb8bb39ddb2c40de1098b769b35bd7ad105..7725da5403810df2fb123cf96b7d563b3b2355fd 100644 (file)
@@ -3,8 +3,8 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { mount } from 'enzyme';
 import * as Enzyme from 'enzyme';
+import { mount } from 'enzyme';
 import * as Adapter from 'enzyme-adapter-react-16';
 import ListItemIcon from '@material-ui/core/ListItemIcon';
 import { Collapse } from '@material-ui/core';
@@ -13,6 +13,8 @@ import CircularProgress from '@material-ui/core/CircularProgress';
 import ProjectTree from './project-tree';
 import { TreeItem } from '../../components/tree/tree';
 import { Project } from '../../models/project';
+import { ResourceKind } from "../../models/resource";
+
 Enzyme.configure({ adapter: new Adapter() });
 
 describe("ProjectTree component", () => {
@@ -26,7 +28,7 @@ describe("ProjectTree component", () => {
                 uuid: "uuid",
                 ownerUuid: "ownerUuid",
                 href: "href",
-                kind: 'example'
+                kind: ResourceKind.PROJECT
             },
             id: "3",
             open: true,
@@ -48,7 +50,7 @@ describe("ProjectTree component", () => {
                     uuid: "uuid",
                     ownerUuid: "ownerUuid",
                     href: "href",
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "3",
                 open: false,
@@ -63,7 +65,7 @@ describe("ProjectTree component", () => {
                     uuid: "uuid",
                     ownerUuid: "ownerUuid",
                     href: "href",
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "3",
                 open: false,
@@ -86,7 +88,7 @@ describe("ProjectTree component", () => {
                     uuid: "uuid",
                     ownerUuid: "ownerUuid",
                     href: "href",
-                    kind: 'example'
+                    kind: ResourceKind.PROJECT
                 },
                 id: "3",
                 open: true,
@@ -101,7 +103,7 @@ describe("ProjectTree component", () => {
                             uuid: "uuid",
                             ownerUuid: "ownerUuid",
                             href: "href",
-                            kind: 'example'
+                            kind: ResourceKind.PROJECT
                         },
                         id: "3",
                         open: true,
@@ -125,7 +127,7 @@ describe("ProjectTree component", () => {
                 uuid: "uuid",
                 ownerUuid: "ownerUuid",
                 href: "href",
-                kind: 'example'
+                kind: ResourceKind.PROJECT
             },
             id: "3",
             open: false,
diff --git a/src/views/project-panel/project-panel-item.ts b/src/views/project-panel/project-panel-item.ts
new file mode 100644 (file)
index 0000000..e0eb84f
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { TreeItem } from "../../components/tree/tree";
+import { Project } from "../../models/project";
+import { getResourceKind, Resource, ResourceKind } from "../../models/resource";
+
+export interface ProjectPanelItem {
+    uuid: string;
+    name: string;
+    kind: ResourceKind;
+    url: string;
+    owner: string;
+    lastModified: string;
+    fileSize?: number;
+    status?: string;
+}
+
+function resourceToDataItem(r: Resource, kind?: ResourceKind) {
+    return {
+        uuid: r.uuid,
+        name: r.name,
+        kind: kind ? kind : getResourceKind(r.kind),
+        owner: r.ownerUuid,
+        lastModified: r.modifiedAt
+    };
+}
+
diff --git a/src/views/project-panel/project-panel-selectors.ts b/src/views/project-panel/project-panel-selectors.ts
new file mode 100644 (file)
index 0000000..5571e91
--- /dev/null
@@ -0,0 +1,58 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { TreeItem } from "../../components/tree/tree";
+import { Project } from "../../models/project";
+import { findTreeItem } from "../../store/project/project-reducer";
+import { ResourceKind } from "../../models/resource";
+import { Collection } from "../../models/collection";
+import { getResourceUrl } from "../../store/navigation/navigation-action";
+import { ProjectPanelItem } from "./project-panel-item";
+
+export const projectPanelItems = (projects: Array<TreeItem<Project>>, treeItemId: string, collections: Array<Collection>): ProjectPanelItem[] => {
+    const dataItems: ProjectPanelItem[] = [];
+
+    const treeItem = findTreeItem(projects, treeItemId);
+    if (treeItem) {
+        dataItems.push({
+            name: "..",
+            url: getResourceUrl(treeItem.data),
+            kind: ResourceKind.LEVEL_UP,
+            owner: "",
+            uuid: treeItem.data.uuid,
+            lastModified: ""
+        });
+
+        if (treeItem.items) {
+            treeItem.items.forEach(p => {
+                const item = {
+                    name: p.data.name,
+                    kind: ResourceKind.PROJECT,
+                    url: getResourceUrl(treeItem.data),
+                    owner: p.data.ownerUuid,
+                    uuid: p.data.uuid,
+                    lastModified: p.data.modifiedAt
+                } as ProjectPanelItem;
+
+                dataItems.push(item);
+            });
+        }
+    }
+
+    collections.forEach(c => {
+        const item = {
+            name: c.name,
+            kind: ResourceKind.COLLECTION,
+            url: getResourceUrl(c),
+            owner: c.ownerUuid,
+            uuid: c.uuid,
+            lastModified: c.modifiedAt
+        } as ProjectPanelItem;
+
+        dataItems.push(item);
+    });
+
+    return dataItems;
+};
+
similarity index 64%
rename from src/views-components/project-explorer/project-explorer.tsx
rename to src/views/project-panel/project-panel.tsx
index 16f670cb008d76b8d4cc3baaebe4ef57df32d843..dbff20e18717d227909f524ec5748225cbe484eb 100644 (file)
@@ -3,19 +3,21 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { ProjectExplorerItem } from './project-explorer-item';
+import { ProjectPanelItem } from './project-panel-item';
 import { Grid, Typography, Button, Toolbar, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { formatDate, formatFileSize } from '../../common/formatters';
-import DataExplorer from '../data-explorer/data-explorer';
-import { DataColumn } from '../../components/data-table/data-column';
+import DataExplorer from "../../views-components/data-explorer/data-explorer";
+import { DataColumn, toggleSortDirection } from '../../components/data-table/data-column';
 import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
 import { ContextMenuAction } from '../../components/context-menu/context-menu';
 import { DispatchProp, connect } from 'react-redux';
 import actions from "../../store/data-explorer/data-explorer-action";
+import { setProjectItem } from "../../store/navigation/navigation-action";
 import { DataColumns } from '../../components/data-table/data-table';
+import { ResourceKind } from "../../models/resource";
 
-export const PROJECT_EXPLORER_ID = "projectExplorer";
-class ProjectExplorer extends React.Component<DispatchProp & WithStyles<CssRules>> {
+export const PROJECT_PANEL_ID = "projectPanel";
+class ProjectPanel extends React.Component<DispatchProp & WithStyles<CssRules>> {
     render() {
         return <div>
             <div className={this.props.classes.toolbar}>
@@ -30,11 +32,11 @@ class ProjectExplorer extends React.Component<DispatchProp & WithStyles<CssRules
                 </Button>
             </div>
             <DataExplorer
-                id={PROJECT_EXPLORER_ID}
+                id={PROJECT_PANEL_ID}
                 contextActions={contextMenuActions}
                 onColumnToggle={this.toggleColumn}
                 onFiltersChange={this.changeFilters}
-                onRowClick={console.log}
+                onRowClick={this.openProject}
                 onSortToggle={this.toggleSort}
                 onSearch={this.search}
                 onContextAction={this.executeAction}
@@ -44,35 +46,39 @@ class ProjectExplorer extends React.Component<DispatchProp & WithStyles<CssRules
     }
 
     componentDidMount() {
-        this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_EXPLORER_ID, columns }));
+        this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
     }
 
-    toggleColumn = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
-        this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_EXPLORER_ID, columnName: toggledColumn.name }));
+    toggleColumn = (toggledColumn: DataColumn<ProjectPanelItem>) => {
+        this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_PANEL_ID, columnName: toggledColumn.name }));
     }
 
-    toggleSort = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
-        this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_EXPLORER_ID, columnName: toggledColumn.name }));
+    toggleSort = (column: DataColumn<ProjectPanelItem>) => {
+        this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_PANEL_ID, columnName: column.name }));
     }
 
-    changeFilters = (filters: DataTableFilterItem[], updatedColumn: DataColumn<ProjectExplorerItem>) => {
-        this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_EXPLORER_ID, columnName: updatedColumn.name, filters }));
+    changeFilters = (filters: DataTableFilterItem[], column: DataColumn<ProjectPanelItem>) => {
+        this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_PANEL_ID, columnName: column.name, filters }));
     }
 
-    executeAction = (action: ContextMenuAction, item: ProjectExplorerItem) => {
+    executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => {
         alert(`Executing ${action.name} on ${item.name}`);
     }
 
     search = (searchValue: string) => {
-        this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_EXPLORER_ID, searchValue }));
+        this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_PANEL_ID, searchValue }));
     }
 
     changePage = (page: number) => {
-        this.props.dispatch(actions.SET_PAGE({ id: PROJECT_EXPLORER_ID, page }));
+        this.props.dispatch(actions.SET_PAGE({ id: PROJECT_PANEL_ID, page }));
     }
 
     changeRowsPerPage = (rowsPerPage: number) => {
-        this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_EXPLORER_ID, rowsPerPage }));
+        this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_PANEL_ID, rowsPerPage }));
+    }
+
+    openProject = (item: ProjectPanelItem) => {
+        this.props.dispatch<any>(setProjectItem(item.uuid));
     }
 }
 
@@ -88,7 +94,7 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     }
 });
 
-const renderName = (item: ProjectExplorerItem) =>
+const renderName = (item: ProjectPanelItem) =>
     <Grid
         container
         alignItems="center"
@@ -104,11 +110,14 @@ const renderName = (item: ProjectExplorerItem) =>
         </Grid>
     </Grid>;
 
-const renderIcon = (item: ProjectExplorerItem) => {
-    switch (item.type) {
-        case "arvados#group":
+
+const renderIcon = (item: ProjectPanelItem) => {
+    switch (item.kind) {
+        case ResourceKind.LEVEL_UP:
+            return <i className="icon-level-up" style={{ fontSize: "1rem" }} />;
+        case ResourceKind.PROJECT:
             return <i className="fas fa-folder fa-lg" />;
-        case "arvados#groupList":
+        case ResourceKind.COLLECTION:
             return <i className="fas fa-th fa-lg" />;
         default:
             return <i />;
@@ -135,27 +144,22 @@ const renderType = (type: string) =>
         {type}
     </Typography>;
 
-const renderStatus = (item: ProjectExplorerItem) =>
+const renderStatus = (item: ProjectPanelItem) =>
     <Typography noWrap align="center">
         {item.status || "-"}
     </Typography>;
 
-const columns: DataColumns<ProjectExplorerItem> = [{
+const columns: DataColumns<ProjectPanelItem> = [{
     name: "Name",
     selected: true,
-    sortDirection: "asc",
-    render: renderName
+    sortDirection: "desc",
+    render: renderName,
+    width: "450px"
 }, {
     name: "Status",
     selected: true,
-    filters: [{
-        name: "In progress",
-        selected: true
-    }, {
-        name: "Complete",
-        selected: true
-    }],
-    render: renderStatus
+    render: renderStatus,
+    width: "75px"
 }, {
     name: "Type",
     selected: true,
@@ -163,23 +167,27 @@ const columns: DataColumns<ProjectExplorerItem> = [{
         name: "Collection",
         selected: true
     }, {
-        name: "Group",
+        name: "Project",
         selected: true
     }],
-    render: item => renderType(item.type)
+    render: item => renderType(item.kind),
+    width: "125px"
 }, {
     name: "Owner",
     selected: true,
-    render: item => renderOwner(item.owner)
+    render: item => renderOwner(item.owner),
+    width: "200px"
 }, {
     name: "File size",
     selected: true,
-    sortDirection: "none",
-    render: item => renderFileSize(item.fileSize)
+    render: item => renderFileSize(item.fileSize),
+    width: "50px"
 }, {
     name: "Last modified",
     selected: true,
-    render: item => renderDate(item.lastModified)
+    sortDirection: "none",
+    render: item => renderDate(item.lastModified),
+    width: "150px"
 }];
 
 const contextMenuActions = [[{
@@ -206,4 +214,4 @@ const contextMenuActions = [[{
 }
 ]];
 
-export default withStyles(styles)(connect()(ProjectExplorer));
+export default withStyles(styles)(connect()(ProjectPanel));
index 6925792293b65c475539ddd9e0a9bf279cb02f9e..6edebaf0887bc5598391256ee813ed6949d2ec94 100644 (file)
@@ -14,7 +14,16 @@ const history = createBrowserHistory();
 
 it('renders without crashing', () => {
     const div = document.createElement('div');
-    const store = configureStore({ projects: [], router: { location: null }, auth: {}, sidePanel: [] }, createBrowserHistory());
+    const store = configureStore({
+        projects: {
+            items: [],
+            currentItemId: ""
+        },
+        collections: [],
+        router: { location: null },
+        auth: {},
+        sidePanel: []
+    }, createBrowserHistory());
     ReactDOM.render(
         <Provider store={store}>
             <ConnectedRouter history={history}>
index 92cbc5d0995b757ab31579eff83c1489afe808e1..72eb0ddcfefe759677f017667532cf43ce075d22 100644 (file)
@@ -11,18 +11,22 @@ import authActions from "../../store/auth/auth-action";
 import dataExplorerActions from "../../store/data-explorer/data-explorer-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 projectActions, { getProjectList } from "../../store/project/project-action";
 import ProjectTree from '../../views-components/project-tree/project-tree';
-import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
+import { TreeItem } from "../../components/tree/tree";
 import { Project } from "../../models/project";
 import { getTreePath, findTreeItem } from '../../store/project/project-reducer';
-import ProjectExplorer, { PROJECT_EXPLORER_ID } from '../../views-components/project-explorer/project-explorer';
-import { ProjectExplorerItem, mapProjectTreeItem } from '../../views-components/project-explorer/project-explorer-item';
 import sidePanelActions from '../../store/side-panel/side-panel-action';
 import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
+import { ResourceKind } from "../../models/resource";
+import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
+import projectActions from "../../store/project/project-action";
+import ProjectPanel from "../project-panel/project-panel";
 
 const drawerWidth = 240;
 const appBarHeight = 102;
@@ -68,6 +72,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
 
 interface WorkbenchDataProps {
     projects: Array<TreeItem<Project>>;
+    currentProjectId: string;
     user?: User;
     sidePanelItems: SidePanelItem[];
 }
@@ -79,7 +84,6 @@ type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp &
 
 interface NavBreadcrumb extends Breadcrumb {
     itemId: string;
-    status: TreeItemStatus;
 }
 
 interface NavMenuItem extends MainAppBarMenuItem {
@@ -88,7 +92,6 @@ interface NavMenuItem extends MainAppBarMenuItem {
 
 interface WorkbenchState {
     anchorEl: any;
-    breadcrumbs: NavBreadcrumb[];
     searchText: string;
     menuItems: {
         accountMenu: NavMenuItem[],
@@ -128,10 +131,9 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
         }
     };
 
-
     mainAppBarActions: MainAppBarActionProps = {
-        onBreadcrumbClick: ({ itemId, status }: NavBreadcrumb) => {
-            this.toggleProjectTreeItemOpen(itemId, status);
+        onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
+            this.props.dispatch<any>(setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.BOTH));
         },
         onSearch: searchText => {
             this.setState({ searchText });
@@ -140,36 +142,6 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
         onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action()
     };
 
-    toggleProjectTreeItemOpen = (itemId: string, status: TreeItemStatus) => {
-        if (status === TreeItemStatus.Loaded) {
-            this.openProjectItem(itemId);
-            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId));
-            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
-        } else {
-            this.props.dispatch<any>(getProjectList(itemId))
-                .then(() => {
-                    this.openProjectItem(itemId);
-                    this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId));
-                    this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
-                });
-        }
-    }
-
-    toggleProjectTreeItemActive = (itemId: string, status: TreeItemStatus) => {
-        if (status === TreeItemStatus.Loaded) {
-            this.openProjectItem(itemId);
-            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
-            this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
-        } else {
-            this.props.dispatch<any>(getProjectList(itemId))
-                .then(() => {
-                    this.openProjectItem(itemId);
-                    this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
-                    this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
-                });
-        }
-    }
-
     toggleSidePanelOpen = (itemId: string) => {
         this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
     }
@@ -179,32 +151,20 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
         this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
     }
 
-    openProjectItem = (itemId: string) => {
-        const branch = getTreePath(this.props.projects, itemId);
-        this.setState({
-            breadcrumbs: branch.map(item => ({
-                label: item.data.name,
-                itemId: item.data.uuid,
-                status: item.status
-            }))
-        });
-        this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
-        this.props.dispatch(push(`/project/${itemId}`));
-
-        const project = findTreeItem(this.props.projects, itemId);
-        const items: ProjectExplorerItem[] = project && project.items
-            ? project.items.map(mapProjectTreeItem)
-            : [];
-        this.props.dispatch(dataExplorerActions.SET_ITEMS({ id: PROJECT_EXPLORER_ID, items }));
-    }
-
     render() {
-        const { classes, user, projects, sidePanelItems } = this.props;
+        const branch = getTreePath(this.props.projects, this.props.currentProjectId);
+        const breadcrumbs = branch.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={this.state.breadcrumbs}
+                        breadcrumbs={breadcrumbs}
                         searchText={this.state.searchText}
                         user={this.props.user}
                         menuItems={this.state.menuItems}
@@ -221,17 +181,24 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                         <SidePanel
                             toggleOpen={this.toggleSidePanelOpen}
                             toggleActive={this.toggleSidePanelActive}
-                            sidePanelItems={sidePanelItems}>
+                            sidePanelItems={this.props.sidePanelItems}>
                             <ProjectTree
-                                projects={projects}
-                                toggleOpen={this.toggleProjectTreeItemOpen}
-                                toggleActive={this.toggleProjectTreeItemActive} />
+                                projects={this.props.projects}
+                                toggleOpen={itemId =>
+                                    this.props.dispatch<any>(
+                                        setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.OPEN)
+                                    )}
+                                toggleActive={itemId =>
+                                    this.props.dispatch<any>(
+                                        setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.ACTIVE)
+                                    )}
+                            />
                         </SidePanel>
                     </Drawer>}
                 <main className={classes.contentWrapper}>
                     <div className={classes.content}>
                         <Switch>
-                            <Route path="/project/:name" component={ProjectExplorer} />
+                            <Route path="/projects/:name" component={ProjectPanel} />
                         </Switch>
                     </div>
                 </main>
@@ -242,9 +209,10 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
 
 export default connect<WorkbenchDataProps>(
     (state: RootState) => ({
-        projects: state.projects,
+        projects: state.projects.items,
+        currentProjectId: state.projects.currentItemId,
         user: state.auth.user,
-        sidePanelItems: state.sidePanel,
+        sidePanelItems: state.sidePanel
     })
 )(
     withStyles(styles)(Workbench)
index 4845e4d2c8ad4a60d59e7dc53d785a3227b171f0..0ddfc627b98e0fa2a637b4a0779d4e1101f6d9c7 100644 (file)
@@ -12,7 +12,9 @@
     "no-debugger": false,
     "no-console": false,
     "no-shadowed-variable": false,
-    "semicolon": true
+    "semicolon": true,
+    "array-type": false,
+    "interface-over-type-literal": false
   },
   "linterOptions": {
     "exclude": [