merge-conflicts
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 25 Jun 2018 10:05:09 +0000 (12:05 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 25 Jun 2018 10:05:09 +0000 (12:05 +0200)
Feature ##13598

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

1  2 
src/components/tree/tree.tsx
src/index.tsx
src/store/project/project-action.ts
src/store/project/project-reducer.test.ts
src/store/project/project-reducer.ts
src/store/store.ts
src/views-components/project-tree/project-tree.test.tsx
src/views-components/project-tree/project-tree.tsx
src/views/workbench/workbench.test.tsx
src/views/workbench/workbench.tsx

index 5dd45878dbe2342dff788bf499bec6991d6c8c6d,6731950c1adb8a78543cffa36000f2441e8144a4..2c19a831ecd1154bfe7de3746787a6b3d6641eb3
@@@ -9,6 -9,38 +9,6 @@@ import { StyleRulesCallback, Theme, wit
  import { ReactElement } from "react";
  import Collapse from "@material-ui/core/Collapse/Collapse";
  import CircularProgress from '@material-ui/core/CircularProgress';
 -import { inherits } from 'util';
 -
 -type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
 -
 -const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
 -    list: {
 -        paddingBottom: '3px',
 -        paddingTop: '3px',
 -    },
 -    activeArrow: {
 -        color: '#4285F6',
 -        position: 'absolute',
 -    },
 -    inactiveArrow: {
 -        position: 'absolute',
 -    },
 -    arrowTransition: {
 -        transition: 'all 0.1s ease',
 -    },
 -    arrowRotate: {
 -        transition: 'all 0.1s ease',
 -        transform: 'rotate(-90deg)',
 -    },
 -    arrowVisibility: {
 -        opacity: 0,
 -    },
 -    loader: {
 -        position: 'absolute',
 -        transform: 'translate(0px)',
 -        top: '3px'
 -    }
 -});
  
  export enum TreeItemStatus {
      Initial,
@@@ -29,28 -61,27 +29,28 @@@ export interface TreeItem<T> 
  interface TreeProps<T> {
      items?: Array<TreeItem<T>>;
      render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
 -    toggleItem: (id: string, status: TreeItemStatus) => any;
 +    toggleItemOpen: (id: string, status: TreeItemStatus) => void;
-     toggleItemActive: (id: string) => void;
++    toggleItemActive: (id: string, status: TreeItemStatus) => void;
      level?: number;
  }
  
  class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
      renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) {
 -        return <i
 -            onClick={() => this.props.toggleItem(id, status)}
 +        const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes;
 +        return <i onClick={() => this.props.toggleItemOpen(id, status)}
              className={`
 -                ${arrowClass} 
 -                ${status === TreeItemStatus.Pending ? this.props.classes.arrowVisibility : ''} 
 -                ${open ? `fas fa-caret-down ${this.props.classes.arrowTransition}` : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />;
 +                    ${arrowClass} 
 +                    ${status === TreeItemStatus.Pending ? arrowVisibility : ''} 
 +                    ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} />;
      }
      render(): ReactElement<any> {
          const level = this.props.level ? this.props.level : 0;
 -        const { classes, render, toggleItem, items } = this.props;
 +        const { classes, render, toggleItemOpen, items, toggleItemActive } = this.props;
          const { list, inactiveArrow, activeArrow, loader } = classes;
          return <List component="div" className={list}>
              {items && items.map((it: TreeItem<T>, idx: number) =>
                  <div key={`item/${level}/${idx}`}>
-                     <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }} onClick={() => toggleItemActive(it.id)}>
 -                    <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}>
++                    <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }} onClick={() => toggleItemActive(it.id, it.status)}>
                          {it.status === TreeItemStatus.Pending ? <CircularProgress size={10} className={loader} /> : null}
                          {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open, it.id)}
                          {render(it, level)}
@@@ -60,8 -91,7 +60,8 @@@
                              <StyledTree
                                  items={it.items}
                                  render={render}
 -                                toggleItem={toggleItem}
 +                                toggleItemOpen={toggleItemOpen}
 +                                toggleItemActive={toggleItemActive}
                                  level={level + 1} />
                          </Collapse>}
                  </div>)}
      }
  }
  
 +type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
 +
 +const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
 +    list: {
 +        paddingBottom: '3px',
 +        paddingTop: '3px',
 +    },
 +    activeArrow: {
 +        color: '#4285F6',
 +        position: 'absolute',
 +    },
 +    inactiveArrow: {
 +        position: 'absolute',
 +    },
 +    arrowTransition: {
 +        transition: 'all 0.1s ease',
 +    },
 +    arrowRotate: {
 +        transition: 'all 0.1s ease',
 +        transform: 'rotate(-90deg)',
 +    },
 +    arrowVisibility: {
 +        opacity: 0,
 +    },
 +    loader: {
 +        position: 'absolute',
 +        transform: 'translate(0px)',
 +        top: '3px'
 +    }
 +});
 +
  const StyledTree = withStyles(styles)(Tree);
  export default StyledTree;
diff --combined src/index.tsx
index 1807bd8dc199cd4e5e2522f67aa81eff4a6914f3,cf1610f83226cd12a279b4a28db1f3494ee740a7..ba395e8b785ab49dd6255427ff90382b43ff6191
@@@ -11,10 -11,10 +11,10 @@@ import { Route } from "react-router"
  import createBrowserHistory from "history/createBrowserHistory";
  import configureStore from "./store/store";
  import { ConnectedRouter } from "react-router-redux";
- import ApiToken from "./components/api-token/api-token";
+ import ApiToken from "./views-components/api-token/api-token";
  import authActions from "./store/auth/auth-action";
- import { authService, projectService } from "./services/services";
- import { sidePanelData } from './store/side-panel/side-panel-reducer';
+ import { authService } from "./services/services";
+ import { getProjectList } from "./store/project/project-action";
  
  const history = createBrowserHistory();
  
@@@ -26,13 -26,12 +26,13 @@@ const store = configureStore(
      },
      auth: {
          user: undefined
 -    }
 +    },
 +    sidePanel: []
  }, history);
  
  store.dispatch(authActions.INIT());
  const rootUuid = authService.getRootUuid();
- store.dispatch<any>(projectService.getProjectList(rootUuid));
+ store.dispatch<any>(getProjectList(rootUuid));
  
  const App = () =>
      <Provider store={store}>
index a58edd3caa994082e9d9431a718442a656435d46,728b1cc95e587112104d032f6cbc56698fe45426..3c264d3ef9fcbba5089a9294a65b3aec0d863c5b
@@@ -1,22 -1,30 +1,34 @@@
  // Copyright (C) The Arvados Authors. All rights reserved.
  //
  // SPDX-License-Identifier: AGPL-3.0
 +import { default as unionize, ofType, UnionOf } from "unionize";
  
  import { Project } from "../../models/project";
 -import { default as unionize, ofType, UnionOf } from "unionize";
+ import { projectService } from "../../services/services";
+ import { Dispatch } from "redux";
  
  const actions = unionize({
      CREATE_PROJECT: ofType<Project>(),
      REMOVE_PROJECT: ofType<string>(),
--    PROJECTS_REQUEST: ofType<any>(),
++    PROJECTS_REQUEST: ofType<string>(),
      PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(),
 -    TOGGLE_PROJECT_TREE_ITEM: ofType<string>()
 +    TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType<string>(),
 +    TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<string>(),
 +    RESET_PROJECT_TREE_ACTIVITY: ofType<string>(),
  }, {
--    tag: 'type',
--    value: 'payload'
--});
++        tag: 'type',
++        value: 'payload'
++    });
+ export const getProjectList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Project[]> => {
 -    dispatch(actions.PROJECTS_REQUEST());
 -    return projectService.getProjectList(parentUuid).then(projects => {
 -        dispatch(actions.PROJECTS_SUCCESS({projects, parentItemId: parentUuid}));
 -        return projects;
 -    });
++    if (parentUuid) {
++        dispatch(actions.PROJECTS_REQUEST(parentUuid));
++        return projectService.getProjectList(parentUuid).then(projects => {
++            dispatch(actions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
++            return projects;
++        });
++    } return Promise.resolve([]);
+ };
  
  export type ProjectAction = UnionOf<typeof actions>;
  export default actions;
index e5cd57e29fe7223f980f69b6b6e1df30100d0a6b,f964e0ea311f333af3486a69ed6188a1cddb9e0c..e8d6afc6154dd004af9729042bbd9d48f7265bff
@@@ -2,8 -2,9 +2,9 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import projectsReducer from "./project-reducer";
+ import projectsReducer, { getTreePath } from "./project-reducer";
  import actions from "./project-action";
+ import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
  
  describe('project-reducer', () => {
      it('should add new project to the list', () => {
@@@ -14,7 -15,8 +15,8 @@@
              createdAt: '2018-01-01',
              modifiedAt: '2018-01-01',
              ownerUuid: 'owner-test123',
-             uuid: 'test123'
+             uuid: 'test123',
+             kind: ""
          };
  
          const state = projectsReducer(initialState, actions.CREATE_PROJECT(project));
              createdAt: '2018-01-01',
              modifiedAt: '2018-01-01',
              ownerUuid: 'owner-test123',
-             uuid: 'test123'
+             uuid: 'test123',
+             kind: ""
          };
  
          const projects = [project, project];
          const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
          expect(state).toEqual([{
 +            active: false,
 +            open: false,
 +            id: "test123",
 +            items: [],
 +            data: project,
 +            status: 0
 +        }, {
 +            active: false,
 +            open: false,
 +            id: "test123",
 +            items: [],
 +            data: project,
 +            status: 0
 +        }
 +        ]);
 +    });
 +
 +    it('should remove activity on projects list', () => {
 +        const initialState = [
 +            {
 +                data: {
 +                    name: 'test',
 +                    href: 'href',
 +                    createdAt: '2018-01-01',
 +                    modifiedAt: '2018-01-01',
 +                    ownerUuid: 'owner-test123',
 +                    uuid: 'test123',
++                    kind: 'example'
 +                },
 +                id: "1",
 +                open: true,
 +                active: true,
 +                status: 1
 +            }
 +        ];
 +        const project = [
 +            {
 +                data: {
 +                    name: 'test',
 +                    href: 'href',
 +                    createdAt: '2018-01-01',
 +                    modifiedAt: '2018-01-01',
 +                    ownerUuid: 'owner-test123',
 +                    uuid: 'test123',
++                    kind: 'example'
 +                },
 +                id: "1",
 +                open: true,
                  active: false,
 -                open: false,
 -                id: "test123",
 -                items: [],
 -                data: project,
 -                status: 0
 -            }, {
 +                status: 1
 +            }
 +        ];
 +
 +        const state = projectsReducer(initialState, actions.RESET_PROJECT_TREE_ACTIVITY(initialState[0].id));
 +        expect(state).toEqual(project);
 +    });
 +
 +    it('should toggle project tree item activity', () => {
 +        const initialState = [
 +            {
 +                data: {
 +                    name: 'test',
 +                    href: 'href',
 +                    createdAt: '2018-01-01',
 +                    modifiedAt: '2018-01-01',
 +                    ownerUuid: 'owner-test123',
 +                    uuid: 'test123',
++                    kind: 'example'
 +                },
 +                id: "1",
 +                open: true,
 +                active: false,
 +                status: 1
 +            }
 +        ];
 +        const project = [
 +            {
 +                data: {
 +                    name: 'test',
 +                    href: 'href',
 +                    createdAt: '2018-01-01',
 +                    modifiedAt: '2018-01-01',
 +                    ownerUuid: 'owner-test123',
 +                    uuid: 'test123',
++                    kind: 'example'
 +                },
 +                id: "1",
 +                open: true,
 +                active: true,
 +                status: 1
 +            }
 +        ];
 +
 +        const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState[0].id));
 +        expect(state).toEqual(project);
 +    });
 +
 +
 +    it('should close project tree item ', () => {
 +        const initialState = [
 +            {
 +                data: {
 +                    name: 'test',
 +                    href: 'href',
 +                    createdAt: '2018-01-01',
 +                    modifiedAt: '2018-01-01',
 +                    ownerUuid: 'owner-test123',
 +                    uuid: 'test123',
++                    kind: 'example'
 +                },
 +                id: "1",
 +                open: true,
                  active: false,
 +                status: 1,
 +                toggled: false,
 +            }
 +        ];
 +        const project = [
 +            {
 +                data: {
 +                    name: 'test',
 +                    href: 'href',
 +                    createdAt: '2018-01-01',
 +                    modifiedAt: '2018-01-01',
 +                    ownerUuid: 'owner-test123',
 +                    uuid: 'test123',
++                    kind: 'example'
 +                },
 +                id: "1",
                  open: false,
 -                id: "test123",
 -                items: [],
 -                data: project,
 -                status: 0
 +                active: false,
 +                status: 1,
 +                toggled: true
              }
 -        ]);
 +        ];
 +
 +        const state = projectsReducer(initialState, actions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState[0].id));
 +        expect(state).toEqual(project);
      });
  });
+ describe("findTreeBranch", () => {
+     const createTreeItem = (id: string, items?: Array<TreeItem<string>>): TreeItem<string> => ({
+         id,
+         items,
+         active: false,
+         data: "",
+         open: false,
+         status: TreeItemStatus.Initial
+     });
+     it("should return an array that matches path to the given item", () => {
+         const tree: Array<TreeItem<string>> = [
+             createTreeItem("1", [
+                 createTreeItem("1.1", [
+                     createTreeItem("1.1.1"),
+                     createTreeItem("1.1.2")
+                 ])
+             ]),
+             createTreeItem("2", [
+                 createTreeItem("2.1", [
+                     createTreeItem("2.1.1"),
+                     createTreeItem("2.1.2")
+                 ])
+             ])
+         ];
+         const branch = getTreePath(tree, "2.1.1");
+         expect(branch.map(item => item.id)).toEqual(["2", "2.1", "2.1.1"]);
+     });
+     it("should return empty array if item is not found", () => {
+         const tree: Array<TreeItem<string>> = [
+             createTreeItem("1", [
+                 createTreeItem("1.1", [
+                     createTreeItem("1.1.1"),
+                     createTreeItem("1.1.2")
+                 ])
+             ]),
+             createTreeItem("2", [
+                 createTreeItem("2.1", [
+                     createTreeItem("2.1.1"),
+                     createTreeItem("2.1.2")
+                 ])
+             ])
+         ];
+         expect(getTreePath(tree, "3")).toHaveLength(0);
+     });
+ });
index 43117ef0a0e507234147cf2d999e6520108157b1,4f7545fc979ea93b9fbe4fd6ee2f4e74559e6a87..48db05df77eb6051fcc5c84509fe317777ad7f26
@@@ -2,15 -2,14 +2,15 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
 +import * as _ from "lodash";
 +
  import { Project } from "../../models/project";
  import actions, { ProjectAction } from "./project-action";
  import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 -import * as _ from "lodash";
  
  export type ProjectState = Array<TreeItem<Project>>;
  
- function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
      let item;
      for (const t of tree) {
          item = t.id === itemId
      return item;
  }
  
+ export function getTreePath<T>(tree: Array<TreeItem<T>>, itemId: string): Array<TreeItem<T>> {
+     for(const item of tree){
+         if(item.id === itemId){
+             return [item];
+         } else {
+             const branch = getTreePath(item.items || [], itemId);
+             if(branch.length > 0){
+                 return [item, ...branch];
+             }
+         }
+     }
+     return [];
+ }
  function resetTreeActivity<T>(tree: Array<TreeItem<T>>) {
      for (const t of tree) {
          t.active = false;
@@@ -70,29 -83,17 +84,29 @@@ const projectsReducer = (state: Project
          PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
              return updateProjectTree(state, projects, parentItemId);
          },
 -        TOGGLE_PROJECT_TREE_ITEM: itemId => {
 +        TOGGLE_PROJECT_TREE_ITEM_OPEN: itemId => {
              const tree = _.cloneDeep(state);
 -            resetTreeActivity(tree);
              const item = findTreeItem(tree, itemId);
              if (item) {
 +                item.toggled = true;
                  item.open = !item.open;
 +            }
 +            return tree;
 +        },
 +        TOGGLE_PROJECT_TREE_ITEM_ACTIVE: itemId => {
 +            const tree = _.cloneDeep(state);
 +            resetTreeActivity(tree);
 +            const item = findTreeItem(tree, itemId);
 +            if (item) {
                  item.active = true;
 -                item.toggled = true;
              }
              return tree;
          },
 +        RESET_PROJECT_TREE_ACTIVITY: () => {
 +            const tree = _.cloneDeep(state);
 +            resetTreeActivity(tree);
 +            return tree;
 +        },
          default: () => state
      });
  };
diff --combined src/store/store.ts
index 49e0b5f7c5e48f2022b3fa3573ecbdde7de6e849,6b9c31ff4ee3bfca4426d3364b8440625c4d6e19..6089caf35cdf409d77ceb5ede5ced2ebc4083967
@@@ -6,28 -6,26 +6,30 @@@ import { createStore, applyMiddleware, 
  import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
  import thunkMiddleware from 'redux-thunk';
  import { History } from "history";
 +
  import projectsReducer, { ProjectState } from "./project/project-reducer";
 +import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer';
  import authReducer, { AuthState } from "./auth/auth-reducer";
+ import collectionsReducer from "./collection/collection-reducer";
  
  const composeEnhancers =
      (process.env.NODE_ENV === 'development' &&
 -    window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
 +        window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
      compose;
  
  export interface RootState {
      auth: AuthState;
      projects: ProjectState;
      router: RouterState;
 +    sidePanel: SidePanelState;
  }
  
  const rootReducer = combineReducers({
      auth: authReducer,
      projects: projectsReducer,
 -    router: routerReducer
+     collections: collectionsReducer,
 +    router: routerReducer,
 +    sidePanel: sidePanelReducer
  });
  
  
index 932a29cc16793aede03e3dd035031cfa0a542d6b,d53121304c817a83b113406b5451f8c433ca37b7..1ba3abb8bb39ddb2c40de1098b769b35bd7ad105
@@@ -11,7 -11,7 +11,7 @@@ import { Collapse } from '@material-ui/
  import CircularProgress from '@material-ui/core/CircularProgress';
  
  import ProjectTree from './project-tree';
- import { TreeItem } from '../tree/tree';
+ import { TreeItem } from '../../components/tree/tree';
  import { Project } from '../../models/project';
  Enzyme.configure({ adapter: new Adapter() });
  
@@@ -26,13 -26,13 +26,14 @@@ describe("ProjectTree component", () =
                  uuid: "uuid",
                  ownerUuid: "ownerUuid",
                  href: "href",
++                kind: 'example'
              },
              id: "3",
              open: true,
              active: true,
              status: 1
          };
--        const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
++        const wrapper = mount(<ProjectTree projects={[project]} toggleOpen={jest.fn()} toggleActive={jest.fn()} />);
  
          expect(wrapper.find(ListItemIcon)).toHaveLength(1);
      });
@@@ -47,6 -47,6 +48,7 @@@
                      uuid: "uuid",
                      ownerUuid: "ownerUuid",
                      href: "href",
++                    kind: 'example'
                  },
                  id: "3",
                  open: false,
@@@ -61,6 -61,6 +63,7 @@@
                      uuid: "uuid",
                      ownerUuid: "ownerUuid",
                      href: "href",
++                    kind: 'example'
                  },
                  id: "3",
                  open: false,
@@@ -68,7 -68,7 +71,7 @@@
                  status: 1
              }
          ];
--        const wrapper = mount(<ProjectTree projects={project} toggleProjectTreeItem={() => { }} />);
++        const wrapper = mount(<ProjectTree projects={project} toggleOpen={jest.fn()} toggleActive={jest.fn()} />);
  
          expect(wrapper.find(ListItemIcon)).toHaveLength(2);
      });
@@@ -83,6 -83,6 +86,7 @@@
                      uuid: "uuid",
                      ownerUuid: "ownerUuid",
                      href: "href",
++                    kind: 'example'
                  },
                  id: "3",
                  open: true,
                              uuid: "uuid",
                              ownerUuid: "ownerUuid",
                              href: "href",
++                            kind: 'example'
                          },
                          id: "3",
                          open: true,
                  ]
              }
          ];
--        const wrapper = mount(<ProjectTree projects={project} toggleProjectTreeItem={() => { }} />);
++        const wrapper = mount(<ProjectTree projects={project} toggleOpen={jest.fn()} toggleActive={jest.fn()}/>);
  
          expect(wrapper.find(Collapse)).toHaveLength(1);
      });
                  uuid: "uuid",
                  ownerUuid: "ownerUuid",
                  href: "href",
++                kind: 'example'
              },
              id: "3",
              open: false,
              active: true,
              status: 1
          };
--        const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
++        const wrapper = mount(<ProjectTree projects={[project]} toggleOpen={jest.fn()} toggleActive={jest.fn()} />);
  
          expect(wrapper.find(CircularProgress)).toHaveLength(1);
      });
index 7406f7f3ec95651480a21a859b98f40d441c3e43,fd32ff040d82dc5dce7f8bbc94583c88357695dd..f51b65e054df7f8ab2d69a263e658f9a1fe2a7d8
@@@ -9,39 -9,9 +9,39 @@@ import ListItemText from "@material-ui/
  import ListItemIcon from '@material-ui/core/ListItemIcon';
  import Typography from '@material-ui/core/Typography';
  
- import Tree, { TreeItem, TreeItemStatus } from '../tree/tree';
+ import Tree, { TreeItem, TreeItemStatus } from '../../components/tree/tree';
  import { Project } from '../../models/project';
  
-     toggleActive: (id: string) => void;
 +export interface ProjectTreeProps {
 +    projects: Array<TreeItem<Project>>;
 +    toggleOpen: (id: string, status: TreeItemStatus) => void;
++    toggleActive: (id: string, status: TreeItemStatus) => void;
 +}
 +
 +class ProjectTree<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
 +    render(): ReactElement<any> {
 +        const { classes, projects, toggleOpen, toggleActive } = this.props;
 +        const { active, listItemText, row, treeContainer } = classes;
 +        return (
 +            <div className={treeContainer}>
 +                <Tree items={projects}
 +                    toggleItemOpen={toggleOpen}
 +                    toggleItemActive={toggleActive}
 +                    render={(project: TreeItem<Project>) =>
 +                        <span className={row}>
 +                            <ListItemIcon className={project.active ? active : ''}>
 +                                <i className="fas fa-folder" />
 +                            </ListItemIcon>
 +                            <ListItemText className={listItemText} primary={
 +                                <Typography className={project.active ? active : ''}>{project.data.name}</Typography>
 +                            } />
 +                        </span>
 +                    } />
 +            </div>
 +        );
 +    }
 +}
 +
  type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer';
  
  const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
          marginLeft: '20px',
      },
      treeContainer: {
 -        marginTop: '37px',
 -        overflowX: 'visible',
 -        overflowY: 'auto',
          minWidth: '240px',
          whiteSpace: 'nowrap',
 +        marginLeft: '13px',
      }
  });
  
 -export interface ProjectTreeProps {
 -    projects: Array<TreeItem<Project>>;
 -    toggleProjectTreeItem: (id: string, status: TreeItemStatus) => void;
 -}
 -
 -class ProjectTree<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
 -    render(): ReactElement<any> {
 -        const {classes, projects} = this.props;
 -        const {active, listItemText, row, treeContainer} = classes;
 -        return (
 -            <div className={treeContainer}>
 -                <Tree items={projects}
 -                    toggleItem={this.props.toggleProjectTreeItem}
 -                    render={(project: TreeItem<Project>, level: number) =>
 -                        <span className={row}>
 -                            <ListItemIcon className={project.active ? active : ''}>
 -                                {level === 0 ? <i className="fas fa-th"/> : <i className="fas fa-folder"/>}
 -                            </ListItemIcon>
 -                            <ListItemText className={listItemText} primary={
 -                                <Typography className={project.active ? active : ''}>
 -                                    {project.data.name}
 -                                </Typography>
 -                            }/>
 -                        </span>
 -                    }/>
 -            </div>
 -        );
 -    }
 -}
 -
  export default withStyles(styles)(ProjectTree);
index 7b9b74d095c65a8a1ad760a2c4c87f360cb13836,7b9b74d095c65a8a1ad760a2c4c87f360cb13836..6925792293b65c475539ddd9e0a9bf279cb02f9e
@@@ -14,7 -14,7 +14,7 @@@ const history = createBrowserHistory()
  
  it('renders without crashing', () => {
      const div = document.createElement('div');
--    const store = configureStore({ projects: [], router: { location: null }, auth: {} }, createBrowserHistory());
++    const store = configureStore({ projects: [], router: { location: null }, auth: {}, sidePanel: [] }, createBrowserHistory());
      ReactDOM.render(
          <Provider store={store}>
              <ConnectedRouter history={history}>
index 9e274325075d6923b9932ada253326e2a6c06c06,bac0b473f10e780016df801e5f7323d05a5fc6ac..4f9843cb0a3e952786ef3489de8ac29b477796ee
@@@ -3,29 -3,28 +3,30 @@@
  // SPDX-License-Identifier: AGPL-3.0
  
  import * as React from 'react';
 -
  import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
  import Drawer from '@material-ui/core/Drawer';
  import { connect, DispatchProp } from "react-redux";
- import ProjectList from "../../components/project-list/project-list";
  import { Route, Switch } from "react-router";
  import authActions from "../../store/auth/auth-action";
  import { User } from "../../models/user";
  import { RootState } from "../../store/store";
- import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItem } from '../../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 from "../../store/project/project-action";
- import sidePanelActions from '../../store/side-panel/side-panel-action';
- import ProjectTree from '../../components/project-tree/project-tree';
+ 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 { Project } from "../../models/project";
+ import { getTreePath } from '../../store/project/project-reducer';
+ import ProjectPanel from '../project-panel/project-panel';
++import sidePanelActions from '../../store/side-panel/side-panel-action';
 +import { projectService } from '../../services/services';
 +import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel';
  
  const drawerWidth = 240;
+ const appBarHeight = 102;
  
- type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'toolbar';
+ type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
  
  const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
      root: {
      drawerPaper: {
          position: 'relative',
          width: drawerWidth,
 +        display: 'flex',
 +        flexDirection: 'column',
      },
-     content: {
-         flexGrow: 1,
+     contentWrapper: {
          backgroundColor: theme.palette.background.default,
-         padding: theme.spacing.unit * 3,
-         height: '100%',
+         display: "flex",
+         flexGrow: 1,
          minWidth: 0,
+         paddingTop: appBarHeight
+     },
+     content: {
+         padding: theme.spacing.unit * 3,
+         overflowY: "auto",
+         flexGrow: 1
      },
      toolbar: theme.mixins.toolbar
  });
@@@ -62,7 -64,6 +68,7 @@@
  interface WorkbenchDataProps {
      projects: Array<TreeItem<Project>>;
      user?: User;
 +    sidePanelItems: SidePanelItem[];
  }
  
  interface WorkbenchActionProps {
@@@ -71,7 -72,8 +77,8 @@@
  type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
  
  interface NavBreadcrumb extends Breadcrumb {
-     path: string;
+     itemId: string;
+     status: TreeItemStatus;
  }
  
  interface NavMenuItem extends MainAppBarMenuItem {
@@@ -93,15 -95,7 +100,7 @@@ class Workbench extends React.Component
      state = {
          anchorEl: null,
          searchText: "",
-         breadcrumbs: [
-             {
-                 label: "Projects",
-                 path: "/projects"
-             }, {
-                 label: "Project 1",
-                 path: "/projects/project-1"
-             }
-         ],
+         breadcrumbs: [],
          menuItems: {
              accountMenu: [
                  {
  
  
      mainAppBarActions: MainAppBarActionProps = {
-         onBreadcrumbClick: (breadcrumb: NavBreadcrumb) => this.props.dispatch(push(breadcrumb.path)),
+         onBreadcrumbClick: ({ itemId, status }: NavBreadcrumb) => {
 -            this.toggleProjectTreeItem(itemId, status);
++            this.toggleProjectTreeItemOpen(itemId, status);
+         },
          onSearch: searchText => {
              this.setState({ searchText });
              this.props.dispatch(push(`/search?q=${searchText}`));
          onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action()
      };
  
 -    toggleProjectTreeItem = (itemId: string, status: TreeItemStatus) => {
 +    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>(projectService.getProjectList(itemId)).then(() => {
-                 this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId));
-                 this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
-             });
+             this.props.dispatch<any>(getProjectList(itemId))
 -                .then(() => this.openProjectItem(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) => {
-         this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
-         this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
++    toggleProjectTreeItemActive = (itemId: string, status: TreeItemStatus) => {
++        if (status === TreeItemStatus.Loaded) {
++            this.openProjectItem(itemId);
++            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
++            this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
++        } else {
++            this.props.dispatch<any>(getProjectList(itemId))
++                .then(() => {
++                    this.openProjectItem(itemId);
++                    this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
++                    this.props.dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(itemId));
++                });
++        }
 +    }
 +
 +    toggleSidePanelOpen = (itemId: string) => {
 +        this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
 +    }
 +
 +    toggleSidePanelActive = (itemId: string) => {
 +        this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
 +        this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
 +    }
 +
+     openProjectItem = (itemId: string) => {
+         const branch = getTreePath(this.props.projects, itemId);
+         this.setState({
+             breadcrumbs: branch.map(item => ({
+                 label: item.data.name,
+                 itemId: item.data.uuid,
+                 status: item.status
+             }))
+         });
 -        this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
++        this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId));
+         this.props.dispatch(push(`/project/${itemId}`));
+     }
      render() {
 -        const { classes, user } = this.props;
 +        const { classes, user, projects, sidePanelItems } = this.props;
          return (
              <div className={classes.root}>
                  <div className={classes.appBar}>
                              paper: classes.drawerPaper,
                          }}>
                          <div className={classes.toolbar} />
-                             <SidePanel
-                                 toggleOpen={this.toggleSidePanelOpen}
-                                 toggleActive={this.toggleSidePanelActive}
-                                 sidePanelItems={sidePanelItems}>
-                                 <ProjectTree
-                                     projects={projects}
-                                     toggleOpen={this.toggleProjectTreeItemOpen}
-                                     toggleActive={this.toggleProjectTreeItemActive} />
-                             </SidePanel>
 -                        <ProjectTree
 -                            projects={this.props.projects}
 -                            toggleProjectTreeItem={this.toggleProjectTreeItem} />
++                        <SidePanel
++                            toggleOpen={this.toggleSidePanelOpen}
++                            toggleActive={this.toggleSidePanelActive}
++                            sidePanelItems={sidePanelItems}>
++                            <ProjectTree
++                                projects={projects}
++                                toggleOpen={this.toggleProjectTreeItemOpen}
++                                toggleActive={this.toggleProjectTreeItemActive} />
++                        </SidePanel>
                      </Drawer>}
-                 <main className={classes.content}>
-                     <div className={classes.toolbar} />
-                     <Switch>
-                         <Route path="/project/:name" component={ProjectList} />
-                     </Switch>
+                 <main className={classes.contentWrapper}>
+                     <div className={classes.content}>
+                         <Switch>
+                             <Route path="/project/:name" component={ProjectPanel} />
+                         </Switch>
+                     </div>
                  </main>
              </div>
          );
  export default connect<WorkbenchDataProps>(
      (state: RootState) => ({
          projects: state.projects,
 -        user: state.auth.user
 +        user: state.auth.user,
 +        sidePanelItems: state.sidePanel,
      })
  )(
      withStyles(styles)(Workbench)