From: Daniel Kos Date: Thu, 30 Aug 2018 17:59:36 +0000 (+0200) Subject: refs #master Merge branch 'origin/master' into 13828-trash-view X-Git-Tag: 1.3.0~121^2~10 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/f63f3a5360ae6381d4b332bf86ef52b4e22107fb refs #master Merge branch 'origin/master' into 13828-trash-view # Conflicts: # package.json # src/models/container-request.ts # src/models/resource.ts # src/services/collection-service/collection-service.ts # src/services/services.ts # src/store/navigation/navigation-action.ts # src/store/project/project-action.ts # src/store/project/project-reducer.test.ts # src/store/project/project-reducer.ts # src/store/side-panel/side-panel-reducer.ts # src/store/store.ts # src/views-components/context-menu/action-sets/collection-action-set.ts # src/views-components/context-menu/action-sets/collection-resource-action-set.ts # src/views-components/context-menu/action-sets/project-action-set.ts # src/views/favorite-panel/favorite-panel-item.ts # src/views/project-panel/project-panel-item.ts # src/views/workbench/workbench.tsx Arvados-DCO-1.1-Signed-off-by: Daniel Kos --- f63f3a5360ae6381d4b332bf86ef52b4e22107fb diff --cc src/models/resource.ts index ab487da0,3290bdfe..aff1b241 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@@ -14,14 -14,10 +14,16 @@@ export interface Resource 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", diff --cc src/services/collection-service/collection-service.ts index ad493b5a,c0d61bd2..e26da788 --- a/src/services/collection-service/collection-service.ts +++ b/src/services/collection-service/collection-service.ts @@@ -43,141 -28,44 +28,61 @@@ export class CollectionService extends 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 { - return new Promise(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 { - return this.readFile(file).then(content => { - return axios.post(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 { - 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 { - 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 { + return this.serverApi + .post(this.resourceType + `${uuid}/trash`) + .then(CommonResourceService.mapResponseKeys); + } + + untrash(uuid: string): Promise { + const params = { + ensure_unique_name: true + }; + return this.serverApi + .post(this.resourceType + `${uuid}/untrash`, { + params: CommonResourceService.mapKeys(_.snakeCase)(params) + }) + .then(CommonResourceService.mapResponseKeys); + } - ++ } diff --cc src/store/context-menu/context-menu-actions.ts index 8e5eb1e7,cf66a53d..a1ed6c55 --- a/src/store/context-menu/context-menu-actions.ts +++ b/src/store/context-menu/context-menu-actions.ts @@@ -2,15 -2,92 +2,105 @@@ // // 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 { 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 { 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; + -export const openContextMenu = (event: React.MouseEvent, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) => ++export type ContextMenuResource = { ++ name: string; ++ uuid: string; ++ ownerUuid: string; ++ description?: string; ++ kind: ContextMenuKind; ++ isTrashed?: boolean; ++} ++ ++export const openContextMenu = (event: React.MouseEvent, 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, projectUuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { - const userResource = getResource(projectUuid)(getState().resources); - if (userResource) { ++ const res = getResource(projectUuid)(getState().resources); ++ if (res) { + dispatch(openContextMenu(event, { + name: '', - uuid: userResource.uuid, - kind: ContextMenuKind.ROOT_PROJECT ++ uuid: res.uuid, ++ ownerUuid: res.uuid, ++ kind: ContextMenuKind.ROOT_PROJECT, ++ isTrashed: false + })); + } + }; + + export const openProjectContextMenu = (event: React.MouseEvent, projectUuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { - const projectResource = getResource(projectUuid)(getState().resources); - if (projectResource) { ++ const res = getResource(projectUuid)(getState().resources); ++ if (res) { + dispatch(openContextMenu(event, { - name: projectResource.name, - uuid: projectResource.uuid, - kind: ContextMenuKind.PROJECT ++ name: res.name, ++ uuid: res.uuid, ++ kind: ContextMenuKind.PROJECT, ++ ownerUuid: res.ownerUuid, ++ isTrashed: res.isTrashed + })); + } + }; + + export const openSidePanelContextMenu = (event: React.MouseEvent, id: string) => + (dispatch: Dispatch, getState: () => RootState) => { + if (!isSidePanelTreeCategory(id)) { + const kind = extractUuidKind(id); + if (kind === ResourceKind.USER) { + dispatch(openRootProjectContextMenu(event, id)); + } else if (kind === ResourceKind.PROJECT) { + dispatch(openProjectContextMenu(event, id)); + } + } + }; + + export const openProcessContextMenu = (event: React.MouseEvent) => + (dispatch: Dispatch, getState: () => RootState) => { + const resource = { + uuid: '', + name: '', + description: '', + kind: ContextMenuKind.PROCESS + }; + dispatch(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; + } + }; diff --cc src/views-components/context-menu/action-sets/collection-action-set.ts index c8fb3cbc,b3fdc3fb..f2600365 --- a/src/views-components/context-menu/action-sets/collection-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-action-set.ts @@@ -6,10 -6,10 +6,12 @@@ import { ContextMenuActionSet } from ". 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 = [[ { diff --cc src/views-components/context-menu/action-sets/collection-resource-action-set.ts index dbc9e236,a299b937..a1df8385 --- a/src/views-components/context-menu/action-sets/collection-resource-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-resource-action-set.ts @@@ -4,12 -4,12 +4,13 @@@ 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 = [[ { diff --cc src/views/collection-panel/collection-panel.tsx index 9e32700d,348b548b..8a0e2f81 --- a/src/views/collection-panel/collection-panel.tsx +++ b/src/views/collection-panel/collection-panel.tsx @@@ -60,76 -63,98 +63,96 @@@ type CollectionPanelProps = CollectionP 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 { - render() { - const { classes, item, tags, onContextMenu } = this.props; + const { classes, item, tags } = this.props; return
- - } - action={ - onContextMenu(event, item)}> - - - } - title={item && item.name } - subheader={item && item.description} /> - - - - - - - + + } + action={ + + + + } + title={item && item.name} + subheader={item && item.description} /> + + + + + + this.onCopy()}> + + + - - - - + + + - - + + + - - - - - - - { - tags.map(tag => { - return ; - }) - } - + + + + + + + { + tags.map(tag => { + return ; + }) + } - - -
- -
-
; + + + +
+ +
+ ; + } + + handleContextMenu = (event: React.MouseEvent) => { + const { uuid, name, description } = this.props.item; + const resource = { + uuid, + name, + description, + kind: ContextMenuKind.COLLECTION + }; + this.props.dispatch(openContextMenu(event, resource)); } handleDelete = (uuid: string) => () => { this.props.dispatch(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 + })); } - } ) ); diff --cc src/views/favorite-panel/favorite-panel.tsx index 49f1f4ab,9fbae5ce..62b037e3 --- a/src/views/favorite-panel/favorite-panel.tsx +++ b/src/views/favorite-panel/favorite-panel.tsx @@@ -9,9 -8,8 +8,8 @@@ import { DataExplorer } from "~/views-c 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'; @@@ -42,10 -45,10 +45,10 @@@ export enum FavoritePanelColumnNames } export interface FavoritePanelFilter extends DataTableFilterItem { - type: ResourceKind | ContainerRequestState; + type: ResourceKind | ProcessState; } - export const columns: DataColumns = [ + export const favoritePanelColumns: DataColumns = [ { name: FavoritePanelColumnNames.NAME, selected: true, @@@ -62,22 -65,22 +65,22 @@@ 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 => , width: "75px" }, { diff --cc src/views/project-panel/project-panel.tsx index f63584b7,06946430..37a6d202 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@@ -47,10 -57,10 +57,10 @@@ export enum ProjectPanelColumnNames } export interface ProjectPanelFilter extends DataTableFilterItem { - type: ResourceKind | ContainerRequestState; + type: ResourceKind | ProcessState; } - export const columns: DataColumns = [ + export const projectPanelColumns: DataColumns = [ { name: ProjectPanelColumnNames.NAME, selected: true, @@@ -67,22 -77,22 +77,22 @@@ 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 => , width: "75px" }, { diff --cc src/views/workbench/workbench.tsx index 8028f2c3,ef5fe215..ea3a278b --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@@ -46,16 -24,24 +24,27 @@@ import { AuthService } from "~/services 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 = (theme: ArvadosTheme) => ({ root: { @@@ -233,11 -163,10 +166,11 @@@ export const Workbench = withStyles(sty
- } /> - - + + + + + -
{user && }