[ARVADOS-WORKBENCH2] created: 1.2.0-126-g540750a

Git user git at public.curoverse.com
Wed Aug 22 17:12:55 EDT 2018


        at  540750a7749cb71ea0a8fde4b7a3689eeaa1c3dd (commit)


commit 540750a7749cb71ea0a8fde4b7a3689eeaa1c3dd
Author: Michal Klobukowski <michal.klobukowski at contractors.roche.com>
Date:   Wed Aug 22 23:12:36 2018 +0200

    Extract major components from workbench
    
    Feature #14102
    
    Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski at contractors.roche.com>

diff --git a/src/store/collection-panel/collection-panel-action.ts b/src/store/collection-panel/collection-panel-action.ts
index 06d4d27..d8ad6d0 100644
--- a/src/store/collection-panel/collection-panel-action.ts
+++ b/src/store/collection-panel/collection-panel-action.ts
@@ -2,16 +2,17 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
 import { loadCollectionFiles } from "./collection-panel-files/collection-panel-files-actions";
-import { CollectionResource } from "~/models/collection";
+import { CollectionResource } from '~/models/collection';
 import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
 import { createTree } from "~/models/tree";
 import { RootState } from "../store";
 import { ServiceRepository } from "~/services/services";
 import { TagResource, TagProperty } from "~/models/tag";
 import { snackbarActions } from "../snackbar/snackbar-actions";
+import { resourcesActions } from "~/store/resources/resources-actions";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 
 export const collectionPanelActions = unionize({
     LOAD_COLLECTION: ofType<{ uuid: string }>(),
@@ -22,22 +23,20 @@ export const collectionPanelActions = unionize({
     CREATE_COLLECTION_TAG_SUCCESS: ofType<{ tag: TagResource }>(),
     DELETE_COLLECTION_TAG: ofType<{ uuid: string }>(),
     DELETE_COLLECTION_TAG_SUCCESS: ofType<{ uuid: string }>()
-}, { tag: 'type', value: 'payload' });
+});
 
 export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
 
 export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
 
 export const loadCollection = (uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
-        return services.collectionService
-            .get(uuid)
-            .then(item => {
-                dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
-                dispatch<any>(loadCollectionFiles(uuid));
-            });
+        const collection = await services.collectionService.get(uuid);
+        dispatch(resourcesActions.SET_RESOURCES([collection]));
+        dispatch<any>(loadCollectionFiles(collection.uuid));
+        dispatch<any>(loadCollectionTags(collection.uuid));
     };
 
 export const loadCollectionTags = (uuid: string) =>
@@ -50,7 +49,6 @@ export const loadCollectionTags = (uuid: string) =>
             });
     };
 
-
 export const createCollectionTag = (data: TagProperty) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(collectionPanelActions.CREATE_COLLECTION_TAG({ data }));
diff --git a/src/store/collections/updater/collection-updater-action.ts b/src/store/collections/updater/collection-updater-action.ts
index 2f520d4..1ca1a83 100644
--- a/src/store/collections/updater/collection-updater-action.ts
+++ b/src/store/collections/updater/collection-updater-action.ts
@@ -2,27 +2,22 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { default as unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
-
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { RootState } from "../../store";
 import { ServiceRepository } from "~/services/services";
 import { CollectionResource } from '~/models/collection';
 import { initialize } from 'redux-form';
 import { collectionPanelActions } from "../../collection-panel/collection-panel-action";
 import { ContextMenuResource } from "../../context-menu/context-menu-reducer";
-import { updateDetails } from "~/store/details-panel/details-panel-action";
+import { resourcesActions } from "~/store/resources/resources-actions";
 
 export const collectionUpdaterActions = unionize({
     OPEN_COLLECTION_UPDATER: ofType<{ uuid: string }>(),
     CLOSE_COLLECTION_UPDATER: ofType<{}>(),
     UPDATE_COLLECTION_SUCCESS: ofType<{}>(),
-}, {
-    tag: 'type',
-    value: 'payload'
 });
 
-
 export const COLLECTION_FORM_NAME = 'collectionEditDialog';
 
 export const openUpdater = (item: ContextMenuResource) =>
@@ -39,11 +34,10 @@ export const updateCollection = (collection: Partial<CollectionResource>) =>
         return services.collectionService
             .update(uuid, collection)
             .then(collection => {
-                    dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
-                    dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS());
-                    dispatch<any>(updateDetails(collection));
-                }
-            );
+                dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
+                dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS());
+                dispatch(resourcesActions.SET_RESOURCES([collection]));
+            });
     };
 
 export type CollectionUpdaterAction = UnionOf<typeof collectionUpdaterActions>;
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 8e5eb1e..b517503 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -2,15 +2,25 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { Dispatch } from 'redux';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
     CLOSE_CONTEXT_MENU: ofType<{}>()
-}, {
-        tag: 'type',
-        value: 'payload'
-    });
+});
 
 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
+
+export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) =>
+    (dispatch: Dispatch) => {
+        event.preventDefault();
+        dispatch(
+            contextMenuActions.OPEN_CONTEXT_MENU({
+                position: { x: event.clientX, y: event.clientY },
+                resource
+            })
+        );
+    };
\ No newline at end of file
diff --git a/src/store/details-panel/details-panel-action.ts b/src/store/details-panel/details-panel-action.ts
index b8021fb..2724a3e 100644
--- a/src/store/details-panel/details-panel-action.ts
+++ b/src/store/details-panel/details-panel-action.ts
@@ -2,48 +2,17 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { unionize, ofType, UnionOf } from "unionize";
-import { Dispatch } from "redux";
-import { Resource, ResourceKind } from "~/models/resource";
-import { RootState } from "../store";
-import { ServiceRepository } from "~/services/services";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 
 export const detailsPanelActions = unionize({
     TOGGLE_DETAILS_PANEL: ofType<{}>(),
-    LOAD_DETAILS: ofType<{ uuid: string, kind: ResourceKind }>(),
-    LOAD_DETAILS_SUCCESS: ofType<{ item: Resource }>(),
-    UPDATE_DETAILS: ofType<{ item: Resource }>()
-}, { tag: 'type', value: 'payload' });
+    LOAD_DETAILS_PANEL: ofType<string>()
+});
 
 export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
 
-export const loadDetails = (uuid: string, kind: ResourceKind) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
-        const item = await getService(services, kind).get(uuid);
-        dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
-    };
-
-export const updateDetails = (item: Resource) => 
-    async (dispatch: Dispatch, getState: () => RootState) => {
-        const currentItem = getState().detailsPanel.item;
-        if (currentItem && (currentItem.uuid === item.uuid)) {
-            dispatch(detailsPanelActions.UPDATE_DETAILS({ item }));
-            dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
-        }
-    };
-
-
-const getService = (services: ServiceRepository, kind: ResourceKind) => {
-    switch (kind) {
-        case ResourceKind.PROJECT:
-            return services.projectService;
-        case ResourceKind.COLLECTION:
-            return services.collectionService;
-        default:
-            return services.projectService;
-    }
-};
+export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+
 
 
 
diff --git a/src/store/details-panel/details-panel-reducer.ts b/src/store/details-panel/details-panel-reducer.ts
index f22add3..091b2fa 100644
--- a/src/store/details-panel/details-panel-reducer.ts
+++ b/src/store/details-panel/details-panel-reducer.ts
@@ -3,21 +3,20 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { detailsPanelActions, DetailsPanelAction } from "./details-panel-action";
-import { Resource } from "~/models/resource";
 
 export interface DetailsPanelState {
-    item: Resource | null;
+    resourceUuid: string;
     isOpened: boolean;
 }
 
 const initialState = {
-    item: null,
+    resourceUuid: '',
     isOpened: false
 };
 
 export const detailsPanelReducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
     detailsPanelActions.match(action, {
         default: () => state,
-        LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
+        LOAD_DETAILS_PANEL: resourceUuid => ({ ...state, resourceUuid }),
         TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
     });
diff --git a/src/store/favorite-panel/favorite-panel-action.ts b/src/store/favorite-panel/favorite-panel-action.ts
index aa1ec8d..067d5ce 100644
--- a/src/store/favorite-panel/favorite-panel-action.ts
+++ b/src/store/favorite-panel/favorite-panel-action.ts
@@ -6,3 +6,5 @@ import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
 
 export const FAVORITE_PANEL_ID = "favoritePanel";
 export const favoritePanelActions = bindDataExplorerActions(FAVORITE_PANEL_ID);
+
+export const loadFavoritePanel = () => favoritePanelActions.REQUEST_ITEMS();
diff --git a/src/store/favorite-panel/favorite-panel-middleware-service.ts b/src/store/favorite-panel/favorite-panel-middleware-service.ts
index 1c2f062..e4be32d 100644
--- a/src/store/favorite-panel/favorite-panel-middleware-service.ts
+++ b/src/store/favorite-panel/favorite-panel-middleware-service.ts
@@ -6,7 +6,6 @@ import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-mi
 import { FavoritePanelColumnNames, FavoritePanelFilter } from "~/views/favorite-panel/favorite-panel";
 import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
-import { FavoritePanelItem, resourceToDataItem } from "~/views/favorite-panel/favorite-panel-item";
 import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
 import { FilterBuilder } from "~/common/api/filter-builder";
@@ -16,6 +15,7 @@ import { Dispatch, MiddlewareAPI } from "redux";
 import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
 import { LinkResource } from "~/models/link";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
+import { resourcesActions } from "~/store/resources/resources-actions";
 
 export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -24,7 +24,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
 
     requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
         const dataExplorer = api.getState().dataExplorer[this.getId()];
-        const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
+        const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
         const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
         const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
 
@@ -55,8 +55,9 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                     .getFilters()
             })
             .then(response => {
+                api.dispatch(resourcesActions.SET_RESOURCES(response.items));
                 api.dispatch(favoritePanelActions.SET_ITEMS({
-                    items: response.items.map(resourceToDataItem),
+                    items: response.items.map(resource => resource.uuid),
                     itemsAvailable: response.itemsAvailable,
                     page: Math.floor(response.offset / response.limit),
                     rowsPerPage: response.limit
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 50ec93d..d440f19 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -8,7 +8,7 @@ import { push } from "react-router-redux";
 import { TreeItemStatus } from "~/components/tree/tree";
 import { findTreeItem } from "../project/project-reducer";
 import { RootState } from "../store";
-import { ResourceKind } from "~/models/resource";
+import { ResourceKind, Resource } from '~/models/resource';
 import { projectPanelActions } from "../project-panel/project-panel-action";
 import { getCollectionUrl } from "~/models/collection";
 import { getProjectUrl, ProjectResource } from "~/models/project";
@@ -17,6 +17,12 @@ import { ServiceRepository } from "~/services/services";
 import { sidePanelActions } from "../side-panel/side-panel-action";
 import { SidePanelId } from "../side-panel/side-panel-reducer";
 import { getUuidObjectType, ObjectTypes } from "~/models/object-types";
+import { getResource } from '~/store/resources/resources';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { loadCollection } from '~/store/collection-panel/collection-panel-action';
+import { GroupContentsResource } from "~/services/groups-service/groups-service";
+import { snackbarActions } from '../snackbar/snackbar-actions';
+import { resourceLabel } from "~/common/labels";
 
 export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
     switch (resourceKind) {
@@ -98,3 +104,47 @@ const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise<any> =>
         return loadBranch(rest, dispatch);
     }
 };
+
+export const navigateToResource = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const resource = getResource(uuid)(getState().resources);
+        resource
+            ? dispatch<any>(getResourceNavigationAction(resource))
+            : dispatch<any>(resourceIsNotLoaded(uuid));
+    };
+
+const getResourceNavigationAction = (resource: Resource) => {
+    switch (resource.kind) {
+        case ResourceKind.COLLECTION:
+            return navigateToCollection(resource);
+        case ResourceKind.PROJECT:
+            return navigateToProject(resource);
+        default:
+            return cannotNavigateToResource(resource);
+    }
+};
+
+export const navigateToProject = ({ uuid }: Resource) =>
+    (dispatch: Dispatch) => {
+        dispatch<any>(setProjectItem(uuid, ItemMode.BOTH));
+        dispatch(loadDetailsPanel(uuid));
+    };
+
+export const navigateToCollection = ({ uuid }: Resource) =>
+    (dispatch: Dispatch) => {
+        dispatch<any>(loadCollection(uuid));
+        dispatch(push(getCollectionUrl(uuid)));
+    };
+
+export const cannotNavigateToResource = ({ kind, uuid }: Resource) =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: `${resourceLabel(kind)} identified by ${uuid} cannot be opened.`,
+        hideDuration: 3000
+    });
+
+
+export const resourceIsNotLoaded = (uuid: string) =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: `Resource identified by ${uuid} is not loaded.`,
+        hideDuration: 3000
+    });
diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts
index 663add3..0196ed4 100644
--- a/src/store/project-panel/project-panel-middleware-service.ts
+++ b/src/store/project-panel/project-panel-middleware-service.ts
@@ -7,7 +7,6 @@ import { ProjectPanelColumnNames, ProjectPanelFilter } from "~/views/project-pan
 import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
-import { ProjectPanelItem, resourceToDataItem } from "~/views/project-panel/project-panel-item";
 import { SortDirection } from "~/components/data-table/data-column";
 import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
 import { FilterBuilder } from "~/common/api/filter-builder";
@@ -16,6 +15,7 @@ import { checkPresenceInFavorites } from "../favorites/favorites-actions";
 import { projectPanelActions } from "./project-panel-action";
 import { Dispatch, MiddlewareAPI } from "redux";
 import { ProjectResource } from "~/models/project";
+import { resourcesActions } from "~/store/resources/resources-actions";
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -25,7 +25,7 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
     requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
         const state = api.getState();
         const dataExplorer = state.dataExplorer[this.getId()];
-        const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
+        const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
         const typeFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
         const statusFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
         const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
@@ -58,8 +58,9 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
                     .getFilters()
             })
             .then(response => {
+                api.dispatch(resourcesActions.SET_RESOURCES(response.items));
                 api.dispatch(projectPanelActions.SET_ITEMS({
-                    items: response.items.map(resourceToDataItem),
+                    items: response.items.map(resource => resource.uuid),
                     itemsAvailable: response.itemsAvailable,
                     page: Math.floor(response.offset / response.limit),
                     rowsPerPage: response.limit
diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts
index 2017658..da58ed2 100644
--- a/src/store/project/project-action.ts
+++ b/src/store/project/project-action.ts
@@ -1,8 +1,8 @@
 // Copyright (C) The Arvados Authors. All rights reserved.
 //
 // SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
 
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { ProjectResource } from "~/models/project";
 import { Dispatch } from "redux";
 import { FilterBuilder } from "~/common/api/filter-builder";
@@ -10,14 +10,15 @@ import { RootState } from "../store";
 import { checkPresenceInFavorites } from "../favorites/favorites-actions";
 import { ServiceRepository } from "~/services/services";
 import { projectPanelActions } from "~/store/project-panel/project-panel-action";
-import { updateDetails } from "~/store/details-panel/details-panel-action";
+import { resourcesActions } from "~/store/resources/resources-actions";
+import { reset } from 'redux-form';
 
 export const projectActions = unionize({
     OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
     CLOSE_PROJECT_CREATOR: ofType<{}>(),
     CREATE_PROJECT: ofType<Partial<ProjectResource>>(),
     CREATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
-    OPEN_PROJECT_UPDATER: ofType<{ uuid: string}>(),
+    OPEN_PROJECT_UPDATER: ofType<{ uuid: string }>(),
     CLOSE_PROJECT_UPDATER: ofType<{}>(),
     UPDATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
     REMOVE_PROJECT: ofType<string>(),
@@ -26,14 +27,11 @@ export const projectActions = unionize({
     TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType<string>(),
     TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<string>(),
     RESET_PROJECT_TREE_ACTIVITY: ofType<string>()
-}, {
-    tag: 'type',
-    value: 'payload'
 });
 
 export const PROJECT_FORM_NAME = 'projectEditDialog';
 
-export const getProjectList = (parentUuid: string = '') => 
+export const getProjectList = (parentUuid: string = '') =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
         return services.projectService.list({
@@ -66,8 +64,16 @@ export const updateProject = (project: Partial<ProjectResource>) =>
                 dispatch(projectActions.UPDATE_PROJECT_SUCCESS(project));
                 dispatch(projectPanelActions.REQUEST_ITEMS());
                 dispatch<any>(getProjectList(project.ownerUuid));
-                dispatch<any>(updateDetails(project));
+                dispatch(resourcesActions.SET_RESOURCES([project]));
             });
     };
 
+export const PROJECT_CREATE_DIALOG = "projectCreateDialog";
+
+export const openProjectCreator = (ownerUuid: string) =>
+    (dispatch: Dispatch) => {
+        dispatch(reset(PROJECT_CREATE_DIALOG));
+        dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid }));
+    };
+
 export type ProjectAction = UnionOf<typeof projectActions>;
diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts
index 01e6004..7f83fb2 100644
--- a/src/views-components/context-menu/action-sets/project-action-set.ts
+++ b/src/views-components/context-menu/action-sets/project-action-set.ts
@@ -2,15 +2,13 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { reset, initialize } from "redux-form";
-
+import { initialize } from "redux-form";
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { projectActions, PROJECT_FORM_NAME } from "~/store/project/project-action";
+import { projectActions, PROJECT_FORM_NAME, openProjectCreator } from '~/store/project/project-action';
 import { NewProjectIcon, RenameIcon, CopyIcon, MoveToIcon } from "~/components/icon/icon";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
-import { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create";
 import { openMoveProjectDialog } from '~/store/move-project-dialog/move-project-dialog';
 import { openProjectCopyDialog } from "~/views-components/project-copy-dialog/project-copy-dialog";
 
@@ -18,10 +16,7 @@ export const projectActionSet: ContextMenuActionSet = [[
     {
         icon: NewProjectIcon,
         name: "New project",
-        execute: (dispatch, resource) => {
-            dispatch(reset(PROJECT_CREATE_DIALOG));
-            dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
-        }
+        execute: (dispatch, resource) => dispatch<any>(openProjectCreator(resource.uuid))
     },
     {
         icon: RenameIcon,
diff --git a/src/views-components/context-menu/action-sets/root-project-action-set.ts b/src/views-components/context-menu/action-sets/root-project-action-set.ts
index de3b954..eb4a9a3 100644
--- a/src/views-components/context-menu/action-sets/root-project-action-set.ts
+++ b/src/views-components/context-menu/action-sets/root-project-action-set.ts
@@ -5,9 +5,8 @@
 import { reset } from "redux-form";
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { projectActions } from "~/store/project/project-action";
+import { openProjectCreator } from "~/store/project/project-action";
 import { collectionCreateActions } from "~/store/collections/creator/collection-creator-action";
-import { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create";
 import { COLLECTION_CREATE_DIALOG } from "../../dialog-create/dialog-collection-create";
 import { NewProjectIcon, CollectionIcon } from "~/components/icon/icon";
 
@@ -15,10 +14,7 @@ export const rootProjectActionSet: ContextMenuActionSet =  [[
     {
         icon: NewProjectIcon,
         name: "New project",
-        execute: (dispatch, resource) => {
-            dispatch(reset(PROJECT_CREATE_DIALOG));
-            dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
-        }
+        execute: (dispatch, resource) => dispatch<any>(openProjectCreator(resource.uuid))
     },
     {
         icon: CollectionIcon,
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 1b07642..abf1839 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -9,9 +9,14 @@ import { ResourceKind } from '~/models/resource';
 import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
 import { formatDate, formatFileSize } from '~/common/formatters';
 import { resourceLabel } from '~/common/labels';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { getResource } from '../../store/resources/resources';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { ProcessResource } from '~/models/process';
 
 
-export const renderName = (item: {name: string; uuid: string, kind: string}) =>
+export const renderName = (item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
             {renderIcon(item)}
@@ -28,8 +33,13 @@ export const renderName = (item: {name: string; uuid: string, kind: string}) =>
         </Grid>
     </Grid>;
 
+export const ResourceName = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return resource || { name: '', uuid: '', kind: '' };
+    })(renderName);
 
-export const renderIcon = (item: {kind: string}) => {
+export const renderIcon = (item: { kind: string }) => {
     switch (item.kind) {
         case ResourceKind.PROJECT:
             return <ProjectIcon />;
@@ -46,22 +56,52 @@ export const renderDate = (date: string) => {
     return <Typography noWrap>{formatDate(date)}</Typography>;
 };
 
+export const ResourceLastModifiedDate = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return { date: resource ? resource.modifiedAt : '' };
+    })((props: { date: string }) => renderDate(props.date));
+
 export const renderFileSize = (fileSize?: number) =>
     <Typography noWrap>
         {formatFileSize(fileSize)}
     </Typography>;
 
+export const ResourceFileSize = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return {};
+    })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
+
 export const renderOwner = (owner: string) =>
     <Typography noWrap color="primary" >
         {owner}
     </Typography>;
 
+export const ResourceOwner = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return { owner: resource ? resource.ownerUuid : '' };
+    })((props: { owner: string }) => renderOwner(props.owner));
+
 export const renderType = (type: string) =>
     <Typography noWrap>
         {resourceLabel(type)}
     </Typography>;
 
-export const renderStatus = (item: {status?: string}) =>
+export const ResourceType = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return { type: resource ? resource.kind : '' };
+    })((props: { type: string }) => renderType(props.type));
+
+export const renderStatus = (item: { status?: string }) =>
     <Typography noWrap align="center" >
         {item.status || "-"}
     </Typography>;
+
+export const ProcessStatus = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as ProcessResource | undefined;
+        return { status: resource ? resource.state : '-' };
+    })((props: { status: string }) => renderType(props.status));
diff --git a/src/views-components/details-panel/details-panel.tsx b/src/views-components/details-panel/details-panel.tsx
index 21d57ae..7aae786 100644
--- a/src/views-components/details-panel/details-panel.tsx
+++ b/src/views-components/details-panel/details-panel.tsx
@@ -20,6 +20,7 @@ import { ProcessDetails } from "./process-details";
 import { EmptyDetails } from "./empty-details";
 import { DetailsData } from "./details-data";
 import { DetailsResource } from "~/models/details";
+import { getResource } from '../../store/resources/resources';
 
 type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
 
@@ -70,10 +71,13 @@ const getItem = (resource: DetailsResource): DetailsData => {
     }
 };
 
-const mapStateToProps = ({ detailsPanel }: RootState) => ({
-    isOpened: detailsPanel.isOpened,
-    item: getItem(detailsPanel.item as DetailsResource)
-});
+const mapStateToProps = ({ detailsPanel, resources }: RootState) => {
+    const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource;
+    return {
+        isOpened: detailsPanel.isOpened,
+        item: getItem(resource)
+    };
+};
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     onCloseDrawer: () => {
@@ -110,7 +114,7 @@ export const DetailsPanel = withStyles(styles)(
                 const { tabsValue } = this.state;
                 return (
                     <Typography component="div"
-                                className={classnames([classes.container, { [classes.opened]: isOpened }])}>
+                        className={classnames([classes.container, { [classes.opened]: isOpened }])}>
                         <Drawer variant="permanent" anchor="right" classes={{ paper: classes.drawerPaper }}>
                             <Typography component="div" className={classes.headerContainer}>
                                 <Grid container alignItems='center' justify='space-around'>
@@ -124,14 +128,14 @@ export const DetailsPanel = withStyles(styles)(
                                     </Grid>
                                     <Grid item>
                                         <IconButton color="inherit" onClick={onCloseDrawer}>
-                                            {<CloseIcon/>}
+                                            {<CloseIcon />}
                                         </IconButton>
                                     </Grid>
                                 </Grid>
                             </Typography>
                             <Tabs value={tabsValue} onChange={this.handleChange}>
-                                <Tab disableRipple label="Details"/>
-                                <Tab disableRipple label="Activity" disabled/>
+                                <Tab disableRipple label="Details" />
+                                <Tab disableRipple label="Activity" disabled />
                             </Tabs>
                             {tabsValue === 0 && this.renderTabContainer(
                                 <Grid container direction="column">
@@ -139,7 +143,7 @@ export const DetailsPanel = withStyles(styles)(
                                 </Grid>
                             )}
                             {tabsValue === 1 && this.renderTabContainer(
-                                <Grid container direction="column"/>
+                                <Grid container direction="column" />
                             )}
                         </Drawer>
                     </Typography>
diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx
index e77114b..d32582e 100644
--- a/src/views-components/dialog-create/dialog-project-create.tsx
+++ b/src/views-components/dialog-create/dialog-project-create.tsx
@@ -10,6 +10,7 @@ import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
 import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '~/validators/validators';
+import { PROJECT_CREATE_DIALOG } from '~/store/project/project-action';
 
 type CssRules = "button" | "lastButton" | "formContainer" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
 
@@ -52,8 +53,6 @@ interface DialogProjectProps {
     pristine: boolean;
 }
 
-export const PROJECT_CREATE_DIALOG = "projectCreateDialog";
-
 export const DialogProjectCreate = compose(
     reduxForm({ form: PROJECT_CREATE_DIALOG }),
     withStyles(styles))(
diff --git a/src/views-components/navigation-panel/navigation-panel.tsx b/src/views-components/navigation-panel/navigation-panel.tsx
new file mode 100644
index 0000000..283e9be
--- /dev/null
+++ b/src/views-components/navigation-panel/navigation-panel.tsx
@@ -0,0 +1,111 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import Drawer from '@material-ui/core/Drawer';
+import { connect } from "react-redux";
+import { ProjectTree } from '~/views-components/project-tree/project-tree';
+import { SidePanel, SidePanelItem } from '~/components/side-panel/side-panel';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { RootState } from '~/store/store';
+import { TreeItem } from '~/components/tree/tree';
+import { ProjectResource } from '~/models/project';
+import { sidePanelActions } from '../../store/side-panel/side-panel-action';
+import { Dispatch } from 'redux';
+import { projectActions } from '~/store/project/project-action';
+import { navigateToResource } from '../../store/navigation/navigation-action';
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+
+
+const DRAWER_WITDH = 240;
+
+type CssRules = 'drawerPaper' | 'toolbar';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    drawerPaper: {
+        position: 'relative',
+        width: DRAWER_WITDH,
+        display: 'flex',
+        flexDirection: 'column',
+    },
+    toolbar: theme.mixins.toolbar
+});
+
+interface NavigationPanelDataProps {
+    projects: Array<TreeItem<ProjectResource>>;
+    sidePanelItems: SidePanelItem[];
+}
+
+interface NavigationPanelActionProps {
+    toggleSidePanelOpen: (panelItemId: string) => void;
+    toggleSidePanelActive: (panelItemId: string) => void;
+    toggleProjectOpen: (projectUuid: string) => void;
+    toggleProjectActive: (projectUuid: string) => void;
+    openRootContextMenu: (event: React.MouseEvent<any>) => void;
+    openProjectContextMenu: (event: React.MouseEvent<any>, item: TreeItem<ProjectResource>) => void;
+}
+
+type NavigationPanelProps = NavigationPanelDataProps & NavigationPanelActionProps & WithStyles<CssRules>;
+
+const mapStateToProps = (state: RootState): NavigationPanelDataProps => ({
+    projects: state.projects.items,
+    sidePanelItems: state.sidePanel
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): NavigationPanelActionProps => ({
+    toggleSidePanelOpen: panelItemId => {
+        dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(panelItemId));
+    },
+    toggleSidePanelActive: panelItemId => {
+        dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(panelItemId));
+
+        // const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
+        // if (panelItem && panelItem.activeAction) {
+        //     panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid());
+        // }
+    },
+    toggleProjectOpen: projectUuid => {
+        dispatch<any>(navigateToResource(projectUuid));
+    },
+    toggleProjectActive: projectUuid => {
+        dispatch<any>(navigateToResource(projectUuid));
+    },
+    openRootContextMenu: event => {
+        dispatch<any>(openContextMenu(event, {
+            uuid: "",
+            name: "",
+            kind: ContextMenuKind.ROOT_PROJECT
+        }));
+    },
+    openProjectContextMenu: (event, item) => {
+        dispatch<any>(openContextMenu(event, {
+            uuid: item.data.uuid,
+            name: item.data.name,
+            kind: ContextMenuKind.PROJECT
+        }));
+    }
+});
+
+export const NavigationPanel = withStyles(styles)(
+    connect(mapStateToProps, mapDispatchToProps)(
+        ({ classes, sidePanelItems, projects, ...actions }: NavigationPanelProps) => <Drawer
+            variant="permanent"
+            classes={{ paper: classes.drawerPaper }}>
+            <div className={classes.toolbar} />
+            <SidePanel
+                toggleOpen={actions.toggleSidePanelOpen}
+                toggleActive={actions.toggleSidePanelOpen}
+                sidePanelItems={sidePanelItems}
+                onContextMenu={actions.openRootContextMenu}>
+                <ProjectTree
+                    projects={projects}
+                    toggleOpen={actions.toggleProjectOpen}
+                    onContextMenu={actions.openProjectContextMenu}
+                    toggleActive={actions.toggleProjectActive} />
+            </SidePanel>
+        </Drawer>
+    )
+);
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index 559d4a9..7621d95 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -20,6 +20,10 @@ import { TagResource } from '~/models/tag';
 import { CollectionTagForm } from './collection-tag-form';
 import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { getResource } from '~/store/resources/resources';
+import { loadCollection } from '../../store/collection-panel/collection-panel-action';
+import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 
 type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
 
@@ -55,81 +59,96 @@ interface CollectionPanelDataProps {
     tags: TagResource[];
 }
 
-interface CollectionPanelActionProps {
-    onItemRouteChange: (collectionId: string) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: CollectionResource) => void;
-}
-
-type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps & DispatchProp
-                            & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
+    & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
 
 export const CollectionPanel = withStyles(styles)(
-    connect((state: RootState) => ({
-        item: state.collectionPanel.item,
-        tags: state.collectionPanel.tags
-    }))(
+    connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
+        const collection = getResource(props.match.params.id)(state.resources);
+        return {
+            item: collection,
+            tags: state.collectionPanel.tags
+        };
+    })(
         class extends React.Component<CollectionPanelProps> {
 
             render() {
-                const { classes, item, tags, onContextMenu } = this.props;
+                const { classes, item, tags } = this.props;
                 return <div>
-                        <Card className={classes.card}>
-                            <CardHeader
-                                avatar={ <CollectionIcon className={classes.iconHeader} /> }
-                                action={
-                                    <IconButton
-                                        aria-label="More options"
-                                        onClick={event => onContextMenu(event, item)}>
-                                        <MoreOptionsIcon />
-                                    </IconButton>
-                                }
-                                title={item && item.name }
-                                subheader={item && item.description} />
-                            <CardContent>
-                                <Grid container direction="column">
-                                    <Grid item xs={6}>
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                                label='Collection UUID'
-                                                value={item && item.uuid}>
-                                            <Tooltip title="Copy uuid">
-                                                <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy() }>
-                                                    <CopyIcon className={classes.copyIcon} />
-                                                </CopyToClipboard>
-                                            </Tooltip>
-                                        </DetailsAttribute>
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
-                                            label='Number of files' value='14' />
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
-                                            label='Content size' value='54 MB' />
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
-                                            label='Owner' value={item && item.ownerUuid} />
-                                    </Grid>
+                    <Card className={classes.card}>
+                        <CardHeader
+                            avatar={<CollectionIcon className={classes.iconHeader} />}
+                            action={
+                                <IconButton
+                                    aria-label="More options"
+                                    onClick={this.handleContextMenu}>
+                                    <MoreOptionsIcon />
+                                </IconButton>
+                            }
+                            title={item && item.name}
+                            subheader={item && item.description} />
+                        <CardContent>
+                            <Grid container direction="column">
+                                <Grid item xs={6}>
+                                    <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                        label='Collection UUID'
+                                        value={item && item.uuid}>
+                                        <Tooltip title="Copy uuid">
+                                            <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy()}>
+                                                <CopyIcon className={classes.copyIcon} />
+                                            </CopyToClipboard>
+                                        </Tooltip>
+                                    </DetailsAttribute>
+                                    <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                        label='Number of files' value='14' />
+                                    <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                        label='Content size' value='54 MB' />
+                                    <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                        label='Owner' value={item && item.ownerUuid} />
                                 </Grid>
-                            </CardContent>
-                        </Card>
+                            </Grid>
+                        </CardContent>
+                    </Card>
 
-                        <Card className={classes.card}>
-                            <CardHeader title="Properties" />
-                            <CardContent>
-                                <Grid container direction="column">
-                                    <Grid item xs={12}><CollectionTagForm /></Grid>
-                                    <Grid item xs={12}>
-                                        {
-                                            tags.map(tag => {
-                                                return <Chip key={tag.etag} className={classes.tag}
-                                                    onDelete={this.handleDelete(tag.uuid)}
-                                                    label={renderTagLabel(tag)}  />;
-                                            })
-                                        }
-                                    </Grid>
+                    <Card className={classes.card}>
+                        <CardHeader title="Properties" />
+                        <CardContent>
+                            <Grid container direction="column">
+                                <Grid item xs={12}><CollectionTagForm /></Grid>
+                                <Grid item xs={12}>
+                                    {
+                                        tags.map(tag => {
+                                            return <Chip key={tag.etag} className={classes.tag}
+                                                onDelete={this.handleDelete(tag.uuid)}
+                                                label={renderTagLabel(tag)} />;
+                                        })
+                                    }
                                 </Grid>
-                            </CardContent>
-                        </Card>
-                        <div className={classes.card}>
-                            <CollectionPanelFiles/>
-                        </div>
-                    </div>;
+                            </Grid>
+                        </CardContent>
+                    </Card>
+                    <div className={classes.card}>
+                        <CollectionPanelFiles />
+                    </div>
+                </div>;
+            }
+
+            handleContextMenu = (event: React.MouseEvent<any>) => {
+                event.preventDefault();
+                const { uuid, name, description } = this.props.item;
+                const resource = {
+                    uuid,
+                    name,
+                    description,
+                    kind: ContextMenuKind.COLLECTION
+                };
+                this.props.dispatch(
+                    contextMenuActions.OPEN_CONTEXT_MENU({
+                        position: { x: event.clientX, y: event.clientY },
+                        resource
+                    })
+                );
             }
 
             handleDelete = (uuid: string) => () => {
@@ -143,9 +162,10 @@ export const CollectionPanel = withStyles(styles)(
                 }));
             }
 
-            componentWillReceiveProps({ match, item, onItemRouteChange }: CollectionPanelProps) {
-                if (!item || match.params.id !== item.uuid) {
-                    onItemRouteChange(match.params.id);
+            componentDidMount() {
+                const { match, item } = this.props;
+                if (!item && match.params.id) {
+                    this.props.dispatch<any>(loadCollection(match.params.id));
                 }
             }
 
diff --git a/src/views/favorite-panel/favorite-panel-item.ts b/src/views/favorite-panel/favorite-panel-item.ts
deleted file mode 100644
index 842b6d6..0000000
--- a/src/views/favorite-panel/favorite-panel-item.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
-import { ResourceKind } from "~/models/resource";
-
-export interface FavoritePanelItem {
-    uuid: string;
-    name: string;
-    kind: string;
-    url: string;
-    owner: string;
-    lastModified: string;
-    fileSize?: number;
-    status?: string;
-}
-
-export function resourceToDataItem(r: GroupContentsResource): FavoritePanelItem {
-    return {
-        uuid: r.uuid,
-        name: r.name,
-        kind: r.kind,
-        url: "",
-        owner: r.ownerUuid,
-        lastModified: r.modifiedAt,
-        status:  r.kind === ResourceKind.PROCESS ? r.state : undefined
-    };
-}
diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx
index 125ea27..dfe107a 100644
--- a/src/views/favorite-panel/favorite-panel.tsx
+++ b/src/views/favorite-panel/favorite-panel.tsx
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { FavoritePanelItem } from './favorite-panel-item';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
@@ -16,9 +15,14 @@ import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
-import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
+import { FAVORITE_PANEL_ID, loadFavoritePanel } from "~/store/favorite-panel/favorite-panel-action";
+import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner, ResourceName } from '~/views-components/data-explorer/renderers';
 import { FavoriteIcon } from '~/components/icon/icon';
+import { Dispatch } from 'redux';
+import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { loadDetailsPanel } from '../../store/details-panel/details-panel-action';
+import { navigateToResource } from '~/store/navigation/navigation-action';
 
 type CssRules = "toolbar" | "button";
 
@@ -45,14 +49,14 @@ export interface FavoritePanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
+export const columns: DataColumns<string, FavoritePanelFilter> = [
     {
         name: FavoritePanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
         filters: [],
-        render: renderName,
+        render: uuid => <ResourceName uuid={uuid} />,
         width: "450px"
     },
     {
@@ -77,7 +81,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
                 type: ContainerRequestState.UNCOMMITTED
             }
         ],
-        render: renderStatus,
+        render: uuid => <ProcessStatus uuid={uuid} />,
         width: "75px"
     },
     {
@@ -102,7 +106,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
                 type: ResourceKind.PROJECT
             }
         ],
-        render: item => renderType(item.kind),
+        render: uuid => <ResourceType uuid={uuid} />,
         width: "125px"
     },
     {
@@ -111,7 +115,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderOwner(item.owner),
+        render: uuid => <ResourceOwner uuid={uuid} />,
         width: "200px"
     },
     {
@@ -120,7 +124,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderFileSize(item.fileSize),
+        render: uuid => <ResourceFileSize uuid={uuid} />,
         width: "50px"
     },
     {
@@ -129,7 +133,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderDate(item.lastModified),
+        render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
         width: "150px"
     }
 ];
@@ -139,18 +143,40 @@ interface FavoritePanelDataProps {
 }
 
 interface FavoritePanelActionProps {
-    onItemClick: (item: FavoritePanelItem) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: FavoritePanelItem) => void;
+    onItemClick: (item: string) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
     onDialogOpen: (ownerUuid: string) => void;
-    onItemDoubleClick: (item: FavoritePanelItem) => void;
-    onItemRouteChange: (itemId: string) => void;
+    onItemDoubleClick: (item: string) => void;
+    onMount: () => void;
 }
 
+const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
+    onContextMenu: (event, resourceUuid) => {
+        event.preventDefault();
+        dispatch(
+            contextMenuActions.OPEN_CONTEXT_MENU({
+                position: { x: event.clientX, y: event.clientY },
+                resource: { name: '', uuid: resourceUuid, kind: ContextMenuKind.RESOURCE }
+            })
+        );
+    },
+    onDialogOpen: (ownerUuid: string) => { return; },
+    onItemClick: (resourceUuid: string) => {
+        dispatch<any>(loadDetailsPanel(resourceUuid));
+    },
+    onItemDoubleClick: uuid => {
+        dispatch<any>(navigateToResource(uuid));
+    },
+    onMount: () => {
+        dispatch(loadFavoritePanel());
+    },
+});
+
 type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
-                        & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+    & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
 export const FavoritePanel = withStyles(styles)(
-    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }), mapDispatchToProps)(
         class extends React.Component<FavoritePanelProps> {
             render() {
                 return <DataExplorer
@@ -159,16 +185,12 @@ export const FavoritePanel = withStyles(styles)(
                     onRowClick={this.props.onItemClick}
                     onRowDoubleClick={this.props.onItemDoubleClick}
                     onContextMenu={this.props.onContextMenu}
-                    extractKey={(item: FavoritePanelItem) => item.uuid}
                     defaultIcon={FavoriteIcon}
-                    defaultMessages={['Your favorites list is empty.']}/>
-                ;
+                    defaultMessages={['Your favorites list is empty.']} />;
             }
 
-            componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: FavoritePanelProps) {
-                if (match.params.id !== currentItemId) {
-                    onItemRouteChange(match.params.id);
-                }
+            componentDidMount() {
+                this.props.onMount();
             }
         }
     )
diff --git a/src/views/project-panel/project-panel-item.ts b/src/views/project-panel/project-panel-item.ts
deleted file mode 100644
index f031859..0000000
--- a/src/views/project-panel/project-panel-item.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
-import { ResourceKind } from "~/models/resource";
-
-export interface ProjectPanelItem {
-    uuid: string;
-    name: string;
-    description?: string;
-    kind: string;
-    url: string;
-    owner: string;
-    lastModified: string;
-    fileSize?: number;
-    status?: string;
-}
-
-export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem {
-    return {
-        uuid: r.uuid,
-        name: r.name,
-        description: r.description,
-        kind: r.kind,
-        url: "",
-        owner: r.ownerUuid,
-        lastModified: r.modifiedAt,
-        status:  r.kind === ResourceKind.PROCESS ? r.state : undefined
-    };
-}
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index 0f958d2..a2ae4cf 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { ProjectPanelItem } from './project-panel-item';
 import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
@@ -16,9 +15,21 @@ import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
-import { restoreBranch } from '~/store/navigation/navigation-action';
+import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
+import { restoreBranch, setProjectItem, ItemMode } from '~/store/navigation/navigation-action';
 import { ProjectIcon } from '~/components/icon/icon';
+import { ResourceName } from '~/views-components/data-explorer/renderers';
+import { ResourcesState, getResource } from '~/store/resources/resources';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
+import { CollectionResource } from '~/models/collection';
+import { ProjectResource } from '~/models/project';
+import { openProjectCreator } from '~/store/project/project-action';
+import { reset } from 'redux-form';
+import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
+import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action';
+import { navigateToResource } from '~/store/navigation/navigation-action';
 
 type CssRules = 'root' | "toolbar" | "button";
 
@@ -50,14 +61,14 @@ export interface ProjectPanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
+export const columns: DataColumns<string, ProjectPanelFilter> = [
     {
         name: ProjectPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
         filters: [],
-        render: renderName,
+        render: uuid => <ResourceName uuid={uuid} />,
         width: "450px"
     },
     {
@@ -82,7 +93,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
                 type: ContainerRequestState.UNCOMMITTED
             }
         ],
-        render: renderStatus,
+        render: uuid => <ProcessStatus uuid={uuid} />,
         width: "75px"
     },
     {
@@ -107,7 +118,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
                 type: ResourceKind.PROJECT
             }
         ],
-        render: item => renderType(item.kind),
+        render: uuid => <ResourceType uuid={uuid} />,
         width: "125px"
     },
     {
@@ -116,7 +127,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderOwner(item.owner),
+        render: uuid => <ResourceOwner uuid={uuid} />,
         width: "200px"
     },
     {
@@ -125,7 +136,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderFileSize(item.fileSize),
+        render: uuid => <ResourceFileSize uuid={uuid} />,
         width: "50px"
     },
     {
@@ -134,7 +145,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderDate(item.lastModified),
+        render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
         width: "150px"
     }
 ];
@@ -143,22 +154,14 @@ export const PROJECT_PANEL_ID = "projectPanel";
 
 interface ProjectPanelDataProps {
     currentItemId: string;
+    resources: ResourcesState;
 }
 
-interface ProjectPanelActionProps {
-    onItemClick: (item: ProjectPanelItem) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
-    onProjectCreationDialogOpen: (ownerUuid: string) => void;
-    onCollectionCreationDialogOpen: (ownerUuid: string) => void;
-    onItemDoubleClick: (item: ProjectPanelItem) => void;
-    onItemRouteChange: (itemId: string) => void;
-}
-
-type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp
+type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
     & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
 export const ProjectPanel = withStyles(styles)(
-    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId, resources: state.resources }))(
         class extends React.Component<ProjectPanelProps> {
             render() {
                 const { classes } = this.props;
@@ -177,32 +180,64 @@ export const ProjectPanel = withStyles(styles)(
                     <DataExplorer
                         id={PROJECT_PANEL_ID}
                         columns={columns}
-                        onRowClick={this.props.onItemClick}
-                        onRowDoubleClick={this.props.onItemDoubleClick}
-                        onContextMenu={this.props.onContextMenu}
-                        extractKey={(item: ProjectPanelItem) => item.uuid}
+                        onRowClick={this.handleRowClick}
+                        onRowDoubleClick={this.handleRowDoubleClick}
+                        onContextMenu={this.handleContextMenu}
                         defaultIcon={ProjectIcon}
                         defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
                 </div>;
             }
 
             handleNewProjectClick = () => {
-                this.props.onProjectCreationDialogOpen(this.props.currentItemId);
+                this.props.dispatch<any>(openProjectCreator(this.props.currentItemId));
             }
 
             handleNewCollectionClick = () => {
-                this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
+                this.props.dispatch(reset(COLLECTION_CREATE_DIALOG));
+                this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: this.props.currentItemId }));
             }
 
-            componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
-                if (match.params.id !== currentItemId) {
-                    onItemRouteChange(match.params.id);
+            handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+                event.preventDefault();
+                const resource = getResource(resourceUuid)(this.props.resources) as CollectionResource | ProjectResource | undefined;
+                if (resource) {
+                    let kind: ContextMenuKind;
+
+                    if (resource.kind === ResourceKind.PROJECT) {
+                        kind = ContextMenuKind.PROJECT;
+                    } else if (resource.kind === ResourceKind.COLLECTION) {
+                        kind = ContextMenuKind.COLLECTION_RESOURCE;
+                    } else {
+                        kind = ContextMenuKind.RESOURCE;
+                    }
+                    if (kind !== ContextMenuKind.RESOURCE) {
+                        this.props.dispatch(
+                            contextMenuActions.OPEN_CONTEXT_MENU({
+                                position: { x: event.clientX, y: event.clientY },
+                                resource: {
+                                    uuid: resource.uuid,
+                                    name: resource.name || '',
+                                    description: resource.description,
+                                    kind,
+                                }
+                            })
+                        );
+                    }
                 }
             }
 
-            componentDidMount() {
+            handleRowDoubleClick = (uuid: string) => {
+                this.props.dispatch<any>(navigateToResource(uuid));
+            }
+
+            handleRowClick = (uuid: string) => {
+                this.props.dispatch(loadDetailsPanel(uuid));
+            }
+
+            async componentDidMount() {
                 if (this.props.match.params.id && this.props.currentItemId === '') {
-                    this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
+                    await this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
+                    this.props.dispatch<any>(setProjectItem(this.props.match.params.id, ItemMode.BOTH));
                 }
             }
         }
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index ed11eb1..2dda4d2 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -4,42 +4,30 @@
 
 import * as React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
 import { connect, DispatchProp } from "react-redux";
-import { Route, RouteComponentProps, Switch, Redirect } from "react-router";
+import { Route, Switch, Redirect } from "react-router";
 import { login, logout } 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 { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
 import { push } from 'react-router-redux';
-import { reset } from 'redux-form';
-import { ProjectTree } from '~/views-components/project-tree/project-tree';
 import { TreeItem } from "~/components/tree/tree";
 import { getTreePath } from '~/store/project/project-reducer';
-import { sidePanelActions } from '~/store/side-panel/side-panel-action';
-import { SidePanel, SidePanelItem } from '~/components/side-panel/side-panel';
 import { ItemMode, setProjectItem } from "~/store/navigation/navigation-action";
-import { projectActions } from "~/store/project/project-action";
-import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action';
 import { ProjectPanel } from "~/views/project-panel/project-panel";
 import { DetailsPanel } from '~/views-components/details-panel/details-panel';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { CreateProjectDialog } from "~/views-components/create-project-dialog/create-project-dialog";
-
-import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
-import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
+import { detailsPanelActions, loadDetailsPanel } from "~/store/details-panel/details-panel-action";
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
 import { ProjectResource } from '~/models/project';
-import { ResourceKind } from '~/models/resource';
 import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
 import { FavoritePanel } from "../favorite-panel/favorite-panel";
 import { CurrentTokenDialog } from '~/views-components/current-token-dialog/current-token-dialog';
 import { Snackbar } from '~/views-components/snackbar/snackbar';
-import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
 import { CreateCollectionDialog } from '~/views-components/create-collection-dialog/create-collection-dialog';
 import { CollectionPanel } from '../collection-panel/collection-panel';
-import { loadCollection, loadCollectionTags } from '~/store/collection-panel/collection-panel-action';
-import { getCollectionUrl } from '~/models/collection';
 import { UpdateCollectionDialog } from '~/views-components/update-collection-dialog/update-collection-dialog.';
 import { UpdateProjectDialog } from '~/views-components/update-project-dialog/update-project-dialog';
 import { AuthService } from "~/services/auth-service/auth-service";
@@ -47,18 +35,16 @@ import { RenameFileDialog } from '~/views-components/rename-file-dialog/rename-f
 import { FileRemoveDialog } from '~/views-components/file-remove-dialog/file-remove-dialog';
 import { MultipleFilesRemoveDialog } from '~/views-components/file-remove-dialog/multiple-files-remove-dialog';
 import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
-import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
-import { PROJECT_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-project-create';
 import { UploadCollectionFilesDialog } from '~/views-components/upload-collection-files-dialog/upload-collection-files-dialog';
 import { ProjectCopyDialog } from '~/views-components/project-copy-dialog/project-copy-dialog';
 import { CollectionPartialCopyDialog } from '../../views-components/collection-partial-copy-dialog/collection-partial-copy-dialog';
 import { MoveProjectDialog } from '~/views-components/move-project-dialog/move-project-dialog';
 import { MoveCollectionDialog } from '~/views-components/move-collection-dialog/move-collection-dialog';
+import { NavigationPanel } from '~/views-components/navigation-panel/navigation-panel';
 
-const DRAWER_WITDH = 240;
 const APP_BAR_HEIGHT = 100;
 
-type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
+type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -75,12 +61,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         position: "absolute",
         width: "100%"
     },
-    drawerPaper: {
-        position: 'relative',
-        width: DRAWER_WITDH,
-        display: 'flex',
-        flexDirection: 'column',
-    },
     contentWrapper: {
         backgroundColor: theme.palette.background.default,
         display: "flex",
@@ -94,7 +74,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         flexGrow: 1,
         position: 'relative'
     },
-    toolbar: theme.mixins.toolbar
 });
 
 interface WorkbenchDataProps {
@@ -102,7 +81,6 @@ interface WorkbenchDataProps {
     currentProjectId: string;
     user?: User;
     currentToken?: string;
-    sidePanelItems: SidePanelItem[];
 }
 
 interface WorkbenchGeneralProps {
@@ -141,12 +119,10 @@ export const Workbench = withStyles(styles)(
             currentProjectId: state.projects.currentItemId,
             user: state.auth.user,
             currentToken: state.auth.apiToken,
-            sidePanelItems: state.sidePanel
         })
     )(
         class extends React.Component<WorkbenchProps, WorkbenchState> {
             state = {
-                isCreationDialogOpen: false,
                 isCurrentTokenDialogOpen: false,
                 anchorEl: null,
                 searchText: "",
@@ -201,43 +177,14 @@ export const Workbench = withStyles(styles)(
                                 buildInfo={this.props.buildInfo}
                                 {...this.mainAppBarActions} />
                         </div>
-                        {user &&
-                            <Drawer
-                                variant="permanent"
-                                classes={{
-                                    paper: classes.drawerPaper,
-                                }}>
-                                <div className={classes.toolbar} />
-                                <SidePanel
-                                    toggleOpen={this.toggleSidePanelOpen}
-                                    toggleActive={this.toggleSidePanelActive}
-                                    sidePanelItems={this.props.sidePanelItems}
-                                    onContextMenu={(event) => this.openContextMenu(event, {
-                                        uuid: this.props.authService.getUuid() || "",
-                                        name: "",
-                                        kind: ContextMenuKind.ROOT_PROJECT
-                                    })}>
-                                    <ProjectTree
-                                        projects={this.props.projects}
-                                        toggleOpen={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))}
-                                        onContextMenu={(event, item) => this.openContextMenu(event, {
-                                            uuid: item.data.uuid,
-                                            name: item.data.name,
-                                            kind: ContextMenuKind.PROJECT
-                                        })}
-                                        toggleActive={itemId => {
-                                            this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
-                                            this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
-                                        }} />
-                                </SidePanel>
-                            </Drawer>}
+                        {user && <NavigationPanel />}
                         <main className={classes.contentWrapper}>
                             <div className={classes.content}>
                                 <Switch>
                                     <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
-                                    <Route path="/projects/:id" render={this.renderProjectPanel} />
-                                    <Route path="/favorites" render={this.renderFavoritePanel} />
-                                    <Route path="/collections/:id" render={this.renderCollectionPanel} />
+                                    <Route path="/projects/:id" component={ProjectPanel} />
+                                    <Route path="/favorites" component={FavoritePanel} />
+                                    <Route path="/collections/:id" component={CollectionPanel} />
                                 </Switch>
                             </div>
                             {user && <DetailsPanel />}
@@ -265,90 +212,10 @@ export const Workbench = withStyles(styles)(
                 );
             }
 
-            renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
-                onItemRouteChange={(collectionId) => {
-                    this.props.dispatch<any>(loadCollection(collectionId));
-                    this.props.dispatch<any>(loadCollectionTags(collectionId));
-                }}
-                onContextMenu={(event, item) => {
-                    this.openContextMenu(event, {
-                        uuid: item.uuid,
-                        name: item.name,
-                        description: item.description,
-                        kind: ContextMenuKind.COLLECTION
-                    });
-                }}
-                {...props} />
-
-            renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
-                onItemRouteChange={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))}
-                onContextMenu={(event, item) => {
-                    let kind: ContextMenuKind;
-
-                    if (item.kind === ResourceKind.PROJECT) {
-                        kind = ContextMenuKind.PROJECT;
-                    } else if (item.kind === ResourceKind.COLLECTION) {
-                        kind = ContextMenuKind.COLLECTION_RESOURCE;
-                    } else {
-                        kind = ContextMenuKind.RESOURCE;
-                    }
-
-                    this.openContextMenu(event, {
-                        uuid: item.uuid,
-                        name: item.name,
-                        description: item.description,
-                        kind
-                    });
-                }}
-                onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen}
-                onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen}
-                onItemClick={item => {
-                    this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
-                }}
-                onItemDoubleClick={item => {
-                    switch (item.kind) {
-                        case ResourceKind.COLLECTION:
-                            this.props.dispatch(loadCollection(item.uuid));
-                            this.props.dispatch(push(getCollectionUrl(item.uuid)));
-                        default:
-                            this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
-                            this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
-                    }
-
-                }}
-                {...props} />
-
-            renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => <FavoritePanel
-                onItemRouteChange={() => this.props.dispatch(favoritePanelActions.REQUEST_ITEMS())}
-                onContextMenu={(event, item) => {
-                    const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
-                    this.openContextMenu(event, {
-                        uuid: item.uuid,
-                        name: item.name,
-                        kind,
-                    });
-                }}
-                onDialogOpen={this.handleProjectCreationDialogOpen}
-                onItemClick={item => {
-                    this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
-                }}
-                onItemDoubleClick={item => {
-                    switch (item.kind) {
-                        case ResourceKind.COLLECTION:
-                            this.props.dispatch(loadCollection(item.uuid));
-                            this.props.dispatch(push(getCollectionUrl(item.uuid)));
-                        default:
-                            this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
-                            this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
-                    }
-
-                }}
-                {...props} />
-
             mainAppBarActions: MainAppBarActionProps = {
                 onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
                     this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
-                    this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
+                    this.props.dispatch(loadDetailsPanel(itemId));
                 },
                 onSearch: searchText => {
                     this.setState({ searchText });
@@ -359,47 +226,14 @@ export const Workbench = withStyles(styles)(
                     this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
                 },
                 onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
-                    this.openContextMenu(event, {
+                    this.props.dispatch<any>(openContextMenu(event, {
                         uuid: breadcrumb.itemId,
                         name: breadcrumb.label,
                         kind: ContextMenuKind.PROJECT
-                    });
+                    }));
                 }
             };
 
-            toggleSidePanelOpen = (itemId: string) => {
-                this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
-            }
-
-            toggleSidePanelActive = (itemId: string) => {
-                this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
-
-                const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
-                if (panelItem && panelItem.activeAction) {
-                    panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid());
-                }
-            }
-
-            handleProjectCreationDialogOpen = (itemUuid: string) => {
-                this.props.dispatch(reset(PROJECT_CREATE_DIALOG));
-                this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
-            }
-
-            handleCollectionCreationDialogOpen = (itemUuid: string) => {
-                this.props.dispatch(reset(COLLECTION_CREATE_DIALOG));
-                this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: itemUuid }));
-            }
-
-            openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) => {
-                event.preventDefault();
-                this.props.dispatch(
-                    contextMenuActions.OPEN_CONTEXT_MENU({
-                        position: { x: event.clientX, y: event.clientY },
-                        resource
-                    })
-                );
-            }
-
             toggleCurrentTokenModal = () => {
                 this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen });
             }

commit 2df6bac4eea43c9079641cd05262b29cbd285905
Author: Michal Klobukowski <michal.klobukowski at contractors.roche.com>
Date:   Wed Aug 22 23:06:06 2018 +0200

    Create common state for resources
    
    Feature #14102
    
    Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski at contractors.roche.com>

diff --git a/src/store/resources/resources-actions.ts b/src/store/resources/resources-actions.ts
new file mode 100644
index 0000000..36f9936
--- /dev/null
+++ b/src/store/resources/resources-actions.ts
@@ -0,0 +1,13 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+import { Resource } from '~/models/resource';
+
+export const resourcesActions = unionize({
+    SET_RESOURCES: ofType<Resource[]>(),
+    DELETE_RESOURCES: ofType<string[]>()
+});
+
+export type ResourcesAction = UnionOf<typeof resourcesActions>;
\ No newline at end of file
diff --git a/src/store/resources/resources-reducer.ts b/src/store/resources/resources-reducer.ts
new file mode 100644
index 0000000..22108e0
--- /dev/null
+++ b/src/store/resources/resources-reducer.ts
@@ -0,0 +1,13 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ResourcesState, setResource, deleteResource } from './resources';
+import { ResourcesAction, resourcesActions } from './resources-actions';
+
+export const resourcesReducer = (state: ResourcesState = {}, action: ResourcesAction) =>
+    resourcesActions.match(action, {
+        SET_RESOURCES: resources => resources.reduce((state, resource) => setResource(resource.uuid, resource)(state), state),
+        DELETE_RESOURCES: ids => ids.reduce((state, id) => deleteResource(id)(state), state),
+        default: () => state,
+    });
\ No newline at end of file
diff --git a/src/store/resources/resources.ts b/src/store/resources/resources.ts
new file mode 100644
index 0000000..7f21332
--- /dev/null
+++ b/src/store/resources/resources.ts
@@ -0,0 +1,37 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "~/models/resource";
+import { ResourceKind } from '../../models/resource';
+
+export type ResourcesState = { [key: string]: Resource };
+
+export const getResource = <T extends Resource>(id: string) =>
+    (state: ResourcesState): Resource | undefined =>
+        state[id];
+
+export const setResource = <T extends Resource>(id: string, data: T) =>
+    (state: ResourcesState) => ({
+        ...state,
+        [id]: data
+    });
+
+export const deleteResource = (id: string) =>
+    (state: ResourcesState) => {
+        const newState = {...state};
+        delete newState[id];
+        return newState;
+    };
+
+export const filterResources = (filter: (resource: Resource) => boolean) =>
+    (state: ResourcesState) =>
+        Object
+            .keys(state)
+            .map(id => getResource(id)(state))
+            .filter(filter);
+
+export const filterResourcesByKind = (kind: ResourceKind) =>
+    (state: ResourcesState) =>
+        filterResources(resource => resource.kind === kind)(state);
+
diff --git a/src/store/store.ts b/src/store/store.ts
index a4bf9d6..eb36fa4 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -29,6 +29,8 @@ import { CollectionsState, collectionsReducer } from './collections/collections-
 import { ServiceRepository } from "~/services/services";
 import { treePickerReducer } from './tree-picker/tree-picker-reducer';
 import { TreePicker } from './tree-picker/tree-picker';
+import { ResourcesState } from '~/store/resources/resources';
+import { resourcesReducer } from '~/store/resources/resources-reducer';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -50,6 +52,7 @@ export interface RootState {
     collectionPanelFiles: CollectionPanelFilesState;
     dialog: DialogState;
     treePicker: TreePicker;
+    resources: ResourcesState;
 }
 
 export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
@@ -71,6 +74,7 @@ export function configureStore(history: History, services: ServiceRepository): R
         collectionPanelFiles: collectionPanelFilesReducer,
         dialog: dialogReducer,
         treePicker: treePickerReducer,
+        resources: resourcesReducer,
     });
 
     const projectPanelMiddleware = dataExplorerMiddleware(

commit 6ce4e6e255691116f2c8e229d45df571dffa6b9a
Author: Michal Klobukowski <michal.klobukowski at contractors.roche.com>
Date:   Wed Aug 22 23:04:24 2018 +0200

    Create unionize wrapper
    
    Feature #14102
    
    Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski at contractors.roche.com>

diff --git a/src/common/unionize.ts b/src/common/unionize.ts
new file mode 100644
index 0000000..b684431
--- /dev/null
+++ b/src/common/unionize.ts
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export * from 'unionize';
+import { unionize as originalUnionize, SingleValueRec } from 'unionize';
+
+export function unionize<Record extends SingleValueRec>(record: Record) {
+    return originalUnionize(record, {
+        tag: 'type',
+        value: 'payload'
+    });
+}
+

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list