--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const fileToArrayBuffer = (file: File) =>
+ new Promise<ArrayBuffer>((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result as ArrayBuffer);
+ };
+ reader.onerror = () => {
+ reject();
+ };
+ reader.readAsArrayBuffer(file);
+ });
it('PUT', async () => {
const { open, send, load, progress, createRequest } = mockCreateRequest();
- const onProgress = jest.fn();
const webdav = new WebDAV(undefined, createRequest);
- const promise = webdav.put('foo', 'Test data', { onProgress });
+ const promise = webdav.put('foo', 'Test data');
progress();
load();
const request = await promise;
expect(open).toHaveBeenCalledWith('PUT', 'foo');
expect(send).toHaveBeenCalledWith('Test data');
- expect(onProgress).toHaveBeenCalled();
expect(request).toBeInstanceOf(XMLHttpRequest);
});
.keys(headers)
.forEach(key => r.setRequestHeader(key, headers[key]));
- if (config.onProgress) {
- r.addEventListener('progress', config.onProgress);
+ if (config.onUploadProgress) {
+ r.upload.addEventListener('progress', config.onUploadProgress);
}
r.addEventListener('load', () => resolve(r));
headers?: {
[key: string]: string;
};
- onProgress?: (event: ProgressEvent) => void;
+ onUploadProgress?: (event: ProgressEvent) => void;
}
interface WebDAVDefaults {
url: string;
headers?: { [key: string]: string };
data?: any;
- onProgress?: (event: ProgressEvent) => void;
+ onUploadProgress?: (event: ProgressEvent) => void;
}
import { FileTree } from '../file-tree/file-tree';
import { IconButton, Grid, Typography, StyleRulesCallback, withStyles, WithStyles, CardHeader, Card, Button } from '@material-ui/core';
import { CustomizeTableIcon } from '../icon/icon';
-import { connect, DispatchProp } from "react-redux";
-import { Dispatch } from "redux";
-import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
export interface CollectionPanelFilesProps {
items: Array<TreeItem<FileTreeData>>;
}
});
-const renameFile = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- services.collectionFilesService.renameTest();
-};
-
-
export const CollectionPanelFiles =
- connect()(
withStyles(styles)(
- ({ onItemMenuOpen, onOptionsMenuOpen, classes, dispatch, ...treeProps }: CollectionPanelFilesProps & DispatchProp & WithStyles<CssRules>) =>
- <Card className={classes.root}>
- <CardHeader
- title="Files"
- action={
- <Button onClick={
- () => {
- dispatch<any>(renameFile());
- }}
- variant='raised'
- color='primary'
- size='small'>
- Upload data
+ ({ onItemMenuOpen, onOptionsMenuOpen, onUploadDataClick, classes, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) =>
+ <Card className={classes.root}>
+ <CardHeader
+ title="Files"
+ action={
+ <Button onClick={onUploadDataClick}
+ 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
+ } />
+ <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 variant="caption" className={classes.fileSizeHeader}>
+ File size
</Typography>
- </Grid>
- <FileTree onMenuOpen={onItemMenuOpen} {...treeProps} />
- </Card>)
-);
+ </Grid>
+ <FileTree onMenuOpen={onItemMenuOpen} {...treeProps} />
+ </Card>);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { FileUpload } from "~/components/file-upload/file-upload";
+import { UploadFile } from '~/store/collections/uploader/collection-uploader-actions';
+import { Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core/';
+import { Button, CircularProgress } from '@material-ui/core';
+import { WithDialogProps } from '../../store/dialog/with-dialog';
+
+export interface FilesUploadDialogProps {
+ files: UploadFile[];
+ uploading: boolean;
+ onSubmit: () => void;
+ onChange: (files: File[]) => void;
+}
+
+export const FilesUploadDialog = (props: FilesUploadDialogProps & WithDialogProps<{}>) =>
+ <Dialog open={props.open}
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}
+ fullWidth={true}
+ maxWidth='sm'>
+ <DialogTitle>Upload data</DialogTitle>
+ <DialogContent>
+ <FileUpload
+ files={props.files}
+ disabled={props.uploading}
+ onDrop={props.onChange}
+ />
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ disabled={props.uploading}
+ onClick={props.closeDialog}>
+ Cancel
+ </Button>
+ <Button
+ variant='contained'
+ color='primary'
+ type='submit'
+ onClick={props.onSubmit}
+ disabled={props.uploading}>
+ {props.uploading
+ ? <CircularProgress size={20} />
+ : 'Upload data'}
+ </Button>
+ </DialogActions>
+ </Dialog>;
{ 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']);
+ expect(Tree.getNodeAncestorsIds('Node 3')(newTree)).toEqual(['Node 1', 'Node 2']);
});
it('gets node descendants', () => {
{ 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']);
+ expect(Tree.getNodeDescendantsIds('Node 1')(newTree)).toEqual(['Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
});
it('gets root descendants', () => {
{ 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']);
+ expect(Tree.getNodeDescendantsIds('')(newTree)).toEqual(['Node 1', 'Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
});
it('gets node children', () => {
{ 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']);
+ expect(Tree.getNodeChildrenIds('Node 1')(newTree)).toEqual(['Node 2', 'Node 3']);
});
it('gets root children', () => {
{ 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']);
+ expect(Tree.getNodeChildrenIds('')(newTree)).toEqual(['Node 1', 'Node 3']);
});
it('maps tree', () => {
export const TREE_ROOT_ID = '';
-export interface TreeNode<T> {
+export interface TreeNode<T = any> {
children: string[];
value: T;
id: string;
const [newTree] = [tree]
.map(tree => getNode(node.id)(tree) === node
? tree
- : {...tree, [node.id]: node})
+ : { ...tree, [node.id]: node })
.map(addChild(node.parent, node.id));
return newTree;
};
};
export const mapTreeValues = <T, R>(mapFn: (value: T) => R) => (tree: Tree<T>): Tree<R> =>
- getNodeDescendants('')(tree)
+ getNodeDescendantsIds('')(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)
+export const mapTree = <T, R = T>(mapFn: (node: TreeNode<T>) => TreeNode<R>) => (tree: Tree<T>): Tree<R> =>
+ getNodeDescendantsIds('')(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[] => {
+export const getNodeAncestors = (id: string) => <T>(tree: Tree<T>) =>
+ mapIdsToNodes(getNodeAncestorsIds(id)(tree))(tree);
+
+
+export const getNodeAncestorsIds = (id: string) => <T>(tree: Tree<T>): string[] => {
const node = getNode(id)(tree);
return node && node.parent
- ? [...getNodeAncestors(node.parent)(tree), node.parent]
+ ? [...getNodeAncestorsIds(node.parent)(tree), node.parent]
: [];
};
-export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
+export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>) =>
+ mapIdsToNodes(getNodeDescendantsIds(id, limit)(tree))(tree);
+
+export const getNodeDescendantsIds = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
const node = getNode(id)(tree);
const children = node ? node.children :
id === TREE_ROOT_ID
.concat(limit < 1
? []
: children
- .map(id => getNodeDescendants(id, limit - 1)(tree))
+ .map(id => getNodeDescendantsIds(id, limit - 1)(tree))
.reduce((nodes, nodeChildren) => [...nodes, ...nodeChildren], []));
};
-export const getNodeChildren = (id: string) => <T>(tree: Tree<T>): string[] =>
- getNodeDescendants(id, 0)(tree);
+export const getNodeChildren = (id: string) => <T>(tree: Tree<T>) =>
+ mapIdsToNodes(getNodeChildrenIds(id)(tree))(tree);
+
+export const getNodeChildrenIds = (id: string) => <T>(tree: Tree<T>): string[] =>
+ getNodeDescendantsIds(id, 0)(tree);
+
+export const mapIdsToNodes = (ids: string[]) => <T>(tree: Tree<T>) =>
+ ids.map(id => getNode(id)(tree)).filter((node): node is TreeNode<T> => node !== undefined);
const mapNodeValue = <T, R>(mapFn: (value: T) => R) => (node: TreeNode<T>): TreeNode<R> =>
({ ...node, value: mapFn(node.value) });
import { uniqBy, groupBy } from 'lodash';
import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "~/models/keep-manifest";
-import { TreeNode, setNode, createTree, getNodeDescendants, getNodeValue } from '~/models/tree';
+import { TreeNode, setNode, createTree, getNodeDescendantsIds, getNodeValue } from '~/models/tree';
import { CollectionFilesTree, CollectionFile, CollectionDirectory, createCollectionDirectory, createCollectionFile, CollectionFileType } from '../../models/collection-file';
export const mapCollectionFilesTreeToManifest = (tree: CollectionFilesTree): KeepManifest => {
- const values = getNodeDescendants('')(tree).map(id => getNodeValue(id)(tree));
+ const values = getNodeDescendantsIds('')(tree).map(id => getNodeValue(id)(tree));
const files = values.filter(value => value && value.type === CollectionFileType.FILE) as CollectionFile[];
const fileGroups = groupBy(files, file => file.path);
return Object
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { createCollectionFilesTree, CollectionDirectory, CollectionFile, CollectionFileType, createCollectionDirectory, createCollectionFile } from "../../models/collection-file";
+import { getTagValue } from "~/common/xml";
+import { getNodeChildren, Tree, mapTree } from '~/models/tree';
+
+export const parseFilesResponse = (document: Document) => {
+ const files = extractFilesData(document);
+ const tree = createCollectionFilesTree(files);
+ return sortFilesTree(tree);
+};
+
+export const sortFilesTree = (tree: Tree<CollectionDirectory | CollectionFile>) => {
+ return mapTree<CollectionDirectory | CollectionFile>(node => {
+ const children = getNodeChildren(node.id)(tree);
+
+ children.sort((a, b) =>
+ a.value.type !== b.value.type
+ ? a.value.type === CollectionFileType.DIRECTORY ? -1 : 1
+ : a.value.name.localeCompare(b.value.name)
+ );
+ return { ...node, children: children.map(child => child.id) };
+ })(tree);
+};
+
+export const extractFilesData = (document: Document) => {
+ const collectionUrlPrefix = /\/c=[0-9a-zA-Z\-]*/;
+ return Array
+ .from(document.getElementsByTagName('D:response'))
+ .slice(1) // omit first element which is collection itself
+ .map(element => {
+ const name = getTagValue(element, 'D:displayname', '');
+ const size = parseInt(getTagValue(element, 'D:getcontentlength', '0'), 10);
+ const url = getTagValue(element, 'D:href', '');
+ const nameSuffix = `/${name || ''}`;
+ const directory = url
+ .replace(collectionUrlPrefix, '')
+ .replace(nameSuffix, '');
+
+ const data = {
+ url,
+ id: `${directory}/${name}`,
+ name,
+ path: directory,
+ };
+
+ return getTagValue(element, 'D:resourcetype', '')
+ ? createCollectionDirectory(data)
+ : createCollectionFile({ ...data, size });
+
+ });
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import * as _ from "lodash";
import { CommonResourceService } from "~/common/api/common-resource-service";
import { CollectionResource } from "~/models/collection";
-import axios, { AxiosInstance } from "axios";
-import { KeepService } from "../keep-service/keep-service";
+import { AxiosInstance } from "axios";
+import { CollectionFile, CollectionDirectory } from "~/models/collection-file";
import { WebDAV } from "~/common/webdav";
import { AuthService } from "../auth-service/auth-service";
-import { mapTree, getNodeChildren, getNode, TreeNode } from "../../models/tree";
-import { getTagValue } from "~/common/xml";
-import { FilterBuilder } from "~/common/api/filter-builder";
-import { CollectionFile, createCollectionFile, CollectionFileType, CollectionDirectory, createCollectionDirectory } from '~/models/collection-file';
-import { parseKeepManifestText, stringifyKeepManifest } from "../collection-files-service/collection-manifest-parser";
-import { KeepManifestStream } from "~/models/keep-manifest";
-import { createCollectionFilesTree } from '~/models/collection-file';
+import { mapTreeValues } from "~/models/tree";
+import { parseFilesResponse } from "./collection-service-files-response";
+import { fileToArrayBuffer } from "~/common/file";
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
export class CollectionService extends CommonResourceService<CollectionResource> {
- constructor(serverApi: AxiosInstance, private keepService: KeepService, private webdavClient: WebDAV, private authService: AuthService) {
+ constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService) {
super(serverApi, "collections");
}
async files(uuid: string) {
const request = await this.webdavClient.propfind(`/c=${uuid}`);
if (request.responseXML != null) {
- const files = this.extractFilesData(request.responseXML);
- const tree = createCollectionFilesTree(files);
- const sortedTree = mapTree(node => {
- const children = getNodeChildren(node.id)(tree).map(id => getNode(id)(tree)) as TreeNode<CollectionDirectory | CollectionFile>[];
- children.sort((a, b) =>
- a.value.type !== b.value.type
- ? a.value.type === CollectionFileType.DIRECTORY ? -1 : 1
- : a.value.name.localeCompare(b.value.name)
- );
- return { ...node, children: children.map(child => child.id) };
- })(tree);
- return sortedTree;
+ const filesTree = parseFilesResponse(request.responseXML);
+ return mapTreeValues(this.extendFileURL)(filesTree);
}
return Promise.reject();
}
return this.webdavClient.delete(`/c=${collectionUuid}${filePath}`);
}
- extractFilesData(document: Document) {
- const collectionUrlPrefix = /\/c=[0-9a-zA-Z\-]*/;
- return Array
- .from(document.getElementsByTagName('D:response'))
- .slice(1) // omit first element which is collection itself
- .map(element => {
- const name = getTagValue(element, 'D:displayname', '');
- const size = parseInt(getTagValue(element, 'D:getcontentlength', '0'), 10);
- const pathname = getTagValue(element, 'D:href', '');
- const nameSuffix = `/${name || ''}`;
- const directory = pathname
- .replace(collectionUrlPrefix, '')
- .replace(nameSuffix, '');
- const href = this.webdavClient.defaults.baseURL + pathname + '?api_token=' + this.authService.getApiToken();
-
- const data = {
- url: href,
- id: `${directory}/${name}`,
- name,
- path: directory,
- };
-
- return getTagValue(element, 'D:resourcetype', '')
- ? createCollectionDirectory(data)
- : createCollectionFile({ ...data, size });
-
- });
- }
-
-
- private readFile(file: File): Promise<ArrayBuffer> {
- return new Promise<ArrayBuffer>(resolve => {
- const reader = new FileReader();
- reader.onload = () => {
- resolve(reader.result as ArrayBuffer);
- };
-
- reader.readAsArrayBuffer(file);
- });
- }
-
- private uploadFile(keepServiceHost: string, file: File, fileId: number, onProgress?: UploadProgress): Promise<CollectionFile> {
- return this.readFile(file).then(content => {
- return axios.post<string>(keepServiceHost, content, {
- headers: {
- 'Content-Type': 'text/octet-stream'
- },
- onUploadProgress: (e: ProgressEvent) => {
- if (onProgress) {
- onProgress(fileId, e.loaded, e.total, Date.now());
- }
- console.log(`${e.loaded} / ${e.total}`);
- }
- }).then(data => createCollectionFile({
- id: data.data,
- name: file.name,
- size: file.size
- }));
- });
+ async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress) {
+ // files have to be uploaded sequentially
+ for (let idx = 0; idx < files.length; idx++) {
+ await this.uploadFile(collectionUuid, files[idx], idx, onProgress);
+ }
}
- private async updateManifest(collectionUuid: string, files: CollectionFile[]): Promise<CollectionResource> {
- const collection = await this.get(collectionUuid);
- const manifest: KeepManifestStream[] = parseKeepManifestText(collection.manifestText);
-
- files.forEach(f => {
- let kms = manifest.find(stream => stream.name === f.path);
- if (!kms) {
- kms = {
- files: [],
- locators: [],
- name: f.path
- };
- manifest.push(kms);
+ private extendFileURL = (file: CollectionDirectory | CollectionFile) => ({
+ ...file,
+ url: this.webdavClient.defaults.baseURL + file.url + '?api_token=' + this.authService.getApiToken()
+ })
+
+ private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }) {
+ const fileURL = `/c=${collectionUuid}/${file.name}`;
+ const fileContent = await fileToArrayBuffer(file);
+ const requestConfig = {
+ headers: {
+ 'Content-Type': 'text/octet-stream'
+ },
+ onUploadProgress: (e: ProgressEvent) => {
+ onProgress(fileId, e.loaded, e.total, Date.now());
}
- kms.locators.push(f.id);
- const len = kms.files.length;
- const nextPos = len > 0
- ? parseInt(kms.files[len - 1].position, 10) + kms.files[len - 1].size
- : 0;
- kms.files.push({
- name: f.name,
- position: nextPos.toString(),
- size: f.size
- });
- });
+ };
+ return this.webdavClient.put(fileURL, fileContent, requestConfig);
- console.log(manifest);
-
- const manifestText = stringifyKeepManifest(manifest);
- const data = { ...collection, manifestText };
- return this.update(collectionUuid, CommonResourceService.mapKeys(_.snakeCase)(data));
}
- uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress): Promise<CollectionResource | never> {
- const filters = new FilterBuilder()
- .addEqual("service_type", "proxy");
-
- return this.keepService.list({ filters: filters.getFilters() }).then(data => {
- if (data.items && data.items.length > 0) {
- const serviceHost =
- (data.items[0].serviceSslFlag ? "https://" : "http://") +
- data.items[0].serviceHost +
- ":" + data.items[0].servicePort;
-
- console.log("serviceHost", serviceHost);
-
- const files$ = files.map((f, idx) => this.uploadFile(serviceHost, f, idx, onProgress));
- return Promise.all(files$).then(values => {
- return this.updateManifest(collectionUuid, values);
- });
- } else {
- return Promise.reject("Missing keep service host");
- }
- });
- }
}
const projectService = new ProjectService(apiClient);
const linkService = new LinkService(apiClient);
const favoriteService = new FavoriteService(linkService, groupsService);
- const collectionService = new CollectionService(apiClient, keepService, webdavClient, authService);
+ const collectionService = new CollectionService(apiClient, webdavClient, authService);
const tagService = new TagService(linkService);
const collectionFilesService = new CollectionFilesService(collectionService);
import { RootState } from "../../store";
import { snackbarActions } from "../../snackbar/snackbar-actions";
import { dialogActions } from "../../dialog/dialog-actions";
-import { getNodeValue, getNodeDescendants } from "~/models/tree";
+import { getNodeValue, getNodeDescendants } from '~/models/tree';
import { CollectionPanelDirectory, CollectionPanelFile } from "./collection-panel-files-state";
export const collectionPanelFilesAction = unionize({
(dispatch: Dispatch, getState: () => RootState) => {
const tree = getState().collectionPanelFiles;
const allFiles = getNodeDescendants('')(tree)
- .map(id => getNodeValue(id)(tree))
- .filter(file => file !== undefined) as Array<CollectionPanelDirectory | CollectionPanelFile>;
+ .map(node => node.value);
const selectedDirectories = allFiles.filter(file => file.selected && file.type === CollectionFileType.DIRECTORY);
const selectedFiles = allFiles.filter(file => file.selected && !selectedDirectories.some(dir => dir.id === file.path));
import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile, mergeCollectionPanelFilesStates } 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 { createTree, mapTreeValues, getNode, setNode, getNodeAncestorsIds, getNodeDescendantsIds, setNodeValueWith, mapTree } from "~/models/tree";
import { CollectionFileType } from "~/models/collection-file";
export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
const toggleDescendants = (id: string) => (tree: CollectionPanelFilesState) => {
const node = getNode(id)(tree);
if (node && node.value.type === CollectionFileType.DIRECTORY) {
- return getNodeDescendants(id)(tree)
+ return getNodeDescendantsIds(id)(tree)
.reduce((newTree, id) =>
setNodeValueWith(v => ({ ...v, selected: node.value.selected }))(id)(newTree), tree);
}
};
const toggleAncestors = (id: string) => (tree: CollectionPanelFilesState) => {
- const ancestors = getNodeAncestors(id)(tree).reverse();
+ const ancestors = getNodeAncestorsIds(id)(tree).reverse();
return ancestors.reduce((newTree, parent) => parent ? toggleParentNode(parent)(newTree) : newTree, tree);
};
import { RootState } from "../../store";
import { CollectionResource } from '~/models/collection';
import { ServiceRepository } from "~/services/services";
-import { collectionUploaderActions } from "../uploader/collection-uploader-actions";
+import { uploadCollectionFiles } from '../uploader/collection-uploader-actions';
import { reset } from "redux-form";
export const collectionCreateActions = unionize({
CREATE_COLLECTION: ofType<{}>(),
CREATE_COLLECTION_SUCCESS: ofType<{}>(),
}, {
- tag: 'type',
- value: 'payload'
-});
+ tag: 'type',
+ value: 'payload'
+ });
export const createCollection = (collection: Partial<CollectionResource>, files: File[]) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const { ownerUuid } = getState().collections.creator;
const collectiontData = { ownerUuid, ...collection };
dispatch(collectionCreateActions.CREATE_COLLECTION(collectiontData));
- return services.collectionService
- .create(collectiontData)
- .then(collection => {
- dispatch(collectionUploaderActions.START_UPLOAD());
- services.collectionService.uploadFiles(collection.uuid, files,
- (fileId, loaded, total, currentTime) => {
- dispatch(collectionUploaderActions.SET_UPLOAD_PROGRESS({ fileId, loaded, total, currentTime }));
- })
- .then(collection => {
- dispatch(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection));
- dispatch(reset('collectionCreateDialog'));
- dispatch(collectionUploaderActions.CLEAR_UPLOAD());
- });
- return collection;
- });
+ const newCollection = await services.collectionService.create(collectiontData);
+ await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
+ dispatch(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection));
+ dispatch(reset('collectionCreateDialog'));
+ return newCollection;
};
export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
// SPDX-License-Identifier: AGPL-3.0\r
\r
import { default as unionize, ofType, UnionOf } from "unionize";\r
+import { Dispatch } from 'redux';\r
+import { RootState } from '~/store/store';\r
+import { ServiceRepository } from '~/services/services';\r
+import { dialogActions } from '~/store/dialog/dialog-actions';\r
+import { loadCollectionFiles } from '../../collection-panel/collection-panel-files/collection-panel-files-actions';\r
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";\r
\r
export interface UploadFile {\r
id: number;\r
});\r
\r
export type CollectionUploaderAction = UnionOf<typeof collectionUploaderActions>;\r
+\r
+export const uploadCollectionFiles = (collectionUuid: string) =>\r
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {\r
+ dispatch(collectionUploaderActions.START_UPLOAD());\r
+ const files = getState().collections.uploader.map(file => file.file);\r
+ await services.collectionService.uploadFiles(collectionUuid, files, handleUploadProgress(dispatch));\r
+ dispatch(collectionUploaderActions.CLEAR_UPLOAD());\r
+ };\r
+\r
+\r
+export const uploadCurrentCollectionFiles = () =>\r
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {\r
+ const currentCollection = getState().collectionPanel.item;\r
+ if (currentCollection) {\r
+ await dispatch<any>(uploadCollectionFiles(currentCollection.uuid));\r
+ dispatch<any>(loadCollectionFiles(currentCollection.uuid));\r
+ dispatch(closeUploadCollectionFilesDialog());\r
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Data has been uploaded.', hideDuration: 2000 }));\r
+ }\r
+ };\r
+\r
+export const UPLOAD_COLLECTION_FILES_DIALOG = 'uploadCollectionFilesDialog';\r
+export const openUploadCollectionFilesDialog = () => (dispatch: Dispatch) => {\r
+ dispatch(collectionUploaderActions.CLEAR_UPLOAD());\r
+ dispatch<any>(dialogActions.OPEN_DIALOG({ id: UPLOAD_COLLECTION_FILES_DIALOG, data: {} }));\r
+};\r
+\r
+export const closeUploadCollectionFilesDialog = () => dialogActions.CLOSE_DIALOG({ id: UPLOAD_COLLECTION_FILES_DIALOG });\r
+\r
+const handleUploadProgress = (dispatch: Dispatch) => (fileId: number, loaded: number, total: number, currentTime: number) => {\r
+ dispatch(collectionUploaderActions.SET_UPLOAD_PROGRESS({ fileId, loaded, total, currentTime }));\r
+};
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-import { createTree, getNodeValue, getNodeChildren } from "~/models/tree";
+import { createTree, getNodeValue, getNodeChildrenIds } from "~/models/tree";
import { TreePickerNode, createTreePickerNode } from "./tree-picker";
import { treePickerReducer } from "./tree-picker-reducer";
import { treePickerActions } from "./tree-picker-actions";
const tree = createTree<TreePickerNode>();
const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [subNode] }));
- expect(getNodeChildren('')(newTree)).toEqual(['1.1']);
+ expect(getNodeChildrenIds('')(newTree)).toEqual(['1.1']);
});
it('LOAD_TREE_PICKER_NODE_SUCCESS', () => {
const [newTree] = [tree]
.map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
.map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode] })));
- expect(getNodeChildren('1')(newTree)).toEqual(['1.1']);
+ expect(getNodeChildrenIds('1')(newTree)).toEqual(['1.1']);
expect(getNodeValue('1')(newTree)).toEqual({
...createTreePickerNode({ id: '1', value: '1' }),
status: TreeItemStatus.LOADED
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 { Tree, getNodeChildrenIds, getNode } from "~/models/tree";
import { CollectionFileType } from "~/models/collection-file";
+import { openUploadCollectionFilesDialog } from '~/store/collections/uploader/collection-uploader-actions';
const memoizedMapStateToProps = () => {
let prevState: CollectionPanelFilesState;
return (state: RootState): Pick<CollectionPanelFilesProps, "items"> => {
if (prevState !== state.collectionPanelFiles) {
prevState = state.collectionPanelFiles;
- prevTree = getNodeChildren('')(state.collectionPanelFiles)
+ prevTree = getNodeChildrenIds('')(state.collectionPanelFiles)
.map(collectionItemToTreeItem(state.collectionPanelFiles));
}
return {
};
const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
- onUploadDataClick: () => { return; },
+ onUploadDataClick: () => {
+ dispatch<any>(openUploadCollectionFilesDialog());
+ },
onCollapseToggle: (id) => {
dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id }));
},
type: node.value.type
},
id: node.id,
- items: getNodeChildren(node.id)(tree)
+ items: getNodeChildrenIds(node.id)(tree)
.map(collectionItemToTreeItem(tree)),
open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
selected: node.value.selected,
import { Tree, TreeProps, TreeItem } from "~/components/tree/tree";
import { RootState } from "~/store/store";
import { TreePicker as TTreePicker, TreePickerNode, createTreePickerNode } from "~/store/tree-picker/tree-picker";
-import { getNodeValue, getNodeChildren } from "~/models/tree";
+import { getNodeValue, getNodeChildrenIds } from "~/models/tree";
const memoizedMapStateToProps = () => {
let prevState: TTreePicker;
return (state: RootState): Pick<TreeProps<any>, 'items'> => {
if (prevState !== state.treePicker) {
prevState = state.treePicker;
- prevTree = getNodeChildren('')(state.treePicker)
+ prevTree = getNodeChildrenIds('')(state.treePicker)
.map(treePickerToTreeItems(state.treePicker));
}
return {
const treePickerToTreeItems = (tree: TTreePicker) =>
(id: string): TreeItem<any> => {
const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ id: '', value: 'InvalidNode' });
- const items = getNodeChildren(node.id)(tree)
+ const items = getNodeChildrenIds(node.id)(tree)
.map(treePickerToTreeItems(tree));
return {
active: node.selected,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Dispatch, compose } from "redux";
+import { withDialog } from '~/store/dialog/with-dialog';
+import { FilesUploadDialog } from '~/components/file-upload-dialog/file-upload-dialog';
+import { RootState } from '../../store/store';
+import { uploadCurrentCollectionFiles, UPLOAD_COLLECTION_FILES_DIALOG, collectionUploaderActions } from '~/store/collections/uploader/collection-uploader-actions';
+
+const mapStateToProps = (state: RootState) => ({
+ files: state.collections.uploader,
+ uploading: state.collections.uploader.some(file => file.loaded < file.total)
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ onSubmit: () => {
+ dispatch<any>(uploadCurrentCollectionFiles());
+ },
+ onChange: (files: File[]) => {
+ dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files));
+ }
+});
+
+export const UploadCollectionFilesDialog = compose(
+ withDialog(UPLOAD_COLLECTION_FILES_DIALOG),
+ connect(mapStateToProps, mapDispatchToProps)
+)(FilesUploadDialog);
\ No newline at end of file
import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
import { PROJECT_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-project-create';
+import { UploadCollectionFilesDialog } from '~/views-components/upload-collection-files-dialog/upload-collection-files-dialog';
const DRAWER_WITDH = 240;
const APP_BAR_HEIGHT = 100;
<main className={classes.contentWrapper}>
<div className={classes.content}>
<Switch>
- <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
+ <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
<Route path="/projects/:id" render={this.renderProjectPanel} />
<Route path="/favorites" render={this.renderFavoritePanel} />
<Route path="/collections/:id" render={this.renderCollectionPanel} />
<FileRemoveDialog />
<MultipleFilesRemoveDialog />
<UpdateCollectionDialog />
+ <UploadCollectionFilesDialog />
<UpdateProjectDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}