From: Michal Klobukowski Date: Tue, 26 Jun 2018 16:42:24 +0000 (+0200) Subject: Merge branch 'master' X-Git-Tag: 1.2.0~63^2~5 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/52cc3b912c703c24bc90e67aaf24e8ad912d3ebf Merge branch 'master' Feature #13678 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- 52cc3b912c703c24bc90e67aaf24e8ad912d3ebf diff --cc src/components/data-table/data-table.test.tsx index 77979af6,726972e0..b9d11252 --- a/src/components/data-table/data-table.test.tsx +++ b/src/components/data-table/data-table.test.tsx @@@ -97,26 -98,8 +97,8 @@@ describe("", () => expect(dataTable.find(TableBody).find(TableCell).key()).toBe("column-1-key"); }); - it("shows information that items array is empty", () => { - const columns: DataColumns = [ - { - name: "Column 1", - render: () => , - selected: true - } - ]; - const dataTable = mount(); - expect(dataTable.find(Typography).text()).toBe("No items"); - }); - it("renders items", () => { - const columns: Array> = [ + const columns: DataColumns = [ { name: "Column 1", render: (item) => {item}, diff --cc src/store/navigation/navigation-action.ts index 00000000,0b4bcdf8..b811f9a2 mode 000000,100644..100644 --- a/src/store/navigation/navigation-action.ts +++ b/src/store/navigation/navigation-action.ts @@@ -1,0 -1,63 +1,80 @@@ + // Copyright (C) The Arvados Authors. All rights reserved. + // + // SPDX-License-Identifier: AGPL-3.0 + + import { Dispatch } from "redux"; + import projectActions, { getProjectList } from "../project/project-action"; + import { push } from "react-router-redux"; -import { TreeItem, TreeItemStatus } from "../../components/tree/tree"; ++import { TreeItemStatus } from "../../components/tree/tree"; + import { getCollectionList } from "../collection/collection-action"; + import { findTreeItem } from "../project/project-reducer"; -import { Project } from "../../models/project"; + import { Resource, ResourceKind } from "../../models/resource"; + import sidePanelActions from "../side-panel/side-panel-action"; ++import dataExplorerActions from "../data-explorer/data-explorer-action"; ++import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel"; ++import { projectPanelItems } from "../../views/project-panel/project-panel-selectors"; ++import { RootState } from "../store"; + + export const getResourceUrl = (resource: Resource): string => { + switch (resource.kind) { + case ResourceKind.LEVEL_UP: return `/projects/${resource.ownerUuid}`; + case ResourceKind.PROJECT: return `/projects/${resource.uuid}`; + case ResourceKind.COLLECTION: return `/collections/${resource.uuid}`; + default: + return "#"; + } + }; + + export enum ItemMode { + BOTH, + OPEN, + ACTIVE + } + -export const setProjectItem = (projects: Array>, itemId: string, itemKind: ResourceKind, itemMode: ItemMode) => (dispatch: Dispatch) => { ++export const setProjectItem = (itemId: string, itemKind = ResourceKind.PROJECT, itemMode = ItemMode.OPEN) => ++ (dispatch: Dispatch, getState: () => RootState) => { ++ const { projects } = getState(); + - const openProjectItem = (resource: Resource) => { - if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) { - dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid)); ++ let treeItem = findTreeItem(projects.items, itemId); ++ if (treeItem && itemKind === ResourceKind.LEVEL_UP) { ++ treeItem = findTreeItem(projects.items, treeItem.data.ownerUuid); + } + - if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) { - dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid)); - } ++ if (treeItem) { ++ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid)); + - dispatch(push(getResourceUrl({...resource, kind: itemKind}))); ++ if (treeItem.status === TreeItemStatus.Loaded) { ++ dispatch(openProjectItem(treeItem.data, itemKind, itemMode)); ++ } else { ++ dispatch(getProjectList(itemId)) ++ .then(() => dispatch(openProjectItem(treeItem!.data, itemKind, itemMode))); ++ } ++ if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) { ++ dispatch(getCollectionList(itemId)); ++ } ++ } + }; + - let treeItem = findTreeItem(projects, itemId); - if (treeItem && itemKind === ResourceKind.LEVEL_UP) { - treeItem = findTreeItem(projects, treeItem.data.ownerUuid); - } ++const openProjectItem = (resource: Resource, itemKind: ResourceKind, itemMode: ItemMode) => ++ (dispatch: Dispatch, getState: () => RootState) => { + - if (treeItem) { - dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid)); ++ const { collections, projects } = getState(); + - if (treeItem.status === TreeItemStatus.Loaded) { - openProjectItem(treeItem.data); - } else { - dispatch(getProjectList(itemId)) - .then(() => openProjectItem(treeItem!.data)); ++ if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) { ++ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(resource.uuid)); + } ++ + if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) { - dispatch(getCollectionList(itemId)); ++ dispatch(sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(resource.uuid)); + } - } -}; ++ ++ dispatch(push(getResourceUrl({ ...resource, kind: itemKind }))); ++ dispatch(dataExplorerActions.SET_ITEMS({ ++ id: PROJECT_PANEL_ID, ++ items: projectPanelItems( ++ projects.items, ++ resource.uuid, ++ collections ++ ) ++ })); ++ }; diff --cc src/store/store.ts index 7092c1d9,40b24a04..68c5d823 --- a/src/store/store.ts +++ b/src/store/store.ts @@@ -10,8 -10,7 +10,8 @@@ import { History } from "history" import projectsReducer, { ProjectState } from "./project/project-reducer"; import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer'; import authReducer, { AuthState } from "./auth/auth-reducer"; - import collectionsReducer from "./collection/collection-reducer"; +import dataExplorerReducer, { DataExplorerState } from './data-explorer/data-explorer-reducer'; + import collectionsReducer, { CollectionState } from "./collection/collection-reducer"; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@@ -21,8 -20,8 +21,9 @@@ export interface RootState { auth: AuthState; projects: ProjectState; + collections: CollectionState; router: RouterState; + dataExplorer: DataExplorerState; sidePanel: SidePanelState; } diff --cc src/views/project-panel/project-panel-item.ts index 00000000,4fa3d3d6..e0eb84f0 mode 000000,100644..100644 --- a/src/views/project-panel/project-panel-item.ts +++ b/src/views/project-panel/project-panel-item.ts @@@ -1,0 -1,27 +1,29 @@@ + // Copyright (C) The Arvados Authors. All rights reserved. + // + // SPDX-License-Identifier: AGPL-3.0 + ++import { TreeItem } from "../../components/tree/tree"; ++import { Project } from "../../models/project"; + import { getResourceKind, Resource, ResourceKind } from "../../models/resource"; + -export interface ProjectExplorerItem { ++export interface ProjectPanelItem { + uuid: string; + name: string; + kind: ResourceKind; + url: string; + owner: string; + lastModified: string; + fileSize?: number; + status?: string; + } + + function resourceToDataItem(r: Resource, kind?: ResourceKind) { + return { + uuid: r.uuid, + name: r.name, + kind: kind ? kind : getResourceKind(r.kind), + owner: r.ownerUuid, + lastModified: r.modifiedAt + }; + } + diff --cc src/views/project-panel/project-panel-selectors.ts index 00000000,83bfd603..5571e912 mode 000000,100644..100644 --- a/src/views/project-panel/project-panel-selectors.ts +++ b/src/views/project-panel/project-panel-selectors.ts @@@ -1,0 -1,58 +1,58 @@@ + // Copyright (C) The Arvados Authors. All rights reserved. + // + // SPDX-License-Identifier: AGPL-3.0 + + import { TreeItem } from "../../components/tree/tree"; + import { Project } from "../../models/project"; + import { findTreeItem } from "../../store/project/project-reducer"; + import { ResourceKind } from "../../models/resource"; + import { Collection } from "../../models/collection"; + import { getResourceUrl } from "../../store/navigation/navigation-action"; -import { ProjectExplorerItem } from "../../views-components/project-explorer/project-explorer-item"; ++import { ProjectPanelItem } from "./project-panel-item"; + -export const projectExplorerItems = (projects: Array>, treeItemId: string, collections: Array): ProjectExplorerItem[] => { - const dataItems: ProjectExplorerItem[] = []; ++export const projectPanelItems = (projects: Array>, treeItemId: string, collections: Array): ProjectPanelItem[] => { ++ const dataItems: ProjectPanelItem[] = []; + + const treeItem = findTreeItem(projects, treeItemId); + if (treeItem) { + dataItems.push({ + name: "..", + url: getResourceUrl(treeItem.data), + kind: ResourceKind.LEVEL_UP, + owner: "", + uuid: treeItem.data.uuid, + lastModified: "" + }); + + if (treeItem.items) { + treeItem.items.forEach(p => { + const item = { + name: p.data.name, + kind: ResourceKind.PROJECT, + url: getResourceUrl(treeItem.data), + owner: p.data.ownerUuid, + uuid: p.data.uuid, + lastModified: p.data.modifiedAt - } as ProjectExplorerItem; ++ } as ProjectPanelItem; + + dataItems.push(item); + }); + } + } + + collections.forEach(c => { + const item = { + name: c.name, + kind: ResourceKind.COLLECTION, + url: getResourceUrl(c), + owner: c.ownerUuid, + uuid: c.uuid, + lastModified: c.modifiedAt - } as ProjectExplorerItem; ++ } as ProjectPanelItem; + + dataItems.push(item); + }); + + return dataItems; + }; + diff --cc src/views/project-panel/project-panel.tsx index 16f670cb,df9721fd..dbff20e1 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@@ -3,78 -3,105 +3,84 @@@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; - import { ProjectExplorerItem } from './project-explorer-item'; -import { RouteComponentProps } from 'react-router'; -import { ProjectState } from '../../store/project/project-reducer'; -import { RootState } from '../../store/store'; -import { connect, DispatchProp } from 'react-redux'; -import { CollectionState } from "../../store/collection/collection-reducer"; -import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action"; -import ProjectExplorer from "../../views-components/project-explorer/project-explorer"; -import { projectExplorerItems } from "./project-panel-selectors"; -import { ProjectExplorerItem } from "../../views-components/project-explorer/project-explorer-item"; -import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; -import { DataColumn, SortDirection } from '../../components/data-table/data-column'; ++import { ProjectPanelItem } from './project-panel-item'; +import { Grid, Typography, Button, Toolbar, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; +import { formatDate, formatFileSize } from '../../common/formatters'; - import DataExplorer from '../data-explorer/data-explorer'; - import { DataColumn } from '../../components/data-table/data-column'; ++import DataExplorer from "../../views-components/data-explorer/data-explorer"; ++import { DataColumn, toggleSortDirection } from '../../components/data-table/data-column'; import { DataTableFilterItem } from '../../components/data-table-filters/data-table-filters'; +import { ContextMenuAction } from '../../components/context-menu/context-menu'; +import { DispatchProp, connect } from 'react-redux'; +import actions from "../../store/data-explorer/data-explorer-action"; ++import { setProjectItem } from "../../store/navigation/navigation-action"; +import { DataColumns } from '../../components/data-table/data-table'; ++import { ResourceKind } from "../../models/resource"; - export const PROJECT_EXPLORER_ID = "projectExplorer"; - class ProjectExplorer extends React.Component> { -interface ProjectPanelDataProps { - projects: ProjectState; - collections: CollectionState; -} ++export const PROJECT_PANEL_ID = "projectPanel"; ++class ProjectPanel extends React.Component> { + render() { + return
+
+ + + +
+ ; +
; + } -type ProjectPanelProps = ProjectPanelDataProps & RouteComponentProps<{ name: string }> & DispatchProp; + componentDidMount() { - this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_EXPLORER_ID, columns })); ++ this.props.dispatch(actions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns })); + } - toggleColumn = (toggledColumn: DataColumn) => { - this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_EXPLORER_ID, columnName: toggledColumn.name })); -interface ProjectPanelState { - sort: { - columnName: string; - direction: SortDirection; - }; - filters: string[]; -} ++ toggleColumn = (toggledColumn: DataColumn) => { ++ this.props.dispatch(actions.TOGGLE_COLUMN({ id: PROJECT_PANEL_ID, columnName: toggledColumn.name })); + } - toggleSort = (toggledColumn: DataColumn) => { - this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_EXPLORER_ID, columnName: toggledColumn.name })); -class ProjectPanel extends React.Component, ProjectPanelState> { - state: ProjectPanelState = { - sort: { - columnName: "Name", - direction: "desc" - }, - filters: ['collection', 'project'] - }; ++ toggleSort = (column: DataColumn) => { ++ this.props.dispatch(actions.TOGGLE_SORT({ id: PROJECT_PANEL_ID, columnName: column.name })); + } - changeFilters = (filters: DataTableFilterItem[], updatedColumn: DataColumn) => { - this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_EXPLORER_ID, columnName: updatedColumn.name, filters })); - render() { - const items = projectExplorerItems( - this.props.projects.items, - this.props.projects.currentItemId, - this.props.collections - ); - const [goBackItem, ...otherItems] = items; - const filteredItems = otherItems.filter(i => this.state.filters.some(f => f === i.kind)); - const sortedItems = sortItems(this.state.sort, filteredItems); - return ( -
-
- - - -
- -
- ); ++ changeFilters = (filters: DataTableFilterItem[], column: DataColumn) => { ++ this.props.dispatch(actions.SET_FILTERS({ id: PROJECT_PANEL_ID, columnName: column.name, filters })); } - executeAction = (action: ContextMenuAction, item: ProjectExplorerItem) => { - goToItem = (item: ProjectExplorerItem) => { - this.props.dispatch(setProjectItem(this.props.projects.items, item.uuid, item.kind, ItemMode.BOTH)); ++ executeAction = (action: ContextMenuAction, item: ProjectPanelItem) => { + alert(`Executing ${action.name} on ${item.name}`); } - toggleSort = (column: DataColumn) => { - this.setState({ - sort: { - columnName: column.name, - direction: column.sortDirection || "none" - } - }); + search = (searchValue: string) => { - this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_EXPLORER_ID, searchValue })); ++ this.props.dispatch(actions.SET_SEARCH_VALUE({ id: PROJECT_PANEL_ID, searchValue })); } - changeFilters = (filters: DataTableFilterItem[]) => { - this.setState({ filters: filters.filter(f => f.selected).map(f => f.name.toLowerCase()) }); + changePage = (page: number) => { - this.props.dispatch(actions.SET_PAGE({ id: PROJECT_EXPLORER_ID, page })); ++ this.props.dispatch(actions.SET_PAGE({ id: PROJECT_PANEL_ID, page })); } -} -const sortItems = (sort: { columnName: string, direction: SortDirection }, items: ProjectExplorerItem[]) => { - const sortedItems = items.slice(0); - const direction = sort.direction === "asc" ? -1 : 1; - sortedItems.sort((a, b) => { - if (sort.columnName === "Last modified") { - return ((new Date(a.lastModified)).getTime() - (new Date(b.lastModified)).getTime()) * direction; - } else { - return a.name.localeCompare(b.name) * direction; - } - }); - return sortedItems; -}; + changeRowsPerPage = (rowsPerPage: number) => { - this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_EXPLORER_ID, rowsPerPage })); ++ this.props.dispatch(actions.SET_ROWS_PER_PAGE({ id: PROJECT_PANEL_ID, rowsPerPage })); ++ } ++ ++ openProject = (item: ProjectPanelItem) => { ++ this.props.dispatch(setProjectItem(item.uuid)); + } +} type CssRules = "toolbar" | "button"; @@@ -88,122 -116,10 +94,124 @@@ const styles: StyleRulesCallback -export default withStyles(styles)( - connect( - (state: RootState) => ({ - projects: state.projects, - collections: state.collections - }) - )(ProjectPanel)); ++const renderName = (item: ProjectPanelItem) => + + + {renderIcon(item)} + + + + {item.name} + + + ; + - const renderIcon = (item: ProjectExplorerItem) => { - switch (item.type) { - case "arvados#group": ++ ++const renderIcon = (item: ProjectPanelItem) => { ++ switch (item.kind) { ++ case ResourceKind.LEVEL_UP: ++ return ; ++ case ResourceKind.PROJECT: + return ; - case "arvados#groupList": ++ case ResourceKind.COLLECTION: + return ; + default: + return ; + } +}; + +const renderDate = (date: string) => + + {formatDate(date)} + ; + +const renderFileSize = (fileSize?: number) => + + {formatFileSize(fileSize)} + ; + +const renderOwner = (owner: string) => + + {owner} + ; + +const renderType = (type: string) => + + {type} + ; + - const renderStatus = (item: ProjectExplorerItem) => ++const renderStatus = (item: ProjectPanelItem) => + + {item.status || "-"} + ; + - const columns: DataColumns = [{ ++const columns: DataColumns = [{ + name: "Name", + selected: true, - sortDirection: "asc", - render: renderName ++ sortDirection: "desc", ++ render: renderName, ++ width: "450px" +}, { + name: "Status", + selected: true, - filters: [{ - name: "In progress", - selected: true - }, { - name: "Complete", - selected: true - }], - render: renderStatus ++ render: renderStatus, ++ width: "75px" +}, { + name: "Type", + selected: true, + filters: [{ + name: "Collection", + selected: true + }, { - name: "Group", ++ name: "Project", + selected: true + }], - render: item => renderType(item.type) ++ render: item => renderType(item.kind), ++ width: "125px" +}, { + name: "Owner", + selected: true, - render: item => renderOwner(item.owner) ++ render: item => renderOwner(item.owner), ++ width: "200px" +}, { + name: "File size", + selected: true, - sortDirection: "none", - render: item => renderFileSize(item.fileSize) ++ render: item => renderFileSize(item.fileSize), ++ width: "50px" +}, { + name: "Last modified", + selected: true, - render: item => renderDate(item.lastModified) ++ sortDirection: "none", ++ render: item => renderDate(item.lastModified), ++ width: "150px" +}]; + +const contextMenuActions = [[{ + icon: "fas fa-users fa-fw", + name: "Share" +}, { + icon: "fas fa-sign-out-alt fa-fw", + name: "Move to" +}, { + icon: "fas fa-star fa-fw", + name: "Add to favourite" +}, { + icon: "fas fa-edit fa-fw", + name: "Rename" +}, { + icon: "fas fa-copy fa-fw", + name: "Make a copy" +}, { + icon: "fas fa-download fa-fw", + name: "Download" +}], [{ + icon: "fas fa-trash-alt fa-fw", + name: "Remove" +} +]]; + - export default withStyles(styles)(connect()(ProjectExplorer)); ++export default withStyles(styles)(connect()(ProjectPanel)); diff --cc src/views/workbench/workbench.tsx index 92cbc5d0,1069de53..72eb0ddc --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@@ -8,21 -8,24 +8,25 @@@ import Drawer from '@material-ui/core/D import { connect, DispatchProp } from "react-redux"; import { Route, Switch } from "react-router"; import authActions from "../../store/auth/auth-action"; +import dataExplorerActions from "../../store/data-explorer/data-explorer-action"; import { User } from "../../models/user"; import { RootState } from "../../store/store"; - import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar'; + import MainAppBar, { + MainAppBarActionProps, + MainAppBarMenuItem + } from '../../views-components/main-app-bar/main-app-bar'; import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs'; import { push } from 'react-router-redux'; - import projectActions, { getProjectList } from "../../store/project/project-action"; import ProjectTree from '../../views-components/project-tree/project-tree'; - import { TreeItem, TreeItemStatus } from "../../components/tree/tree"; + import { TreeItem } from "../../components/tree/tree"; import { Project } from "../../models/project"; -import { getTreePath } from '../../store/project/project-reducer'; -import ProjectPanel from '../project-panel/project-panel'; +import { getTreePath, findTreeItem } from '../../store/project/project-reducer'; - import ProjectExplorer, { PROJECT_EXPLORER_ID } from '../../views-components/project-explorer/project-explorer'; - import { ProjectExplorerItem, mapProjectTreeItem } from '../../views-components/project-explorer/project-explorer-item'; import sidePanelActions from '../../store/side-panel/side-panel-action'; import SidePanel, { SidePanelItem } from '../../components/side-panel/side-panel'; + import { ResourceKind } from "../../models/resource"; + import { ItemMode, setProjectItem } from "../../store/navigation/navigation-action"; + import projectActions from "../../store/project/project-action"; ++import ProjectPanel from "../project-panel/project-panel"; const drawerWidth = 240; const appBarHeight = 102; @@@ -128,10 -130,11 +131,9 @@@ class Workbench extends React.Component } }; - mainAppBarActions: MainAppBarActionProps = { - onBreadcrumbClick: ({ itemId, status }: NavBreadcrumb) => { - this.toggleProjectTreeItemOpen(itemId, status); + onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => { - this.props.dispatch( - setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.BOTH) - ); ++ this.props.dispatch(setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.BOTH)); }, onSearch: searchText => { this.setState({ searchText }); @@@ -221,11 -182,18 +181,18 @@@ + sidePanelItems={this.props.sidePanelItems}> + projects={this.props.projects} + toggleOpen={itemId => + this.props.dispatch( - setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.OPEN) ++ setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.OPEN) + )} + toggleActive={itemId => + this.props.dispatch( - setProjectItem(this.props.projects, itemId, ResourceKind.PROJECT, ItemMode.ACTIVE) ++ setProjectItem(itemId, ResourceKind.PROJECT, ItemMode.ACTIVE) + )} + /> }