"@material-ui/icons": "2.0.0",
"@types/lodash": "4.14.116",
"@types/react-copy-to-clipboard": "4.2.5",
+ "@types/react-dropzone": "4.2.1",
"@types/redux-form": "7.4.4",
"axios": "0.18.0",
"classnames": "2.2.6",
"react": "16.4.2",
"react-copy-to-clipboard": "5.0.1",
"react-dom": "16.4.2",
+ "react-dropzone": "4.2.13",
"react-redux": "5.0.7",
"react-router": "4.3.1",
"react-router-dom": "4.3.1",
this.resourceType = '/' + resourceType + '/';
}
- create(data: Partial<T>) {
+ create(data?: Partial<T> | any) {
return CommonResourceService.defaultResponse(
this.serverApi
- .post<T>(this.resourceType, CommonResourceService.mapKeys(_.snakeCase)(data)));
+ .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
}
delete(uuid: string): Promise<T> {
return CommonResourceService.defaultResponse(
this.serverApi
.put<T>(this.resourceType + uuid, data));
-
+
}
}
// SPDX-License-Identifier: AGPL-3.0
import * as _ from "lodash";
-import { Resource } from "../../models/resource";
-export class FilterBuilder<T extends Resource = Resource> {
- static create<T extends Resource = Resource>(resourcePrefix = "") {
- return new FilterBuilder<T>(resourcePrefix);
+export class FilterBuilder {
+ static create(resourcePrefix = "") {
+ return new FilterBuilder(resourcePrefix);
}
constructor(
private resourcePrefix = "",
private filters = "") { }
- public addEqual(field: keyof T, value?: string) {
+ public addEqual(field: string, value?: string) {
return this.addCondition(field, "=", value);
}
- public addLike(field: keyof T, value?: string) {
+ public addLike(field: string, value?: string) {
return this.addCondition(field, "like", value, "%", "%");
}
- public addILike(field: keyof T, value?: string) {
+ public addILike(field: string, value?: string) {
return this.addCondition(field, "ilike", value, "%", "%");
}
- public addIsA(field: keyof T, value?: string | string[]) {
+ public addIsA(field: string, value?: string | string[]) {
return this.addCondition(field, "is_a", value);
}
- public addIn(field: keyof T, value?: string | string[]) {
+ public addIn(field: string, value?: string | string[]) {
return this.addCondition(field, "in", value);
}
- public concat<O extends Resource>(filterBuilder: FilterBuilder<O>) {
+ public concat(filterBuilder: FilterBuilder) {
return new FilterBuilder(this.resourcePrefix, this.filters + (this.filters && filterBuilder.filters ? "," : "") + filterBuilder.getFilters());
}
return "[" + this.filters + "]";
}
- private addCondition(field: keyof T, cond: string, value?: string | string[], prefix: string = "", postfix: string = "") {
+ private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "") {
if (value) {
value = typeof value === "string"
? `"${prefix}${value}${postfix}"`
return "";
};
+export const formatProgress = (loaded: number, total: number) => {
+ const progress = loaded >= 0 && total > 0 ? loaded * 100 / total : 0;
+ return `${progress.toFixed(2)}%`;
+};
+
+export function formatUploadSpeed(prevLoaded: number, loaded: number, prevTime: number, currentTime: number) {
+ const speed = loaded > prevLoaded && currentTime > prevTime
+ ? (loaded - prevLoaded) / (currentTime - prevTime)
+ : 0;
+ return `${(speed / 1000).toFixed(2)} KB/s`;
+}
+
const FILE_SIZES = [
{
base: 1000000000000,
base: 1,
unit: "B"
}
-];
\ No newline at end of file
+];
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';
+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>>;
}
});
-export const CollectionPanelFiles = withStyles(styles)(
- ({ onItemMenuOpen, onOptionsMenuOpen, classes, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) =>
+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
- variant='raised'
+ <Button onClick={
+ () => {
+ dispatch<any>(renameFile());
+ }}
+ variant='raised'
color='primary'
size='small'>
Upload data
</Typography>
</Grid>
<FileTree onMenuOpen={onItemMenuOpen} {...treeProps} />
- </Card>);
+ </Card>)
+);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+ Grid,
+ StyleRulesCallback,
+ Table, TableBody, TableCell, TableHead, TableRow,
+ Typography,
+ WithStyles
+} from '@material-ui/core';
+import { withStyles } from '@material-ui/core';
+import Dropzone from 'react-dropzone';
+import { CloudUploadIcon } from "../icon/icon";
+import { formatFileSize, formatProgress, formatUploadSpeed } from "../../common/formatters";
+import { UploadFile } from "../../store/collections/uploader/collection-uploader-actions";
+
+type CssRules = "root" | "dropzone" | "container" | "uploadIcon";
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ root: {
+ },
+ dropzone: {
+ width: "100%",
+ height: "200px",
+ overflow: "auto",
+ border: "1px dashed black",
+ borderRadius: "5px"
+ },
+ container: {
+ height: "100%"
+ },
+ uploadIcon: {
+ verticalAlign: "middle"
+ }
+});
+
+interface FileUploadProps {
+ files: UploadFile[];
+ disabled: boolean;
+ onDrop: (files: File[]) => void;
+}
+
+export const FileUpload = withStyles(styles)(
+ ({ classes, files, disabled, onDrop }: FileUploadProps & WithStyles<CssRules>) =>
+ <Grid container direction={"column"}>
+ <Typography variant={"subheading"}>
+ Upload data
+ </Typography>
+ <Dropzone className={classes.dropzone} onDrop={files => onDrop(files)} disabled={disabled}>
+ {files.length === 0 &&
+ <Grid container justify="center" alignItems="center" className={classes.container}>
+ <Grid item component={"span"}>
+ <Typography variant={"subheading"}>
+ <CloudUploadIcon className={classes.uploadIcon}/> Drag and drop data or click to browse
+ </Typography>
+ </Grid>
+ </Grid>}
+ {files.length > 0 &&
+ <Table style={{width: "100%"}}>
+ <TableHead>
+ <TableRow>
+ <TableCell>File name</TableCell>
+ <TableCell>File size</TableCell>
+ <TableCell>Upload speed</TableCell>
+ <TableCell>Upload progress</TableCell>
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {files.map(f =>
+ <TableRow key={f.id}>
+ <TableCell>{f.file.name}</TableCell>
+ <TableCell>{formatFileSize(f.file.size)}</TableCell>
+ <TableCell>{formatUploadSpeed(f.prevLoaded, f.loaded, f.prevTime, f.currentTime)}</TableCell>
+ <TableCell>{formatProgress(f.loaded, f.total)}</TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
+ }
+ </Dropzone>
+ </Grid>
+);
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import BubbleChart from '@material-ui/icons/BubbleChart';
import Cached from '@material-ui/icons/Cached';
+import CloudUpload from '@material-ui/icons/CloudUpload';
import Code from '@material-ui/icons/Code';
import ChevronLeft from '@material-ui/icons/ChevronLeft';
import ChevronRight from '@material-ui/icons/ChevronRight';
export const CopyIcon: IconType = (props) => <ContentCopy {...props} />;
export const CollectionIcon: IconType = (props) => <LibraryBooks {...props} />;
export const CloseIcon: IconType = (props) => <Close {...props} />;
+export const CloudUploadIcon: IconType = (props) => <CloudUpload {...props} />;
export const DefaultIcon: IconType = (props) => <RateReview {...props} />;
export const DetailsIcon: IconType = (props) => <Info {...props} />;
export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
}
export interface CollectionDirectory {
- parentId: string;
+ path: string;
id: string;
name: string;
type: CollectionFileType.DIRECTORY;
}
export interface CollectionFile {
- parentId: string;
+ path: string;
id: string;
name: string;
size: number;
type: CollectionFileType.FILE;
}
+export interface CollectionUploadFile {
+ name: string;
+}
+
export const createCollectionDirectory = (data: Partial<CollectionDirectory>): CollectionDirectory => ({
id: '',
name: '',
- parentId: '',
+ path: '',
type: CollectionFileType.DIRECTORY,
...data
});
export const createCollectionFile = (data: Partial<CollectionFile>): CollectionFile => ({
id: '',
name: '',
- parentId: '',
+ path: '',
size: 0,
type: CollectionFileType.FILE,
...data
-});
\ No newline at end of file
+});
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.\r
+//\r
+// SPDX-License-Identifier: AGPL-3.0\r
+\r
+import { Resource } from "./resource";\r
+\r
+export interface KeepResource extends Resource {\r
+ serviceHost: string;\r
+ servicePort: number;\r
+ serviceSslFlag: boolean;\r
+ serviceType: string;\r
+}\r
// SPDX-License-Identifier: AGPL-3.0
import { CollectionService } from "../collection-service/collection-service";
-import { parseKeepManifestText } from "./collection-manifest-parser";
+import { parseKeepManifestText, stringifyKeepManifest } from "./collection-manifest-parser";
import { mapManifestToCollectionFilesTree } from "./collection-manifest-mapper";
+import { CommonResourceService } from "../../common/api/common-resource-service";
+import * as _ from "lodash";
export class CollectionFilesService {
-
+
constructor(private collectionService: CollectionService) { }
getFiles(collectionUuid: string) {
);
}
-}
\ No newline at end of file
+ async renameFile(collectionUuid: string, file: { name: string, path: string }, newName: string) {
+ const collection = await this.collectionService.get(collectionUuid);
+ const manifest = parseKeepManifestText(collection.manifestText);
+ const updatedManifest = manifest.map(
+ stream => stream.name === file.path
+ ? {
+ ...stream,
+ files: stream.files.map(
+ f => f.name === file.name
+ ? { ...f, name: newName }
+ : f
+ )
+ }
+ : stream
+ );
+ const manifestText = stringifyKeepManifest(updatedManifest);
+ const data = { ...collection, manifestText };
+ return this.collectionService.update(collectionUuid, CommonResourceService.mapKeys(_.snakeCase)(data));
+ }
+
+ async deleteFile(collectionUuid: string, file: { name: string, path: string }) {
+ const collection = await this.collectionService.get(collectionUuid);
+ const manifest = parseKeepManifestText(collection.manifestText);
+ const updatedManifest = manifest.map(stream =>
+ stream.name === file.path
+ ? {
+ ...stream,
+ files: stream.files.filter(f => f.name !== file.name)
+ }
+ : stream
+ );
+ const manifestText = stringifyKeepManifest(updatedManifest);
+ return this.collectionService.update(collectionUuid, { manifestText });
+ }
+
+ renameTest() {
+ const u = this.renameFile('qr1hi-4zz18-n0sx074erl4p0ph', {
+ name: 'extracted2.txt.png',
+ path: ''
+ }, 'extracted-new.txt.png');
+ }
+}
// SPDX-License-Identifier: AGPL-3.0
import { parseKeepManifestText } from "./collection-manifest-parser";
-import { mapManifestToFiles, mapManifestToDirectories } from "./collection-manifest-mapper";
+import { mapManifestToFiles, mapManifestToDirectories, mapManifestToCollectionFilesTree, mapCollectionFilesTreeToManifest } 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: '',
+ path: '',
id: '/a',
name: 'a',
size: 0,
type: 'file'
}, {
- parentId: '',
+ path: '',
id: '/b',
name: 'b',
size: 0,
type: 'file'
}, {
- parentId: '',
+ path: '',
id: '/output.txt',
name: 'output.txt',
size: 33,
type: 'file'
}, {
- parentId: '/c',
+ path: '/c',
id: '/c/d',
name: 'd',
size: 0,
const manifest = parseKeepManifestText(manifestText);
const directories = mapManifestToDirectories(manifest);
expect(directories).toEqual([{
- parentId: "",
+ path: "",
id: '/c',
name: 'c',
type: 'directory'
}, {
- parentId: '/c',
+ path: '/c',
id: '/c/user',
name: 'user',
type: 'directory'
}, {
- parentId: '/c/user',
+ path: '/c/user',
id: '/c/user/results',
name: 'results',
type: 'directory'
},]);
+});
+
+test('mapCollectionFilesTreeToManifest', () => {
+ const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:22:test.txt\n./c/user/results 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n`;
+ const tree = mapManifestToCollectionFilesTree(parseKeepManifestText(manifestText));
+ const manifest = mapCollectionFilesTreeToManifest(tree);
+ expect(manifest).toEqual([{
+ name: '',
+ locators: [],
+ files: [{
+ name: 'test.txt',
+ position: '',
+ size: 22
+ },],
+ }, {
+ name: '/c/user/results',
+ locators: [],
+ files: [{
+ name: 'a',
+ position: '',
+ size: 0
+ }, {
+ name: 'b',
+ position: '',
+ size: 0
+ }, {
+ name: 'output.txt',
+ position: '',
+ size: 33
+ },],
+ },]);
+
});
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-import { uniqBy } from 'lodash';
+import { uniqBy, groupBy } 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';
+import { TreeNode, setNode, createTree, getNodeDescendants, 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 files = values.filter(value => value && value.type === CollectionFileType.FILE) as CollectionFile[];
+ const fileGroups = groupBy(files, file => file.path);
+ return Object
+ .keys(fileGroups)
+ .map(dirName => ({
+ name: dirName,
+ locators: [],
+ files: fileGroups[dirName].map(mapCollectionFile)
+ }));
+};
export const mapManifestToCollectionFilesTree = (manifest: KeepManifest): CollectionFilesTree =>
manifestToCollectionFiles(manifest)
export const mapCollectionFileToTreeNode = (file: CollectionFile): TreeNode<CollectionFile> => ({
children: [],
id: file.id,
- parent: file.parentId,
+ parent: file.path,
value: file
});
const mapPathComponentToDirectory = (component: string, index: number, components: string[]): CollectionDirectory =>
createCollectionDirectory({
- parentId: index === 0 ? '' : joinPathComponents(components, index),
+ path: index === 0 ? '' : joinPathComponents(components, index),
id: joinPathComponents(components, index + 1),
name: component,
});
const joinPathComponents = (components: string[], index: number) =>
`/${components.slice(0, index).join('/')}`;
+const mapCollectionFile = (file: CollectionFile): KeepManifestStreamFile => ({
+ name: file.name,
+ position: '',
+ size: file.size
+});
+
const mapStreamDirectory = (stream: KeepManifestStream): CollectionDirectory =>
createCollectionDirectory({
- parentId: '',
+ path: '',
id: stream.name,
name: stream.name,
});
const mapStreamFile = (stream: KeepManifestStream) =>
(file: KeepManifestStreamFile): CollectionFile =>
createCollectionFile({
- parentId: stream.name,
+ path: stream.name,
id: `${stream.name}/${file.name}`,
name: file.name,
size: file.size,
//
// SPDX-License-Identifier: AGPL-3.0
-import { parseKeepManifestText, parseKeepManifestStream } from "./collection-manifest-parser";
+import { parseKeepManifestText, parseKeepManifestStream, stringifyKeepManifest } from "./collection-manifest-parser";
describe('parseKeepManifestText', () => {
it('should parse text into streams', () => {
});
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},
+ { name: 'a', position: '0', size: 0 },
+ { name: 'b', position: '0', size: 0 },
+ { name: 'output.txt', position: '0', size: 33 },
]);
});
+});
+
+test('stringifyKeepManifest', () => {
+ const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:22:test.txt\n./c/user/results 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n`;
+ const manifest = parseKeepManifestText(manifestText);
+ expect(stringifyKeepManifest(manifest)).toEqual(`. 930625b054ce894ac40596c3f5a0d947+33 0:22:test.txt\n./c/user/results 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n`);
});
\ No newline at end of file
//
// SPDX-License-Identifier: AGPL-3.0
-import { KeepManifestStream, KeepManifestStreamFile } from "../../models/keep-manifest";
+import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "../../models/keep-manifest";
/**
* Documentation [http://doc.arvados.org/api/storage.html](http://doc.arvados.org/api/storage.html)
*/
-export const parseKeepManifestText = (text: string) =>
+export const parseKeepManifestText: (text: string) => KeepManifestStream[] = (text: string) =>
text
.split(/\n/)
.filter(streamText => streamText.length > 0)
};
};
+export const stringifyKeepManifest = (manifest: KeepManifest) =>
+ manifest.map(stringifyKeepManifestStream).join('');
+
+export const stringifyKeepManifestStream = (stream: KeepManifestStream) =>
+ `.${stream.name} ${stream.locators.join(' ')} ${stream.files.map(stringifyFile).join(' ')}\n`;
+
const FILE_LOCATOR_REGEXP = /^([0-9a-f]{32})\+([0-9]+)(\+[A-Z][-A-Za-z0-9@_]*)*$/;
const FILE_REGEXP = /([0-9]+):([0-9]+):(.*)/;
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
+};
+
+const stringifyFile = (file: KeepManifestStreamFile) =>
+ `${file.position}:${file.size}:${file.name}`;
import { CommonResourceService } from "../../common/api/common-resource-service";
import { CollectionResource } from "../../models/collection";
-import { AxiosInstance } from "axios";
+import axios, { AxiosInstance } from "axios";
+import { KeepService } from "../keep-service/keep-service";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { CollectionFile, createCollectionFile } from "../../models/collection-file";
+import { parseKeepManifestText, stringifyKeepManifest } from "../collection-files-service/collection-manifest-parser";
+import * as _ from "lodash";
+import { KeepManifestStream } from "../../models/keep-manifest";
+
+export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
export class CollectionService extends CommonResourceService<CollectionResource> {
- constructor(serverApi: AxiosInstance) {
+ constructor(serverApi: AxiosInstance, private keepService: KeepService) {
super(serverApi, "collections");
}
-}
\ No newline at end of file
+
+ 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
+ }));
+ });
+ }
+
+ 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);
+ }
+ 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
+ });
+ });
+
+ 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 = FilterBuilder.create()
+ .addEqual("service_type", "proxy");
+
+ return this.keepService.list({ filters }).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");
+ }
+ });
+ }
+}
it("unmarks resource as favorite", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "linkUuid" }] }));
const filters = FilterBuilder
- .create<LinkResource>()
+ .create()
.addEqual('tailUuid', "userUuid")
.addEqual('headUuid', "resourceUuid")
.addEqual('linkClass', LinkClass.STAR);
it("lists favorite resources", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "headUuid" }] }));
const listFilters = FilterBuilder
- .create<LinkResource>()
+ .create()
.addEqual('tailUuid', "userUuid")
.addEqual('linkClass', LinkClass.STAR);
const contents = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "resourceUuid" }] }));
- const contentFilters = FilterBuilder.create<GroupContentsResource>().addIn('uuid', ["headUuid"]);
+ const contentFilters = FilterBuilder.create().addIn('uuid', ["headUuid"]);
linkService.list = list;
groupService.contents = contents;
const favoriteService = new FavoriteService(linkService, groupService);
it("checks if resources are present in favorites", async () => {
const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "foo" }] }));
const listFilters = FilterBuilder
- .create<LinkResource>()
+ .create()
.addIn("headUuid", ["foo", "oof"])
.addEqual("tailUuid", "userUuid")
.addEqual("linkClass", LinkClass.STAR);
export interface FavoriteListArguments {
limit?: number;
offset?: number;
- filters?: FilterBuilder<LinkResource>;
+ filters?: FilterBuilder;
order?: FavoriteOrderBuilder;
}
return this.linkService
.list({
filters: FilterBuilder
- .create<LinkResource>()
+ .create()
.addEqual('tailUuid', data.userUuid)
.addEqual('headUuid', data.resourceUuid)
.addEqual('linkClass', LinkClass.STAR)
list(userUuid: string, { filters, limit, offset, order }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
const listFilter = FilterBuilder
- .create<LinkResource>()
+ .create()
.addEqual('tailUuid', userUuid)
.addEqual('linkClass', LinkClass.STAR);
limit,
offset,
order: order ? order.getContentOrder() : OrderBuilder.create<GroupContentsResource>(),
- filters: FilterBuilder.create<GroupContentsResource>().addIn('uuid', uuids),
+ filters: FilterBuilder.create().addIn('uuid', uuids),
recursive: true
});
});
return this.linkService
.list({
filters: FilterBuilder
- .create<LinkResource>()
+ .create()
.addIn("headUuid", resourceUuids)
.addEqual("tailUuid", userUuid)
.addEqual("linkClass", LinkClass.STAR)
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.\r
+//\r
+// SPDX-License-Identifier: AGPL-3.0\r
+\r
+import { CommonResourceService } from "../../common/api/common-resource-service";\r
+import { AxiosInstance } from "axios";\r
+import { KeepResource } from "../../models/keep";\r
+\r
+export class KeepService extends CommonResourceService<KeepResource> {\r
+ constructor(serverApi: AxiosInstance) {\r
+ super(serverApi, "keep_services");\r
+ }\r
+}\r
expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
params: {
filters: FilterBuilder
- .create<ProjectResource>()
+ .create()
.addEqual("groupClass", "project")
.serialize()
}
? filters
: FilterBuilder.create())
.concat(FilterBuilder
- .create<ProjectResource>()
+ .create()
.addEqual("groupClass", GroupClass.PROJECT));
}
}
import { TagService } from "./tag-service/tag-service";
import Axios from "axios";
import { CollectionFilesService } from "./collection-files-service/collection-files-service";
+import { KeepService } from "./keep-service/keep-service";
export interface ServiceRepository {
apiClient: AxiosInstance;
authService: AuthService;
+ keepService: KeepService;
groupsService: GroupsService;
projectService: ProjectService;
linkService: LinkService;
apiClient.defaults.baseURL = `${baseUrl}/arvados/v1`;
const authService = new AuthService(apiClient, baseUrl);
+ const keepService = new KeepService(apiClient);
const groupsService = new GroupsService(apiClient);
const projectService = new ProjectService(apiClient);
const linkService = new LinkService(apiClient);
const favoriteService = new FavoriteService(linkService, groupsService);
- const collectionService = new CollectionService(apiClient);
+ const collectionService = new CollectionService(apiClient, keepService);
const tagService = new TagService(linkService);
const collectionFilesService = new CollectionFilesService(collectionService);
return {
apiClient,
authService,
+ keepService,
groupsService,
projectService,
linkService,
list(uuid: string) {
const filters = FilterBuilder
- .create<TagResource>()
+ .create()
.addEqual("headUuid", uuid)
.addEqual("tailUuid", TagTailType.COLLECTION)
.addEqual("linkClass", LinkClass.TAG);
});
}
-}
\ No newline at end of file
+}
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' }),
+ createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', path: '' }),
+ createCollectionDirectory({ id: 'Directory 2', name: 'Directory 2', path: 'Directory 1' }),
+ createCollectionDirectory({ id: 'Directory 3', name: 'Directory 3', path: '' }),
+ createCollectionDirectory({ id: 'Directory 4', name: 'Directory 4', path: 'Directory 3' }),
+ createCollectionFile({ id: 'file1.txt', name: 'file1.txt', path: 'Directory 2' }),
+ createCollectionFile({ id: 'file2.txt', name: 'file2.txt', path: 'Directory 2' }),
+ createCollectionFile({ id: 'file3.txt', name: 'file3.txt', path: 'Directory 3' }),
+ createCollectionFile({ id: 'file4.txt', name: 'file4.txt', path: 'Directory 3' }),
+ createCollectionFile({ id: 'file5.txt', name: 'file5.txt', path: 'Directory 4' }),
];
const collectionFilesTree = files.reduce((tree, file) => setNode({
children: [],
id: file.id,
- parent: file.parentId,
+ parent: file.path,
value: file
})(tree), createTree<CollectionFile | CollectionDirectory>());
it('SET_COLLECTION_FILES', () => {
expect(getNodeValue('Directory 1')(collectionPanelFilesTree)).toEqual({
- ...createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', parentId: '' }),
+ ...createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', path: '' }),
collapsed: true,
selected: false
});
// SPDX-License-Identifier: AGPL-3.0
import { combineReducers } from 'redux';
-import * as creator from "./creator/collection-creator-reducer";
-import * as updator from "./updator/collection-updator-reducer";
+import { collectionCreatorReducer, CollectionCreatorState } from "./creator/collection-creator-reducer";
+import { collectionUpdaterReducer, CollectionUpdaterState } from "./updater/collection-updater-reducer";
+import { collectionUploaderReducer, CollectionUploaderState } from "./uploader/collection-uploader-reducer";
export type CollectionsState = {
- creator: creator.CollectionCreatorState;
- updator: updator.CollectionUpdatorState;
+ creator: CollectionCreatorState;
+ updater: CollectionUpdaterState;
+ uploader: CollectionUploaderState
};
export const collectionsReducer = combineReducers({
- creator: creator.collectionCreationReducer,
- updator: updator.collectionCreationReducer
-});
\ No newline at end of file
+ creator: collectionCreatorReducer,
+ updater: collectionUpdaterReducer,
+ uploader: collectionUploaderReducer
+});
import { RootState } from "../../store";
import { CollectionResource } from '../../../models/collection';
import { ServiceRepository } from "../../../services/services";
+import { collectionUploaderActions } from "../uploader/collection-uploader-actions";
+import { reset } from "redux-form";
export const collectionCreateActions = unionize({
OPEN_COLLECTION_CREATOR: ofType<{ ownerUuid: string }>(),
CREATE_COLLECTION: ofType<{}>(),
CREATE_COLLECTION_SUCCESS: ofType<{}>(),
}, {
- tag: 'type',
- value: 'payload'
- });
+ tag: 'type',
+ value: 'payload'
+});
-export const createCollection = (collection: Partial<CollectionResource>) =>
+export const createCollection = (collection: Partial<CollectionResource>, files: File[]) =>
(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(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection)));
+ .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;
+ });
};
export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
//
// SPDX-License-Identifier: AGPL-3.0
-import { collectionCreationReducer } from "./collection-creator-reducer";
+import { collectionCreatorReducer } from "./collection-creator-reducer";
import { collectionCreateActions } from "./collection-creator-action";
describe('collection-reducer', () => {
const initialState = { opened: false, ownerUuid: "" };
const collection = { opened: true, ownerUuid: "" };
- const state = collectionCreationReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState));
+ const state = collectionCreatorReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState));
expect(state).toEqual(collection);
});
const initialState = { opened: true, ownerUuid: "" };
const collection = { opened: false, ownerUuid: "" };
- const state = collectionCreationReducer(initialState, collectionCreateActions.CLOSE_COLLECTION_CREATOR());
+ const state = collectionCreatorReducer(initialState, collectionCreateActions.CLOSE_COLLECTION_CREATOR());
expect(state).toEqual(collection);
});
const initialState = { opened: true, ownerUuid: "test" };
const collection = { opened: false, ownerUuid: "" };
- const state = collectionCreationReducer(initialState, collectionCreateActions.CREATE_COLLECTION_SUCCESS());
+ const state = collectionCreatorReducer(initialState, collectionCreateActions.CREATE_COLLECTION_SUCCESS());
expect(state).toEqual(collection);
});
-});
\ No newline at end of file
+});
import { collectionCreateActions, CollectionCreateAction } from './collection-creator-action';
-export type CollectionCreatorState = CollectionCreator;
-
-interface CollectionCreator {
+export interface CollectionCreatorState {
opened: boolean;
ownerUuid: string;
}
-const updateCreator = (state: CollectionCreatorState, creator?: Partial<CollectionCreator>) => ({
+const updateCreator = (state: CollectionCreatorState, creator?: Partial<CollectionCreatorState>) => ({
...state,
...creator
});
ownerUuid: ''
};
-export const collectionCreationReducer = (state: CollectionCreatorState = initialState, action: CollectionCreateAction) => {
+export const collectionCreatorReducer = (state: CollectionCreatorState = initialState, action: CollectionCreateAction) => {
return collectionCreateActions.match(action, {
OPEN_COLLECTION_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
CLOSE_COLLECTION_CREATOR: () => updateCreator(state, { opened: false }),
import { initialize } from 'redux-form';
import { collectionPanelActions } from "../../collection-panel/collection-panel-action";
-export const collectionUpdatorActions = unionize({
- OPEN_COLLECTION_UPDATOR: ofType<{ uuid: string }>(),
- CLOSE_COLLECTION_UPDATOR: ofType<{}>(),
+export const collectionUpdaterActions = unionize({
+ OPEN_COLLECTION_UPDATER: ofType<{ uuid: string }>(),
+ CLOSE_COLLECTION_UPDATER: ofType<{}>(),
UPDATE_COLLECTION_SUCCESS: ofType<{}>(),
}, {
- tag: 'type',
- value: 'payload'
- });
+ tag: 'type',
+ value: 'payload'
+});
export const COLLECTION_FORM_NAME = 'collectionEditDialog';
-
-export const openUpdator = (uuid: string) =>
+
+export const openUpdater = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
- dispatch(collectionUpdatorActions.OPEN_COLLECTION_UPDATOR({ uuid }));
+ dispatch(collectionUpdaterActions.OPEN_COLLECTION_UPDATER({ uuid }));
const item = getState().collectionPanel.item;
if(item) {
dispatch(initialize(COLLECTION_FORM_NAME, { name: item.name, description: item.description }));
export const updateCollection = (collection: Partial<CollectionResource>) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { uuid } = getState().collections.updator;
+ const { uuid } = getState().collections.updater;
return services.collectionService
.update(uuid, collection)
.then(collection => {
dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
- dispatch(collectionUpdatorActions.UPDATE_COLLECTION_SUCCESS());
+ dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS());
}
);
};
-export type CollectionUpdatorAction = UnionOf<typeof collectionUpdatorActions>;
\ No newline at end of file
+export type CollectionUpdaterAction = UnionOf<typeof collectionUpdaterActions>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionUpdaterActions, CollectionUpdaterAction } from './collection-updater-action';
+
+export interface CollectionUpdaterState {
+ opened: boolean;
+ uuid: string;
+}
+
+const updateCollection = (state: CollectionUpdaterState, updater?: Partial<CollectionUpdaterState>) => ({
+ ...state,
+ ...updater
+});
+
+const initialState: CollectionUpdaterState = {
+ opened: false,
+ uuid: ''
+};
+
+export const collectionUpdaterReducer = (state: CollectionUpdaterState = initialState, action: CollectionUpdaterAction) => {
+ return collectionUpdaterActions.match(action, {
+ OPEN_COLLECTION_UPDATER: ({ uuid }) => updateCollection(state, { uuid, opened: true }),
+ CLOSE_COLLECTION_UPDATER: () => updateCollection(state, { opened: false }),
+ UPDATE_COLLECTION_SUCCESS: () => updateCollection(state, { opened: false, uuid: "" }),
+ default: () => state
+ });
+};
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { collectionUpdatorActions, CollectionUpdatorAction } from './collection-updator-action';
-
-export type CollectionUpdatorState = CollectionUpdator;
-
-interface CollectionUpdator {
- opened: boolean;
- uuid: string;
-}
-
-const updateCollection = (state: CollectionUpdatorState, updator?: Partial<CollectionUpdator>) => ({
- ...state,
- ...updator
-});
-
-const initialState: CollectionUpdatorState = {
- opened: false,
- uuid: ''
-};
-
-export const collectionCreationReducer = (state: CollectionUpdatorState = initialState, action: CollectionUpdatorAction) => {
- return collectionUpdatorActions.match(action, {
- OPEN_COLLECTION_UPDATOR: ({ uuid }) => updateCollection(state, { uuid, opened: true }),
- CLOSE_COLLECTION_UPDATOR: () => updateCollection(state, { opened: false }),
- UPDATE_COLLECTION_SUCCESS: () => updateCollection(state, { opened: false, uuid: "" }),
- default: () => state
- });
-};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.\r
+//\r
+// SPDX-License-Identifier: AGPL-3.0\r
+\r
+import { default as unionize, ofType, UnionOf } from "unionize";\r
+\r
+export interface UploadFile {\r
+ id: number;\r
+ file: File;\r
+ prevLoaded: number;\r
+ loaded: number;\r
+ total: number;\r
+ startTime: number;\r
+ prevTime: number;\r
+ currentTime: number;\r
+}\r
+\r
+export const collectionUploaderActions = unionize({\r
+ SET_UPLOAD_FILES: ofType<File[]>(),\r
+ START_UPLOAD: ofType(),\r
+ SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(),\r
+ CLEAR_UPLOAD: ofType()\r
+}, {\r
+ tag: 'type',\r
+ value: 'payload'\r
+});\r
+\r
+export type CollectionUploaderAction = UnionOf<typeof collectionUploaderActions>;\r
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionUploaderAction, collectionUploaderActions, UploadFile } from "./collection-uploader-actions";
+
+export type CollectionUploaderState = UploadFile[];
+
+const initialState: CollectionUploaderState = [];
+
+export const collectionUploaderReducer = (state: CollectionUploaderState = initialState, action: CollectionUploaderAction) => {
+ return collectionUploaderActions.match(action, {
+ SET_UPLOAD_FILES: files => files.map((f, idx) => ({
+ id: idx,
+ file: f,
+ prevLoaded: 0,
+ loaded: 0,
+ total: 0,
+ startTime: 0,
+ prevTime: 0,
+ currentTime: 0
+ })),
+ START_UPLOAD: () => {
+ const startTime = Date.now();
+ return state.map(f => ({...f, startTime, prevTime: startTime}));
+ },
+ SET_UPLOAD_PROGRESS: ({ fileId, loaded, total, currentTime }) =>
+ state.map(f => f.id === fileId ? {
+ ...f,
+ prevLoaded: f.loaded,
+ loaded,
+ total,
+ prevTime: f.currentTime,
+ currentTime
+ } : f),
+ CLEAR_UPLOAD: () => [],
+ default: () => state
+ });
+};
: order.addAsc("name")
: order,
filters: FilterBuilder
- .create<LinkResource>()
+ .create()
.addIsA("headUuid", typeFilters.map(filter => filter.type))
.addILike("name", dataExplorer.searchValue)
})
.create()
.addIsA("uuid", typeFilters.map(f => f.type)))
.concat(FilterBuilder
- .create<ProcessResource>(GroupContentsResourcePrefix.PROCESS)
+ .create(GroupContentsResourcePrefix.PROCESS)
.addIn("state", statusFilters.map(f => f.type)))
.concat(getSearchFilter(dataExplorer.searchValue))
})
const getSearchFilter = (searchValue: string) =>
searchValue
? [
- FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
- FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
- FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)]
+ FilterBuilder.create(GroupContentsResourcePrefix.COLLECTION),
+ FilterBuilder.create(GroupContentsResourcePrefix.PROCESS),
+ FilterBuilder.create(GroupContentsResourcePrefix.PROJECT)]
.reduce((acc, b) =>
acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
: FilterBuilder.create();
TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<string>(),
RESET_PROJECT_TREE_ACTIVITY: ofType<string>()
}, {
- tag: 'type',
- value: 'payload'
- });
+ tag: 'type',
+ value: 'payload'
+});
export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
return services.projectService.list({
filters: FilterBuilder
- .create<ProjectResource>()
+ .create()
.addEqual("ownerUuid", parentUuid)
}).then(({ items: projects }) => {
dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "../../../store/favorites/favorites-actions";
import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "../../../components/icon/icon";
-import { openUpdator } from "../../../store/collections/updator/collection-updator-action";
+import { openUpdater } from "../../../store/collections/updater/collection-updater-action";
import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
export const collectionActionSet: ContextMenuActionSet = [[
icon: RenameIcon,
name: "Edit collection",
execute: (dispatch, resource) => {
- dispatch<any>(openUpdator(resource.uuid));
+ dispatch<any>(openUpdater(resource.uuid));
}
},
{
component: ToggleFavoriteAction,
execute: (dispatch, resource) => {
dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
});
}
},
import { RootState } from "../../store/store";
import { DialogCollectionCreate } from "../dialog-create/dialog-collection-create";
import { collectionCreateActions, createCollection } from "../../store/collections/creator/collection-creator-action";
-import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
-import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+import { UploadFile } from "../../store/collections/uploader/collection-uploader-actions";
const mapStateToProps = (state: RootState) => ({
open: state.collections.creator.opened
handleClose: () => {
dispatch(collectionCreateActions.CLOSE_COLLECTION_CREATOR());
},
- onSubmit: (data: { name: string, description: string }) => {
- return dispatch<any>(addCollection(data))
+ onSubmit: (data: { name: string, description: string }, files: UploadFile[]) => {
+ return dispatch<any>(addCollection(data, files.map(f => f.file)))
.catch((e: any) => {
throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Collection with this name already exists." : "" });
});
}
});
-const addCollection = (data: { name: string, description: string }) =>
+const addCollection = (data: { name: string, description: string }, files: File[]) =>
(dispatch: Dispatch) => {
- return dispatch<any>(createCollection(data)).then(() => {
+ return dispatch<any>(createCollection(data, files)).then(() => {
dispatch(snackbarActions.OPEN_SNACKBAR({
message: "Collection has been successfully created.",
hideDuration: 2000
}));
- dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
});
};
import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
+import { FileUpload } from "../../components/file-upload/file-upload";
+import { connect, DispatchProp } from "react-redux";
+import { RootState } from "../../store/store";
+import { collectionUploaderActions, UploadFile } from "../../store/collections/uploader/collection-uploader-actions";
type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
marginBottom: theme.spacing.unit * 3
}
});
+
interface DialogCollectionCreateProps {
open: boolean;
handleClose: () => void;
- onSubmit: (data: { name: string, description: string }) => void;
+ onSubmit: (data: { name: string, description: string }, files: UploadFile[]) => void;
handleSubmit: any;
submitting: boolean;
invalid: boolean;
pristine: boolean;
+ files: UploadFile[];
}
interface TextFieldProps {
}
export const DialogCollectionCreate = compose(
+ connect((state: RootState) => ({
+ files: state.collections.uploader
+ })),
reduxForm({ form: 'collectionCreateDialog' }),
withStyles(styles))(
- class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & WithStyles<CssRules>> {
+ class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & DispatchProp & WithStyles<CssRules>> {
render() {
- const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
-
+ const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine, files } = this.props;
+ const busy = submitting || files.reduce(
+ (prev, curr) => prev + (curr.loaded > 0 && curr.loaded < curr.total ? 1 : 0), 0
+ ) > 0;
return (
<Dialog
open={open}
maxWidth='sm'
disableBackdropClick={true}
disableEscapeKeyDown={true}>
- <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+ <form onSubmit={handleSubmit((data: any) => onSubmit(data, files))}>
<DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
<DialogContent className={classes.formContainer}>
<Field name="name"
validate={COLLECTION_DESCRIPTION_VALIDATION}
className={classes.textField}
label="Description - optional"/>
+ <FileUpload
+ files={files}
+ disabled={busy}
+ onDrop={files => this.props.dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files))}/>
</DialogContent>
<DialogActions className={classes.dialogActions}>
<Button onClick={handleClose} className={classes.button} color="primary"
- disabled={submitting}>CANCEL</Button>
+ disabled={busy}>CANCEL</Button>
<Button type="submit"
className={classes.lastButton}
color="primary"
- disabled={invalid || submitting || pristine}
+ disabled={invalid || busy || pristine}
variant="contained">
CREATE A COLLECTION
</Button>
- {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
+ {busy && <CircularProgress size={20} className={classes.createProgress}/>}
</DialogActions>
</form>
</Dialog>
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-collection/create-collection-validator';
-import { COLLECTION_FORM_NAME } from '../../store/collections/updator/collection-updator-action';
+import { COLLECTION_FORM_NAME } from '../../store/collections/updater/collection-updater-action';
type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
/>
)
}
- );
\ No newline at end of file
+ );
import { SubmissionError } from "redux-form";
import { RootState } from "../../store/store";
import { snackbarActions } from "../../store/snackbar/snackbar-actions";
-import { collectionUpdatorActions, updateCollection } from "../../store/collections/updator/collection-updator-action";
+import { collectionUpdaterActions, updateCollection } from "../../store/collections/updater/collection-updater-action";
import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
import { DialogCollectionUpdate } from "../dialog-update/dialog-collection-update";
const mapStateToProps = (state: RootState) => ({
- open: state.collections.updator.opened
+ open: state.collections.updater.opened
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
handleClose: () => {
- dispatch(collectionUpdatorActions.CLOSE_COLLECTION_UPDATOR());
+ dispatch(collectionUpdaterActions.CLOSE_COLLECTION_UPDATER());
},
onSubmit: (data: { name: string, description: string }) => {
return dispatch<any>(editCollection(data))
});
};
-export const UpdateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionUpdate);
\ No newline at end of file
+export const UpdateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionUpdate);
"@types/node" "*"
"@types/react" "*"
+"@types/react-dropzone@4.2.1":
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/@types/react-dropzone/-/react-dropzone-4.2.1.tgz#4a973b63a8a227e263ff4eece053f643220f28fc"
+ dependencies:
+ "@types/react" "*"
+
"@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"
version "2.1.1"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
+attr-accept@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
+ dependencies:
+ core-js "^2.5.0"
+
autoprefixer@7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.6.tgz#fb933039f74af74a83e71225ce78d9fd58ba84d7"
object-assign "^4.1.1"
prop-types "^15.6.0"
+react-dropzone@4.2.13:
+ version "4.2.13"
+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-4.2.13.tgz#31393c079b4e5ddcc176c095cebc3545d1248b9d"
+ dependencies:
+ attr-accept "^1.0.3"
+ prop-types "^15.5.7"
+
react-error-overlay@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"