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",
- WORKFLOW = "arvados#workflow"
+ USER = "arvados#user",
+ WORKFLOW = "arvados#workflow",
}
+
+ export enum ResourceObjectType {
+ COLLECTION = '4zz18',
+ CONTAINER = 'dz642',
+ CONTAINER_REQUEST = 'xvhdp',
+ GROUP = 'j7d0g',
+ USER = 'tpzed',
+ }
+
+ export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
+ export const RESOURCE_UUID_REGEX = new RegExp(RESOURCE_UUID_PATTERN);
+
+ export const isResourceUuid = (uuid: string) =>
+ RESOURCE_UUID_REGEX.test(uuid);
+
+ export const extractUuidObjectType = (uuid: string) => {
+ const match = RESOURCE_UUID_REGEX.exec(uuid);
+ return match
+ ? match[0].split('-')[1]
+ : undefined;
+ };
+
+ export const extractUuidKind = (uuid: string = '') => {
+ const objectType = extractUuidObjectType(uuid);
+ switch (objectType) {
+ case ResourceObjectType.USER:
+ return ResourceKind.USER;
+ case ResourceObjectType.GROUP:
+ return ResourceKind.GROUP;
+ case ResourceObjectType.COLLECTION:
+ return ResourceKind.COLLECTION;
+ case ResourceObjectType.CONTAINER_REQUEST:
+ return ResourceKind.CONTAINER_REQUEST;
+ case ResourceObjectType.CONTAINER:
+ return ResourceKind.CONTAINER;
+ default:
+ return undefined;
+ }
+ };
//
// SPDX-License-Identifier: AGPL-3.0
- import * as _ from "lodash";
import { CommonResourceService } from "~/common/api/common-resource-service";
import { CollectionResource } from "~/models/collection";
- import axios, { AxiosInstance } from "axios";
- import { KeepService } from "../keep-service/keep-service";
+ import { AxiosInstance } from "axios";
+ import { CollectionFile, CollectionDirectory } from "~/models/collection-file";
import { WebDAV } from "~/common/webdav";
import { AuthService } from "../auth-service/auth-service";
- import { mapTree, getNodeChildren, getNode, TreeNode } from "../../models/tree";
- import { getTagValue } from "~/common/xml";
- import { FilterBuilder } from "~/common/api/filter-builder";
- import { CollectionFile, createCollectionFile, CollectionFileType, CollectionDirectory, createCollectionDirectory } from '~/models/collection-file';
- import { parseKeepManifestText, stringifyKeepManifest } from "../collection-files-service/collection-manifest-parser";
- import { KeepManifestStream } from "~/models/keep-manifest";
- import { createCollectionFilesTree } from '~/models/collection-file';
+ import { mapTreeValues } from "~/models/tree";
+ import { parseFilesResponse } from "./collection-service-files-response";
+ import { fileToArrayBuffer } from "~/common/file";
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
export class CollectionService extends CommonResourceService<CollectionResource> {
- constructor(serverApi: AxiosInstance, private keepService: KeepService, private webdavClient: WebDAV, private authService: AuthService) {
+ constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService) {
super(serverApi, "collections");
}
async files(uuid: string) {
- const request = await this.webdavClient.propfind(`/c=${uuid}`);
+ const request = await this.webdavClient.propfind(`c=${uuid}`);
if (request.responseXML != null) {
- const files = this.extractFilesData(request.responseXML);
- const tree = createCollectionFilesTree(files);
- const sortedTree = mapTree(node => {
- const children = getNodeChildren(node.id)(tree).map(id => getNode(id)(tree)) as TreeNode<CollectionDirectory | CollectionFile>[];
- children.sort((a, b) =>
- a.value.type !== b.value.type
- ? a.value.type === CollectionFileType.DIRECTORY ? -1 : 1
- : a.value.name.localeCompare(b.value.name)
- );
- return { ...node, children: children.map(child => child.id) };
- })(tree);
- return sortedTree;
+ const filesTree = parseFilesResponse(request.responseXML);
+ return mapTreeValues(this.extendFileURL)(filesTree);
}
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;
+ }
+ };
// SPDX-License-Identifier: AGPL-3.0
import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
- import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
+ import { routerMiddleware, routerReducer } from "react-router-redux";
import thunkMiddleware from 'redux-thunk';
import { History } from "history";
- import { projectsReducer, ProjectState } from "./project/project-reducer";
- import { sidePanelReducer, SidePanelState } from './side-panel/side-panel-reducer';
- import { authReducer, AuthState } from "./auth/auth-reducer";
- import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-explorer-reducer';
- import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
- import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
+ import { authReducer } from "./auth/auth-reducer";
+ import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
+ import { detailsPanelReducer } from './details-panel/details-panel-reducer';
+ import { contextMenuReducer } from './context-menu/context-menu-reducer';
import { reducer as formReducer } from 'redux-form';
- import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
- import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
- import { CollectionPanelFilesState } from './collection-panel/collection-panel-files/collection-panel-files-state';
+ import { favoritesReducer } from './favorites/favorites-reducer';
+ import { snackbarReducer } from './snackbar/snackbar-reducer';
import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collection-panel-files-reducer';
import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
- import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
- import { DialogState, dialogReducer } from './dialog/dialog-reducer';
- import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
+ import { collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+ import { dialogReducer } from './dialog/dialog-reducer';
import { ServiceRepository } from "~/services/services";
import { treePickerReducer } from './tree-picker/tree-picker-reducer';
- import { TreePicker } from './tree-picker/tree-picker';
- import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service";
- import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
+ import { resourcesReducer } from '~/store/resources/resources-reducer';
+ import { propertiesReducer } from './properties/properties-reducer';
+ import { RootState } from './store';
+ import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
- export interface RootState {
- auth: AuthState;
- projects: ProjectState;
- collections: CollectionsState;
- router: RouterState;
- dataExplorer: DataExplorerState;
- sidePanel: SidePanelState;
- collectionPanel: CollectionPanelState;
- detailsPanel: DetailsPanelState;
- contextMenu: ContextMenuState;
- favorites: FavoritesState;
- snackbar: SnackbarState;
- collectionPanelFiles: CollectionPanelFilesState;
- dialog: DialogState;
- treePicker: TreePicker;
- }
+ export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
export function configureStore(history: History, services: ServiceRepository): RootStore {
- const rootReducer = combineReducers({
- auth: authReducer(services),
- projects: projectsReducer,
- collections: collectionsReducer,
- router: routerReducer,
- dataExplorer: dataExplorerReducer,
- sidePanel: sidePanelReducer,
- collectionPanel: collectionPanelReducer,
- detailsPanel: detailsPanelReducer,
- contextMenu: contextMenuReducer,
- form: formReducer,
- favorites: favoritesReducer,
- snackbar: snackbarReducer,
- collectionPanelFiles: collectionPanelFilesReducer,
- dialog: dialogReducer,
- treePicker: treePickerReducer,
- });
+ const rootReducer = createRootReducer(services);
const projectPanelMiddleware = dataExplorerMiddleware(
new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
const favoritePanelMiddleware = dataExplorerMiddleware(
new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID)
);
+ const trashPanelMiddleware = dataExplorerMiddleware(
+ new TrashPanelMiddlewareService(services, TRASH_PANEL_ID)
+ );
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
projectPanelMiddleware,
- favoritePanelMiddleware
+ favoritePanelMiddleware,
+ trashPanelMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
}
+
+ const createRootReducer = (services: ServiceRepository) => combineReducers({
+ auth: authReducer(services),
+ collectionPanel: collectionPanelReducer,
+ collectionPanelFiles: collectionPanelFilesReducer,
+ contextMenu: contextMenuReducer,
+ dataExplorer: dataExplorerReducer,
+ detailsPanel: detailsPanelReducer,
+ dialog: dialogReducer,
+ favorites: favoritesReducer,
+ form: formReducer,
+ properties: propertiesReducer,
+ resources: resourcesReducer,
+ router: routerReducer,
+ snackbar: snackbarReducer,
+ treePicker: treePickerReducer,
+ fileUploader: fileUploaderReducer,
+ });
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 = [[
{
icon: RenameIcon,
name: "Edit collection",
execute: (dispatch, resource) => {
- dispatch<any>(openUpdater(resource));
+ dispatch<any>(openCollectionUpdateDialog(resource));
}
},
{
{
icon: MoveToIcon,
name: "Move to",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
},
{
component: ToggleFavoriteAction,
});
}
},
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleCollectionTrashed(resource));
+ }
+ },
{
icon: CopyIcon,
name: "Copy to project",
execute: (dispatch, resource) => {
- // add code
+ dispatch<any>(openCollectionCopyDialog(resource));
}
},
{
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 = [[
{
icon: RenameIcon,
name: "Edit collection",
execute: (dispatch, resource) => {
- dispatch<any>(openUpdater(resource));
+ dispatch<any>(openCollectionUpdateDialog(resource));
}
},
{
{
icon: MoveToIcon,
name: "Move to",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
},
{
component: ToggleFavoriteAction,
});
}
},
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resource) => {
+ dispatch<any>(toggleCollectionTrashed(resource));
+ }
+ },
{
icon: CopyIcon,
name: "Copy to project",
execute: (dispatch, resource) => {
- // add code
- }
+ dispatch<any>(openCollectionCopyDialog(resource));
+ },
},
{
icon: DetailsIcon,
import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
import { formatDate, formatFileSize } from '~/common/formatters';
import { resourceLabel } from '~/common/labels';
+ import { connect } from 'react-redux';
+ import { RootState } from '~/store/store';
+ import { getResource } from '../../store/resources/resources';
+ import { GroupContentsResource } from '~/services/groups-service/groups-service';
+ import { ProcessResource } from '~/models/process';
- export const renderName = (item: {name: string; uuid: string, kind: string}) =>
+ export const renderName = (item: { name: string; uuid: string, kind: string }) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
<Grid item>
{renderIcon(item)}
</Grid>
</Grid>;
+ export const ResourceName = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return resource || { name: '', uuid: '', kind: '' };
+ })(renderName);
- export const renderIcon = (item: {kind: string}) => {
+ export const renderIcon = (item: { kind: string }) => {
switch (item.kind) {
case ResourceKind.PROJECT:
return <ProjectIcon />;
}
};
-export const renderDate = (date: string) => {
+export const renderDate = (date?: string) => {
return <Typography noWrap>{formatDate(date)}</Typography>;
};
+ export const ResourceLastModifiedDate = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { date: resource ? resource.modifiedAt : '' };
+ })((props: { date: string }) => renderDate(props.date));
+
export const renderFileSize = (fileSize?: number) =>
<Typography noWrap>
{formatFileSize(fileSize)}
</Typography>;
+ export const ResourceFileSize = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return {};
+ })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
+
export const renderOwner = (owner: string) =>
<Typography noWrap color="primary" >
{owner}
</Typography>;
+ export const ResourceOwner = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { owner: resource ? resource.ownerUuid : '' };
+ })((props: { owner: string }) => renderOwner(props.owner));
+
export const renderType = (type: string) =>
<Typography noWrap>
{resourceLabel(type)}
</Typography>;
- export const renderStatus = (item: {status?: string}) =>
+ export const ResourceType = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+ return { type: resource ? resource.kind : '' };
+ })((props: { type: string }) => renderType(props.type));
+
+ export const renderStatus = (item: { status?: string }) =>
<Typography noWrap align="center" >
{item.status || "-"}
</Typography>;
+
+ export const ProcessStatus = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource(props.uuid)(state.resources) as ProcessResource | undefined;
+ return { status: resource ? resource.state : '-' };
+ })((props: { status: string }) => renderType(props.status));
import * as React from 'react';
import {
StyleRulesCallback, WithStyles, withStyles, Card,
- CardHeader, IconButton, CardContent, Grid, Chip
+ CardHeader, IconButton, CardContent, Grid, Chip, Tooltip
} from '@material-ui/core';
import { connect, DispatchProp } from "react-redux";
import { RouteComponentProps } from 'react-router';
import { TagResource } from '~/models/tag';
import { CollectionTagForm } from './collection-tag-form';
import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
+ import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+ import { getResource } from '~/store/resources/resources';
+ import { contextMenuActions, openContextMenu } from '~/store/context-menu/context-menu-actions';
+ import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
- type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'value';
+ type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
color: theme.palette.grey["500"],
cursor: 'pointer'
},
+ label: {
+ fontSize: '0.875rem'
+ },
value: {
- textTransform: 'none'
+ textTransform: 'none',
+ fontSize: '0.875rem'
}
});
tags: TagResource[];
}
- interface CollectionPanelActionProps {
- onItemRouteChange: (collectionId: string) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: CollectionResource) => void;
- }
-
- type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+ type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
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
+ }));
}
-
}
)
);
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
- import { FavoritePanelItem } from './favorite-panel-item';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
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';
import { ArvadosTheme } from '~/common/custom-theme';
- import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
+ import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner, ResourceName } from '~/views-components/data-explorer/renderers';
import { FavoriteIcon } from '~/components/icon/icon';
+ import { Dispatch } from 'redux';
+ import { contextMenuActions, openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+ import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+ import { loadDetailsPanel } from '../../store/details-panel/details-panel-action';
+ import { navigateTo } from '~/store/navigation/navigation-action';
type CssRules = "toolbar" | "button";
}
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,
configurable: true,
sortDirection: SortDirection.ASC,
filters: [],
- render: renderName,
+ render: uuid => <ResourceName uuid={uuid} />,
width: "450px"
},
{
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"
},
{
type: ResourceKind.PROJECT
}
],
- render: item => renderType(item.kind),
+ render: uuid => <ResourceType uuid={uuid} />,
width: "125px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderOwner(item.owner),
+ render: uuid => <ResourceOwner uuid={uuid} />,
width: "200px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderFileSize(item.fileSize),
+ render: uuid => <ResourceFileSize uuid={uuid} />,
width: "50px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderDate(item.lastModified),
+ render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
width: "150px"
}
];
}
interface FavoritePanelActionProps {
- onItemClick: (item: FavoritePanelItem) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: FavoritePanelItem) => void;
+ onItemClick: (item: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
onDialogOpen: (ownerUuid: string) => void;
- onItemDoubleClick: (item: FavoritePanelItem) => void;
- onItemRouteChange: (itemId: string) => void;
+ onItemDoubleClick: (item: string) => void;
}
+ const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
+ onContextMenu: (event, resourceUuid) => {
+ const kind = resourceKindToContextMenuKind(resourceUuid);
+ if (kind) {
+ dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
+ }
+ },
+ onDialogOpen: (ownerUuid: string) => { return; },
+ onItemClick: (resourceUuid: string) => {
+ dispatch<any>(loadDetailsPanel(resourceUuid));
+ },
+ onItemDoubleClick: uuid => {
+ dispatch<any>(navigateTo(uuid));
+ }
+ });
+
type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+ & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const FavoritePanel = withStyles(styles)(
- connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ connect(undefined, mapDispatchToProps)(
class extends React.Component<FavoritePanelProps> {
render() {
return <DataExplorer
id={FAVORITE_PANEL_ID}
- columns={columns}
onRowClick={this.props.onItemClick}
onRowDoubleClick={this.props.onItemDoubleClick}
onContextMenu={this.props.onContextMenu}
- extractKey={(item: FavoritePanelItem) => item.uuid}
defaultIcon={FavoriteIcon}
- defaultMessages={['Your favorites list is empty.']}/>
- ;
- }
-
- componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: FavoritePanelProps) {
- if (match.params.id !== currentItemId) {
- onItemRouteChange(match.params.id);
- }
+ defaultMessages={['Your favorites list is empty.']} />;
}
}
)
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
- import { ProjectPanelItem } from './project-panel-item';
import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
import { DispatchProp, connect } from 'react-redux';
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';
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';
+ import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
import { ProjectIcon } from '~/components/icon/icon';
+ import { ResourceName } from '~/views-components/data-explorer/renderers';
+ import { ResourcesState, getResource } from '~/store/resources/resources';
+ import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+ import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+ import { contextMenuActions, resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
+ import { CollectionResource } from '~/models/collection';
+ import { ProjectResource } from '~/models/project';
+ import { navigateTo } from '~/store/navigation/navigation-action';
+ import { getProperty } from '~/store/properties/properties';
+ import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
+ import { openCollectionCreateDialog } from '../../store/collections/collection-create-actions';
+ import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
type CssRules = 'root' | "toolbar" | "button";
}
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,
configurable: true,
sortDirection: SortDirection.ASC,
filters: [],
- render: renderName,
+ render: uuid => <ResourceName uuid={uuid} />,
width: "450px"
},
{
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"
},
{
type: ResourceKind.PROJECT
}
],
- render: item => renderType(item.kind),
+ render: uuid => <ResourceType uuid={uuid} />,
width: "125px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderOwner(item.owner),
+ render: uuid => <ResourceOwner uuid={uuid} />,
width: "200px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderFileSize(item.fileSize),
+ render: uuid => <ResourceFileSize uuid={uuid} />,
width: "50px"
},
{
configurable: true,
sortDirection: SortDirection.NONE,
filters: [],
- render: item => renderDate(item.lastModified),
+ render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
width: "150px"
}
];
interface ProjectPanelDataProps {
currentItemId: string;
+ resources: ResourcesState;
}
- interface ProjectPanelActionProps {
- onItemClick: (item: ProjectPanelItem) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
- onProjectCreationDialogOpen: (ownerUuid: string) => void;
- onCollectionCreationDialogOpen: (ownerUuid: string) => void;
- onItemDoubleClick: (item: ProjectPanelItem) => void;
- onItemRouteChange: (itemId: string) => void;
- }
-
- type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp
+ type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const ProjectPanel = withStyles(styles)(
- connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+ connect((state: RootState) => ({
+ currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+ resources: state.resources
+ }))(
class extends React.Component<ProjectPanelProps> {
render() {
const { classes } = this.props;
</div>
<DataExplorer
id={PROJECT_PANEL_ID}
- columns={columns}
- onRowClick={this.props.onItemClick}
- onRowDoubleClick={this.props.onItemDoubleClick}
- onContextMenu={this.props.onContextMenu}
- extractKey={(item: ProjectPanelItem) => item.uuid}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
defaultIcon={ProjectIcon}
defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
</div>;
}
handleNewProjectClick = () => {
- this.props.onProjectCreationDialogOpen(this.props.currentItemId);
+ this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
}
handleNewCollectionClick = () => {
- this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
+ this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
}
- componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
- if (match.params.id !== currentItemId) {
- onItemRouteChange(match.params.id);
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ const kind = resourceKindToContextMenuKind(resourceUuid);
+ if (kind) {
+ this.props.dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
}
}
- componentDidMount() {
- if (this.props.match.params.id && this.props.currentItemId === '') {
- this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
- }
+ handleRowDoubleClick = (uuid: string) => {
+ this.props.dispatch<any>(navigateTo(uuid));
}
+
+ handleRowClick = (uuid: string) => {
+ this.props.dispatch(loadDetailsPanel(uuid));
+ }
+
}
)
);
import * as React from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
- import Drawer from '@material-ui/core/Drawer';
import { connect, DispatchProp } from "react-redux";
- import { Route, RouteComponentProps, Switch, Redirect } from "react-router";
+ import { Route, Switch } from "react-router";
import { login, logout } from "~/store/auth/auth-action";
import { User } from "~/models/user";
import { RootState } from "~/store/store";
import { MainAppBar, MainAppBarActionProps, MainAppBarMenuItem } from '~/views-components/main-app-bar/main-app-bar';
- import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
import { push } from 'react-router-redux';
- import { reset } from 'redux-form';
- import { ProjectTree } from '~/views-components/project-tree/project-tree';
- import { TreeItem } from "~/components/tree/tree";
- import { getTreePath } from '~/store/project/project-reducer';
- import { sidePanelActions } from '~/store/side-panel/side-panel-action';
- import { SidePanel, SidePanelItem } from '~/components/side-panel/side-panel';
- import { ItemMode, setProjectItem } from "~/store/navigation/navigation-action";
- import { projectActions } from "~/store/project/project-action";
- import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action';
import { ProjectPanel } from "~/views/project-panel/project-panel";
import { DetailsPanel } from '~/views-components/details-panel/details-panel';
import { ArvadosTheme } from '~/common/custom-theme';
- import { CreateProjectDialog } from "~/views-components/create-project-dialog/create-project-dialog";
-
- import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
- import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
- import { ProjectResource } from '~/models/project';
- import { ResourceKind } from '~/models/resource';
- import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
+ import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
+ import { ContextMenu } from "~/views-components/context-menu/context-menu";
import { FavoritePanel } from "../favorite-panel/favorite-panel";
import { CurrentTokenDialog } from '~/views-components/current-token-dialog/current-token-dialog';
import { Snackbar } from '~/views-components/snackbar/snackbar';
- 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, 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 { UpdateProjectDialog } from '~/views-components/update-project-dialog/update-project-dialog';
import { AuthService } from "~/services/auth-service/auth-service";
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: {
position: "absolute",
width: "100%"
},
- drawerPaper: {
- position: 'relative',
- width: DRAWER_WITDH,
- display: 'flex',
- flexDirection: 'column',
- },
contentWrapper: {
backgroundColor: theme.palette.background.default,
display: "flex",
flexGrow: 1,
position: 'relative'
},
- toolbar: theme.mixins.toolbar
});
interface WorkbenchDataProps {
- projects: Array<TreeItem<ProjectResource>>;
- currentProjectId: string;
user?: User;
currentToken?: string;
- sidePanelItems: SidePanelItem[];
}
interface WorkbenchGeneralProps {
type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
- interface NavBreadcrumb extends Breadcrumb {
- itemId: string;
- }
-
interface NavMenuItem extends MainAppBarMenuItem {
action: () => void;
}
export const Workbench = withStyles(styles)(
connect<WorkbenchDataProps>(
(state: RootState) => ({
- projects: state.projects.items,
- currentProjectId: state.projects.currentItemId,
user: state.auth.user,
currentToken: state.auth.apiToken,
- sidePanelItems: state.sidePanel
})
)(
class extends React.Component<WorkbenchProps, WorkbenchState> {
state = {
- isCreationDialogOpen: false,
isCurrentTokenDialogOpen: false,
anchorEl: null,
searchText: "",
};
render() {
- const path = getTreePath(this.props.projects, this.props.currentProjectId);
- const breadcrumbs = path.map(item => ({
- label: item.data.name,
- itemId: item.data.uuid,
- status: item.status
- }));
-
const { classes, user } = this.props;
return (
<div className={classes.root}>
<div className={classes.appBar}>
<MainAppBar
- breadcrumbs={breadcrumbs}
+ breadcrumbs={Breadcrumbs}
searchText={this.state.searchText}
user={this.props.user}
menuItems={this.state.menuItems}
buildInfo={this.props.buildInfo}
{...this.mainAppBarActions} />
</div>
- {user &&
- <Drawer
- variant="permanent"
- classes={{
- paper: classes.drawerPaper,
- }}>
- <div className={classes.toolbar} />
- <SidePanel
- toggleOpen={this.toggleSidePanelOpen}
- toggleActive={this.toggleSidePanelActive}
- sidePanelItems={this.props.sidePanelItems}
- onContextMenu={(event) => this.openContextMenu(event, {
- uuid: this.props.authService.getUuid() || "",
- name: "",
- kind: ContextMenuKind.ROOT_PROJECT
- })}>
- <ProjectTree
- projects={this.props.projects}
- toggleOpen={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))}
- onContextMenu={(event, item) => this.openContextMenu(event, {
- uuid: item.data.uuid,
- ownerUuid: item.data.ownerUuid || this.props.authService.getUuid(),
- isTrashed: item.data.isTrashed,
- name: item.data.name,
- kind: ContextMenuKind.PROJECT
- })}
- toggleActive={itemId => {
- this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
- this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
- }} />
- </SidePanel>
- </Drawer>}
+ {user && <SidePanel />}
<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 />}
<CreateProjectDialog />
<CreateCollectionDialog />
<RenameFileDialog />
- <DialogCollectionCreateWithSelectedFile />
+ <PartialCopyCollectionDialog />
+ <FileRemoveDialog />
+ <CopyCollectionDialog />
<FileRemoveDialog />
<MultipleFilesRemoveDialog />
<UpdateCollectionDialog />
+ <FilesUploadCollectionDialog />
<UpdateProjectDialog />
+ <MoveCollectionDialog />
+ <MoveProjectDialog />
<CurrentTokenDialog
currentToken={this.props.currentToken}
open={this.state.isCurrentTokenDialogOpen}
);
}
- renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
- onItemRouteChange={(collectionId) => {
- this.props.dispatch<any>(loadCollection(collectionId));
- this.props.dispatch<any>(loadCollectionTags(collectionId));
- }}
- onContextMenu={(event, item) => {
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- description: item.description,
- isTrashed: item.isTrashed,
- kind: ContextMenuKind.COLLECTION
- });
- }}
- {...props} />
-
- renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
- onItemRouteChange={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))}
- onContextMenu={(event, item) => {
- let kind: ContextMenuKind;
-
- if (item.kind === ResourceKind.PROJECT) {
- kind = ContextMenuKind.PROJECT;
- } else if (item.kind === ResourceKind.COLLECTION) {
- kind = ContextMenuKind.COLLECTION_RESOURCE;
- } else {
- kind = ContextMenuKind.RESOURCE;
- }
-
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- description: item.description,
- isTrashed: item.isTrashed,
- ownerUuid: item.owner || this.props.authService.getUuid(),
- kind
- });
- }}
- onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen}
- onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen}
- onItemClick={item => {
- this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- switch (item.kind) {
- case ResourceKind.COLLECTION:
- this.props.dispatch(loadCollection(item.uuid));
- this.props.dispatch(push(getCollectionUrl(item.uuid)));
- default:
- this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }
-
- }}
- {...props} />
-
- renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => <FavoritePanel
- onItemRouteChange={() => this.props.dispatch(favoritePanelActions.REQUEST_ITEMS())}
- onContextMenu={(event, item) => {
- const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- isTrashed: item.isTrashed,
- kind,
- });
- }}
- onDialogOpen={this.handleProjectCreationDialogOpen}
- onItemClick={item => {
- this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- switch (item.kind) {
- case ResourceKind.COLLECTION:
- this.props.dispatch(loadCollection(item.uuid));
- this.props.dispatch(push(getCollectionUrl(item.uuid)));
- default:
- this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
- this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- }
-
- }}
- {...props} />
-
- renderTrashPanel = (props: RouteComponentProps<{ id: string }>) => <TrashPanel
- onItemRouteChange={() => this.props.dispatch(trashPanelActions.REQUEST_ITEMS())}
- onContextMenu={(event, item) => {
- const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.COLLECTION;
- this.openContextMenu(event, {
- uuid: item.uuid,
- name: item.name,
- isTrashed: item.isTrashed,
- ownerUuid: item.owner,
- kind,
- });
- }}
- onDialogOpen={this.handleProjectCreationDialogOpen}
- onItemClick={item => {
- // this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
- }}
- onItemDoubleClick={item => {
- // switch (item.kind) {
- // case ResourceKind.COLLECTION:
- // this.props.dispatch(loadCollection(item.uuid));
- // this.props.dispatch(push(getCollectionUrl(item.uuid)));
- // default:
- // this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
- // this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
- // }
-
- }}
- {...props} />
-
mainAppBarActions: MainAppBarActionProps = {
- onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
- this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
- this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
- },
onSearch: searchText => {
this.setState({ searchText });
this.props.dispatch(push(`/search?q=${searchText}`));
onDetailsPanelToggle: () => {
this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
},
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
- this.openContextMenu(event, {
- uuid: breadcrumb.itemId,
- name: breadcrumb.label,
- kind: ContextMenuKind.PROJECT
- });
- }
};
- toggleSidePanelOpen = (itemId: string) => {
- this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
- }
-
- toggleSidePanelActive = (itemId: string) => {
- this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
-
- const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
- if (panelItem && panelItem.activeAction) {
- panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid());
- }
- }
-
- handleProjectCreationDialogOpen = (itemUuid: string) => {
- this.props.dispatch(reset(PROJECT_CREATE_DIALOG));
- this.props.dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: itemUuid }));
- }
-
- handleCollectionCreationDialogOpen = (itemUuid: string) => {
- this.props.dispatch(reset(COLLECTION_CREATE_DIALOG));
- this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: itemUuid }));
- }
-
- openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; isTrashed?: boolean, ownerUuid?: string, kind: ContextMenuKind; }) => {
- event.preventDefault();
- this.props.dispatch(
- contextMenuActions.OPEN_CONTEXT_MENU({
- position: { x: event.clientX, y: event.clientY },
- resource
- })
- );
- }
-
toggleCurrentTokenModal = () => {
this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen });
}