"@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}"`
const grey600 = grey["600"];
const grey700 = grey["700"];
const grey900 = grey["900"];
+const rocheBlue = '#06C';
const themeOptions: ArvadosThemeOptions = {
customs: {
},
palette: {
primary: {
- main: '#06C',
+ main: rocheBlue,
dark: blue.A100
}
}
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>)
+);
import Typography from '@material-ui/core/Typography';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
import { ArvadosTheme } from '../../common/custom-theme';
+import * as classnames from "classnames";
type CssRules = 'attribute' | 'label' | 'value' | 'link';
interface DetailsAttributeDataProps {
label: string;
+ classLabel?: string;
value?: string | number;
+ classValue?: string;
link?: string;
children?: React.ReactNode;
}
type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
-export const DetailsAttribute = withStyles(styles)(({ label, link, value, children, classes }: DetailsAttributeProps) =>
+export const DetailsAttribute = withStyles(styles)(({ label, link, value, children, classes, classLabel, classValue }: DetailsAttributeProps) =>
<Typography component="div" className={classes.attribute}>
- <Typography component="span" className={classes.label}>{label}</Typography>
+ <Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
{ link
? <a href={link} className={classes.link} target='_blank'>{value}</a>
- : <Typography component="span" className={classes.value}>
+ : <Typography component="span" className={classnames([classes.value, classValue])}>
{value}
{children}
</Typography> }
--- /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
}
export enum LinkClass {
- STAR = 'star'
+ STAR = 'star',
+ TAG = 'tag'
}
\ No newline at end of file
--- /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 { LinkResource } from "./link";
+
+export interface TagResource extends LinkResource {
+ tailUuid: TagTailType;
+ properties: TagProperty;
+}
+
+export interface TagProperty {
+ key: string;
+ value: string;
+}
+
+export enum TagTailType {
+ COLLECTION = 'Collection',
+ JOB = 'Job'
+}
\ No newline at end of file
// 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 { FavoriteService } from "./favorite-service/favorite-service";
import { AxiosInstance } from "axios";
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";
+import { KeepService } from "./keep-service/keep-service";
export interface ServiceRepository {
apiClient: AxiosInstance;
authService: AuthService;
+ keepService: KeepService;
groupsService: GroupsService;
projectService: ProjectService;
linkService: LinkService;
favoriteService: FavoriteService;
+ tagService: TagService;
collectionService: CollectionService;
collectionFilesService: CollectionFilesService;
}
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,
favoriteService,
collectionService,
+ tagService,
collectionFilesService
};
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { LinkService } from "../link-service/link-service";
+import { LinkClass } from "../../models/link";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { TagTailType, TagResource } from "../../models/tag";
+import { OrderBuilder } from "../../common/api/order-builder";
+
+export class TagService {
+
+ constructor(private linkService: LinkService) { }
+
+ create(uuid: string, data: { key: string; value: string } ) {
+ return this.linkService
+ .create({
+ headUuid: uuid,
+ tailUuid: TagTailType.COLLECTION,
+ linkClass: LinkClass.TAG,
+ name: '',
+ properties: data
+ })
+ .then(tag => tag as TagResource );
+ }
+
+ list(uuid: string) {
+ const filters = FilterBuilder
+ .create()
+ .addEqual("headUuid", uuid)
+ .addEqual("tailUuid", TagTailType.COLLECTION)
+ .addEqual("linkClass", LinkClass.TAG);
+
+ const order = OrderBuilder
+ .create<TagResource>()
+ .addAsc('createdAt');
+
+ return this.linkService
+ .list({ filters, order })
+ .then(results => {
+ return results.items.map((tag => tag as TagResource ));
+ });
+ }
+
+}
import { createTree } from "../../models/tree";
import { RootState } from "../store";
import { ServiceRepository } from "../../services/services";
+import { TagResource, TagProperty } from "../../models/tag";
+import { snackbarActions } from "../snackbar/snackbar-actions";
export const collectionPanelActions = unionize({
LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
- LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>()
+ LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
+ LOAD_COLLECTION_TAGS: ofType<{ uuid: string }>(),
+ LOAD_COLLECTION_TAGS_SUCCESS: ofType<{ tags: TagResource[] }>(),
+ CREATE_COLLECTION_TAG: ofType<{ data: any }>(),
+ CREATE_COLLECTION_TAG_SUCCESS: ofType<{ tag: TagResource }>(),
+ DELETE_COLLECTION_TAG: ofType<{ uuid: string }>(),
+ DELETE_COLLECTION_TAG_SUCCESS: ofType<{ uuid: string }>()
}, { tag: 'type', value: 'payload' });
export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
+export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
+
export const loadCollection = (uuid: string, kind: ResourceKind) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
});
};
+export const loadCollectionTags = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION_TAGS({ uuid }));
+ return services.tagService
+ .list(uuid)
+ .then(tags => {
+ dispatch(collectionPanelActions.LOAD_COLLECTION_TAGS_SUCCESS({ tags }));
+ });
+ };
+
+export const createCollectionTag = (data: TagProperty) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(collectionPanelActions.CREATE_COLLECTION_TAG({ data }));
+ const item = getState().collectionPanel.item;
+ const uuid = item ? item.uuid : '';
+ return services.tagService
+ .create(uuid, data)
+ .then(tag => {
+ dispatch(collectionPanelActions.CREATE_COLLECTION_TAG_SUCCESS({ tag }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Tag has been successfully added.",
+ hideDuration: 2000
+ }));
+ });
+ };
+export const deleteCollectionTag = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(collectionPanelActions.DELETE_COLLECTION_TAG({ uuid }));
+ return services.linkService
+ .delete(uuid)
+ .then(tag => {
+ dispatch(collectionPanelActions.DELETE_COLLECTION_TAG_SUCCESS({ uuid: tag.uuid }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Tag has been successfully deleted.",
+ hideDuration: 2000
+ }));
+ });
+ };
\ 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
});
import { collectionPanelActions, CollectionPanelAction } from "./collection-panel-action";
import { CollectionResource } from "../../models/collection";
+import { TagResource } from "../../models/tag";
export interface CollectionPanelState {
item: CollectionResource | null;
+ tags: TagResource[];
}
const initialState = {
- item: null
+ item: null,
+ tags: []
};
export const collectionPanelReducer = (state: CollectionPanelState = initialState, action: CollectionPanelAction) =>
collectionPanelActions.match(action, {
default: () => state,
- LOAD_COLLECTION: () => state,
LOAD_COLLECTION_SUCCESS: ({ item }) => ({ ...state, item }),
+ LOAD_COLLECTION_TAGS_SUCCESS: ({ tags }) => ({...state, tags }),
+ CREATE_COLLECTION_TAG_SUCCESS: ({ tag }) => ({...state, tags: [...state.tags, tag] }),
+ DELETE_COLLECTION_TAG_SUCCESS: ({ uuid }) => ({...state, tags: state.tags.filter(tag => tag.uuid !== uuid) })
});
// 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
+ });
+};
import { DataExplorerMiddlewareService } from "./data-explorer-middleware-service";
export const dataExplorerMiddleware = (service: DataExplorerMiddlewareService): Middleware => api => next => {
- const handleAction = <T extends { id: string }>(handler: (data: T) => void) =>
- (data: T) => {
- if (data.id === service.getId()) {
- handler(data);
- }
- };
const actions = bindDataExplorerActions(service.getId());
return action => {
+ const handleAction = <T extends { id: string }>(handler: (data: T) => void) =>
+ (data: T) => {
+ next(action);
+ if (data.id === service.getId()) {
+ handler(data);
+ }
+ };
dataExplorerActions.match(action, {
SET_PAGE: handleAction(() => {
api.dispatch(actions.REQUEST_ITEMS());
: order.addAsc("name")
: order,
filters: FilterBuilder
- .create<LinkResource>()
+ .create()
.addIsA("headUuid", typeFilters.map(filter => filter.type))
.addILike("name", dataExplorer.searchValue)
})
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);
+ }
+};
.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 }));
--- /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
--- /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 TAG_KEY_VALIDATION = [require, maxLength(255)];
+export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
\ No newline at end of file
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 }));
});
};
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());
import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
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';
+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[];
}
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 { 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_FORM_NAME } from '../../store/collections/updator/collection-updator-action';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
+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
+ );
const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
const filters = FilterBuilder
- .create<ProjectResource>()
+ .create()
.addEqual('ownerUuid', ownerUuid);
const { items } = await services.projectService.list({ filters });
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);
StyleRulesCallback, WithStyles, withStyles, Card,
CardHeader, IconButton, CardContent, Grid, Chip
} from '@material-ui/core';
-import { connect } from 'react-redux';
+import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
import { ArvadosTheme } from '../../common/custom-theme';
import { RootState } from '../../store/store';
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';
+import { deleteCollectionTag } from '../../store/collection-panel/collection-panel-action';
-type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon';
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'value';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
- marginBottom: '20px'
+ marginBottom: theme.spacing.unit * 2
},
iconHeader: {
fontSize: '1.875rem',
color: theme.customs.colors.yellow700
},
tag: {
- marginRight: theme.spacing.unit
+ marginRight: theme.spacing.unit,
+ marginBottom: theme.spacing.unit
},
copyIcon: {
marginLeft: theme.spacing.unit,
fontSize: '1.125rem',
+ color: theme.palette.grey["500"],
cursor: 'pointer'
+ },
+ value: {
+ textTransform: 'none'
}
});
interface CollectionPanelDataProps {
item: CollectionResource;
+ tags: TagResource[];
}
interface CollectionPanelActionProps {
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: CollectionResource) => void;
}
-type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps
+type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps & DispatchProp
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const CollectionPanel = withStyles(styles)(
- connect((state: RootState) => ({ item: state.collectionPanel.item! }))(
- class extends React.Component<CollectionPanelProps> {
+ connect((state: RootState) => ({
+ item: state.collectionPanel.item,
+ tags: state.collectionPanel.tags
+ }))(
+ class extends React.Component<CollectionPanelProps> {
render() {
- const { classes, item, onContextMenu } = this.props;
+ const { classes, item, tags, onContextMenu } = this.props;
return <div>
<Card className={classes.card}>
<CardHeader
<CardContent>
<Grid container direction="column">
<Grid item xs={6}>
- <DetailsAttribute label='Collection UUID' value={item && item.uuid}>
+ <DetailsAttribute classValue={classes.value}
+ label='Collection UUID'
+ value={item && item.uuid}>
<CopyToClipboard text={item && item.uuid}>
<CopyIcon className={classes.copyIcon} />
</CopyToClipboard>
</DetailsAttribute>
+ <DetailsAttribute label='Number of files' value='14' />
<DetailsAttribute label='Content size' value='54 MB' />
- <DetailsAttribute label='Owner' value={item && item.ownerUuid} />
+ <DetailsAttribute classValue={classes.value} label='Owner' value={item && item.ownerUuid} />
</Grid>
</Grid>
</CardContent>
<CardHeader title="Tags" />
<CardContent>
<Grid container direction="column">
- <Grid item xs={4}>
- <Chip label="Tag 1" className={classes.tag}/>
- <Chip label="Tag 2" className={classes.tag}/>
- <Chip label="Tag 3" className={classes.tag}/>
+ <Grid item xs={12}><CollectionTagForm /></Grid>
+ <Grid item xs={12}>
+ {
+ tags.map(tag => {
+ return <Chip key={tag.etag} className={classes.tag}
+ onDelete={this.handleDelete(tag.uuid)}
+ label={renderTagLabel(tag)} />;
+ })
+ }
</Grid>
</Grid>
</CardContent>
</div>;
}
+ handleDelete = (uuid: string) => () => {
+ this.props.dispatch<any>(deleteCollectionTag(uuid));
+ }
+
componentWillReceiveProps({ match, item, onItemRouteChange }: CollectionPanelProps) {
if (!item || match.params.id !== item.uuid) {
onItemRouteChange(match.params.id);
}
)
);
+
+const renderTagLabel = (tag: TagResource) => {
+ const { properties } = tag;
+ return `${properties.key}: ${properties.value}`;
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field, reset } from 'redux-form';
+import { compose, Dispatch } from 'redux';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { StyleRulesCallback, withStyles, WithStyles, TextField, Button, CircularProgress } from '@material-ui/core';
+import { TagProperty } from '../../models/tag';
+import { createCollectionTag, COLLECTION_TAG_FORM_NAME } from '../../store/collection-panel/collection-panel-action';
+import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '../../validators/validators';
+
+type CssRules = 'form' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ form: {
+ marginBottom: theme.spacing.unit * 4
+ },
+ textField: {
+ marginRight: theme.spacing.unit
+ },
+ buttonWrapper: {
+ position: 'relative',
+ display: 'inline-block'
+ },
+ saveButton: {
+ boxShadow: 'none'
+ },
+ circularProgress: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ margin: 'auto'
+ }
+});
+
+interface CollectionTagFormDataProps {
+ submitting: boolean;
+ invalid: boolean;
+ pristine: boolean;
+}
+
+interface CollectionTagFormActionProps {
+ handleSubmit: any;
+}
+
+interface TextFieldProps {
+ label: string;
+ floatinglabeltext: string;
+ className?: string;
+ input?: string;
+ meta?: any;
+}
+
+type CollectionTagFormProps = CollectionTagFormDataProps & CollectionTagFormActionProps & WithStyles<CssRules>;
+
+export const CollectionTagForm = compose(
+ reduxForm({
+ form: COLLECTION_TAG_FORM_NAME,
+ onSubmit: (data: TagProperty, dispatch: Dispatch) => {
+ dispatch<any>(createCollectionTag(data));
+ dispatch(reset(COLLECTION_TAG_FORM_NAME));
+ }
+ }),
+ withStyles(styles))(
+
+ class CollectionTagForm extends React.Component<CollectionTagFormProps> {
+
+ render() {
+ const { classes, submitting, pristine, invalid, handleSubmit } = this.props;
+ return (
+ <form className={classes.form} onSubmit={handleSubmit}>
+ <Field name="key"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Key"
+ validate={TAG_KEY_VALIDATION}
+ className={classes.textField}
+ label="Key" />
+ <Field name="value"
+ disabled={submitting}
+ component={this.renderTextField}
+ floatinglabeltext="Value"
+ validate={TAG_VALUE_VALIDATION}
+ className={classes.textField}
+ label="Value" />
+ <div className={classes.buttonWrapper}>
+ <Button type="submit" className={classes.saveButton}
+ color="primary"
+ size='small'
+ disabled={invalid || submitting || pristine}
+ variant="contained">
+ ADD
+ </Button>
+ {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+ </div>
+ </form>
+ );
+ }
+
+ renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
+ <TextField
+ helperText={touched && error}
+ label={label}
+ className={this.props.classes.textField}
+ error={touched && !!error}
+ autoComplete='off'
+ {...input}
+ {...custom}
+ />
+ )
+
+ }
+
+ );
\ 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 { 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 } from '../../store/collection-panel/collection-panel-action';
+import { loadCollection, loadCollectionTags } from '../../store/collection-panel/collection-panel-action';
import { getCollectionUrl } from '../../models/collection';
import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
import { AuthService } from "../../services/auth-service/auth-service";
);
}
- renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
- onItemRouteChange={(collectionId) => this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION))}
+ renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
+ onItemRouteChange={(collectionId) => {
+ this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION));
+ this.props.dispatch<any>(loadCollectionTags(collectionId));
+ }}
onContextMenu={(event, item) => {
this.openContextMenu(event, {
uuid: item.uuid,
"@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"