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>

1  2 
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.test.tsx
src/store/navigation/navigation-action.ts
src/store/store.ts
src/views/project-panel/project-panel-item.ts
src/views/project-panel/project-panel-selectors.ts
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

index 98f6f868c6f19defe2a3da55d4bfb46344328ce6,557b0158ee365a33f34f70e38a61f04071b46dcc..de9cb45d47235baccc41d92cdbe0dad9da48a4e5
@@@ -3,11 -3,11 +3,11 @@@
  // 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";
 -import DataTable from "../../components/data-table/data-table";
 +import DataTable, { DataColumns } from "../../components/data-table/data-table";
  import { mockAnchorFromMouseEvent } from "../../components/popover/helpers";
  import { DataColumn } from "../../components/data-table/data-column";
  import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters';
@@@ -15,11 -15,10 +15,11 @@@ import SearchInput from '../search-inpu
  
  interface DataExplorerProps<T> {
      items: T[];
 -    columns: Array<DataColumn<T>>;
 +    columns: DataColumns<T>;
      contextActions: ContextMenuActionGroup[];
      searchValue: string;
      rowsPerPage: number;
 +    rowsPerPageOptions?: number[];
      page: number;
      onSearch: (value: string) => void;
      onRowClick: (item: T) => void;
@@@ -51,18 -50,16 +51,16 @@@ class DataExplorer<T> extends React.Com
                  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={[
@@@ -79,7 -76,6 +77,7 @@@
                          <TablePagination
                              count={this.props.items.length}
                              rowsPerPage={this.props.rowsPerPage}
 +                            rowsPerPageOptions={this.props.rowsPerPageOptions}
                              page={this.props.page}
                              onChangePage={this.changePage}
                              onChangeRowsPerPage={this.changeRowsPerPage}
@@@ -92,6 -88,7 +90,7 @@@
  
      openContextMenu = (event: React.MouseEvent<HTMLElement>, item: T) => {
          event.preventDefault();
+         event.stopPropagation();
          this.setState({
              contextMenu: {
                  anchorEl: mockAnchorFromMouseEvent(event),
  
      renderContextMenuTrigger = (item: T) =>
          <Grid container justify="flex-end">
 -            <IconButton onClick={event => this.openContextMenu(event, item)}>
 +            <IconButton onClick={event => this.openContextMenuWithTrigger(event, item)}>
                  <MoreVertIcon />
              </IconButton>
          </Grid>
  
 +    openContextMenuWithTrigger = (event: React.MouseEvent<HTMLElement>, item: T) => {
 +        event.preventDefault();
 +        this.setState({
 +            contextMenu: {
 +                anchorEl: event.currentTarget,
 +                item
 +            }
 +        });
 +    }
 +
      contextMenuColumn = {
          name: "Actions",
          selected: true,
          key: "context-actions",
          renderHeader: () => null,
-         render: this.renderContextMenuTrigger
+         render: this.renderContextMenuTrigger,
+         width: "auto"
      };
  
  }
index 77979af6901ea19b3c6d3ce7b702f5289b09dd4f,726972e0282f7774b2b5592243cf964f7fcbbddb..b9d112520acc33ec0083d05a6e022161711c08fb
@@@ -6,14 -6,15 +6,14 @@@ import * as React from "react"
  import { mount, configure } from "enzyme";
  import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
  import * as Adapter from "enzyme-adapter-react-16";
 -import DataTable from "./data-table";
 -import { DataColumn } from "./data-column";
 +import DataTable, { DataColumns } from "./data-table";
  import DataTableFilters from "../data-table-filters/data-table-filters";
  
  configure({ adapter: new Adapter() });
  
  describe("<DataTable />", () => {
      it("shows only selected columns", () => {
 -        const columns: Array<DataColumn<string>> = [
 +        const columns: DataColumns<string> = [
              {
                  name: "Column 1",
                  render: () => <span />,
@@@ -41,7 -42,7 +41,7 @@@
      });
  
      it("renders column name", () => {
 -        const columns: Array<DataColumn<string>> = [
 +        const columns: DataColumns<string> = [
              {
                  name: "Column 1",
                  render: () => <span />,
@@@ -59,7 -60,7 +59,7 @@@
      });
  
      it("uses renderHeader instead of name prop", () => {
 -        const columns: Array<DataColumn<string>> = [
 +        const columns: DataColumns<string> = [
              {
                  name: "Column 1",
                  renderHeader: () => <span>Column Header</span>,
@@@ -78,7 -79,7 +78,7 @@@
      });
  
      it("passes column key prop to corresponding cells", () => {
 -        const columns: Array<DataColumn<string>> = [
 +        const columns: DataColumns<string> = [
              {
                  name: "Column 1",
                  key: "column-1-key",
          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: Array<DataColumn<string>> = [
 +        const columns: DataColumns<string> = [
              {
                  name: "Column 1",
                  render: (item) => <Typography>{item}</Typography>,
      });
  
      it("passes sorting props to <TableSortLabel />", () => {
 -        const columns: Array<DataColumn<string>> = [{
 +        const columns: DataColumns<string> = [{
              name: "Column 1",
              sortDirection: "asc",
              selected: true,
      });
  
      it("passes filter props to <DataTableFilter />", () => {
 -        const columns: Array<DataColumn<string>> = [{
 +        const columns: DataColumns<string> = [{
              name: "Column 1",
              sortDirection: "asc",
              selected: true,
index 0000000000000000000000000000000000000000,0b4bcdf87ab033421319eaa472a3ea15449ea296..b811f9a292b8b42bf9cfb6ba8fcdf93db017351d
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,63 +1,80 @@@
 -import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
+ // 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 { Project } from "../../models/project";
++import { TreeItemStatus } from "../../components/tree/tree";
+ import { getCollectionList } from "../collection/collection-action";
+ import { findTreeItem } from "../project/project-reducer";
 -export const setProjectItem = (projects: Array<TreeItem<Project>>, itemId: string, itemKind: ResourceKind, itemMode: ItemMode) => (dispatch: Dispatch) => {
+ 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
+ }
 -    const openProjectItem = (resource: Resource) => {
 -        if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
 -            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid));
++export const setProjectItem = (itemId: string, itemKind = ResourceKind.PROJECT, itemMode = ItemMode.OPEN) =>
++    (dispatch: Dispatch, getState: () => RootState) => {
++        const { projects } = getState();
 -        if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
 -            dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid));
 -        }
++        let treeItem = findTreeItem(projects.items, itemId);
++        if (treeItem && itemKind === ResourceKind.LEVEL_UP) {
++            treeItem = findTreeItem(projects.items, treeItem.data.ownerUuid);
+         }
 -        dispatch(push(getResourceUrl({...resource, kind: itemKind})));
++        if (treeItem) {
++            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
 -    let treeItem = findTreeItem(projects, itemId);
 -    if (treeItem && itemKind === ResourceKind.LEVEL_UP) {
 -        treeItem = findTreeItem(projects, treeItem.data.ownerUuid);
 -    }
++            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));
++            }
++        }
+     };
 -    if (treeItem) {
 -        dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
++const openProjectItem = (resource: Resource, itemKind: ResourceKind, itemMode: ItemMode) =>
++    (dispatch: Dispatch, getState: () => RootState) => {
 -        if (treeItem.status === TreeItemStatus.Loaded) {
 -            openProjectItem(treeItem.data);
 -        } else {
 -            dispatch<any>(getProjectList(itemId))
 -                .then(() => openProjectItem(treeItem!.data));
++        const { collections, projects } = getState();
 -            dispatch<any>(getCollectionList(itemId));
++        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
++            )
++        }));
++    };
diff --combined src/store/store.ts
index 7092c1d9e80c9740d22f73f308e21b999ae894f9,40b24a049d7bcb05cf320e466307a773e848a691..68c5d8238c74894857e08f7d34f8e0df90bcfb9b
@@@ -10,8 -10,7 +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,8 -20,8 +21,9 @@@
  export interface RootState {
      auth: AuthState;
      projects: ProjectState;
+     collections: CollectionState;
      router: RouterState;
 +    dataExplorer: DataExplorerState;
      sidePanel: SidePanelState;
  }
  
@@@ -31,16 -30,15 +32,16 @@@ const rootReducer = combineReducers(
      projects: projectsReducer,
      collections: collectionsReducer,
      router: routerReducer,
 +    dataExplorer: dataExplorerReducer,
      sidePanel: sidePanelReducer
  });
  
  
 -export default function configureStore(initialState: RootState, history: History) {
 +export default function configureStore(history: History) {
      const middlewares: Middleware[] = [
          routerMiddleware(history),
          thunkMiddleware
      ];
      const enhancer = composeEnhancers(applyMiddleware(...middlewares));
 -    return createStore(rootReducer, initialState!, enhancer);
 +    return createStore(rootReducer, enhancer);
  }
index 0000000000000000000000000000000000000000,4fa3d3d67ceccfabf20462450a8367a1e9199b2b..e0eb84f05ad4c16c810dd6a8b9e477b89ae11df7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,27 +1,29 @@@
 -export interface ProjectExplorerItem {
+ // 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
+     };
+ }
index 0000000000000000000000000000000000000000,83bfd603898700502884c20529f7da671e031118..5571e91257653a9f3bfb297fa0601eefa931d262
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,58 +1,58 @@@
 -import { ProjectExplorerItem } from "../../views-components/project-explorer/project-explorer-item";
+ // 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";
 -export const projectExplorerItems = (projects: Array<TreeItem<Project>>, treeItemId: string, collections: Array<Collection>): ProjectExplorerItem[] => {
 -    const dataItems: ProjectExplorerItem[] = [];
++import { ProjectPanelItem } from "./project-panel-item";
 -                } as ProjectExplorerItem;
++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 ProjectExplorerItem;
++                } 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;
+ };
index 16f670cb008d76b8d4cc3baaebe4ef57df32d843,df9721fda775546308052ce26625c1d4480b14dc..dbff20e18717d227909f524ec5748225cbe484eb
  // SPDX-License-Identifier: AGPL-3.0
  
  import * as React from 'react';
- import { ProjectExplorerItem } from './project-explorer-item';
 -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 { ItemMode, setProjectItem } from "../../store/navigation/navigation-action";
 -import ProjectExplorer from "../../views-components/project-explorer/project-explorer";
 -import { projectExplorerItems } from "./project-panel-selectors";
 -import { ProjectExplorerItem } from "../../views-components/project-explorer/project-explorer-item";
 -import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 -import { DataColumn, SortDirection } from '../../components/data-table/data-column';
++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>> {
 -interface ProjectPanelDataProps {
 -    projects: ProjectState;
 -    collections: CollectionState;
 -}
++export const PROJECT_PANEL_ID = "projectPanel";
++class ProjectPanel extends React.Component<DispatchProp & WithStyles<CssRules>> {
 +    render() {
 +        return <div>
 +            <div className={this.props.classes.toolbar}>
 +                <Button color="primary" variant="raised" className={this.props.classes.button}>
 +                    Create a collection
 +                </Button>
 +                <Button color="primary" variant="raised" className={this.props.classes.button}>
 +                    Run a process
 +                </Button>
 +                <Button color="primary" variant="raised" className={this.props.classes.button}>
 +                    Create a project
 +                </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}
 +                onChangePage={this.changePage}
 +                onChangeRowsPerPage={this.changeRowsPerPage} />;
 +        </div>;
 +    }
  
 -type ProjectPanelProps = ProjectPanelDataProps & RouteComponentProps<{ name: string }> & DispatchProp;
 +    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 }));
 -interface ProjectPanelState {
 -    sort: {
 -        columnName: string;
 -        direction: SortDirection;
 -    };
 -    filters: string[];
 -}
++    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 }));
 -class ProjectPanel extends React.Component<ProjectPanelProps & WithStyles<CssRules>, ProjectPanelState> {
 -    state: ProjectPanelState = {
 -        sort: {
 -            columnName: "Name",
 -            direction: "desc"
 -        },
 -        filters: ['collection', 'project']
 -    };
++    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 }));
 -    render() {
 -        const items = projectExplorerItems(
 -            this.props.projects.items,
 -            this.props.projects.currentItemId,
 -            this.props.collections
 -        );
 -        const [goBackItem, ...otherItems] = items;
 -        const filteredItems = otherItems.filter(i => this.state.filters.some(f => f === i.kind));
 -        const sortedItems = sortItems(this.state.sort, filteredItems);
 -        return (
 -            <div>
 -                <div className={this.props.classes.toolbar}>
 -                    <Button color="primary" variant="raised" className={this.props.classes.button}>
 -                        Create a collection
 -                    </Button>
 -                    <Button color="primary" variant="raised" className={this.props.classes.button}>
 -                        Run a process
 -                    </Button>
 -                    <Button color="primary" variant="raised" className={this.props.classes.button}>
 -                        Create a project
 -                    </Button>
 -                </div>
 -                <ProjectExplorer
 -                    items={goBackItem ? [goBackItem, ...sortedItems] : sortedItems}
 -                    onRowClick={this.goToItem}
 -                    onToggleSort={this.toggleSort}
 -                    onChangeFilters={this.changeFilters}
 -                />
 -            </div>
 -        );
++    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) => {
 -    goToItem = (item: ProjectExplorerItem) => {
 -        this.props.dispatch<any>(setProjectItem(this.props.projects.items, item.uuid, item.kind, ItemMode.BOTH));
++    executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => {
 +        alert(`Executing ${action.name} on ${item.name}`);
      }
  
 -    toggleSort = (column: DataColumn<ProjectExplorerItem>) => {
 -        this.setState({
 -            sort: {
 -                columnName: column.name,
 -                direction: column.sortDirection || "none"
 -            }
 -        });
 +    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 }));
      }
  
 -    changeFilters = (filters: DataTableFilterItem[]) => {
 -        this.setState({ filters: filters.filter(f => f.selected).map(f => f.name.toLowerCase()) });
 +    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 }));
      }
 -}
  
 -const sortItems = (sort: { columnName: string, direction: SortDirection }, items: ProjectExplorerItem[]) => {
 -    const sortedItems = items.slice(0);
 -    const direction = sort.direction === "asc" ? -1 : 1;
 -    sortedItems.sort((a, b) => {
 -        if (sort.columnName === "Last modified") {
 -            return ((new Date(a.lastModified)).getTime() - (new Date(b.lastModified)).getTime()) * direction;
 -        } else {
 -            return a.name.localeCompare(b.name) * direction;
 -        }
 -    });
 -    return sortedItems;
 -};
 +    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));
 +    }
 +}
  
  type CssRules = "toolbar" | "button";
  
  const styles: StyleRulesCallback<CssRules> = theme => ({
      toolbar: {
 -        marginBottom: theme.spacing.unit * 3,
 -        display: "flex",
 -        justifyContent: "flex-end"
 +        paddingBottom: theme.spacing.unit * 3,
 +        textAlign: "right"
      },
      button: {
          marginLeft: theme.spacing.unit
      }
  });
  
- const renderName = (item: ProjectExplorerItem) =>
 -export default withStyles(styles)(
 -    connect(
 -        (state: RootState) => ({
 -            projects: state.projects,
 -            collections: state.collections
 -        })
 -    )(ProjectPanel));
++const renderName = (item: ProjectPanelItem) =>
 +    <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.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 />;
 +    }
 +};
 +
 +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) =>
++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,
 +    filters: [{
 +        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 = [[{
 +    icon: "fas fa-users fa-fw",
 +    name: "Share"
 +}, {
 +    icon: "fas fa-sign-out-alt fa-fw",
 +    name: "Move to"
 +}, {
 +    icon: "fas fa-star fa-fw",
 +    name: "Add to favourite"
 +}, {
 +    icon: "fas fa-edit fa-fw",
 +    name: "Rename"
 +}, {
 +    icon: "fas fa-copy fa-fw",
 +    name: "Make a copy"
 +}, {
 +    icon: "fas fa-download fa-fw",
 +    name: "Download"
 +}], [{
 +    icon: "fas fa-trash-alt fa-fw",
 +    name: "Remove"
 +}
 +]];
 +
- export default withStyles(styles)(connect()(ProjectExplorer));
++export default withStyles(styles)(connect()(ProjectPanel));
index 92cbc5d0995b757ab31579eff83c1489afe808e1,1069de530c02986b78d86859d3910d9cfab7b72d..72eb0ddcfefe759677f017667532cf43ce075d22
@@@ -8,21 -8,24 +8,25 @@@ import Drawer from '@material-ui/core/D
  import { connect, DispatchProp } from "react-redux";
  import { Route, Switch } from "react-router";
  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 } from '../../store/project/project-reducer';
 -import ProjectPanel from '../project-panel/project-panel';
 +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 -71,7 +72,7 @@@ const styles: StyleRulesCallback<CssRul
  
  interface WorkbenchDataProps {
      projects: Array<TreeItem<Project>>;
+     currentProjectId: string;
      user?: User;
      sidePanelItems: SidePanelItem[];
  }
@@@ -79,7 -83,6 +84,6 @@@ type WorkbenchProps = WorkbenchDataProp
  
  interface NavBreadcrumb extends Breadcrumb {
      itemId: string;
-     status: TreeItemStatus;
  }
  
  interface NavMenuItem extends MainAppBarMenuItem {
@@@ -88,7 -91,6 +92,6 @@@
  
  interface WorkbenchState {
      anchorEl: any;
-     breadcrumbs: NavBreadcrumb[];
      searchText: string;
      menuItems: {
          accountMenu: NavMenuItem[],
@@@ -128,10 -130,11 +131,9 @@@ class Workbench extends React.Component
          }
      };
  
      mainAppBarActions: MainAppBarActionProps = {
-         onBreadcrumbClick: ({ itemId, status }: NavBreadcrumb) => {
-             this.toggleProjectTreeItemOpen(itemId, status);
+         onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
 -            this.props.dispatch<any>(
 -                setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.BOTH)
 -            );
++            this.props.dispatch<any>(setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.BOTH));
          },
          onSearch: searchText => {
              this.setState({ searchText });
          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));
      }
          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}
                          <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, ItemMode.OPEN)
++                                        setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.OPEN)
+                                     )}
+                                 toggleActive={itemId =>
+                                     this.props.dispatch<any>(
 -                                        setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.ACTIVE)
++                                        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>
  
  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)