Merge branch 'origin/master' into 13666-data-explorer-mapper
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 26 Jun 2018 06:09:29 +0000 (08:09 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 26 Jun 2018 06:09:29 +0000 (08:09 +0200)
Feature #13666

# Conflicts:
# src/components/tree/tree.tsx
# src/store/project/project-reducer.ts
# src/views-components/data-explorer/data-explorer.tsx
# src/views-components/project-explorer/project-explorer-item.ts
# src/views-components/project-tree/project-tree.tsx
# src/views/data-explorer/data-explorer-selectors.ts
# src/views/data-explorer/data-explorer.tsx
# src/views/workbench/workbench.tsx

Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

1  2 
src/index.tsx
src/store/navigation/navigation-action.ts
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
src/views-components/project-explorer/project-explorer.tsx
src/views/project-panel/project-panel-selectors.ts
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

diff --cc src/index.tsx
Simple merge
index 80318ec7c6625d5774301687d7c62f897b84b9c0,0000000000000000000000000000000000000000..6d1754d92cd827f3d191f6ff1510c50a1bd541af
mode 100644,000000..100644
--- /dev/null
@@@ -1,47 -1,0 +1,65 @@@
-         dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(resource.uuid));
 +// 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 { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 +import { getCollectionList } from "../collection/collection-action";
 +import { findTreeItem } from "../project/project-reducer";
 +import { Project } from "../../models/project";
 +import { Resource, ResourceKind } from "../../models/resource";
++import sidePanelActions from "../side-panel/side-panel-action";
 +
 +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 const setProjectItem = (projects: Array<TreeItem<Project>>, itemId: string, itemKind: ResourceKind) => (dispatch: Dispatch) => {
 +
 +    const openProjectItem = (resource: Resource) => {
++        dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid));
++        dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(resource.uuid));
++        dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid));
 +        dispatch(push(getResourceUrl({...resource, kind: itemKind})));
 +    };
 +    const treeItem = findTreeItem(projects, itemId);
 +
 +    if (treeItem) {
 +        if (treeItem.status === TreeItemStatus.Loaded) {
 +            openProjectItem(treeItem.data);
 +        } else {
 +            dispatch<any>(getProjectList(itemId))
 +                .then(() => openProjectItem(treeItem.data));
 +        }
 +        dispatch<any>(getCollectionList(itemId));
 +
 +        // if (item.type === ResourceKind.PROJECT || item.type === ResourceKind.LEVEL_UP) {
 +        //     this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(item.uuid));
 +        // }
 +        // this.props.dispatch<any>(getCollectionList(item.uuid));
 +
 +    }
 +};
++
++    // 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));
++    //             });
++    //     }
++    // }
index 8082809b08465b474bcfd8cab41b15117d672021,e8d6afc6154dd004af9729042bbd9d48f7265bff..65a856bddb8720109cf8d75f044d346c955c4de6
@@@ -39,26 -38,149 +39,154 @@@ describe('project-reducer', () => 
          const projects = [project, project];
          const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
          expect(state).toEqual([{
 -        const initialState = [
 -            {
+             active: false,
+             open: false,
+             id: "test123",
+             items: [],
+             data: project,
+             status: 0
+         }, {
+             active: false,
+             open: false,
+             id: "test123",
+             items: [],
+             data: project,
+             status: 0
+         }
+         ]);
+     });
+     it('should remove activity on projects list', () => {
 -                    kind: 'example'
++        const initialState = {
++            items: [{
+                 data: {
+                     name: 'test',
+                     href: 'href',
+                     createdAt: '2018-01-01',
+                     modifiedAt: '2018-01-01',
+                     ownerUuid: 'owner-test123',
+                     uuid: 'test123',
 -            }
 -        ];
 -        const project = [
 -            {
++                    kind: ResourceKind.PROJECT
+                 },
+                 id: "1",
+                 open: true,
+                 active: true,
+                 status: 1
 -                    kind: 'example'
++            }],
++            currentItemId: "1"
++        };
++        const project = {
++            items: [{
+                 data: {
+                     name: 'test',
+                     href: 'href',
+                     createdAt: '2018-01-01',
+                     modifiedAt: '2018-01-01',
+                     ownerUuid: 'owner-test123',
+                     uuid: 'test123',
++                    kind: ResourceKind.PROJECT
+                 },
+                 id: "1",
+                 open: true,
                  active: false,
-                 open: false,
-                 id: "test123",
-                 items: [],
-                 data: project,
-                 status: 0
-             }, {
+                 status: 1
 -            }
 -        ];
++            }],
++            currentItemId: "1"
++        };
+         const state = projectsReducer(initialState, actions.RESET_PROJECT_TREE_ACTIVITY(initialState[0].id));
+         expect(state).toEqual(project);
+     });
+     it('should toggle project tree item activity', () => {
 -        const initialState = [
 -            {
++        const initialState = {
++            items: [{
+                 data: {
+                     name: 'test',
+                     href: 'href',
+                     createdAt: '2018-01-01',
+                     modifiedAt: '2018-01-01',
+                     ownerUuid: 'owner-test123',
+                     uuid: 'test123',
 -                    kind: 'example'
++                    kind: ResourceKind.PROJECT
+                 },
+                 id: "1",
+                 open: true,
                  active: false,
 -            }
 -        ];
 -        const project = [
 -            {
+                 status: 1
 -                    kind: 'example'
++            }],
++            currentItemId: "1"
++        };
++        const project = {
++            items: [{
+                 data: {
+                     name: 'test',
+                     href: 'href',
+                     createdAt: '2018-01-01',
+                     modifiedAt: '2018-01-01',
+                     ownerUuid: 'owner-test123',
+                     uuid: 'test123',
 -            }
 -        ];
++                    kind: ResourceKind.PROJECT
+                 },
+                 id: "1",
+                 open: true,
+                 active: true,
+                 status: 1
 -        const initialState = [
 -            {
++            }],
++            currentItemId: "1"
++        };
+         const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState[0].id));
+         expect(state).toEqual(project);
+     });
+     it('should close project tree item ', () => {
 -                    kind: 'example'
++        const initialState = {
++            items: [{
+                 data: {
+                     name: 'test',
+                     href: 'href',
+                     createdAt: '2018-01-01',
+                     modifiedAt: '2018-01-01',
+                     ownerUuid: 'owner-test123',
+                     uuid: 'test123',
 -            }
 -        ];
 -        const project = [
 -            {
++                    kind: ResourceKind.PROJECT
+                 },
+                 id: "1",
+                 open: true,
+                 active: false,
+                 status: 1,
+                 toggled: false,
 -                    kind: 'example'
++            }],
++            currentItemId: "1"
++        };
++        const project = {
++            items: [{
+                 data: {
+                     name: 'test',
+                     href: 'href',
+                     createdAt: '2018-01-01',
+                     modifiedAt: '2018-01-01',
+                     ownerUuid: 'owner-test123',
+                     uuid: 'test123',
++                    kind: ResourceKind.PROJECT
+                 },
+                 id: "1",
                  open: false,
-                 id: "test123",
-                 items: [],
-                 data: project,
-                 status: 0
-             }
-         ]);
+                 active: false,
+                 status: 1,
+                 toggled: true
 -            }
 -        ];
++            }],
++            currentItemId: "1"
++        };
+         const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState[0].id));
+         expect(state).toEqual(project);
      });
  });
  
  describe("findTreeBranch", () => {
--
      const createTreeItem = (id: string, items?: Array<TreeItem<string>>): TreeItem<string> => ({
          id,
          items,
index a40d48d4bfd0424d2171f43d4cf580c94938c735,48db05df77eb6051fcc5c84509fe317777ad7f26..39d194ddbd96435dccea29116faa2739dd1653eb
@@@ -5,12 -7,8 +7,11 @@@ import * as _ from "lodash"
  import { Project } from "../../models/project";
  import actions, { ProjectAction } from "./project-action";
  import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
- import * as _ from "lodash";
  
 -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;
@@@ -84,50 -69,44 +85,68 @@@ function updateProjectTree(tree: Array<
      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: itemId => {
+         TOGGLE_PROJECT_TREE_ITEM_OPEN: itemId => {
 -            const tree = _.cloneDeep(state);
 -            const item = findTreeItem(tree, itemId);
 +            const items = _.cloneDeep(state.items);
-             resetTreeActivity(items);
 +            const item = findTreeItem(items, itemId);
              if (item) {
+                 item.toggled = true;
                  item.open = !item.open;
 -            return tree;
+             }
 -            const tree = _.cloneDeep(state);
 -            resetTreeActivity(tree);
 -            const item = findTreeItem(tree, itemId);
++            return {
++                ...state,
++                items
++            };
+         },
+         TOGGLE_PROJECT_TREE_ITEM_ACTIVE: itemId => {
++            const items = _.cloneDeep(state.items);
++            resetTreeActivity(items);
++            const item = findTreeItem(items, itemId);
+             if (item) {
                  item.active = true;
-                 item.toggled = true;
              }
 -            return tree;
 +            return {
 +                items,
 +                currentItemId: itemId
 +            };
          },
 -            const tree = _.cloneDeep(state);
 -            resetTreeActivity(tree);
 -            return tree;
+         RESET_PROJECT_TREE_ACTIVITY: () => {
++            const items = _.cloneDeep(state.items);
++            resetTreeActivity(items);
++            return {
++                ...state,
++                items
++            };
+         },
          default: () => state
      });
  };
index 6053e0378a28864e05f1f270e82a425c30a95061,6089caf35cdf409d77ceb5ede5ced2ebc4083967..40b24a049d7bcb05cf320e466307a773e848a691
@@@ -6,9 -6,11 +6,11 @@@ import { createStore, applyMiddleware, 
  import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
  import thunkMiddleware from 'redux-thunk';
  import { History } from "history";
  import projectsReducer, { ProjectState } from "./project/project-reducer";
+ import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer';
  import authReducer, { AuthState } from "./auth/auth-reducer";
 -import collectionsReducer from "./collection/collection-reducer";
 +import collectionsReducer, { CollectionState } from "./collection/collection-reducer";
  
  const composeEnhancers =
      (process.env.NODE_ENV === 'development' &&
@@@ -18,8 -20,8 +20,9 @@@
  export interface RootState {
      auth: AuthState;
      projects: ProjectState;
 +    collections: CollectionState;
      router: RouterState;
+     sidePanel: SidePanelState;
  }
  
  const rootReducer = combineReducers({
index cde2bd99f1d420b7d23da78e512fdb9ca4878e84,055c22cfeabe89adf593fb4e4e8e435a7ea0006d..4fa3d3d67ceccfabf20462450a8367a1e9199b2b
@@@ -2,13 -2,10 +2,13 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- export interface DataItem {
 +import { getResourceKind, Resource, ResourceKind } from "../../models/resource";
 +
+ export interface ProjectExplorerItem {
      uuid: string;
      name: string;
 -    type: string;
 +    kind: ResourceKind;
 +    url: string;
      owner: string;
      lastModified: string;
      fileSize?: number;
index 0000000000000000000000000000000000000000,4931c09a5149b67c1b3f5b300a6bf3b8798da093..1018ef5e5fa428a351226ff8cf4efdc21b6d2412
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,224 +1,229 @@@
 -            render: item => renderType(item.type)
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+ import * as React from 'react';
+ import { ProjectExplorerItem } from './project-explorer-item';
+ import { Grid, Typography } from '@material-ui/core';
+ import { formatDate, formatFileSize } from '../../common/formatters';
+ import DataExplorer from '../../components/data-explorer/data-explorer';
+ import { DataColumn, toggleSortDirection, resetSortDirection } 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 { ResourceKind } from "../../models/resource";
+ export interface ProjectExplorerContextActions {
+     onAddToFavourite: (item: ProjectExplorerItem) => void;
+     onCopy: (item: ProjectExplorerItem) => void;
+     onDownload: (item: ProjectExplorerItem) => void;
+     onMoveTo: (item: ProjectExplorerItem) => void;
+     onRemove: (item: ProjectExplorerItem) => void;
+     onRename: (item: ProjectExplorerItem) => void;
+     onShare: (item: ProjectExplorerItem) => void;
+ }
+ interface ProjectExplorerProps {
+     items: ProjectExplorerItem[];
++    onRowClick: (item: ProjectExplorerItem) => void;
+ }
+ interface ProjectExplorerState {
+     columns: Array<DataColumn<ProjectExplorerItem>>;
+     searchValue: string;
+     page: number;
+     rowsPerPage: number;
+ }
+ class ProjectExplorer extends React.Component<ProjectExplorerProps, ProjectExplorerState> {
+     state: ProjectExplorerState = {
+         searchValue: "",
+         page: 0,
+         rowsPerPage: 10,
+         columns: [{
+             name: "Name",
+             selected: true,
+             sortDirection: "asc",
+             render: renderName
+         }, {
+             name: "Status",
+             selected: true,
+             filters: [{
+                 name: "In progress",
+                 selected: true
+             }, {
+                 name: "Complete",
+                 selected: true
+             }],
+             render: renderStatus
+         }, {
+             name: "Type",
+             selected: true,
+             filters: [{
+                 name: "Collection",
+                 selected: true
+             }, {
+                 name: "Group",
+                 selected: true
+             }],
 -            onRowClick={console.log}
++            render: item => renderType(item.kind)
+         }, {
+             name: "Owner",
+             selected: true,
+             render: item => renderOwner(item.owner)
+         }, {
+             name: "File size",
+             selected: true,
+             sortDirection: "none",
+             render: item => renderFileSize(item.fileSize)
+         }, {
+             name: "Last modified",
+             selected: true,
+             render: item => renderDate(item.lastModified)
+         }]
+     };
+     contextMenuActions = [[{
+         icon: "fas fa-users fa-fw",
+         name: "Share"
+     }, {
+         icon: "fas fa-sign-out-alt fa-fw",
+         name: "Move to"
+     }, {
+         icon: "fas fa-star fa-fw",
+         name: "Add to favourite"
+     }, {
+         icon: "fas fa-edit fa-fw",
+         name: "Rename"
+     }, {
+         icon: "fas fa-copy fa-fw",
+         name: "Make a copy"
+     }, {
+         icon: "fas fa-download fa-fw",
+         name: "Download"
+     }], [{
+         icon: "fas fa-trash-alt fa-fw",
+         name: "Remove"
+     }
+     ]];
+     render() {
+         return <DataExplorer
+             items={this.props.items}
+             columns={this.state.columns}
+             contextActions={this.contextMenuActions}
+             searchValue={this.state.searchValue}
+             page={this.state.page}
+             rowsPerPage={this.state.rowsPerPage}
+             onColumnToggle={this.toggleColumn}
+             onFiltersChange={this.changeFilters}
 -    switch (item.type) {
 -        case "arvados#group":
 -            return <i className="fas fa-folder fa-lg" />;
 -        case "arvados#groupList":
 -            return <i className="fas fa-th fa-lg" />;
++            onRowClick={this.props.onRowClick}
+             onSortToggle={this.toggleSort}
+             onSearch={this.search}
+             onContextAction={this.executeAction}
+             onChangePage={this.changePage}
+             onChangeRowsPerPage={this.changeRowsPerPage} />;
+     }
+     toggleColumn = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
+         this.setState({
+             columns: this.state.columns.map(column =>
+                 column.name === toggledColumn.name
+                     ? { ...column, selected: !column.selected }
+                     : column
+             )
+         });
+     }
+     toggleSort = (toggledColumn: DataColumn<ProjectExplorerItem>) => {
+         this.setState({
+             columns: this.state.columns.map(column =>
+                 column.name === toggledColumn.name
+                     ? toggleSortDirection(column)
+                     : resetSortDirection(column)
+             )
+         });
+     }
+     changeFilters = (filters: DataTableFilterItem[], updatedColumn: DataColumn<ProjectExplorerItem>) => {
+         this.setState({
+             columns: this.state.columns.map(column =>
+                 column.name === updatedColumn.name
+                     ? { ...column, filters }
+                     : column
+             )
+         });
+     }
+     executeAction = (action: ContextMenuAction, item: ProjectExplorerItem) => {
+         alert(`Executing ${action.name} on ${item.name}`);
+     }
+     search = (searchValue: string) => {
+         this.setState({ searchValue });
+     }
+     changePage = (page: number) => {
+         this.setState({ page });
+     }
+     changeRowsPerPage = (rowsPerPage: number) => {
+         this.setState({ rowsPerPage });
+     }
+ }
+ const renderName = (item: ProjectExplorerItem) =>
+     <Grid
+         container
+         alignItems="center"
+         wrap="nowrap"
+         spacing={16}>
+         <Grid item>
+             {renderIcon(item)}
+         </Grid>
+         <Grid item>
+             <Typography color="primary">
+                 {item.name}
+             </Typography>
+         </Grid>
+     </Grid>;
++
+ const renderIcon = (item: ProjectExplorerItem) => {
++    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 ResourceKind.COLLECTION:
++            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 = (item: ProjectExplorerItem) =>
+     <Typography noWrap align="center">
+         {item.status || "-"}
+     </Typography>;
+ export default ProjectExplorer;
index ceb87d22cd637ea1a565ea853b8cf3d6273b1e6f,610f2fa95522178ba1a0b4f2ff63bf1eb1a5ecd9..c798ec3db2976396685bddd5bb60c63ba7312258
@@@ -4,55 -4,12 +4,55 @@@
  
  import { TreeItem } from "../../components/tree/tree";
  import { Project } from "../../models/project";
- import { DataItem } from "../../views-components/data-explorer/data-item";
 +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 { ProjectExplorerItem } from "../../views-components/project-explorer/project-explorer-item";
  
- export const projectExplorerItems = (projects: Array<TreeItem<Project>>, treeItemId: string, collections: Array<Collection>): DataItem[] => {
-     const dataItems: DataItem[] = [];
 -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
 -});
++export const projectExplorerItems = (projects: Array<TreeItem<Project>>, treeItemId: string, collections: Array<Collection>): ProjectExplorerItem[] => {
++    const dataItems: ProjectExplorerItem[] = [];
 +
 +    const treeItem = findTreeItem(projects, treeItemId);
 +    if (treeItem) {
 +        dataItems.push({
 +            name: "..",
 +            url: getResourceUrl(treeItem.data),
 +            kind: ResourceKind.LEVEL_UP,
 +            owner: treeItem.data.ownerUuid,
 +            uuid: treeItem.data.uuid,
 +            lastModified: treeItem.data.modifiedAt
 +        });
 +
 +        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 DataItem;
++                } as ProjectExplorerItem;
 +
 +                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 DataItem;
++        } as ProjectExplorerItem;
 +
 +        dataItems.push(item);
 +    });
 +
 +    return dataItems;
 +};
 +
index 0000000000000000000000000000000000000000,f9e6c8b8e2c0b7f50ce690d0a445de582f71460c..68cbc8ec73758b169a343b1f1581fc71333db3f1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,34 +1,48 @@@
 -import { RouteComponentProps } from 'react-router-dom';
 -import { DispatchProp, connect } from 'react-redux';
 -import { ProjectState, findTreeItem } from '../../store/project/project-reducer';
 -import ProjectExplorer from '../../views-components/project-explorer/project-explorer';
+ // Copyright (C) The Arvados Authors. All rights reserved.
+ //
+ // SPDX-License-Identifier: AGPL-3.0
+ import * as React from 'react';
 -import { mapProjectTreeItem } from './project-panel-selectors';
++import { RouteComponentProps } from 'react-router';
++import { ProjectState } from '../../store/project/project-reducer';
+ import { RootState } from '../../store/store';
 -
++import { connect, DispatchProp } from 'react-redux';
++import { CollectionState } from "../../store/collection/collection-reducer";
++import { setProjectItem } from "../../store/navigation/navigation-action";
++import ProjectExplorer, { ProjectExplorerContextActions } from "../../views-components/project-explorer/project-explorer";
++import { projectExplorerItems } from "./project-panel-selectors";
++import { ProjectExplorerItem } from "../../views-components/project-explorer/project-explorer-item";
+ interface ProjectPanelDataProps {
+     projects: ProjectState;
++    collections: CollectionState;
+ }
+ type ProjectPanelProps = ProjectPanelDataProps & RouteComponentProps<{ name: string }> & DispatchProp;
+ class ProjectPanel extends React.Component<ProjectPanelProps> {
 -        const project = findTreeItem(this.props.projects, this.props.match.params.name);
 -        const projectItems = project && project.items || [];
+     render() {
 -            <ProjectExplorer items={projectItems.map(mapProjectTreeItem)} />
++        const items = projectExplorerItems(
++            this.props.projects.items,
++            this.props.projects.currentItemId,
++            this.props.collections
++        );
+         return (
 -        projects: state.projects
++            <ProjectExplorer
++                items={items}
++                onRowClick={this.goToItem}
++            />
+         );
+     }
++
++    goToItem = (item: ProjectExplorerItem) => {
++        this.props.dispatch<any>(setProjectItem(this.props.projects.items, item.uuid, item.kind));
++    }
+ }
+ export default connect(
+     (state: RootState) => ({
++        projects: state.projects,
++        collections: state.collections
+     })
+ )(ProjectPanel);
index aed281567cef07936e50019a3ee6a57eed72c754,4f9843cb0a3e952786ef3489de8ac29b477796ee..b6a8457a6fd3e482f23b04d6e0deae6fb13c5d32
@@@ -11,19 -10,18 +10,21 @@@ import { Route, Switch } from "react-ro
  import authActions from "../../store/auth/auth-action";
  import { User } from "../../models/user";
  import { RootState } from "../../store/store";
 -import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
 +import MainAppBar, {
 +    MainAppBarActionProps,
 +    MainAppBarMenuItem
 +} from '../../views-components/main-app-bar/main-app-bar';
  import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs';
  import { push } from 'react-router-redux';
 -import 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 } from '../../store/project/project-reducer';
- import DataExplorer from '../data-explorer/data-explorer';
- import { setProjectItem } from "../../store/navigation/navigation-action";
+ import ProjectPanel from '../project-panel/project-panel';
+ import sidePanelActions from '../../store/side-panel/side-panel-action';
 -import { projectService } from '../../services/services';
+ import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
 +import { ResourceKind } from "../../models/resource";
++import { setProjectItem } from "../../store/navigation/navigation-action";
  
  const drawerWidth = 240;
  const appBarHeight = 102;
@@@ -67,8 -67,8 +70,9 @@@ const styles: StyleRulesCallback<CssRul
  
  interface WorkbenchDataProps {
      projects: Array<TreeItem<Project>>;
 +    currentProjectId: string;
      user?: User;
+     sidePanelItems: SidePanelItem[];
  }
  
  interface WorkbenchActionProps {
@@@ -136,15 -139,60 +140,24 @@@ class Workbench extends React.Component
          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));
+     }
+     toggleSidePanelActive = (itemId: string) => {
+         this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
 -        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}`));
++        // this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
+     }
      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}>
                          {...this.mainAppBarActions}
                      />
                  </div>
-                 {user && <Drawer
-                     variant="permanent"
-                     classes={{
-                         paper: classes.drawerPaper,
-                     }}>
-                     <div className={classes.toolbar} />
-                     <ProjectTree
-                         projects={this.props.projects}
-                         toggleProjectTreeItem={itemId =>
-                             this.props.dispatch<any>(
-                                 setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT)
-                             )}/>
-                 </Drawer>}
+                 {user &&
+                     <Drawer
+                         variant="permanent"
+                         classes={{
+                             paper: classes.drawerPaper,
+                         }}>
+                         <div className={classes.toolbar} />
+                         <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(this.props.projects, itemId, ResourceKind.PROJECT)
++                                    )}
++                                toggleActive={itemId =>
++                                    this.props.dispatch<any>(
++                                        setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT)
++                                    )}
++                            />
+                         </SidePanel>
+                     </Drawer>}
                  <main className={classes.contentWrapper}>
                      <div className={classes.content}>
                          <Switch>
          );
      }
  }
++/*
++                    <ProjectTree
++                        projects={this.props.projects}
++                        toggleProjectTreeItem={itemId =>
++                            this.props.dispatch<any>(
++                                setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT)
++                            )}/>
++*/
  
  export default connect<WorkbenchDataProps>(
      (state: RootState) => ({
 -        projects: state.projects,
 +        projects: state.projects.items,
 +        currentProjectId: state.projects.currentItemId,
-         user: state.auth.user
+         user: state.auth.user,
 -        sidePanelItems: state.sidePanel,
++        sidePanelItems: state.sidePanel
      })
  )(
      withStyles(styles)(Workbench)