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}"`
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { Grid, StyleRulesCallback, Typography, WithStyles } from '@material-ui/core';
+import { Grid, List, ListItem, ListItemText, StyleRulesCallback, Typography, WithStyles } from '@material-ui/core';
import { withStyles } from '@material-ui/core';
import Dropzone from 'react-dropzone';
import { CloudUploadIcon } from "../icon/icon";
+import { formatFileSize } from "../../common/formatters";
type CssRules = "root" | "dropzone" | "container" | "uploadIcon";
},
dropzone: {
width: "100%",
- height: "100px",
+ height: "200px",
+ overflow: "auto",
border: "1px dashed black",
borderRadius: "5px"
},
});
interface FileUploadProps {
+ files: File[];
+ onDrop: (files: File[]) => void;
}
export const FileUpload = withStyles(styles)(
- ({ classes }: FileUploadProps & WithStyles<CssRules>) =>
+ ({ classes, files, onDrop }: FileUploadProps & WithStyles<CssRules>) =>
<Grid container direction={"column"}>
<Typography variant={"subheading"}>
Upload data
</Typography>
- <Dropzone className={classes.dropzone}>
- <Grid container justify="center" alignItems="center" className={classes.container}>
+ <Dropzone className={classes.dropzone} onDrop={files => onDrop(files)}>
+ <Grid container justify="center" alignItems="center" className={classes.container} direction={"column"}>
<Grid item component={"span"}>
<Typography variant={"subheading"}>
<CloudUploadIcon className={classes.uploadIcon}/> Drag and drop data or <a>browse</a>
</Typography>
</Grid>
+ <Grid item>
+ <List>
+ {files.map((f, idx) =>
+ <ListItem button key={idx}>
+ <ListItemText
+ primary={f.name} primaryTypographyProps={{variant: "body2"}}
+ secondary={formatFileSize(f.size)}/>
+ </ListItem>)}
+ </List>
+ </Grid>
</Grid>
</Dropzone>
</Grid>
type: CollectionFileType.FILE;
}
+export interface CollectionUploadFile {
+ name: string;
+}
+
export const createCollectionDirectory = (data: Partial<CollectionDirectory>): CollectionDirectory => ({
id: '',
name: '',
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
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";
export class CollectionService extends CommonResourceService<CollectionResource> {
- constructor(serverApi: AxiosInstance) {
+ constructor(serverApi: AxiosInstance, private keepService: KeepService) {
super(serverApi, "collections");
}
-}
\ No newline at end of file
+
+ uploadFiles(files: File[]) {
+ console.log("Uploading files", files);
+
+ const fd = new FormData();
+ fd.append("filters", `[["service_type","=","proxy"]]`);
+ fd.append("_method", "GET");
+
+ const filters = new FilterBuilder();
+ filters.addEqual("service_type", "proxy");
+
+ return this.keepService.list({ filters }).then(data => {
+ console.log(data);
+
+ const serviceHost = (data.items[0].serviceSslFlag ? "https://" : "http://") + data.items[0].serviceHost + ":" + data.items[0].servicePort;
+ console.log("Servicehost", serviceHost);
+
+ const fd = new FormData();
+ files.forEach((f, idx) => fd.append(`file_${idx}`, f));
+
+ axios.post(serviceHost, fd, {
+ onUploadProgress: (e: ProgressEvent) => {
+ console.log(`${e.loaded} / ${e.total}`);
+ }
+ });
+ });
+ }
+}
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 { CollectionService } from "./collection-service/collection-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 collectionFilesService = new CollectionFilesService(collectionService);
return {
apiClient,
authService,
+ keepService,
groupsService,
projectService,
linkService,
// SPDX-License-Identifier: AGPL-3.0
import { combineReducers } from 'redux';
-import * as creator from "./creator/collection-creator-reducer";
-import * as updater from "./updater/collection-updater-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;
- updater: updater.CollectionUpdaterState;
+ creator: CollectionCreatorState;
+ updater: CollectionUpdaterState;
+ uploader: CollectionUploaderState
};
export const collectionsReducer = combineReducers({
- creator: creator.collectionCreatorReducer,
- updater: updater.collectionUpdaterReducer
+ creator: collectionCreatorReducer,
+ updater: collectionUpdaterReducer,
+ uploader: collectionUploaderReducer
});
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
});
--- /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 const collectionUploaderActions = unionize({\r
+ SET_UPLOAD_FILES: ofType<File[]>(),\r
+ START_UPLOADING: ofType<{}>(),\r
+ UPDATE_UPLOAD_PROGRESS: 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 } from "./collection-uploader-actions";
+import { CollectionUploadFile } from "../../../models/collection-file";
+
+export interface CollectionUploaderState {
+ files: File[];
+}
+
+const initialState: CollectionUploaderState = {
+ files: []
+};
+
+export const collectionUploaderReducer = (state: CollectionUploaderState = initialState, action: CollectionUploaderAction) => {
+ return collectionUploaderActions.match(action, {
+ SET_UPLOAD_FILES: (files) => ({
+ ...state,
+ files
+ }),
+ 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 { 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 { ServiceRepository } from "../../services/services";
const mapStateToProps = (state: RootState) => ({
open: state.collections.creator.opened
handleClose: () => {
dispatch(collectionCreateActions.CLOSE_COLLECTION_CREATOR());
},
- onSubmit: (data: { name: string, description: string }) => {
+ onSubmit: (data: { name: string, description: string, files: File[] }) => {
return dispatch<any>(addCollection(data))
.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 }) =>
- (dispatch: Dispatch) => {
+const addCollection = (data: { name: string, description: string, files: File[] }) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
return dispatch<any>(createCollection(data)).then(() => {
dispatch(snackbarActions.OPEN_SNACKBAR({
message: "Collection has been successfully created.",
hideDuration: 2000
}));
- dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ services.collectionService.uploadFiles(data.files).then(() => {
+ dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+ });
});
};
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
import { FileUpload } from "../../components/file-upload/file-upload";
+import { connect, DispatchProp } from "react-redux";
+import { RootState } from "../../store/store";
+import { collectionUploaderActions } 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: File[] }) => void;
handleSubmit: any;
submitting: boolean;
invalid: boolean;
pristine: boolean;
+ files: File[];
}
interface TextFieldProps {
}
export const DialogCollectionCreate = compose(
+ connect((state: RootState) => ({
+ files: state.collections.uploader.files
+ })),
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;
maxWidth='sm'
disableBackdropClick={true}
disableEscapeKeyDown={true}>
- <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+ <form onSubmit={handleSubmit((data: any) => onSubmit({ ...data, files: this.props.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/>
+ <FileUpload
+ files={this.props.files}
+ onDrop={files => this.props.dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files))}/>
</DialogContent>
<DialogActions className={classes.dialogActions}>
<Button onClick={handleClose} className={classes.button} color="primary"