- Previously, the project panel loaded the entire collection file tree to sum file count and total size. Now it uses the file_count and file_size_total collection attributes.
- Also updated the collection service to not do any tree manipulation when using propfind for files.
Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti@veritasgenetics.com>
version: number;
preserveVersion: boolean;
unsignedManifestText?: string;
+ fileCount: number;
+ fileSizeTotal: number;
}
export const getCollectionUrl = (uuid: string) => {
//
// SPDX-License-Identifier: AGPL-3.0
-import { createCollectionFilesTree, CollectionDirectory, CollectionFile, CollectionFileType, createCollectionDirectory, createCollectionFile } from "../../models/collection-file";
+import { CollectionDirectory, CollectionFile, CollectionFileType, createCollectionDirectory, createCollectionFile } from "../../models/collection-file";
import { getTagValue } from "~/common/xml";
import { getNodeChildren, Tree, mapTree } from '~/models/tree';
-export const parseFilesResponse = (document: Document) => {
- const files = extractFilesData(document);
- const tree = createCollectionFilesTree(files);
- return sortFilesTree(tree);
-};
-
export const sortFilesTree = (tree: Tree<CollectionDirectory | CollectionFile>) => {
return mapTree<CollectionDirectory | CollectionFile>(node => {
const children = getNodeChildren(node.id)(tree);
import { CollectionFile, CollectionDirectory } from "~/models/collection-file";
import { WebDAV } from "~/common/webdav";
import { AuthService } from "../auth-service/auth-service";
-import { mapTreeValues } from "~/models/tree";
-import { parseFilesResponse } from "./collection-service-files-response";
+import { extractFilesData } from "./collection-service-files-response";
import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
import { ApiActions } from "~/services/api/api-actions";
async files(uuid: string) {
const request = await this.webdavClient.propfind(`c=${uuid}`);
if (request.responseXML != null) {
- const filesTree = parseFilesResponse(request.responseXML);
- return mapTreeValues(this.extendFileURL)(filesTree);
+ return extractFilesData(request.responseXML);
}
return Promise.reject();
}
);
}
- private extendFileURL = (file: CollectionDirectory | CollectionFile) => {
+ extendFileURL = (file: CollectionDirectory | CollectionFile) => {
const baseUrl = this.webdavClient.defaults.baseURL.endsWith('/')
? this.webdavClient.defaults.baseURL.slice(0, -1)
: this.webdavClient.defaults.baseURL;
const collectionApiResponse = (apiResponse: CollectionResource) => {
const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, properties, portableDataHash, replicationDesired,
replicationConfirmedAt, replicationConfirmed, manifestText, deleteAt, trashAt, isTrashed, storageClassesDesired,
- storageClassesConfirmed, storageClassesConfirmedAt, currentVersionUuid, version, preserveVersion } = apiResponse;
+ storageClassesConfirmed, storageClassesConfirmedAt, currentVersionUuid, version, preserveVersion, fileCount, fileSizeTotal } = apiResponse;
const response = `
"uuid": "${uuid}",
"owner_uuid": "${ownerUuid}",
"storage_classes_confirmed_at": ${stringify(storageClassesConfirmedAt)},
"current_version_uuid": ${stringify(currentVersionUuid)},
"version": ${version},
-"preserve_version": ${preserveVersion}`;
+"preserve_version": ${preserveVersion},
+"file_count": ${fileCount},
+"file_size_total": ${fileSizeTotal}`;
return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
};
import { unionize, ofType, UnionOf } from "~/common/unionize";
import { Dispatch } from "redux";
-import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
+import { CollectionFilesTree, CollectionFileType, createCollectionFilesTree } from "~/models/collection-file";
import { ServiceRepository } from "~/services/services";
import { RootState } from "../../store";
import { snackbarActions, SnackbarKind } from "../../snackbar/snackbar-actions";
import { dialogActions } from '../../dialog/dialog-actions';
-import { getNodeValue } from "~/models/tree";
+import { getNodeValue, mapTreeValues } from "~/models/tree";
import { filterCollectionFilesBySelection } from './collection-panel-files-state';
import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
import { getDialog } from "~/store/dialog/dialog-reducer";
-import { getFileFullPath } from "~/services/collection-service/collection-service-files-response";
-import { resourcesDataActions } from "~/store/resources-data/resources-data-actions";
+import { getFileFullPath, sortFilesTree } from "~/services/collection-service/collection-service-files-response";
export const collectionPanelFilesAction = unionize({
SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
export const loadCollectionFiles = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const files = await services.collectionService.files(uuid);
- dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
- dispatch(resourcesDataActions.SET_FILES({ uuid, files }));
+ const tree = createCollectionFilesTree(files);
+ const sorted = sortFilesTree(tree);
+ const mapped = mapTreeValues(services.collectionService.extendFileURL)(sorted);
+ dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(mapped));
};
export const removeCollectionFiles = (filePaths: string[]) =>
import { ListResults } from '~/services/common-service/common-service';
import { loadContainers } from '~/store/processes/processes-actions';
import { ResourceKind } from '~/models/resource';
-import { getResource } from "~/store/resources/resources";
-import { CollectionResource } from "~/models/collection";
-import { resourcesDataActions } from "~/store/resources-data/resources-data-actions";
import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
import { serializeResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
import { updatePublicFavorites } from '~/store/public-favorites/public-favorites-actions';
api.dispatch<any>(updateFavorites(resourceUuids));
api.dispatch<any>(updatePublicFavorites(resourceUuids));
api.dispatch(updateResources(response.items));
- api.dispatch<any>(updateResourceData(resourceUuids));
await api.dispatch<any>(loadMissingProcessesInformation(response.items));
api.dispatch(setItems(response));
} catch (e) {
}
};
-export const updateResourceData = (resourceUuids: string[]) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- resourceUuids.map(async uuid => {
- const resource = getResource<CollectionResource>(uuid)(getState().resources);
- if (resource && resource.kind === ResourceKind.COLLECTION) {
- const files = await services.collectionService.files(uuid);
- if (files) {
- dispatch(resourcesDataActions.SET_FILES({ uuid, files }));
- }
- }
- });
- };
-
export const setItems = (listResults: ListResults<GroupContentsResource>) =>
projectPanelActions.SET_ITEMS({
...listResultsToDataExplorerItemsMeta(listResults),
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { unionize, ofType, UnionOf } from "~/common/unionize";
-import { CollectionDirectory, CollectionFile } from "~/models/collection-file";
-import { Tree } from "~/models/tree";
-
-export const resourcesDataActions = unionize({
- SET_FILES: ofType<{uuid: string, files: Tree<CollectionFile | CollectionDirectory>}>()
-});
-
-export type ResourcesDataActions = UnionOf<typeof resourcesDataActions>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ResourcesDataActions, resourcesDataActions } from "~/store/resources-data/resources-data-actions";
-import { getNodeDescendantsIds, TREE_ROOT_ID } from "~/models/tree";
-import { CollectionFileType } from "~/models/collection-file";
-
-export interface ResourceData {
- fileCount: number;
- fileSize: number;
-}
-
-export type ResourcesDataState = {
- [key: string]: ResourceData
-};
-
-export const resourcesDataReducer = (state: ResourcesDataState = {}, action: ResourcesDataActions) =>
- resourcesDataActions.match(action, {
- SET_FILES: ({uuid, files}) => {
- const flattenFiles = getNodeDescendantsIds(TREE_ROOT_ID)(files).map(id => files[id]);
- const [fileSize, fileCount] = flattenFiles.reduce(([size, cnt], f) =>
- f && f.value.type === CollectionFileType.FILE
- ? [size + f.value.size, cnt + 1]
- : [size, cnt]
- , [0, 0]);
- return {
- ...state,
- [uuid]: { fileCount, fileSize }
- };
- },
- default: () => state,
- });
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ResourceData, ResourcesDataState } from "~/store/resources-data/resources-data-reducer";
-
-export const getResourceData = (id: string) =>
- (state: ResourcesDataState): ResourceData | undefined => state[id];
import { searchBarReducer } from './search-bar/search-bar-reducer';
import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service';
-import { resourcesDataReducer } from "~/store/resources-data/resources-data-reducer";
import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machines-reducer";
import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
processLogsPanel: processLogsPanelReducer,
properties: propertiesReducer,
resources: resourcesReducer,
- resourcesData: resourcesDataReducer,
router: routerReducer,
snackbar: snackbarReducer,
treePicker: treePickerReducer,
import { unionize, ofType, UnionOf } from "~/common/unionize";
import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree } from '~/models/tree';
+import { createCollectionFilesTree } from "~/models/collection-file";
import { Dispatch } from 'redux';
import { RootState } from '~/store/store';
import { ServiceRepository } from '~/services/services';
import { ProjectResource } from '~/models/project';
import { mapTree } from '../../models/tree';
import { LinkResource, LinkClass } from "~/models/link";
+import { mapTreeValues } from "~/models/tree";
+import { sortFilesTree } from "~/services/collection-service/collection-service-files-response";
export const treePickerActions = unionize({
LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
const node = getNode(id)(picker);
if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
- const filesTree = await services.collectionService.files(node.value.portableDataHash);
+ const files = await services.collectionService.files(node.value.portableDataHash);
+ const tree = createCollectionFilesTree(files);
+ const sorted = sortFilesTree(tree);
+ const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
dispatch(
treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
import { WorkflowResource } from '~/models/workflow';
import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view';
import { getUuidPrefix, openRunProcess } from '~/store/workflow-panel/workflow-panel-actions';
-import { getResourceData } from "~/store/resources-data/resources-data";
import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
import { UserResource } from '~/models/user';
import { toggleIsActive, toggleIsAdmin } from '~/store/users/users-actions';
import { LinkResource } from '~/models/link';
import { navigateTo } from '~/store/navigation/navigation-action';
import { withResourceData } from '~/views-components/data-explorer/with-resources';
+import { CollectionResource } from '~/models/collection';
const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind: string }) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
export const ResourceFileSize = connect(
(state: RootState, props: { uuid: string }) => {
- const resource = getResourceData(props.uuid)(state.resourcesData);
- return { fileSize: resource ? resource.fileSize : 0 };
+ const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+ return { fileSize: resource ? resource.fileSizeTotal : 0 };
})((props: { fileSize?: number }) => renderFileSize(props.fileSize));
const renderOwner = (owner: string) =>
<DetailsAttribute label='Collection UUID' linkToUuid={this.item.uuid} value={this.item.uuid} />
<DetailsAttribute label='Content address' linkToUuid={this.item.portableDataHash} value={this.item.portableDataHash} />
{/* Missing attrs */}
- <DetailsAttribute label='Number of files' value={this.data && this.data.fileCount} />
- <DetailsAttribute label='Content size' value={formatFileSize(this.data && this.data.fileSize)} />
+ <DetailsAttribute label='Number of files' value={this.item.fileCount} />
+ <DetailsAttribute label='Content size' value={formatFileSize(this.item.fileSizeTotal)} />
</div>;
}
}
import * as React from 'react';
import { DetailsResource } from "~/models/details";
-import { ResourceData } from "~/store/resources-data/resources-data-reducer";
export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
- constructor(protected item: T, protected data?: ResourceData) {}
+ constructor(protected item: T) { }
getTitle(): string {
return this.item.name || 'Projects';
abstract getDetails(): React.ReactElement<any>;
getActivity(): React.ReactElement<any> {
- return <div/>;
+ return <div />;
}
}
import { DetailsData } from "./details-data";
import { DetailsResource } from "~/models/details";
import { getResource } from '~/store/resources/resources';
-import { ResourceData } from "~/store/resources-data/resources-data-reducer";
-import { getResourceData } from "~/store/resources-data/resources-data";
import { toggleDetailsPanel, SLIDE_TIMEOUT } from '~/store/details-panel/details-panel-action';
import { FileDetails } from '~/views-components/details-panel/file-details';
import { getNode } from '~/models/tree';
const EMPTY_RESOURCE: EmptyResource = { kind: undefined, name: 'Projects' };
-const getItem = (res: DetailsResource, resourceData?: ResourceData): DetailsData => {
+const getItem = (res: DetailsResource): DetailsData => {
if ('kind' in res) {
switch (res.kind) {
case ResourceKind.PROJECT:
return new ProjectDetails(res);
case ResourceKind.COLLECTION:
- return new CollectionDetails(res, resourceData);
+ return new CollectionDetails(res);
case ResourceKind.PROCESS:
return new ProcessDetails(res);
default:
}
};
-const mapStateToProps = ({ detailsPanel, resources, resourcesData, collectionPanelFiles }: RootState) => {
+const mapStateToProps = ({ detailsPanel, resources, collectionPanelFiles }: RootState) => {
const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource | undefined;
const file = getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
- const resourceData = getResourceData(detailsPanel.resourceUuid)(resourcesData);
return {
isOpened: detailsPanel.isOpened,
- item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData),
+ item: getItem(resource || (file && file.value) || EMPTY_RESOURCE),
};
};
import { openContextMenu } from '~/store/context-menu/context-menu-actions';
import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
import { formatFileSize } from "~/common/formatters";
-import { getResourceData } from "~/store/resources-data/resources-data";
-import { ResourceData } from "~/store/resources-data/resources-data-reducer";
import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { PropertyChipComponent } from '~/views-components/resource-properties-form/property-chip';
interface CollectionPanelDataProps {
item: CollectionResource;
- data: ResourceData;
}
type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const CollectionPanel = withStyles(styles)(
- connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
- const item = getResource(props.match.params.id)(state.resources);
- const data = getResourceData(props.match.params.id)(state.resourcesData);
- return { item, data };
- })(
+ connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
+ const item = getResource(props.match.params.id)(state.resources);
+ const data = getResourceData(props.match.params.id)(state.resourcesData);
+ return { item, data };
+ })(
class extends React.Component<CollectionPanelProps> {
render() {
- const { classes, item, data, dispatch } = this.props;
+ const { classes, item, dispatch } = this.props;
return item
? <>
<Card className={classes.card}>
label='Portable data hash'
linkToUuid={item && item.portableDataHash} />
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Number of files' value={data && data.fileCount} />
+ label='Number of files' value={item && item.fileCount} />
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Content size' value={data && formatFileSize(data.fileSize)} />
+ label='Content size' value={item && formatFileSize(item.fileSizeTotal)} />
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
label='Owner' linkToUuid={item && item.ownerUuid} />
{(item.properties.container_request || item.properties.containerRequest) &&