--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+FROM node:latest
+MAINTAINER Ward Vandewege <ward@curoverse.com>
+RUN apt-get update
+RUN apt-get -q -y install libsecret-1-0 libsecret-1-dev rpm
+RUN apt-get install -q -y ruby ruby-dev rubygems build-essential
+RUN gem install --no-ri --no-rdoc fpm
#
# SPDX-License-Identifier: Apache-2.0
+APP_NAME?=arvados-workbench2
+
+# GIT_TAG is the last tagged stable release (i.e. 1.2.0)
+GIT_TAG?=$(shell git describe --abbrev=0)
+
+# TS_GIT is the timestamp in the current directory (i.e. 1528815021).
+# Note that it will only change if files change.
+TS_GIT?=$(shell git log -n1 --first-parent "--format=format:%ct" .)
+
+# DATE_FROM_TS_GIT is the human(ish)-readable version of TS_GIT
+# 1528815021 -> 20180612145021
+DATE_FROM_TS_GIT?=$(shell date -ud @$(TS_GIT) +%Y%m%d%H%M%S)
+
+# VERSION uses all the above to produce X.Y.Z.timestamp
+# something in the lines of 1.2.0.20180612145021, this will be the package version
+# it can be overwritten when invoking make as in make packages VERSION=1.2.0
+VERSION?=$(GIT_TAG).$(DATE_FROM_TS_GIT)
+
+# ITERATION is the package iteration, intended for manual change if anything non-code related
+# changes in the package. (i.e. example config files externally added
+ITERATION?=1
+
+DESCRIPTION=Arvados Workbench2 - Arvados is a free and open source platform for big data science.
+MAINTAINER=Ward Vandewege <wvandewege@veritasgenetics.com>
+
+# DEST_DIR will have the build package copied.
+DEST_DIR=/var/www/arvados-workbench2/workbench2/
+
+# Debian package file
+DEB_FILE=$(APP_NAME)_$(VERSION)-$(ITERATION)_amd64.deb
+
+# redHat package file
+RPM_FILE=$(APP_NAME)_$(VERSION)-$(ITERATION).x86_64.rpm
+
export WORKSPACE?=$(shell pwd)
+
+.PHONY: help clean* yarn-install test build packages packages-with-version
+
help:
@echo >&2
@echo >&2 "There is no default make target here. Did you mean 'make test'?"
@echo >&2
@false
-test:
- @yarn install
- @yarn test --no-watchAll
+clean-deb:
+ rm -f $(WORKSPACE)/*.deb
+
+clean-rpm:
+ rm -f $(WORKSPACE)/*.rpm
+
+clean-node-modules:
+ rm -rf $(WORKSPACE)/node_modules
+
+clean: clean-rpm clean-deb clean-node-modules
+
+yarn-install:
+ yarn install
+
+test: yarn-install
+ yarn test --no-watchAll --bail --ci
+
+build: test
+ yarn build
+
+$(DEB_FILE): build
+ fpm \
+ -s dir \
+ -t deb \
+ -n "$(APP_NAME)" \
+ -v "$(VERSION)" \
+ --iteration "$(ITERATION)" \
+ --maintainer="$(MAINTAINER)" \
+ --description="$(DESCRIPTION)" \
+ --deb-no-default-config-files \
+ $(WORKSPACE)/build/=$(DEST_DIR)
+
+$(RPM_FILE): build
+ fpm \
+ -s dir \
+ -t rpm \
+ -n "$(APP_NAME)" \
+ -v "$(VERSION)" \
+ --iteration "$(ITERATION)" \
+ --maintainer="$(MAINTAINER)" \
+ --description="$(DESCRIPTION)" \
+ $(WORKSPACE)/build/=$(DEST_DIR)
-build:
- @yarn install
- @yarn build
+# use FPM to create DEB and RPM
+packages: $(DEB_FILE) $(RPM_FILE)
"dependencies": {
"@material-ui/core": "1.2.0",
"@material-ui/icons": "^1.1.0",
+ "@types/lodash": "^4.14.109",
"axios": "0.18.0",
+ "lodash": "4.17.10",
"react": "16.4.0",
"react-dom": "16.4.0",
"react-redux": "5.0.7",
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
+ <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Arvados Workbench 2</title>
+ <script>FontAwesomeConfig = { autoReplaceSvg: 'nest' }</script>
+ <script defer src="https://use.fontawesome.com/releases/v5.0.13/js/all.js" integrity="sha384-xymdQtn1n3lH2wcu0qhcdaOpQwyoarkgLVxC/wZ5q7h9gHtxICrpcaSUfygqZGOe" crossorigin="anonymous"></script>
</head>
<body>
<noscript>
+++ /dev/null
-#!/bin/bash -x
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-# The script uses the docker image composer-build:latest
-# Usage docker run -ti -v /var/lib/jenkins/workspace/build-packages-workbench2/:/tmp/workbench2 composer-build:latest /tmp/workbench2/run-tests-build.sh --build_version 1.0.1
-format_last_commit_here() {
- local format="$1"; shift
- TZ=UTC git log -n1 --first-parent "--format=format:$format" .
-}
-
-version_from_git() {
- # Output the version being built, or if we're building a
- # dev/prerelease, output a version number based on the git log for
- # the current working directory.
- if [[ -n "$ARVADOS_BUILDING_VERSION" ]]; then
- echo "$ARVADOS_BUILDING_VERSION"
- return
- fi
-
- local git_ts git_hash prefix
- if [[ -n "$1" ]] ; then
- prefix="$1"
- else
- prefix="0.1"
- fi
-
- declare $(format_last_commit_here "git_ts=%ct git_hash=%h")
- ARVADOS_BUILDING_VERSION="$(git describe --abbrev=0).$(date -ud "@$git_ts" +%Y%m%d%H%M%S)"
- echo "$ARVADOS_BUILDING_VERSION"
-}
-
-nohash_version_from_git() {
- version_from_git $1 | cut -d. -f1-3
-}
-
-timestamp_from_git() {
- format_last_commit_here "%ct"
-}
-
-WORKDIR="/tmp/workbench2"
-cd $WORKDIR
-if [[ -n "$2" ]]; then
- build_version="$2"
-else
- build_version="$(version_from_git)"
-fi
-rm -Rf $WORKDIR/node_modules
-rm -f $WORKDIR/*.deb; rm -f $WORKDIR/*.rpm
-# run test and build dist
-make test
-#make build
-yarn build
-
-# Build deb and rpm packages using fpm from dist passing the destination folder for the deploy to be /var/www/arvados-workbench2/
-fpm -s dir -t deb -n arvados-workbench2 -v "$build_version" "--maintainer=Ward Vandewege <ward@curoverse.com>" --description "workbench2 Package" --deb-no-default-config-files $WORKDIR/build/=/var/www/arvados-workbench2/workbench2/
-fpm -s dir -t rpm -n arvados-workbench2 -v "$build_version" "--maintainer=Ward Vandewege <ward@curoverse.com>" --description "workbench2 Package" $WORKDIR/build/=/var/www/arvados-workbench2/workbench2/
-
-mkdir $WORKDIR/packages
-mkdir $WORKDIR/packages/centos7
-mkdir $WORKDIR/packages/ubuntu1404
-mkdir $WORKDIR/packages/ubuntu1604
-mkdir $WORKDIR/packages/debian8
-mkdir $WORKDIR/packages/debian9
-cp $WORKDIR/*.rpm $WORKDIR/packages/centos7/
-cp $WORKDIR/*.deb $WORKDIR/packages/ubuntu1404/
-cp $WORKDIR/*.deb $WORKDIR/packages/ubuntu1604/
-cp $WORKDIR/*.deb $WORKDIR/packages/debian8
-cp $WORKDIR/*.deb $WORKDIR/packages/debian9
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export enum FilterField {
+ UUID = "uuid",
+ OWNER_UUID = "owner_uuid"
+}
+
+export default class FilterBuilder {
+ private filters = "";
+
+ private addCondition(field: FilterField, cond: string, value?: string) {
+ if (value) {
+ this.filters += `["${field}","${cond}","${value}"]`;
+ }
+ return this;
+ }
+
+ public addEqual(field: FilterField, value?: string) {
+ return this.addCondition(field, "=", value);
+ }
+
+ public addLike(field: FilterField, value?: string) {
+ return this.addCondition(field, "like", value);
+ }
+
+ public addILike(field: FilterField, value?: string) {
+ return this.addCondition(field, "ilike", value);
+ }
+
+ public get() {
+ return "[" + this.filters + "]";
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export default class UrlBuilder {
+ private url: string = "";
+ private query: string = "";
+
+ constructor(host: string) {
+ this.url = host;
+ }
+
+ public addParam(param: string, value: string) {
+ if (this.query.length === 0) {
+ this.query += "?";
+ } else {
+ this.query += "&";
+ }
+ this.query += `${param}=${value}`;
+ return this;
+ }
+
+ public get() {
+ return this.url + this.query;
+ }
+}
const apiToken = ApiToken.getUrlParameter(search, 'api_token');
this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
this.props.dispatch(authService.getUserDetails());
- this.props.dispatch(projectService.getTopProjectList());
+ this.props.dispatch(projectService.getProjectList());
}
render() {
return <Redirect to="/"/>
import * as React from 'react';
import { Theme } from "@material-ui/core";
-import { StyleRulesCallback, WithStyles } from "@material-ui/core/styles";
+import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper/Paper";
-import withStyles from "@material-ui/core/es/styles/withStyles";
import Table from "@material-ui/core/Table/Table";
import TableHead from "@material-ui/core/TableHead/TableHead";
import TableRow from "@material-ui/core/TableRow/TableRow";
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { mount } from 'enzyme';
+import * as Enzyme from 'enzyme';
+import * as Adapter from 'enzyme-adapter-react-16';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import { Collapse } from '@material-ui/core';
+
+import ProjectTree from './project-tree';
+import { TreeItem } from '../tree/tree';
+import { Project } from '../../models/project';
+Enzyme.configure({ adapter: new Adapter() });
+
+describe("ProjectTree component", () => {
+
+ it("checks is there ListItemIcon in the ProjectTree component", () => {
+ const project: TreeItem<Project> = {
+ data: {
+ name: "sample name",
+ createdAt: "2018-06-12",
+ modifiedAt: "2018-06-13",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid",
+ href: "href",
+ },
+ id: "3",
+ open: true,
+ active: true
+ };
+ const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
+
+ expect(wrapper.find(ListItemIcon).length).toEqual(1);
+ });
+
+ it("checks are there two ListItemIcon's in the ProjectTree component", () => {
+ const project: Array<TreeItem<Project>> = [
+ {
+ data: {
+ name: "sample name",
+ createdAt: "2018-06-12",
+ modifiedAt: "2018-06-13",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid",
+ href: "href",
+ },
+ id: "3",
+ open: false,
+ active: true
+ },
+ {
+ data: {
+ name: "sample name",
+ createdAt: "2018-06-12",
+ modifiedAt: "2018-06-13",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid",
+ href: "href",
+ },
+ id: "3",
+ open: false,
+ active: true
+ }
+ ];
+ const wrapper = mount(<ProjectTree projects={project} toggleProjectTreeItem={() => { }} />);
+
+ expect(wrapper.find(ListItemIcon).length).toEqual(2);
+ });
+
+ it("check ProjectTree, when open is changed", () => {
+ const project: TreeItem<Project> = {
+ data: {
+ name: "sample name",
+ createdAt: "2018-06-12",
+ modifiedAt: "2018-06-13",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid",
+ href: "href",
+ },
+ id: "3",
+ open: true,
+ active: true,
+ items: [
+ {
+ data: {
+ name: "sample name",
+ createdAt: "2018-06-12",
+ modifiedAt: "2018-06-13",
+ uuid: "uuid",
+ ownerUuid: "ownerUuid",
+ href: "href",
+ },
+ id: "4",
+ open: false,
+ active: true
+ }
+ ]
+ };
+ const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
+ wrapper.setState({open: true });
+
+ expect(wrapper.find(Collapse).length).toEqual(1);
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { ReactElement } from 'react';
+import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
+import ListItemText from "@material-ui/core/ListItemText/ListItemText";
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import Typography from '@material-ui/core/Typography';
+
+import Tree, { TreeItem } from '../tree/tree';
+import { Project } from '../../models/project';
+
+type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer';
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+ active: {
+ color: '#4285F6',
+ },
+ listItemText: {
+ padding: '0px',
+ },
+ row: {
+ display: 'flex',
+ alignItems: 'center',
+ marginLeft: '20px',
+ },
+ treeContainer: {
+ position: 'absolute',
+ overflowX: 'visible',
+ marginTop: '80px',
+ minWidth: '240px',
+ whiteSpace: 'nowrap',
+ }
+});
+
+export interface ProjectTreeProps {
+ projects: Array<TreeItem<Project>>;
+ toggleProjectTreeItem: (id: string) => 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)
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+it("should render the tree", () => {
+ expect(true).toBe(true);
+});
\ No newline at end of file
import * as React from 'react';
import List from "@material-ui/core/List/List";
import ListItem from "@material-ui/core/ListItem/ListItem";
+import { StyleRulesCallback, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
import { ReactElement } from "react";
+import Collapse from "@material-ui/core/Collapse/Collapse";
+
+type CssRules = 'list' | 'activeArrow' | 'arrow' | 'arrowRotate';
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+ list: {
+ paddingBottom: '3px',
+ paddingTop: '3px',
+ },
+ activeArrow: {
+ color: '#4285F6',
+ position: 'absolute',
+ },
+ arrow: {
+ position: 'absolute',
+ },
+ arrowRotate: {
+ transform: 'rotate(-90deg)',
+ }
+});
+
+export interface TreeItem<T> {
+ data: T;
+ id: string;
+ open: boolean;
+ active: boolean;
+ items?: Array<TreeItem<T>>;
+}
interface TreeProps<T> {
- items: T[],
- render: (item: T) => ReactElement<{}>
+ items?: Array<TreeItem<T>>;
+ render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
+ toggleItem: (id: string) => any;
+ level?: number;
}
-class Tree<T> extends React.Component<TreeProps<T>, {}> {
- render() {
- return <List>
- {this.props.items && this.props.items.map((it: T, idx: number) =>
- <ListItem key={`item/${idx}`} button>
- {this.props.render(it)}
+class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
+ renderArrow (items: boolean, arrowClass: string, open: boolean){
+ return <i className={`${arrowClass} ${open ? "fas fa-caret-down" : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />
+ }
+ render(): ReactElement<any> {
+ const level = this.props.level ? this.props.level : 0;
+ const {classes, render, toggleItem, items} = this.props;
+ const {list, arrow, activeArrow} = classes;
+ return <List component="div" className={list}>
+ {items && items.map((it: TreeItem<T>, idx: number) =>
+ <div key={`item/${level}/${idx}`}>
+ <ListItem button onClick={() => toggleItem(it.id)} className={list} style={{paddingLeft: (level + 1) * 20}}>
+ {this.renderArrow(true, it.active ? activeArrow : arrow, it.open)}
+ {render(it, level)}
</ListItem>
- )}
+ {it.items && it.items.length > 0 &&
+ <Collapse in={it.open} timeout="auto" unmountOnExit>
+ <StyledTree
+ items={it.items}
+ render={render}
+ toggleItem={toggleItem}
+ level={level + 1}/>
+ </Collapse>}
+ </div>)}
</List>
}
}
-export default Tree;
+const StyledTree = withStyles(styles)(Tree);
+export default StyledTree
import { projectService } from "./services/services";
const history = createBrowserHistory();
+
const store = configureStore({
projects: [
],
}, history);
store.dispatch(authActions.INIT());
-store.dispatch<any>(projectService.getTopProjectList());
+store.dispatch<any>(projectService.getProjectList());
const App = () =>
<Provider store={store}>
modifiedAt: string;
uuid: string;
ownerUuid: string;
- href: string
+ href: string;
}
//
// SPDX-License-Identifier: AGPL-3.0
-import { API_HOST, serverApi } from "../../common/server-api";
+import { API_HOST, serverApi } from "../../common/api/server-api";
import { User } from "../../models/user";
import { Dispatch } from "redux";
import actions from "../../store/auth/auth-action";
//
// SPDX-License-Identifier: AGPL-3.0
-import { serverApi } from "../../common/server-api";
+import { serverApi } from "../../common/api/server-api";
import { Dispatch } from "redux";
import actions from "../../store/project/project-action";
import { Project } from "../../models/project";
+import UrlBuilder from "../../common/api/url-builder";
+import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
interface GroupsResponse {
offset: number;
}
export default class ProjectService {
- public getTopProjectList = () => (dispatch: Dispatch) => {
- dispatch(actions.TOP_PROJECTS_REQUEST());
- serverApi.get<GroupsResponse>('/groups').then(groups => {
+ public getProjectList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Project[]> => {
+ dispatch(actions.PROJECTS_REQUEST());
+
+ const ub = new UrlBuilder('/groups');
+ const fb = new FilterBuilder();
+ fb.addEqual(FilterField.OWNER_UUID, parentUuid);
+ const url = ub.addParam('filters', fb.get()).get();
+
+ return serverApi.get<GroupsResponse>(url).then(groups => {
const projects = groups.data.items.map(g => ({
name: g.name,
createdAt: g.created_at,
uuid: g.uuid,
ownerUuid: g.owner_uuid
} as Project));
- dispatch(actions.TOP_PROJECTS_SUCCESS(projects));
+ dispatch(actions.PROJECTS_SUCCESS({projects, parentItemId: parentUuid}));
+ return projects;
});
- }
+ };
}
USER_FIRST_NAME_KEY,
USER_LAST_NAME_KEY
} from "../../services/auth-service/auth-service";
-import { API_HOST } from "../../common/server-api";
+import { API_HOST } from "../../common/api/server-api";
import 'jest-localstorage-mock';
import actions, { AuthAction } from "./auth-action";
import { User } from "../../models/user";
import { authService } from "../../services/services";
-import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/server-api";
+import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
import { UserDetailsResponse } from "../../services/auth-service/auth-service";
export interface AuthState {
const actions = unionize({
CREATE_PROJECT: ofType<Project>(),
REMOVE_PROJECT: ofType<string>(),
- TOP_PROJECTS_REQUEST: {},
- TOP_PROJECTS_SUCCESS: ofType<Project[]>()
+ PROJECTS_REQUEST: {},
+ PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(),
+ TOGGLE_PROJECT_TREE_ITEM: ofType<string>()
}, {
tag: 'type',
value: 'payload'
uuid: 'test123'
};
- const topProjects = [project, project];
- const state = projectsReducer(initialState, actions.TOP_PROJECTS_SUCCESS(topProjects));
- expect(state).toEqual(topProjects);
+ 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
+ }, {
+ active: false,
+ open: false,
+ id: "test123",
+ items: [],
+ data: project
+ }
+ ]);
});
});
import { Project } from "../../models/project";
import actions, { ProjectAction } from "./project-action";
+import { TreeItem } 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 {
+ let item;
+ for (const t of tree) {
+ item = t.id === itemId
+ ? t
+ : findTreeItem(t.items ? t.items : [], itemId);
+ if (item) {
+ break;
+ }
+ }
+ return item;
+}
+
+function resetTreeActivity<T>(tree: Array<TreeItem<T>>) {
+ for (const t of tree) {
+ t.active = false;
+ resetTreeActivity(t.items ? t.items : []);
+ }
+}
+
+function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[], parentItemId?: string): Array<TreeItem<Project>> {
+ let treeItem;
+ if (parentItemId) {
+ treeItem = findTreeItem(tree, parentItemId);
+ }
+ const items = projects.map((p, idx) => ({
+ id: p.uuid,
+ open: false,
+ active: false,
+ data: p,
+ items: []
+ } as TreeItem<Project>));
+
+ if (treeItem) {
+ treeItem.items = items;
+ return tree;
+ }
+
+ return items;
+}
-export type ProjectState = Project[];
const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
return actions.match(action, {
CREATE_PROJECT: project => [...state, project],
REMOVE_PROJECT: () => state,
- TOP_PROJECTS_REQUEST: () => state,
- TOP_PROJECTS_SUCCESS: projects => {
- return projects;
+ PROJECTS_REQUEST: () => state,
+ PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
+ return updateProjectTree(state, projects, parentItemId);
+ },
+ TOGGLE_PROJECT_TREE_ITEM: itemId => {
+ const tree = _.cloneDeep(state);
+ resetTreeActivity(tree);
+ const item = findTreeItem(tree, itemId);
+ if (item) {
+ item.open = !item.open;
+ item.active = true;
+ }
+ return tree;
},
default: () => state
});
import { Provider } from "react-redux";
import configureStore from "../../store/store";
import createBrowserHistory from "history/createBrowserHistory";
+import { ConnectedRouter } from "react-router-redux";
+
+const history = createBrowserHistory();
it('renders without crashing', () => {
const div = document.createElement('div');
const store = configureStore({ projects: [], router: { location: null }, auth: {} }, createBrowserHistory());
- ReactDOM.render(<Provider store={store}><Workbench/></Provider>, div);
+ ReactDOM.render(
+ <Provider store={store}>
+ <ConnectedRouter history={history}>
+ <Workbench/>
+ </ConnectedRouter>
+ </Provider>,
+ div);
ReactDOM.unmountComponentAtNode(div);
});
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import { connect, DispatchProp } from "react-redux";
-import Tree from "../../components/tree/tree";
-import { Project } from "../../models/project";
import ProjectList from "../../components/project-list/project-list";
import { Route, Switch } from "react-router";
import { Link } from "react-router-dom";
import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItems, MainAppBarMenuItem } from '../../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 ProjectTree from '../../components/project-tree/project-tree';
+import { TreeItem } from "../../components/tree/tree";
+import { Project } from "../../models/project";
+import { projectService } from '../../services/services';
const drawerWidth = 240;
});
interface WorkbenchDataProps {
- projects: Project[];
+ projects: Array<TreeItem<Project>>;
user?: User;
}
onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action()
}
+ toggleProjectTreeItem = (itemId: string) => {
+ this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => {
+ this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
+ });
+ };
+
render() {
const { classes, user } = this.props;
return (
/>
</div>
{user &&
- <Drawer
- variant="permanent"
- classes={{
- paper: classes.drawerPaper,
- }}>
- <div className={classes.toolbar} />
- <div className={classes.toolbar} />
- <Tree items={this.props.projects} render={(p: Project) =>
- <Link to={`/project/${p.name}`}>{p.name}</Link>
- } />
- </Drawer>}
+ <Drawer
+ variant="permanent"
+ classes={{
+ paper: classes.drawerPaper,
+ }}>
+ <div className={classes.toolbar}/>
+ <ProjectTree
+ projects={this.props.projects}
+ toggleProjectTreeItem={this.toggleProjectTreeItem}/>
+ </Drawer>}
<main className={classes.content}>
<div className={classes.toolbar} />
<div className={classes.toolbar} />
"acceptance-tests",
"webpack",
"jest",
- "src/setupTests.ts"
+ "src/setupTests.ts",
+ "**/*.test.tsx"
]
}
"compilerOptions": {
"module": "commonjs"
}
-}
\ No newline at end of file
+}
"jsx-boolean-value": false,
"jsx-no-lambda": false,
"no-debugger": false,
- "no-console": false
+ "no-console": false,
+ "no-shadowed-variable": false
},
"linterOptions": {
"exclude": [
csstype "^2.0.0"
indefinite-observable "^1.0.1"
+"@types/lodash@^4.14.109":
+ version "4.14.109"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.109.tgz#b1c4442239730bf35cabaf493c772b18c045886d"
+
"@types/node@*", "@types/node@10.3.0":
version "10.3.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.0.tgz#078516315a84d56216b5d4fed8f75d59d3b16cac"
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
-"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
+lodash@4.17.10, "lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"