etag: string;
}
+export interface TrashResource extends Resource {
+ trashAt: string;
+ deleteAt: string;
+ isTrashed: boolean;
+}
+
export enum ResourceKind {
COLLECTION = "arvados#collection",
+ CONTAINER = "arvados#container",
+ CONTAINER_REQUEST = "arvados#containerRequest",
GROUP = "arvados#group",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
return Promise.reject();
}
- async deleteFile(collectionUuid: string, filePath: string) {
- return this.webdavClient.delete(`/c=${collectionUuid}${filePath}`);
- }
-
- extractFilesData(document: Document) {
- const collectionUrlPrefix = /\/c=[0-9a-zA-Z\-]*/;
- return Array
- .from(document.getElementsByTagName('D:response'))
- .slice(1) // omit first element which is collection itself
- .map(element => {
- const name = getTagValue(element, 'D:displayname', '');
- const size = parseInt(getTagValue(element, 'D:getcontentlength', '0'), 10);
- const pathname = getTagValue(element, 'D:href', '');
- const nameSuffix = `/${name || ''}`;
- const directory = pathname
- .replace(collectionUrlPrefix, '')
- .replace(nameSuffix, '');
- const href = this.webdavClient.defaults.baseURL + pathname + '?api_token=' + this.authService.getApiToken();
-
- const data = {
- url: href,
- id: `${directory}/${name}`,
- name,
- path: directory,
- };
-
- return getTagValue(element, 'D:resourcetype', '')
- ? createCollectionDirectory(data)
- : createCollectionFile({ ...data, size });
-
- });
+ async deleteFiles(collectionUuid: string, filePaths: string[]) {
+ for (const path of filePaths) {
+ await this.webdavClient.delete(`c=${collectionUuid}${path}`);
+ }
}
- 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);
- });
+ async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress) {
+ // files have to be uploaded sequentially
+ for (let idx = 0; idx < files.length; idx++) {
+ await this.uploadFile(collectionUuid, files[idx], idx, onProgress);
+ }
}
- private 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
- }));
- });
+ moveFile(collectionUuid: string, oldPath: string, newPath: string) {
+ return this.webdavClient.move(
+ `c=${collectionUuid}${oldPath}`,
+ `c=${collectionUuid}${encodeURI(newPath)}`
+ );
}
- private async updateManifest(collectionUuid: string, files: CollectionFile[]): Promise<CollectionResource> {
- const collection = await this.get(collectionUuid);
- const manifest: KeepManifestStream[] = parseKeepManifestText(collection.manifestText);
-
- files.forEach(f => {
- let kms = manifest.find(stream => stream.name === f.path);
- if (!kms) {
- kms = {
- files: [],
- locators: [],
- name: f.path
- };
- manifest.push(kms);
+ private extendFileURL = (file: CollectionDirectory | CollectionFile) => ({
+ ...file,
+ url: this.webdavClient.defaults.baseURL + file.url + '?api_token=' + this.authService.getApiToken()
+ })
+
+ private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }) {
+ const fileURL = `c=${collectionUuid}/${file.name}`;
+ const fileContent = await fileToArrayBuffer(file);
+ const requestConfig = {
+ headers: {
+ 'Content-Type': 'text/octet-stream'
+ },
+ onUploadProgress: (e: ProgressEvent) => {
+ onProgress(fileId, e.loaded, e.total, Date.now());
}
- kms.locators.push(f.id);
- const len = kms.files.length;
- const nextPos = len > 0
- ? parseInt(kms.files[len - 1].position, 10) + kms.files[len - 1].size
- : 0;
- kms.files.push({
- name: f.name,
- position: nextPos.toString(),
- size: f.size
- });
- });
-
- console.log(manifest);
-
- const manifestText = stringifyKeepManifest(manifest);
- const data = { ...collection, manifestText };
- return this.update(collectionUuid, CommonResourceService.mapKeys(_.snakeCase)(data));
- }
-
- uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress): Promise<CollectionResource | never> {
- const filters = new FilterBuilder()
- .addEqual("service_type", "proxy");
-
- return this.keepService.list({ filters: filters.getFilters() }).then(data => {
- if (data.items && data.items.length > 0) {
- const serviceHost =
- (data.items[0].serviceSslFlag ? "https://" : "http://") +
- data.items[0].serviceHost +
- ":" + data.items[0].servicePort;
-
- console.log("serviceHost", serviceHost);
+ };
+ return this.webdavClient.put(fileURL, fileContent, requestConfig);
- 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");
- }
- });
}
-
+ trash(uuid: string): Promise<CollectionResource> {
+ return this.serverApi
+ .post(this.resourceType + `${uuid}/trash`)
+ .then(CommonResourceService.mapResponseKeys);
+ }
+
+ untrash(uuid: string): Promise<CollectionResource> {
+ const params = {
+ ensure_unique_name: true
+ };
+ return this.serverApi
+ .post(this.resourceType + `${uuid}/untrash`, {
+ params: CommonResourceService.mapKeys(_.snakeCase)(params)
+ })
+ .then(CommonResourceService.mapResponseKeys);
+ }
++
}
//
// SPDX-License-Identifier: AGPL-3.0
- import { default as unionize, ofType, UnionOf } from "unionize";
+ import { unionize, ofType, UnionOf } from '~/common/unionize';
import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
-import { UserResource } from '../../models/user';
+ import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+ import { Dispatch } from 'redux';
+ import { RootState } from '~/store/store';
+ import { getResource } from '../resources/resources';
+ import { ProjectResource } from '~/models/project';
++import { UserResource } from '~/models/user';
+ import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+ import { extractUuidKind, ResourceKind } from '~/models/resource';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
CLOSE_CONTEXT_MENU: ofType<{}>()
- }, {
- tag: 'type',
- value: 'payload'
- });
+ });
export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
-export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) =>
+
- const userResource = getResource<UserResource>(projectUuid)(getState().resources);
- if (userResource) {
++export type ContextMenuResource = {
++ name: string;
++ uuid: string;
++ ownerUuid: string;
++ description?: string;
++ kind: ContextMenuKind;
++ isTrashed?: boolean;
++}
++
++export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) =>
+ (dispatch: Dispatch) => {
+ event.preventDefault();
+ dispatch(
+ contextMenuActions.OPEN_CONTEXT_MENU({
+ position: { x: event.clientX, y: event.clientY },
+ resource
+ })
+ );
+ };
+
+ export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
- uuid: userResource.uuid,
- kind: ContextMenuKind.ROOT_PROJECT
++ const res = getResource<UserResource>(projectUuid)(getState().resources);
++ if (res) {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
- const projectResource = getResource<ProjectResource>(projectUuid)(getState().resources);
- if (projectResource) {
++ uuid: res.uuid,
++ ownerUuid: res.uuid,
++ kind: ContextMenuKind.ROOT_PROJECT,
++ isTrashed: false
+ }));
+ }
+ };
+
+ export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
- name: projectResource.name,
- uuid: projectResource.uuid,
- kind: ContextMenuKind.PROJECT
++ const res = getResource<ProjectResource>(projectUuid)(getState().resources);
++ if (res) {
+ dispatch<any>(openContextMenu(event, {
++ name: res.name,
++ uuid: res.uuid,
++ kind: ContextMenuKind.PROJECT,
++ ownerUuid: res.ownerUuid,
++ isTrashed: res.isTrashed
+ }));
+ }
+ };
+
+ export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ if (!isSidePanelTreeCategory(id)) {
+ const kind = extractUuidKind(id);
+ if (kind === ResourceKind.USER) {
+ dispatch<any>(openRootProjectContextMenu(event, id));
+ } else if (kind === ResourceKind.PROJECT) {
+ dispatch<any>(openProjectContextMenu(event, id));
+ }
+ }
+ };
+
+ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ const resource = {
+ uuid: '',
+ name: '',
+ description: '',
+ kind: ContextMenuKind.PROCESS
+ };
+ dispatch<any>(openContextMenu(event, resource));
+ };
+
+ export const resourceKindToContextMenuKind = (uuid: string) => {
+ const kind = extractUuidKind(uuid);
+ switch (kind) {
+ case ResourceKind.PROJECT:
+ return ContextMenuKind.PROJECT;
+ case ResourceKind.COLLECTION:
+ return ContextMenuKind.COLLECTION_RESOURCE;
+ case ResourceKind.USER:
+ return ContextMenuKind.ROOT_PROJECT;
+ default:
+ return;
+ }
+ };
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 { openUpdater } from "~/store/collections/updater/collection-updater-action";
+ import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+ import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
+ import { openCollectionCopyDialog } from "~/store/collections/collection-copy-actions";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleCollectionTrashed } from "~/store/collections/collection-trash-actions";
export const collectionActionSet: ContextMenuActionSet = [[
{
import { ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
++import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
import { toggleFavorite } from "~/store/favorites/favorites-actions";
import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
- import { openUpdater } from "~/store/collections/updater/collection-updater-action";
+ import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
- import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
- import { toggleCollectionTrashed } from "~/store/collections/collection-trash-actions";
+ import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
+ import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
export const collectionResourceActionSet: ContextMenuActionSet = [[
{
export const CollectionPanel = withStyles(styles)(
- connect((state: RootState) => ({
- item: state.collectionPanel.item,
- tags: state.collectionPanel.tags
- }))(
+ connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
+ const collection = getResource(props.match.params.id)(state.resources);
+ return {
+ item: collection,
+ tags: state.collectionPanel.tags
+ };
+ })(
class extends React.Component<CollectionPanelProps> {
-
render() {
- const { classes, item, tags, onContextMenu } = this.props;
+ const { classes, item, tags } = this.props;
return <div>
- <Card className={classes.card}>
- <CardHeader
- avatar={ <CollectionIcon className={classes.iconHeader} /> }
- action={
- <IconButton
- aria-label="More options"
- onClick={event => onContextMenu(event, item)}>
- <MoreOptionsIcon />
- </IconButton>
- }
- title={item && item.name }
- subheader={item && item.description} />
- <CardContent>
- <Grid container direction="column">
- <Grid item xs={6}>
- <DetailsAttribute classValue={classes.value}
- label='Collection UUID'
- value={item && item.uuid}>
- <CopyToClipboard text={item && item.uuid}>
- <CopyIcon className={classes.copyIcon} />
- </CopyToClipboard>
+ <Card className={classes.card}>
+ <CardHeader
+ avatar={<CollectionIcon className={classes.iconHeader} />}
+ action={
+ <IconButton
+ aria-label="More options"
+ onClick={this.handleContextMenu}>
+ <MoreOptionsIcon />
+ </IconButton>
+ }
+ title={item && item.name}
+ subheader={item && item.description} />
+ <CardContent>
+ <Grid container direction="column">
+ <Grid item xs={6}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Collection UUID'
+ value={item && item.uuid}>
+ <Tooltip title="Copy uuid">
+ <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy()}>
+ <CopyIcon className={classes.copyIcon} />
+ </CopyToClipboard>
+ </Tooltip>
</DetailsAttribute>
- <DetailsAttribute label='Number of files' value='14' />
- <DetailsAttribute label='Content size' value='54 MB' />
- <DetailsAttribute classValue={classes.value} label='Owner' value={item && item.ownerUuid} />
- </Grid>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Number of files' value='14' />
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Content size' value='54 MB' />
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Owner' value={item && item.ownerUuid} />
</Grid>
- </CardContent>
- </Card>
+ </Grid>
+ </CardContent>
+ </Card>
- <Card className={classes.card}>
- <CardHeader title="Properties" />
- <CardContent>
- <Grid container direction="column">
- <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>
+ <Card className={classes.card}>
+ <CardHeader title="Properties" />
+ <CardContent>
+ <Grid container direction="column">
+ <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>
- </CardContent>
- </Card>
- <div className={classes.card}>
- <CollectionPanelFiles/>
- </div>
- </div>;
+ </Grid>
+ </CardContent>
+ </Card>
+ <div className={classes.card}>
+ <CollectionPanelFiles />
+ </div>
+ </div>;
+ }
+
+ handleContextMenu = (event: React.MouseEvent<any>) => {
+ const { uuid, name, description } = this.props.item;
+ const resource = {
+ uuid,
+ name,
+ description,
+ kind: ContextMenuKind.COLLECTION
+ };
+ this.props.dispatch<any>(openContextMenu(event, resource));
}
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);
- }
+ onCopy = () => {
+ this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Uuid has been copied",
+ hideDuration: 2000
+ }));
}
-
}
)
);
import { DispatchProp, connect } from 'react-redux';
import { DataColumns } from '~/components/data-table/data-table';
import { RouteComponentProps } from 'react-router';
- import { RootState } from '~/store/store';
import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
-import { ContainerRequestState } from '~/models/container-request';
+import { ProcessState } from '~/models/process';
import { SortDirection } from '~/components/data-table/data-column';
import { ResourceKind } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
}
export interface FavoritePanelFilter extends DataTableFilterItem {
- type: ResourceKind | ContainerRequestState;
+ type: ResourceKind | ProcessState;
}
- export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
+ export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
{
name: FavoritePanelColumnNames.NAME,
selected: true,
sortDirection: SortDirection.NONE,
filters: [
{
- name: ContainerRequestState.COMMITTED,
+ name: ProcessState.COMMITTED,
selected: true,
- type: ContainerRequestState.COMMITTED
+ type: ProcessState.COMMITTED
},
{
- name: ContainerRequestState.FINAL,
+ name: ProcessState.FINAL,
selected: true,
- type: ContainerRequestState.FINAL
+ type: ProcessState.FINAL
},
{
- name: ContainerRequestState.UNCOMMITTED,
+ name: ProcessState.UNCOMMITTED,
selected: true,
- type: ContainerRequestState.UNCOMMITTED
+ type: ProcessState.UNCOMMITTED
}
],
- render: renderStatus,
+ render: uuid => <ProcessStatus uuid={uuid} />,
width: "75px"
},
{
}
export interface ProjectPanelFilter extends DataTableFilterItem {
- type: ResourceKind | ContainerRequestState;
+ type: ResourceKind | ProcessState;
}
- export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
+ export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
{
name: ProjectPanelColumnNames.NAME,
selected: true,
sortDirection: SortDirection.NONE,
filters: [
{
- name: ContainerRequestState.COMMITTED,
+ name: ProcessState.COMMITTED,
selected: true,
- type: ContainerRequestState.COMMITTED
+ type: ProcessState.COMMITTED
},
{
- name: ContainerRequestState.FINAL,
+ name: ProcessState.FINAL,
selected: true,
- type: ContainerRequestState.FINAL
+ type: ProcessState.FINAL
},
{
- name: ContainerRequestState.UNCOMMITTED,
+ name: ProcessState.UNCOMMITTED,
selected: true,
- type: ContainerRequestState.UNCOMMITTED
+ type: ProcessState.UNCOMMITTED
}
],
- render: renderStatus,
+ render: uuid => <ProcessStatus uuid={uuid} />,
width: "75px"
},
{
import { RenameFileDialog } from '~/views-components/rename-file-dialog/rename-file-dialog';
import { FileRemoveDialog } from '~/views-components/file-remove-dialog/file-remove-dialog';
import { MultipleFilesRemoveDialog } from '~/views-components/file-remove-dialog/multiple-files-remove-dialog';
- import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
- import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
- import { PROJECT_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-project-create';
+ import { Routes } from '~/routes/routes';
+ import { SidePanel } from '~/views-components/side-panel/side-panel';
+ import { ProcessPanel } from '~/views/process-panel/process-panel';
+ import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
+ import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
+ import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
+ import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
+ import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
+ import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
+ import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
+ import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-collection-dialog';
+
+ import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
+ import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
+
+import { TrashPanel } from "~/views/trash-panel/trash-panel";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+
- const DRAWER_WITDH = 240;
const APP_BAR_HEIGHT = 100;
- type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
+ type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
<main className={classes.contentWrapper}>
<div className={classes.content}>
<Switch>
- <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
- <Route path="/projects/:id" render={this.renderProjectPanel} />
- <Route path="/favorites" render={this.renderFavoritePanel} />
+ <Route path={Routes.PROJECTS} component={ProjectPanel} />
+ <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+ <Route path={Routes.FAVORITES} component={FavoritePanel} />
+ <Route path={Routes.PROCESSES} component={ProcessPanel} />
+ <Route path="/trash" render={this.renderTrashPanel} />
- <Route path="/collections/:id" render={this.renderCollectionPanel} />
</Switch>
</div>
{user && <DetailsPanel />}