"version": "0.1.0",
"private": true,
"dependencies": {
- "@material-ui/core": "1.4.0",
- "@material-ui/icons": "1.1.0",
- "@types/lodash": "4.14.112",
+ "@material-ui/core": "1.4.2",
+ "@material-ui/icons": "2.0.0",
+ "@types/lodash": "4.14.116",
"@types/react-copy-to-clipboard": "4.2.5",
- "@types/redux-form": "7.4.1",
+ "@types/redux-form": "7.4.4",
"axios": "0.18.0",
"classnames": "2.2.6",
"lodash": "4.17.10",
- "react": "16.4.1",
+ "react": "16.4.2",
"react-copy-to-clipboard": "5.0.1",
- "react-dom": "16.4.1",
+ "react-dom": "16.4.2",
"react-redux": "5.0.7",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
"@types/classnames": "^2.2.4",
"@types/enzyme": "3.1.12",
"@types/enzyme-adapter-react-16": "1.0.2",
- "@types/jest": "23.3.0",
- "@types/node": "10.5.2",
+ "@types/jest": "23.3.1",
+ "@types/node": "10.5.5",
"@types/react": "16.4",
"@types/react-dom": "16.0.6",
- "@types/react-redux": "6.0.4",
+ "@types/react-redux": "6.0.6",
"@types/react-router": "4.0.29",
- "@types/react-router-dom": "4.2.7",
+ "@types/react-router-dom": "4.3.0",
"@types/react-router-redux": "5.0.15",
"@types/redux-devtools": "3.0.44",
- "@types/redux-form": "7.4.1",
+ "@types/redux-form": "7.4.4",
"axios-mock-adapter": "1.15.0",
"enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1",
"jest-localstorage-mock": "2.2.0",
"redux-devtools": "3.4.1",
"redux-form": "7.4.2",
- "typescript": "2.9.2"
+ "typescript": "3.0.1"
},
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/src/$1"
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { TreeItem, TreeItemStatus } from '../tree/tree';
+import { FileTreeData } from '../file-tree/file-tree-data';
+import { FileTree } from '../file-tree/file-tree';
+import { IconButton, Grid, Typography, StyleRulesCallback, withStyles, WithStyles, CardHeader, CardContent, Card, Button } from '@material-ui/core';
+import { CustomizeTableIcon } from '../icon/icon';
+
+export interface CollectionPanelFilesProps {
+ items: Array<TreeItem<FileTreeData>>;
+ onUploadDataClick: () => void;
+ onItemMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+ onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>) => void;
+ onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+ onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+}
+
+type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ root: {
+ paddingBottom: theme.spacing.unit
+ },
+ cardSubheader: {
+ paddingTop: 0,
+ paddingBottom: 0
+ },
+ nameHeader: {
+ marginLeft: '75px'
+ },
+ fileSizeHeader: {
+ marginRight: '65px'
+ }
+});
+
+export const CollectionPanelFiles = withStyles(styles)(
+ ({ onItemMenuOpen, onOptionsMenuOpen, classes, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) =>
+ <Card className={classes.root}>
+ <CardHeader
+ title="Files"
+ action={
+ <Button
+ variant='raised'
+ color='primary'
+ size='small'>
+ Upload data
+ </Button>
+ } />
+ <CardHeader
+ className={classes.cardSubheader}
+ action={
+ <IconButton onClick={onOptionsMenuOpen}>
+ <CustomizeTableIcon />
+ </IconButton>
+ } />
+ <Grid container justify="space-between">
+ <Typography variant="caption" className={classes.nameHeader}>
+ Name
+ </Typography>
+ <Typography variant="caption" className={classes.fileSizeHeader}>
+ File size
+ </Typography>
+ </Grid>
+ <FileTree onMenuOpen={onItemMenuOpen} {...treeProps} />
+ </Card>);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface FileTreeData {
+ name: string;
+ type: string;
+ size?: number;
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { TreeItem } from "../tree/tree";
+import { ProjectIcon, MoreOptionsIcon, DefaultIcon, CollectionIcon } from "../icon/icon";
+import { Typography, IconButton, StyleRulesCallback, withStyles, WithStyles } from "@material-ui/core";
+import { formatFileSize } from "../../common/formatters";
+import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
+import { FileTreeData } from "./file-tree-data";
+
+type CssRules = "root" | "spacer" | "sizeInfo" | "button";
+
+const fileTreeItemStyle: StyleRulesCallback<CssRules> = theme => ({
+ root: {
+ display: "flex",
+ alignItems: "center",
+ paddingRight: `${theme.spacing.unit * 1.5}px`
+ },
+ spacer: {
+ flex: "1"
+ },
+ sizeInfo: {
+ width: `${theme.spacing.unit * 8}px`
+ },
+ button: {
+ width: theme.spacing.unit * 3,
+ height: theme.spacing.unit * 3,
+ marginRight: theme.spacing.unit
+ }
+});
+
+export interface FileTreeItemProps {
+ item: TreeItem<FileTreeData>;
+ onMoreClick: (event: React.MouseEvent<any>, item: TreeItem<FileTreeData>) => void;
+}
+export const FileTreeItem = withStyles(fileTreeItemStyle)(
+ class extends React.Component<FileTreeItemProps & WithStyles<CssRules>> {
+ render() {
+ const { classes, item } = this.props;
+ return <div className={classes.root}>
+ <ListItemTextIcon
+ icon={getIcon(item)}
+ name={item.data.name} />
+ <div className={classes.spacer} />
+ <Typography
+ className={classes.sizeInfo}
+ variant="caption">{formatFileSize(item.data.size)}</Typography>
+ <IconButton
+ className={classes.button}
+ onClick={this.handleClick}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </div >;
+ }
+
+ handleClick = (event: React.MouseEvent<any>) => {
+ this.props.onMoreClick(event, this.props.item);
+ }
+ });
+
+const getIcon = (item: TreeItem<FileTreeData>) => {
+ switch(item.data.type){
+ case 'directory':
+ return ProjectIcon;
+ case 'file':
+ return CollectionIcon;
+ default:
+ return DefaultIcon;
+ }
+};
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Tree, TreeItem, TreeItemStatus } from "../tree/tree";
+import { FileTreeData } from "./file-tree-data";
+import { FileTreeItem } from "./file-tree-item";
+
+export interface FileTreeProps {
+ items: Array<TreeItem<FileTreeData>>;
+ onMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+ onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+ onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+}
+
+export class FileTree extends React.Component<FileTreeProps> {
+ render() {
+ return <Tree
+ showSelection={true}
+ items={this.props.items}
+ disableRipple={true}
+ render={this.renderItem}
+ onContextMenu={this.handleContextMenu}
+ toggleItemActive={this.handleToggleActive}
+ toggleItemOpen={this.handleToggle}
+ onSelectionChange={this.handleSelectionChange} />;
+ }
+
+ handleContextMenu = (event: React.MouseEvent<any>, item: TreeItem<FileTreeData>) => {
+ event.stopPropagation();
+ this.props.onMenuOpen(event, item);
+ }
+
+ handleToggle = (id: string, status: TreeItemStatus) => {
+ this.props.onCollapseToggle(id, status);
+ }
+
+ handleToggleActive = () => { return; };
+
+ handleSelectionChange = (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
+ event.stopPropagation();
+ this.props.onSelectionToggle(event, item);
+ }
+
+ renderItem = (item: TreeItem<FileTreeData>) =>
+ <FileTreeItem
+ item={item}
+ onMoreClick={this.handleContextMenu} />
+
+}
import ChevronLeft from '@material-ui/icons/ChevronLeft';
import ChevronRight from '@material-ui/icons/ChevronRight';
import Close from '@material-ui/icons/Close';
-import ContentCopy from '@material-ui/icons/ContentCopy';
+import ContentCopy from '@material-ui/icons/FileCopyOutlined';
import CreateNewFolder from '@material-ui/icons/CreateNewFolder';
import Delete from '@material-ui/icons/Delete';
import DeviceHub from '@material-ui/icons/DeviceHub';
export const TrashIcon: IconType = (props) => <Delete {...props} />;
export const UserPanelIcon: IconType = (props) => <Person {...props} />;
export const UsedByIcon: IconType = (props) => <Folder {...props} />;
-export const WorkflowIcon: IconType = (props) => <Code {...props} />;
\ No newline at end of file
+export const WorkflowIcon: IconType = (props) => <Code {...props} />;
color: theme.palette.primary.main,
},
hasMargin: {
- marginLeft: '18px',
+ marginLeft: `${theme.spacing.unit}px`,
},
});
toggableIconContainer: {
color: theme.palette.grey["700"],
height: '14px',
- position: 'absolute'
+ width: '14px'
},
toggableIcon: {
fontSize: '14px'
import { Tree, TreeItem } from './tree';
import { ProjectResource } from '../../models/project';
import { mockProjectResource } from '../../models/test-utils';
+import { Checkbox } from '@material-ui/core';
Enzyme.configure({ adapter: new Adapter() });
items={[project]} />);
expect(wrapper.find('i')).toHaveLength(1);
});
+
+ it("should render checkbox", () => {
+ const project: TreeItem<ProjectResource> = {
+ data: mockProjectResource(),
+ id: "3",
+ open: true,
+ active: true,
+ status: 1,
+ };
+ const wrapper = mount(<Tree
+ showSelection={true}
+ render={() => <div />}
+ toggleItemOpen={jest.fn()}
+ toggleItemActive={jest.fn()}
+ onContextMenu={jest.fn()}
+ items={[project]} />);
+ expect(wrapper.find(Checkbox)).toHaveLength(1);
+ });
+
+ it("call onSelectionChanged with associated item", () => {
+ const project: TreeItem<ProjectResource> = {
+ data: mockProjectResource(),
+ id: "3",
+ open: true,
+ active: true,
+ status: 1,
+ };
+ const spy = jest.fn();
+ const onSelectionChanged = (event: any, item: TreeItem<any>) => spy(item);
+ const wrapper = mount(<Tree
+ showSelection={true}
+ render={() => <div />}
+ toggleItemOpen={jest.fn()}
+ toggleItemActive={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSelectionChange={onSelectionChanged}
+ items={[project]} />);
+ wrapper.find(Checkbox).prop('onClick')();
+ expect(spy).toHaveBeenLastCalledWith({
+ data: mockProjectResource(),
+ id: "3",
+ open: true,
+ active: true,
+ status: 1,
+ });
+ });
+
});
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
+import { List, ListItem, ListItemIcon, Collapse, Checkbox } from "@material-ui/core";
import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
import { ReactElement } from "react";
import CircularProgress from '@material-ui/core/CircularProgress';
import { ArvadosTheme } from '../../common/custom-theme';
import { SidePanelRightArrowIcon } from '../icon/icon';
-type CssRules = 'list' | 'active' | 'loader' | 'toggableIconContainer' | 'iconClose' | 'iconOpen' | 'toggableIcon';
+type CssRules = 'list'
+ | 'listItem'
+ | 'active'
+ | 'loader'
+ | 'toggableIconContainer'
+ | 'iconClose'
+ | 'renderContainer'
+ | 'iconOpen'
+ | 'toggableIcon'
+ | 'checkbox';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
list: {
padding: '3px 0px'
},
+ listItem: {
+ padding: '3px 0px',
+ },
loader: {
position: 'absolute',
transform: 'translate(0px)',
toggableIconContainer: {
color: theme.palette.grey["700"],
height: '14px',
- position: 'absolute'
+ width: '14px',
},
toggableIcon: {
fontSize: '14px'
},
+ renderContainer: {
+ flex: 1
+ },
active: {
color: theme.palette.primary.main,
},
iconOpen: {
transition: 'all 0.1s ease',
transform: 'rotate(90deg)',
+ },
+ checkbox: {
+ width: theme.spacing.unit * 3,
+ height: theme.spacing.unit * 3,
+ margin: `0 ${theme.spacing.unit}px`,
+ color: theme.palette.grey["500"]
}
});
id: string;
open: boolean;
active: boolean;
+ selected?: boolean;
status: TreeItemStatus;
items?: Array<TreeItem<T>>;
}
toggleItemActive: (id: string, status: TreeItemStatus) => void;
level?: number;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+ showSelection?: boolean;
+ onSelectionChange?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+ disableRipple?: boolean;
}
export const Tree = withStyles(styles)(
class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
render(): ReactElement<any> {
const level = this.props.level ? this.props.level : 0;
- const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
- const { list, loader, toggableIconContainer } = classes;
+ const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu, disableRipple } = this.props;
+ const { list, listItem, loader, toggableIconContainer, renderContainer } = 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 }}
+ <ListItem button className={listItem} style={{ paddingLeft: (level + 1) * 20 }}
+ disableRipple={disableRipple}
onClick={() => toggleItemActive(it.id, it.status)}
onContextMenu={this.handleRowContextMenu(it)}>
{it.status === TreeItemStatus.PENDING ?
{it.status !== TreeItemStatus.INITIAL && it.items && it.items.length === 0 ? <span /> : <SidePanelRightArrowIcon />}
</ListItemIcon>
</i>
- {render(it, level)}
+ {this.props.showSelection &&
+ <Checkbox
+ checked={it.selected}
+ className={classes.checkbox}
+ color="primary"
+ onClick={this.handleCheckboxChange(it)} />}
+ <div className={renderContainer}>
+ {render(it, level)}
+ </div>
</ListItem>
{it.items && it.items.length > 0 &&
<Collapse in={it.open} timeout="auto" unmountOnExit>
<Tree
+ showSelection={this.props.showSelection}
items={it.items}
render={render}
+ disableRipple={disableRipple}
toggleItemOpen={toggleItemOpen}
toggleItemActive={toggleItemActive}
level={level + 1}
- onContextMenu={onContextMenu} />
+ onContextMenu={onContextMenu}
+ onSelectionChange={this.props.onSelectionChange} />
</Collapse>}
</div>)}
</List>;
handleRowContextMenu = (item: TreeItem<T>) =>
(event: React.MouseEvent<HTMLElement>) =>
this.props.onContextMenu(event, item)
+
+ handleCheckboxChange = (item: TreeItem<T>) => {
+ const { onSelectionChange } = this.props;
+ return onSelectionChange
+ ? (event: React.MouseEvent<HTMLElement>) => {
+ onSelectionChange(event, item);
+ }
+ : undefined;
+ }
}
);
import { projectActionSet } from "./views-components/context-menu/action-sets/project-action-set";
import { resourceActionSet } from './views-components/context-menu/action-sets/resource-action-set';
import { favoriteActionSet } from "./views-components/context-menu/action-sets/favorite-action-set";
+import { collectionFilesActionSet } from './views-components/context-menu/action-sets/collection-files-action-set';
+import { collectionFilesItemActionSet } from './views-components/context-menu/action-sets/collection-files-item-action-set';
import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet);
+addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet);
+addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
fetchConfig()
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Tree } from './tree';
+
+export type CollectionFilesTree = Tree<CollectionDirectory | CollectionFile>;
+
+export enum CollectionFileType {
+ DIRECTORY = 'directory',
+ FILE = 'file'
+}
+
+export interface CollectionDirectory {
+ parentId: string;
+ id: string;
+ name: string;
+ type: CollectionFileType.DIRECTORY;
+}
+
+export interface CollectionFile {
+ parentId: string;
+ id: string;
+ name: string;
+ size: number;
+ type: CollectionFileType.FILE;
+}
+
+export const createCollectionDirectory = (data: Partial<CollectionDirectory>): CollectionDirectory => ({
+ id: '',
+ name: '',
+ parentId: '',
+ type: CollectionFileType.DIRECTORY,
+ ...data
+});
+
+export const createCollectionFile = (data: Partial<CollectionFile>): CollectionFile => ({
+ id: '',
+ name: '',
+ parentId: '',
+ size: 0,
+ type: CollectionFileType.FILE,
+ ...data
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type KeepManifest = KeepManifestStream[];
+
+export interface KeepManifestStream {
+ name: string;
+ locators: string[];
+ files: Array<KeepManifestStreamFile>;
+}
+
+export interface KeepManifestStreamFile {
+ name: string;
+ position: string;
+ size: number;
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+const USER_UUID_REGEX = /.*tpzed.*/;
+const GROUP_UUID_REGEX = /.*-j7d0g-.*/;
+
+export enum ObjectTypes {
+ USER = "User",
+ GROUP = "Group",
+ UNKNOWN = "Unknown"
+}
+
+export const getUuidObjectType = (uuid: string) => {
+ switch (true) {
+ case USER_UUID_REGEX.test(uuid):
+ return ObjectTypes.USER;
+ case GROUP_UUID_REGEX.test(uuid):
+ return ObjectTypes.GROUP;
+ default:
+ return ObjectTypes.UNKNOWN;
+ }
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as Tree from './tree';
+
+describe('Tree', () => {
+ let tree: Tree.Tree<string>;
+
+ beforeEach(() => {
+ tree = Tree.createTree();
+ });
+
+ it('sets new node', () => {
+ const newTree = Tree.setNode({ children: [], id: 'Node 1', parent: '', value: 'Value 1' })(tree);
+ expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: [], id: 'Node 1', parent: '', value: 'Value 1' });
+ });
+
+ it('adds new node reference to parent children', () => {
+ const [newTree] = [tree]
+ .map(Tree.setNode({ children: [], id: 'Node 1', parent: '', value: 'Value 1' }))
+ .map(Tree.setNode({ children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' }));
+
+ expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: ['Node 2'], id: 'Node 1', parent: '', value: 'Value 1' });
+ });
+
+ it('gets node ancestors', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: 'Node 2', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeAncestors('Node 3')(newTree)).toEqual(['Node 1', 'Node 2']);
+ });
+
+ it('gets node descendants', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeDescendants('Node 1')(newTree)).toEqual(['Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
+ });
+
+ it('gets root descendants', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeDescendants('')(newTree)).toEqual(['Node 1', 'Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
+ });
+
+ it('gets node children', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeChildren('Node 1')(newTree)).toEqual(['Node 2', 'Node 3']);
+ });
+
+ it('gets root children', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+ { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+ { children: [], id: 'Node 3', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ expect(Tree.getNodeChildren('')(newTree)).toEqual(['Node 1', 'Node 3']);
+ });
+
+ it('maps tree', () => {
+ const newTree = [
+ { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+ { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' },
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+ const mappedTree = Tree.mapTreeValues<string, number>(value => parseInt(value.split(' ')[1], 10))(newTree);
+ expect(Tree.getNode('Node 2')(mappedTree)).toEqual({ children: [], id: 'Node 2', parent: 'Node 1', value: 2 }, );
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type Tree<T> = Record<string, TreeNode<T>>;
+
+export const TREE_ROOT_ID = '';
+
+export interface TreeNode<T> {
+ children: string[];
+ value: T;
+ id: string;
+ parent: string;
+}
+
+export const createTree = <T>(): Tree<T> => ({});
+
+export const getNode = (id: string) => <T>(tree: Tree<T>): TreeNode<T> | undefined => tree[id];
+
+export const setNode = <T>(node: TreeNode<T>) => (tree: Tree<T>): Tree<T> => {
+ const [newTree] = [tree]
+ .map(tree => getNode(node.id)(tree) === node
+ ? tree
+ : {...tree, [node.id]: node})
+ .map(addChild(node.parent, node.id));
+ return newTree;
+};
+
+export const getNodeValue = (id: string) => <T>(tree: Tree<T>) => {
+ const node = getNode(id)(tree);
+ return node ? node.value : undefined;
+};
+
+export const setNodeValue = (id: string) => <T>(value: T) => (tree: Tree<T>) => {
+ const node = getNode(id)(tree);
+ return node
+ ? setNode(mapNodeValue(() => value)(node))(tree)
+ : tree;
+};
+
+export const setNodeValueWith = <T>(mapFn: (value: T) => T) => (id: string) => (tree: Tree<T>) => {
+ const node = getNode(id)(tree);
+ return node
+ ? setNode(mapNodeValue(mapFn)(node))(tree)
+ : tree;
+};
+
+export const mapTreeValues = <T, R>(mapFn: (value: T) => R) => (tree: Tree<T>): Tree<R> =>
+ getNodeDescendants('')(tree)
+ .map(id => getNode(id)(tree))
+ .map(mapNodeValue(mapFn))
+ .reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
+
+export const mapTree = <T, R>(mapFn: (node: TreeNode<T>) => TreeNode<R>) => (tree: Tree<T>): Tree<R> =>
+ getNodeDescendants('')(tree)
+ .map(id => getNode(id)(tree))
+ .map(mapFn)
+ .reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
+
+export const getNodeAncestors = (id: string) => <T>(tree: Tree<T>): string[] => {
+ const node = getNode(id)(tree);
+ return node && node.parent
+ ? [...getNodeAncestors(node.parent)(tree), node.parent]
+ : [];
+};
+
+export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
+ const node = getNode(id)(tree);
+ const children = node ? node.children :
+ id === TREE_ROOT_ID
+ ? getRootNodeChildren(tree)
+ : [];
+
+ return children
+ .concat(limit < 1
+ ? []
+ : children
+ .map(id => getNodeDescendants(id, limit - 1)(tree))
+ .reduce((nodes, nodeChildren) => [...nodes, ...nodeChildren], []));
+};
+
+export const getNodeChildren = (id: string) => <T>(tree: Tree<T>): string[] =>
+ getNodeDescendants(id, 0)(tree);
+
+const mapNodeValue = <T, R>(mapFn: (value: T) => R) => (node: TreeNode<T>): TreeNode<R> =>
+ ({ ...node, value: mapFn(node.value) });
+
+const getRootNodeChildren = <T>(tree: Tree<T>) =>
+ Object
+ .keys(tree)
+ .filter(id => getNode(id)(tree)!.parent === TREE_ROOT_ID);
+
+const addChild = (parentId: string, childId: string) => <T>(tree: Tree<T>): Tree<T> => {
+ const node = getNode(parentId)(tree);
+ if (node) {
+ const children = node.children.some(id => id === childId)
+ ? node.children
+ : [...node.children, childId];
+
+ const newNode = children === node.children
+ ? node
+ : { ...node, children };
+
+ return setNode(newNode)(tree);
+ }
+ return tree;
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionService } from "../collection-service/collection-service";
+import { parseKeepManifestText } from "./collection-manifest-parser";
+import { mapManifestToCollectionFilesTree } from "./collection-manifest-mapper";
+
+export class CollectionFilesService {
+
+ constructor(private collectionService: CollectionService) { }
+
+ getFiles(collectionUuid: string) {
+ return this.collectionService
+ .get(collectionUuid)
+ .then(collection =>
+ mapManifestToCollectionFilesTree(
+ parseKeepManifestText(
+ collection.manifestText
+ )
+ )
+ );
+ }
+
+}
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { parseKeepManifestText } from "./collection-manifest-parser";
+import { mapManifestToFiles, mapManifestToDirectories } from "./collection-manifest-mapper";
+
+test('mapManifestToFiles', () => {
+ const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n./c d41d8cd98f00b204e9800998ecf8427e+0 0:0:d`;
+ const manifest = parseKeepManifestText(manifestText);
+ const files = mapManifestToFiles(manifest);
+ expect(files).toEqual([{
+ parentId: '',
+ id: '/a',
+ name: 'a',
+ size: 0,
+ type: 'file'
+ }, {
+ parentId: '',
+ id: '/b',
+ name: 'b',
+ size: 0,
+ type: 'file'
+ }, {
+ parentId: '',
+ id: '/output.txt',
+ name: 'output.txt',
+ size: 33,
+ type: 'file'
+ }, {
+ parentId: '/c',
+ id: '/c/d',
+ name: 'd',
+ size: 0,
+ type: 'file'
+ },]);
+});
+
+test('mapManifestToDirectories', () => {
+ const manifestText = `./c/user/results 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n`;
+ const manifest = parseKeepManifestText(manifestText);
+ const directories = mapManifestToDirectories(manifest);
+ expect(directories).toEqual([{
+ parentId: "",
+ id: '/c',
+ name: 'c',
+ type: 'directory'
+ }, {
+ parentId: '/c',
+ id: '/c/user',
+ name: 'user',
+ type: 'directory'
+ }, {
+ parentId: '/c/user',
+ id: '/c/user/results',
+ name: 'results',
+ type: 'directory'
+ },]);
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { uniqBy } from 'lodash';
+import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "../../models/keep-manifest";
+import { TreeNode, setNode, createTree } from '../../models/tree';
+import { CollectionFilesTree, CollectionFile, CollectionDirectory, createCollectionDirectory, createCollectionFile } from '../../models/collection-file';
+
+export const mapManifestToCollectionFilesTree = (manifest: KeepManifest): CollectionFilesTree =>
+ manifestToCollectionFiles(manifest)
+ .map(mapCollectionFileToTreeNode)
+ .reduce((tree, node) => setNode(node)(tree), createTree<CollectionFile>());
+
+
+export const mapCollectionFileToTreeNode = (file: CollectionFile): TreeNode<CollectionFile> => ({
+ children: [],
+ id: file.id,
+ parent: file.parentId,
+ value: file
+});
+
+export const manifestToCollectionFiles = (manifest: KeepManifest): Array<CollectionDirectory | CollectionFile> => ([
+ ...mapManifestToDirectories(manifest),
+ ...mapManifestToFiles(manifest)
+]);
+
+export const mapManifestToDirectories = (manifest: KeepManifest): CollectionDirectory[] =>
+ uniqBy(
+ manifest
+ .map(mapStreamDirectory)
+ .map(splitDirectory)
+ .reduce((all, splitted) => ([...all, ...splitted]), []),
+ directory => directory.id);
+
+export const mapManifestToFiles = (manifest: KeepManifest): CollectionFile[] =>
+ manifest
+ .map(stream => stream.files.map(mapStreamFile(stream)))
+ .reduce((all, current) => ([...all, ...current]), []);
+
+const splitDirectory = (directory: CollectionDirectory): CollectionDirectory[] => {
+ return directory.name
+ .split('/')
+ .slice(1)
+ .map(mapPathComponentToDirectory);
+};
+
+const mapPathComponentToDirectory = (component: string, index: number, components: string[]): CollectionDirectory =>
+ createCollectionDirectory({
+ parentId: index === 0 ? '' : joinPathComponents(components, index),
+ id: joinPathComponents(components, index + 1),
+ name: component,
+ });
+
+const joinPathComponents = (components: string[], index: number) =>
+ `/${components.slice(0, index).join('/')}`;
+
+const mapStreamDirectory = (stream: KeepManifestStream): CollectionDirectory =>
+ createCollectionDirectory({
+ parentId: '',
+ id: stream.name,
+ name: stream.name,
+ });
+
+const mapStreamFile = (stream: KeepManifestStream) =>
+ (file: KeepManifestStreamFile): CollectionFile =>
+ createCollectionFile({
+ parentId: stream.name,
+ id: `${stream.name}/${file.name}`,
+ name: file.name,
+ size: file.size,
+ });
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { parseKeepManifestText, parseKeepManifestStream } from "./collection-manifest-parser";
+
+describe('parseKeepManifestText', () => {
+ it('should parse text into streams', () => {
+ const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n./c d41d8cd98f00b204e9800998ecf8427e+0 0:0:d\n`;
+ const manifest = parseKeepManifestText(manifestText);
+ expect(manifest[0].name).toBe('');
+ expect(manifest[1].name).toBe('/c');
+ expect(manifest.length).toBe(2);
+ });
+});
+
+describe('parseKeepManifestStream', () => {
+ const streamText = './c 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt';
+ const stream = parseKeepManifestStream(streamText);
+
+ it('should parse stream name', () => {
+ expect(stream.name).toBe('/c');
+ });
+ it('should parse stream locators', () => {
+ expect(stream.locators).toEqual(['930625b054ce894ac40596c3f5a0d947+33']);
+ });
+ it('should parse stream files', () => {
+ expect(stream.files).toEqual([
+ {name: 'a', position: '0', size: 0},
+ {name: 'b', position: '0', size: 0},
+ {name: 'output.txt', position: '0', size: 33},
+ ]);
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { KeepManifestStream, KeepManifestStreamFile } from "../../models/keep-manifest";
+
+/**
+ * Documentation [http://doc.arvados.org/api/storage.html](http://doc.arvados.org/api/storage.html)
+ */
+export const parseKeepManifestText = (text: string) =>
+ text
+ .split(/\n/)
+ .filter(streamText => streamText.length > 0)
+ .map(parseKeepManifestStream);
+
+/**
+ * Documentation [http://doc.arvados.org/api/storage.html](http://doc.arvados.org/api/storage.html)
+ */
+export const parseKeepManifestStream = (stream: string): KeepManifestStream => {
+ const tokens = stream.split(' ');
+ return {
+ name: streamName(tokens),
+ locators: locators(tokens),
+ files: files(tokens)
+ };
+};
+
+const FILE_LOCATOR_REGEXP = /^([0-9a-f]{32})\+([0-9]+)(\+[A-Z][-A-Za-z0-9@_]*)*$/;
+
+const FILE_REGEXP = /([0-9]+):([0-9]+):(.*)/;
+
+const streamName = (tokens: string[]) => tokens[0].slice(1);
+
+const locators = (tokens: string[]) => tokens.filter(isFileLocator);
+
+const files = (tokens: string[]) => tokens.filter(isFile).map(parseFile);
+
+const isFileLocator = (token: string) => FILE_LOCATOR_REGEXP.test(token);
+
+const isFile = (token: string) => FILE_REGEXP.test(token);
+
+const parseFile = (token: string): KeepManifestStreamFile => {
+ const match = FILE_REGEXP.exec(token);
+ const [position, size, name] = match!.slice(1);
+ return { name, position, size: parseInt(size, 10) };
+};
\ No newline at end of file
import { LinkService } from "./link-service/link-service";
import { FavoriteService } from "./favorite-service/favorite-service";
import { AxiosInstance } from "axios";
-import { CommonResourceService } from "../common/api/common-resource-service";
-import { Resource } from "../models/resource";
import { CollectionService } from "./collection-service/collection-service";
import { TagService } from "./tag-service/tag-service";
import Axios from "axios";
+import { CollectionFilesService } from "./collection-files-service/collection-files-service";
export interface ServiceRepository {
apiClient: AxiosInstance;
projectService: ProjectService;
linkService: LinkService;
favoriteService: FavoriteService;
- collectionService: CommonResourceService<Resource>;
tagService: TagService;
+ collectionService: CollectionService;
+ collectionFilesService: CollectionFilesService;
}
export const createServices = (baseUrl: string): ServiceRepository => {
const favoriteService = new FavoriteService(linkService, groupsService);
const collectionService = new CollectionService(apiClient);
const tagService = new TagService(linkService);
-
+ const collectionFilesService = new CollectionFilesService(collectionService);
+
return {
apiClient,
authService,
linkService,
favoriteService,
collectionService,
- tagService
+ tagService,
+ collectionFilesService
};
};
import { Dispatch } from "redux";
import { ResourceKind } from "../../models/resource";
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";
export const loadCollection = (uuid: string, kind: ResourceKind) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
+ dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
return services.collectionService
.get(uuid)
.then(item => {
- dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: item as CollectionResource }));
+ dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
+ return services.collectionFilesService.getFiles(item.uuid);
+ })
+ .then(files => {
+ dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
});
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { CollectionFilesTree } from "../../../models/collection-file";
+
+export const collectionPanelFilesAction = unionize({
+ SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
+ TOGGLE_COLLECTION_FILE_COLLAPSE: ofType<{ id: string }>(),
+ TOGGLE_COLLECTION_FILE_SELECTION: ofType<{ id: string }>(),
+ SELECT_ALL_COLLECTION_FILES: ofType<{}>(),
+ UNSELECT_ALL_COLLECTION_FILES: ofType<{}>(),
+}, { tag: 'type', value: 'payload' });
+
+export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionPanelFilesReducer } from "./collection-panel-files-reducer";
+import { collectionPanelFilesAction } from "./collection-panel-files-actions";
+import { CollectionFile, CollectionDirectory, createCollectionFile, createCollectionDirectory } from "../../../models/collection-file";
+import { createTree, setNode, getNodeValue, mapTreeValues, Tree } from "../../../models/tree";
+import { CollectionPanelFile, CollectionPanelDirectory } from "./collection-panel-files-state";
+
+describe('CollectionPanelFilesReducer', () => {
+
+ const files: Array<CollectionFile | CollectionDirectory> = [
+ createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', parentId: '' }),
+ createCollectionDirectory({ id: 'Directory 2', name: 'Directory 2', parentId: 'Directory 1' }),
+ createCollectionDirectory({ id: 'Directory 3', name: 'Directory 3', parentId: '' }),
+ createCollectionDirectory({ id: 'Directory 4', name: 'Directory 4', parentId: 'Directory 3' }),
+ createCollectionFile({ id: 'file1.txt', name: 'file1.txt', parentId: 'Directory 2' }),
+ createCollectionFile({ id: 'file2.txt', name: 'file2.txt', parentId: 'Directory 2' }),
+ createCollectionFile({ id: 'file3.txt', name: 'file3.txt', parentId: 'Directory 3' }),
+ createCollectionFile({ id: 'file4.txt', name: 'file4.txt', parentId: 'Directory 3' }),
+ createCollectionFile({ id: 'file5.txt', name: 'file5.txt', parentId: 'Directory 4' }),
+ ];
+
+ const collectionFilesTree = files.reduce((tree, file) => setNode({
+ children: [],
+ id: file.id,
+ parent: file.parentId,
+ value: file
+ })(tree), createTree<CollectionFile | CollectionDirectory>());
+
+ const collectionPanelFilesTree = collectionPanelFilesReducer(
+ createTree<CollectionPanelFile | CollectionPanelDirectory>(),
+ collectionPanelFilesAction.SET_COLLECTION_FILES(collectionFilesTree));
+
+ it('SET_COLLECTION_FILES', () => {
+ expect(getNodeValue('Directory 1')(collectionPanelFilesTree)).toEqual({
+ ...createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', parentId: '' }),
+ collapsed: true,
+ selected: false
+ });
+ });
+
+ it('TOGGLE_COLLECTION_FILE_COLLAPSE', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id: 'Directory 3' }));
+
+ const value = getNodeValue('Directory 3')(newTree)! as CollectionPanelDirectory;
+ expect(value.collapsed).toBe(false);
+ });
+
+ it('TOGGLE_COLLECTION_FILE_SELECTION', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 3' }));
+
+ const value = getNodeValue('Directory 3')(newTree);
+ expect(value!.selected).toBe(true);
+ });
+
+ it('TOGGLE_COLLECTION_FILE_SELECTION ancestors', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' }));
+
+ const value = getNodeValue('Directory 1')(newTree);
+ expect(value!.selected).toBe(true);
+ });
+
+ it('TOGGLE_COLLECTION_FILE_SELECTION descendants', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' }));
+ expect(getNodeValue('file1.txt')(newTree)!.selected).toBe(true);
+ expect(getNodeValue('file2.txt')(newTree)!.selected).toBe(true);
+ });
+
+ it('TOGGLE_COLLECTION_FILE_SELECTION unselect ancestors', () => {
+ const [newTree] = [collectionPanelFilesTree]
+ .map(tree => collectionPanelFilesReducer(
+ tree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' })))
+ .map(tree => collectionPanelFilesReducer(
+ tree,
+ collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'file1.txt' })));
+
+ expect(getNodeValue('Directory 2')(newTree)!.selected).toBe(false);
+ });
+
+ it('SELECT_ALL_COLLECTION_FILES', () => {
+ const newTree = collectionPanelFilesReducer(
+ collectionPanelFilesTree,
+ collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES());
+
+ mapTreeValues((v: CollectionPanelFile | CollectionPanelDirectory) => {
+ expect(v.selected).toEqual(true);
+ return v;
+ })(newTree);
+ });
+
+ it('SELECT_ALL_COLLECTION_FILES', () => {
+ const [newTree] = [collectionPanelFilesTree]
+ .map(tree => collectionPanelFilesReducer(
+ tree,
+ collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES()))
+ .map(tree => collectionPanelFilesReducer(
+ tree,
+ collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES()));
+
+ mapTreeValues((v: CollectionPanelFile | CollectionPanelDirectory) => {
+ expect(v.selected).toEqual(false);
+ return v;
+ })(newTree);
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile } from "./collection-panel-files-state";
+import { CollectionPanelFilesAction, collectionPanelFilesAction } from "./collection-panel-files-actions";
+import { createTree, mapTreeValues, getNode, setNode, getNodeAncestors, getNodeDescendants, setNodeValueWith, mapTree } from "../../../models/tree";
+import { CollectionFileType } from "../../../models/collection-file";
+
+export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
+ return collectionPanelFilesAction.match(action, {
+ SET_COLLECTION_FILES: files =>
+ mapTree(mapCollectionFileToCollectionPanelFile)(files),
+
+ TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
+ toggleCollapse(data.id)(state),
+
+ TOGGLE_COLLECTION_FILE_SELECTION: data => [state]
+ .map(toggleSelected(data.id))
+ .map(toggleAncestors(data.id))
+ .map(toggleDescendants(data.id))[0],
+
+ SELECT_ALL_COLLECTION_FILES: () =>
+ mapTreeValues(v => ({ ...v, selected: true }))(state),
+
+ UNSELECT_ALL_COLLECTION_FILES: () =>
+ mapTreeValues(v => ({ ...v, selected: false }))(state),
+
+ default: () => state
+ }) as CollectionPanelFilesState;
+};
+
+const toggleCollapse = (id: string) => (tree: CollectionPanelFilesState) =>
+ setNodeValueWith((v: CollectionPanelDirectory | CollectionPanelFile) =>
+ v.type === CollectionFileType.DIRECTORY
+ ? { ...v, collapsed: !v.collapsed }
+ : v)(id)(tree);
+
+
+const toggleSelected = (id: string) => (tree: CollectionPanelFilesState) =>
+ setNodeValueWith((v: CollectionPanelDirectory | CollectionPanelFile) => ({ ...v, selected: !v.selected }))(id)(tree);
+
+
+const toggleDescendants = (id: string) => (tree: CollectionPanelFilesState) => {
+ const node = getNode(id)(tree);
+ if (node && node.value.type === CollectionFileType.DIRECTORY) {
+ return getNodeDescendants(id)(tree)
+ .reduce((newTree, id) =>
+ setNodeValueWith(v => ({ ...v, selected: node.value.selected }))(id)(newTree), tree);
+ }
+ return tree;
+};
+
+const toggleAncestors = (id: string) => (tree: CollectionPanelFilesState) => {
+ const ancestors = getNodeAncestors(id)(tree).reverse();
+ return ancestors.reduce((newTree, parent) => parent ? toggleParentNode(parent)(newTree) : newTree, tree);
+};
+
+const toggleParentNode = (id: string) => (tree: CollectionPanelFilesState) => {
+ const node = getNode(id)(tree);
+ if (node) {
+ const parentNode = getNode(node.id)(tree);
+ if (parentNode) {
+ const selected = parentNode.children
+ .map(id => getNode(id)(tree))
+ .every(node => node !== undefined && node.value.selected);
+ return setNodeValueWith(v => ({ ...v, selected }))(parentNode.id)(tree);
+ }
+ return setNode(node)(tree);
+ }
+ return tree;
+};
+
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionFile, CollectionDirectory, CollectionFileType } from '../../../models/collection-file';
+import { Tree, TreeNode } from '../../../models/tree';
+
+export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
+
+export interface CollectionPanelDirectory extends CollectionDirectory {
+ collapsed: boolean;
+ selected: boolean;
+}
+
+export interface CollectionPanelFile extends CollectionFile {
+ selected: boolean;
+}
+
+export const mapCollectionFileToCollectionPanelFile = (node: TreeNode<CollectionDirectory | CollectionFile>): TreeNode<CollectionPanelDirectory | CollectionPanelFile> => {
+ return {
+ ...node,
+ value: node.value.type === CollectionFileType.DIRECTORY
+ ? { ...node.value, selected: false, collapsed: true }
+ : { ...node.value, selected: false }
+ };
+};
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "unionize";
-import { CommonResourceService } from "../../common/api/common-resource-service";
import { Dispatch } from "redux";
import { Resource, ResourceKind } from "../../models/resource";
import { RootState } from "../store";
export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
export const loadDetails = (uuid: string, kind: ResourceKind) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
- getService(services, kind)
- .get(uuid)
- .then(project => {
- dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item: project }));
- });
+ const item = await getService(services, kind).get(uuid);
+ dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
};
const getService = (services: ServiceRepository, kind: ResourceKind) => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+
+export const dialogActions = unionize({
+ OPEN_DIALOG: ofType<{ id: string, data: any }>(),
+ CLOSE_DIALOG: ofType<{ id: string }>()
+}, {
+ tag: 'type',
+ value: 'payload'
+ });
+
+export type DialogAction = UnionOf<typeof dialogActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogReducer } from "./dialog-reducer";
+import { dialogActions } from "./dialog-actions";
+
+describe('DialogReducer', () => {
+ it('OPEN_DIALOG', () => {
+ const id = 'test id';
+ const data = 'test data';
+ const state = dialogReducer({}, dialogActions.OPEN_DIALOG({ id, data }));
+ expect(state[id]).toEqual({ open: true, data });
+ });
+
+ it('CLOSE_DIALOG', () => {
+ const id = 'test id';
+ const state = dialogReducer({}, dialogActions.CLOSE_DIALOG({ id }));
+ expect(state[id]).toEqual({ open: false });
+ });
+
+ it('CLOSE_DIALOG persist data', () => {
+ const id = 'test id';
+ const [newState] = [{}]
+ .map(state => dialogReducer(state, dialogActions.OPEN_DIALOG({ id, data: 'test data' })))
+ .map(state => dialogReducer(state, dialogActions.CLOSE_DIALOG({ id })));
+
+ expect(newState[id]).toEqual({ open: false, data: 'test data' });
+ });
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DialogAction, dialogActions } from "./dialog-actions";
+
+export type DialogState = Record<string, Dialog>;
+
+export interface Dialog {
+ open: boolean;
+ data?: any;
+}
+
+export const dialogReducer = (state: DialogState = {}, action: DialogAction) =>
+ dialogActions.match(action, {
+ OPEN_DIALOG: ({ id, data }) => ({ ...state, [id]: { open: true, data } }),
+ CLOSE_DIALOG: ({ id }) => ({
+ ...state,
+ [id]: state[id] ? { ...state[id], open: false } : { open: false } }),
+ default: () => state,
+ });
+
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { DialogState } from './dialog-reducer';
+import { Dispatch } from 'redux';
+import { dialogActions } from './dialog-actions';
+
+export type WithDialog<T> = {
+ open: boolean;
+ data?: T;
+};
+
+export type WithDialogActions = {
+ closeDialog: () => void;
+};
+
+export const withDialog = (id: string) =>
+ <T>(component: React.ComponentType<WithDialog<T> & WithDialogActions>) =>
+ connect(mapStateToProps(id), mapDispatchToProps(id))(component);
+
+export const mapStateToProps = (id: string) => <T>(state: { dialog: DialogState }): WithDialog<T> => {
+ const dialog = state.dialog[id];
+ return dialog ? dialog : { open: false };
+};
+
+export const mapDispatchToProps = (id: string) => (dispatch: Dispatch): WithDialogActions => ({
+ closeDialog: () => {
+ dispatch(dialogActions.CLOSE_DIALOG({ id }));
+ }
+});
\ No newline at end of file
import { Resource, ResourceKind } from "../../models/resource";
import { projectPanelActions } from "../project-panel/project-panel-action";
import { getCollectionUrl } from "../../models/collection";
-import { getProjectUrl } from "../../models/project";
+import { getProjectUrl, ProjectResource } from "../../models/project";
+import { ProjectService } from "../../services/project-service/project-service";
+import { ServiceRepository } from "../../services/services";
+import { sidePanelActions } from "../side-panel/side-panel-action";
+import { SidePanelIdentifiers } from "../side-panel/side-panel-reducer";
+import { getUuidObjectType, ObjectTypes } from "../../models/object-types";
export const getResourceUrl = <T extends Resource>(resource: T): string => {
switch (resource.kind) {
}
};
+export const restoreBranch = (itemId: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const ancestors = await loadProjectAncestors(itemId, services.projectService);
+ const uuids = ancestors.map(ancestor => ancestor.uuid);
+ await loadBranch(uuids, dispatch);
+ dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelIdentifiers.PROJECTS));
+ dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+ uuids.forEach(uuid => {
+ dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
+ });
+ };
+
+export const loadProjectAncestors = async (uuid: string, projectService: ProjectService): Promise<Array<ProjectResource>> => {
+ if (getUuidObjectType(uuid) === ObjectTypes.USER) {
+ return [];
+ } else {
+ const currentProject = await projectService.get(uuid);
+ const ancestors = await loadProjectAncestors(currentProject.ownerUuid, projectService);
+ return [...ancestors, currentProject];
+ }
+};
+
+const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise<any> => {
+ const [uuid, ...rest] = uuids;
+ if (uuid) {
+ await dispatch<any>(getProjectList(uuid));
+ return loadBranch(rest, dispatch);
+ }
+};
import { reducer as formReducer } from 'redux-form';
import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
+import { CollectionPanelFilesState } from './collection-panel/collection-panel-files/collection-panel-files-state';
+import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collection-panel-files-reducer';
import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+import { DialogState, dialogReducer } from './dialog/dialog-reducer';
import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
import { ServiceRepository } from "../services/services";
contextMenu: ContextMenuState;
favorites: FavoritesState;
snackbar: SnackbarState;
+ collectionPanelFiles: CollectionPanelFilesState;
+ dialog: DialogState;
}
export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
export function configureStore(history: History, services: ServiceRepository): RootStore {
- const rootReducer = combineReducers({
- auth: authReducer(services),
- projects: projectsReducer,
+ const rootReducer = combineReducers({
+ auth: authReducer(services),
+ projects: projectsReducer,
collections: collectionsReducer,
- router: routerReducer,
- dataExplorer: dataExplorerReducer,
- sidePanel: sidePanelReducer,
- collectionPanel: collectionPanelReducer,
- detailsPanel: detailsPanelReducer,
- contextMenu: contextMenuReducer,
- form: formReducer,
- favorites: favoritesReducer,
- snackbar: snackbarReducer,
- });
+ router: routerReducer,
+ dataExplorer: dataExplorerReducer,
+ sidePanel: sidePanelReducer,
+ collectionPanel: collectionPanelReducer,
+ detailsPanel: detailsPanelReducer,
+ contextMenu: contextMenuReducer,
+ form: formReducer,
+ favorites: favoritesReducer,
+ snackbar: snackbarReducer,
+ collectionPanelFiles: collectionPanelFilesReducer,
+ dialog: dialogReducer
+ });
const projectPanelMiddleware = dataExplorerMiddleware(
new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { require } from '../require';
+import { maxLength } from '../max-length';
+
+export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
+export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
\ No newline at end of file
export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
-export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { CollectionPanelFiles as Component, CollectionPanelFilesProps } from "../../components/collection-panel-files/collection-panel-files";
+import { RootState } from "../../store/store";
+import { TreeItemStatus, TreeItem } from "../../components/tree/tree";
+import { CollectionPanelFilesState, CollectionPanelDirectory, CollectionPanelFile } from "../../store/collection-panel/collection-panel-files/collection-panel-files-state";
+import { FileTreeData } from "../../components/file-tree/file-tree-data";
+import { Dispatch } from "redux";
+import { collectionPanelFilesAction } from "../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
+import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
+import { ContextMenuKind } from "../context-menu/context-menu";
+import { Tree, getNodeChildren, getNode } from "../../models/tree";
+import { CollectionFileType } from "../../models/collection-file";
+
+const memoizedMapStateToProps = () => {
+ let prevState: CollectionPanelFilesState;
+ let prevTree: Array<TreeItem<FileTreeData>>;
+
+ return (state: RootState): Pick<CollectionPanelFilesProps, "items"> => {
+ if (prevState !== state.collectionPanelFiles) {
+ prevState = state.collectionPanelFiles;
+ prevTree = getNodeChildren('')(state.collectionPanelFiles)
+ .map(collectionItemToTreeItem(state.collectionPanelFiles));
+ }
+ return {
+ items: prevTree
+ };
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
+ onUploadDataClick: () => { return; },
+ onCollapseToggle: (id) => {
+ dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id }));
+ },
+ onSelectionToggle: (event, item) => {
+ dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: item.id }));
+ },
+ onItemMenuOpen: (event, item) => {
+ event.preventDefault();
+ dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource: { kind: ContextMenuKind.COLLECTION_FILES_ITEM, name: item.data.name, uuid: item.id }
+ }));
+ },
+ onOptionsMenuOpen: (event) =>
+ dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource: { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }
+ }))
+});
+
+
+export const CollectionPanelFiles = connect(memoizedMapStateToProps(), mapDispatchToProps)(Component);
+
+const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
+ (id: string): TreeItem<FileTreeData> => {
+ const node = getNode(id)(tree) || {
+ id: '',
+ children: [],
+ parent: '',
+ value: {
+ name: 'Invalid node',
+ type: CollectionFileType.DIRECTORY,
+ selected: false,
+ collapsed: true
+ }
+ };
+ return {
+ active: false,
+ data: {
+ name: node.value.name,
+ size: node.value.type === CollectionFileType.FILE ? node.value.size : undefined,
+ type: node.value.type
+ },
+ id: node.id,
+ items: getNodeChildren(node.id)(tree)
+ .map(collectionItemToTreeItem(tree)),
+ open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
+ selected: node.value.selected,
+ status: TreeItemStatus.LOADED
+ };
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { collectionPanelFilesAction } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
+import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
+
+
+export const collectionFilesActionSet: ContextMenuActionSet = [[{
+ name: "Select all",
+ execute: (dispatch) => {
+ dispatch(collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES());
+ }
+},{
+ name: "Unselect all",
+ execute: (dispatch) => {
+ dispatch(collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES());
+ }
+},{
+ name: "Remove selected",
+ execute: (dispatch, resource) => {
+ dispatch(openRemoveDialog('selected files'));
+ }
+},{
+ name: "Download selected",
+ execute: (dispatch, resource) => {
+ return;
+ }
+},{
+ name: "Create a new collection with selected",
+ execute: (dispatch, resource) => {
+ return;
+ }
+}]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { RenameIcon, DownloadIcon, RemoveIcon } from "../../../components/icon/icon";
+import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
+import { openRenameDialog } from "../../rename-dialog/rename-dialog";
+
+
+export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
+ name: "Rename",
+ icon: RenameIcon,
+ execute: (dispatch, resource) => {
+ dispatch(openRenameDialog('the item'));
+ }
+},{
+ name: "Download",
+ icon: DownloadIcon,
+ execute: (dispatch, resource) => {
+ return;
+ }
+},{
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, resource) => {
+ dispatch(openRemoveDialog('selected file'));
+ }
+}]];
import { RootState } from "../../../store/store";
const mapStateToProps = (state: RootState) => ({
- isFavorite: state.contextMenu.resource && state.favorites[state.contextMenu.resource.uuid] === true
+ isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true
});
export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean }) =>
PROJECT = "Project",
RESOURCE = "Resource",
FAVORITE = "Favorite",
+ COLLECTION_FILES = "CollectionFiles",
+ COLLECTION_FILES_ITEM = "CollectionFilesItem",
COLLECTION = 'Collection'
}
const { ownerUuid } = getState().projects.creator;
return dispatch<any>(createProject(data)).then(() => {
dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Created a new project",
+ message: "Project has been successfully created.",
hideDuration: 2000
}));
dispatch(projectPanelActions.REQUEST_ITEMS());
const mapStateToProps = (state: RootState, { id }: Props) =>
getDataExplorer(state.dataExplorer, id);
-const mapDispatchToProps = (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
- dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
- return {
- onSearch: (searchValue: string) => {
- dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
- },
+const mapDispatchToProps = () => {
+ let prevColumns: DataColumns<any>;
+ return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
+ if (columns !== prevColumns) {
+ prevColumns = columns;
+ dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
+ }
+ return {
+ onSearch: (searchValue: string) => {
+ dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
+ },
- onColumnToggle: (column: DataColumn<any>) => {
- dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
- },
+ onColumnToggle: (column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
+ },
- onSortToggle: (column: DataColumn<any>) => {
- dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
- },
+ onSortToggle: (column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
+ },
- onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
- dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
- },
+ onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+ dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
+ },
- onChangePage: (page: number) => {
- dispatch(dataExplorerActions.SET_PAGE({ id, page }));
- },
+ onChangePage: (page: number) => {
+ dispatch(dataExplorerActions.SET_PAGE({ id, page }));
+ },
- onChangeRowsPerPage: (rowsPerPage: number) => {
- dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
- },
+ onChangeRowsPerPage: (rowsPerPage: number) => {
+ dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+ },
- onRowClick,
+ onRowClick,
- onRowDoubleClick,
+ onRowDoubleClick,
- onContextMenu,
+ onContextMenu,
+ };
};
};
-export const DataExplorer = connect(mapStateToProps, mapDispatchToProps)(DataExplorerComponent);
+export const DataExplorer = connect(mapStateToProps, mapDispatchToProps())(DataExplorerComponent);
import DialogTitle from '@material-ui/core/DialogTitle';
import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
import { compose } from 'redux';
import { ArvadosTheme } from '../../common/custom-theme';
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '../../../node_modules/@material-ui/core';
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
import { COLLECTION_FORM_NAME } from '../../store/collections/updator/collection-updator-action';
type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@material-ui/core";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+
+export const REMOVE_DIALOG = 'removeCollectionFilesDialog';
+
+export const RemoveDialog = withDialog(REMOVE_DIALOG)(
+ (props) =>
+ <Dialog open={props.open}>
+ <DialogTitle>{`Removing ${props.data}`}</DialogTitle>
+ <DialogContent>
+ {`Are you sure you want to remove ${props.data}?`}
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={props.closeDialog}>
+ Cancel
+ </Button>
+ <Button variant='raised' color='primary'>
+ Remove
+ </Button>
+ </DialogActions>
+ </Dialog>
+);
+
+export const openRemoveDialog = (removedDataName: string) =>
+ dialogActions.OPEN_DIALOG({ id: REMOVE_DIALOG, data: removedDataName });
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography } from "@material-ui/core";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+
+export const RENAME_DIALOG = 'nameDialog';
+
+export const RenameDialog = withDialog(RENAME_DIALOG)(
+ (props) =>
+ <Dialog open={props.open}>
+ <DialogTitle>{`Rename`}</DialogTitle>
+ <DialogContent>
+ <Typography variant='body1' gutterBottom>
+ {`Please, enter a new name for ${props.data}`}
+ </Typography>
+ <TextField fullWidth={true} placeholder='New name' />
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={props.closeDialog}>
+ Cancel
+ </Button>
+ <Button variant='raised' color='primary'>
+ Ok
+ </Button>
+ </DialogActions>
+ </Dialog>
+);
+
+export const openRenameDialog = (originalName: string, ) =>
+ dialogActions.OPEN_DIALOG({ id: RENAME_DIALOG, data: originalName });
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import {
- StyleRulesCallback, WithStyles, withStyles, Card,
- CardHeader, IconButton, CardContent, Grid, Chip, TextField, Button
+import {
+ StyleRulesCallback, WithStyles, withStyles, Card,
+ CardHeader, IconButton, CardContent, Grid, Chip
} from '@material-ui/core';
import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '../../components/icon/icon';
import { DetailsAttribute } from '../../components/details-attribute/details-attribute';
import { CollectionResource } from '../../models/collection';
+import { CollectionPanelFiles } from '../../views-components/collection-panel-files/collection-panel-files';
import * as CopyToClipboard from 'react-copy-to-clipboard';
import { TagResource } from '../../models/tag';
import { CollectionTagForm } from './collection-tag-form';
const { classes, item, tags, onContextMenu } = this.props;
return <div>
<Card className={classes.card}>
- <CardHeader
+ <CardHeader
avatar={ <CollectionIcon className={classes.iconHeader} /> }
- action={
+ action={
<IconButton
aria-label="More options"
onClick={event => onContextMenu(event, item)}>
<MoreOptionsIcon />
- </IconButton>
+ </IconButton>
}
- title={item && item.name }
+ title={item && item.name }
subheader={item && item.description} />
<CardContent>
<Grid container direction="column">
</Grid>
</CardContent>
</Card>
-
- <Card className={classes.card}>
- <CardHeader title="Files" />
- <CardContent>
- <Grid container direction="column">
- <Grid item xs={4}>
- Files
- </Grid>
- </Grid>
- </CardContent>
- </Card>
+ <div className={classes.card}>
+ <CollectionPanelFiles/>
+ </div>
</div>;
}
const renderTagLabel = (tag: TagResource) => {
const { properties } = tag;
return `${properties.key}: ${properties.value}`;
-};
\ No newline at end of file
+};
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';
type CssRules = "toolbar" | "button";
handleNewCollectionClick = () => {
this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
}
+
componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
if (match.params.id !== currentItemId) {
onItemRouteChange(match.params.id);
}
}
+
+ componentDidMount() {
+ if (this.props.match.params.id && this.props.currentItemId === '') {
+ this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
+ }
+ }
}
)
);
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 { dataExplorerActions } from '../../store/data-explorer/data-explorer-action';
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 { RemoveDialog } from '../../views-components/remove-dialog/remove-dialog';
+import { RenameDialog } from '../../views-components/rename-dialog/rename-dialog';
import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
import { AuthService } from "../../services/auth-service/auth-service";
<Snackbar />
<CreateProjectDialog />
<CreateCollectionDialog />
+ <RemoveDialog />
+ <RenameDialog />
<UpdateCollectionDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}
core-js "^2.5.7"
regenerator-runtime "^0.12.0"
-"@material-ui/core@1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.0.tgz#e535fef84576b096c46e1fb7d6c4c61895155fd3"
+"@material-ui/core@1.4.2":
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.2.tgz#8a1282e985d4922a4d2b4f7e287d8a716a2fc108"
dependencies:
"@babel/runtime" "^7.0.0-beta.42"
"@types/jss" "^9.5.3"
jss-vendor-prefixer "^7.0.0"
keycode "^2.1.9"
normalize-scroll-left "^0.1.2"
- popper.js "^1.0.0"
+ popper.js "^1.14.1"
prop-types "^15.6.0"
react-event-listener "^0.6.0"
react-jss "^8.1.0"
react-transition-group "^2.2.1"
recompose "^0.27.0"
- scroll "^2.0.3"
warning "^4.0.1"
-"@material-ui/icons@1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-1.1.0.tgz#4d025df7b0ba6ace8d6710079ed76013a4d26595"
+"@material-ui/icons@2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.0.tgz#f2c4e80d0cb4bbbd433127781da67d93393535f8"
dependencies:
- recompose "^0.26.0 || ^0.27.0"
+ "@babel/runtime" "^7.0.0-beta.42"
+ recompose "^0.27.0"
"@types/cheerio@*":
version "0.22.8"
version "4.6.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
-"@types/jest@23.3.0":
- version "23.3.0"
- resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.0.tgz#5dd70033b616a6228042244ebd992f6426808810"
+"@types/jest@23.3.1":
+ version "23.3.1"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.1.tgz#a4319aedb071d478e6f407d1c4578ec8156829cf"
"@types/jss@^9.5.3":
version "9.5.4"
csstype "^2.0.0"
indefinite-observable "^1.0.1"
-"@types/lodash@4.14.112":
- version "4.14.112"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.112.tgz#4a8d8e5716b97a1ac01fe1931ad1e4cba719de5a"
+"@types/lodash@4.14.116":
+ version "4.14.116"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
-"@types/node@*", "@types/node@10.5.2":
+"@types/node@*":
version "10.5.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
+"@types/node@10.5.5":
+ version "10.5.5"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.5.tgz#8e84d24e896cd77b0d4f73df274027e3149ec2ba"
+
+"@types/react-copy-to-clipboard@4.2.5":
+ version "4.2.5"
+ resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.5.tgz#bda288b4256288676019b75ca86f1714bbd206d4"
+ dependencies:
+ "@types/react" "*"
+
"@types/react-dom@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
"@types/node" "*"
"@types/react" "*"
-"@types/react-redux@6.0.4":
- version "6.0.4"
- resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.4.tgz#c1cfce0a0bd88983c75dbf393576f8dc59181586"
+"@types/react-redux@6.0.6":
+ version "6.0.6"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.6.tgz#87f1d0a6ea901b93fcaf95fa57641ff64079d277"
dependencies:
"@types/react" "*"
redux "^4.0.0"
-"@types/react-router-dom@4.2.7":
- version "4.2.7"
- resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.7.tgz#9d36bfe175f916dd8d7b6b0237feed6cce376b4c"
+"@types/react-router-dom@4.3.0":
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.0.tgz#c91796d02deb3a5b24bc1c5db4a255df0d18b8b5"
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react" "*"
redux "^3.6.0"
-"@types/redux-form@7.4.1":
- version "7.4.1"
- resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.1.tgz#df84bbda5f06e4d517210797c3cfdc573c3bda36"
+"@types/redux-form@7.4.4":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.4.tgz#2cf62b8eb1dc1b1df95b6b25c2763db196e5c190"
dependencies:
"@types/react" "*"
redux "^3.6.0 || ^4.0.0"
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+copy-to-clipboard@^3:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+ dependencies:
+ toggle-selection "^1.0.3"
+
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
dependencies:
urijs "^1.16.1"
-dom-walk@^0.1.0:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
-
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
is-windows "^1.0.1"
which "^1.2.14"
-global@~4.3.0:
- version "4.3.2"
- resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
- dependencies:
- min-document "^2.19.0"
- process "~0.5.1"
-
globals@^9.18.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
-min-document@^2.19.0:
- version "2.19.0"
- resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
- dependencies:
- dom-walk "^0.1.0"
-
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
-popper.js@^1.0.0:
- version "1.14.3"
- resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
+popper.js@^1.14.1:
+ version "1.14.4"
+ resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.4.tgz#8eec1d8ff02a5a3a152dd43414a15c7b79fd69b6"
portfinder@^1.0.9:
version "1.0.13"
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
-process@~0.5.1:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
-
promise-inflight@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
dependencies:
performance-now "^2.1.0"
-rafl@~1.2.1:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/rafl/-/rafl-1.2.2.tgz#fe930f758211020d47e38815f5196a8be4150740"
- dependencies:
- global "~4.3.0"
-
railroad-diagrams@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+react-copy-to-clipboard@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
+ dependencies:
+ copy-to-clipboard "^3"
+ prop-types "^15.5.8"
+
react-dev-utils@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"
strip-ansi "3.0.1"
text-table "0.2.0"
-react-dom@16.4.1:
- version "16.4.1"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
+react-dom@16.4.2:
+ version "16.4.2"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.2.tgz#4afed569689f2c561d2b8da0b819669c38a0bda4"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.1.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
-react@16.4.1:
- version "16.4.1"
- resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+react@16.4.2:
+ version "16.4.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.4.2.tgz#2cd90154e3a9d9dd8da2991149fdca3c260e129f"
dependencies:
fbjs "^0.8.16"
loose-envify "^1.1.0"
dependencies:
util.promisify "^1.0.0"
-"recompose@^0.26.0 || ^0.27.0", recompose@^0.27.0:
+recompose@^0.27.0:
version "0.27.1"
resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
dependencies:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
-scroll@^2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/scroll/-/scroll-2.0.3.tgz#0951b785544205fd17753bc3d294738ba16fc2ab"
- dependencies:
- rafl "~1.2.1"
-
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
regex-not "^1.0.2"
safe-regex "^1.1.0"
+toggle-selection@^1.0.3:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+
toposort@^1.0.0:
version "1.0.7"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-typescript@2.9.2:
- version "2.9.2"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
+typescript@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb"
ua-parser-js@^0.7.18:
version "0.7.18"