Remove resource extending, add separate resource data store
authorDaniel Kos <daniel.kos@contractors.roche.com>
Mon, 22 Oct 2018 19:53:46 +0000 (21:53 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Mon, 22 Oct 2018 19:54:39 +0000 (21:54 +0200)
Feature #14349

Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

13 files changed:
src/models/collection.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/resources-data/resources-data-actions.ts [new file with mode: 0644]
src/store/resources-data/resources-data-reducer.ts [new file with mode: 0644]
src/store/resources-data/resources-data.ts [new file with mode: 0644]
src/store/resources/resources.ts
src/store/store.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/details-panel/collection-details.tsx
src/views-components/details-panel/details-data.tsx
src/views-components/details-panel/details-panel.tsx
src/views/collection-panel/collection-panel.tsx

index 735ffa83d8fbda4bb18e660c28bf6277dbb9f688..f8e38f9a0fac227bd2cfeccd62654723f9c66ef5 100644 (file)
@@ -14,8 +14,6 @@ export interface CollectionResource extends TrashableResource {
     replicationDesired: number;
     replicationConfirmed: number;
     replicationConfirmedAt: string;
-    fileSize: number;
-    fileCount: number;
 }
 
 export const getCollectionUrl = (uuid: string) => {
index 99ab6829fa4ff66f96d715691479f34e0297432d..9a1d645b25bda8011e939d82b32eef9e50d72e00 100644 (file)
@@ -14,6 +14,7 @@ import { filterCollectionFilesBySelection } from './collection-panel-files-state
 import { startSubmit, stopSubmit, reset } 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";
 
 export const collectionPanelFilesAction = unionize({
     SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
@@ -29,6 +30,7 @@ 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 }));
     };
 
 export const removeCollectionFiles = (filePaths: string[]) =>
index 27efcd035ecaba857a99e8d0b3f5b534f93c860f..0da669a261ccd223fcdf75ef4d4d1181e3aebe2a 100644 (file)
@@ -20,18 +20,17 @@ import { updateFavorites } from "../favorites/favorites-actions";
 import { PROJECT_PANEL_CURRENT_UUID, projectPanelActions } from './project-panel-action';
 import { Dispatch, MiddlewareAPI } from "redux";
 import { ProjectResource } from "~/models/project";
-import { resourcesActions, updateResources } from "~/store/resources/resources-actions";
+import { updateResources } from "~/store/resources/resources-actions";
 import { getProperty } from "~/store/properties/properties";
 import { snackbarActions, SnackbarKind } from '../snackbar/snackbar-actions';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
 import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
 import { ListResults } from '~/services/common-service/common-resource-service';
 import { loadContainers } from '../processes/processes-actions';
-import { Resource, ResourceKind } from '~/models/resource';
+import { ResourceKind } from '~/models/resource';
 import { getResource } from "~/store/resources/resources";
 import { CollectionResource } from "~/models/collection";
-import { getNode, getNodeDescendantsIds, TreeNode } from "~/models/tree";
-import { CollectionDirectory, CollectionFile, CollectionFileType } from "~/models/collection-file";
+import { resourcesDataActions } from "~/store/resources-data/resources-data-actions";
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -54,7 +53,7 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
                 const resourceUuids = response.items.map(item => item.uuid);
                 api.dispatch<any>(updateFavorites(resourceUuids));
                 api.dispatch(updateResources(response.items));
-                api.dispatch<any>(updateFilesInfo(resourceUuids));
+                api.dispatch<any>(updateResourceData(resourceUuids));
                 await api.dispatch<any>(loadMissingProcessesInformation(response.items));
                 api.dispatch(setItems(response));
             } catch (e) {
@@ -87,28 +86,17 @@ export const loadMissingProcessesInformation = (resources: GroupContentsResource
         }
     };
 
-export const updateFilesInfo = (resourceUuids: string[]) =>
+export const updateResourceData = (resourceUuids: string[]) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const resources = await Promise.all(resourceUuids.map(async uuid => {
+        resourceUuids.map(async uuid => {
             const resource = getResource<CollectionResource>(uuid)(getState().resources);
             if (resource && resource.kind === ResourceKind.COLLECTION) {
                 const files = await services.collectionService.files(uuid);
-                const flattenFiles: (TreeNode<CollectionFile | CollectionDirectory> | undefined)[] = getNodeDescendantsIds('')(files).map(id => getNode(id)(files));
-                let fileSize = 0;
-                let fileCount = 0;
-                if (flattenFiles) {
-                    fileCount = flattenFiles.length;
-                    fileSize = flattenFiles.reduce((acc, f) => {
-                        return acc + (f && f.value.type === CollectionFileType.FILE ? f.value.size : 0);
-                    }, 0);
+                if (files) {
+                    dispatch(resourcesDataActions.SET_FILES({ uuid, files }));
                 }
-
-                resource.fileCount = fileCount;
-                resource.fileSize = fileSize;
             }
-            return resource;
-        }));
-        dispatch(resourcesActions.SET_RESOURCES(resources.filter(res => res) as Resource[]));
+        });
     };
 
 export const setItems = (listResults: ListResults<GroupContentsResource>) =>
diff --git a/src/store/resources-data/resources-data-actions.ts b/src/store/resources-data/resources-data-actions.ts
new file mode 100644 (file)
index 0000000..660699c
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize } from "src/common/unionize";
+import { ofType, UnionOf } from "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>;
diff --git a/src/store/resources-data/resources-data-reducer.ts b/src/store/resources-data/resources-data-reducer.ts
new file mode 100644 (file)
index 0000000..07a3a66
--- /dev/null
@@ -0,0 +1,33 @@
+// 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,
+    });
diff --git a/src/store/resources-data/resources-data.ts b/src/store/resources-data/resources-data.ts
new file mode 100644 (file)
index 0000000..48c1e2b
--- /dev/null
@@ -0,0 +1,8 @@
+// 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];
index 10c82ffe7f36bbd13c7ac8c193b6f1a182c94eac..e7153decd70af11a91131c8f8009a26a74df73eb 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Resource } from "~/models/resource";
-import { ResourceKind } from '../../models/resource';
+import { ResourceKind } from '~/models/resource';
 
 export type ResourcesState = { [key: string]: Resource };
 
index 7a0d58f8a568f555e7dc633c69eec2ee3642517e..fa2a5be9bb658dedcf8a87829b209da72474b2b8 100644 (file)
@@ -42,6 +42,7 @@ import { appInfoReducer } from '~/store/app-info/app-info-reducer';
 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";
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -101,6 +102,7 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     processLogsPanel: processLogsPanelReducer,
     properties: propertiesReducer,
     resources: resourcesReducer,
+    resourcesData: resourcesDataReducer,
     router: routerReducer,
     snackbar: snackbarReducer,
     treePicker: treePickerReducer,
index e76600ed9667ccc41ba02485fa085038a4f37cd4..6e25508d701c212e95343cfefceb31db70a402e5 100644 (file)
@@ -20,6 +20,7 @@ import { WorkflowResource } from '~/models/workflow';
 import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view';
 import { getUuidPrefix } from '~/store/workflow-panel/workflow-panel-actions';
 import { CollectionResource } from "~/models/collection";
+import { getResourceData } from "~/store/resources-data/resources-data";
 
 export const renderName = (item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
@@ -150,7 +151,7 @@ export const renderFileSize = (fileSize?: number) =>
 
 export const ResourceFileSize = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        const resource = getResourceData(props.uuid)(state.resourcesData);
         return { fileSize: resource ? resource.fileSize : 0 };
     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
 
index aec99110f1e2f740ecf7039d8fc20ec221e266f8..98fa388640757d4bd5a09d9cfbdd9d806a617b47 100644 (file)
@@ -28,8 +28,8 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
             <DetailsAttribute label='Collection UUID' link={this.item.uuid} value={this.item.uuid} />
             <DetailsAttribute label='Content address' link={this.item.portableDataHash} value={this.item.portableDataHash} />
             {/* Missing attrs */}
-            <DetailsAttribute label='Number of files' value={this.item.fileCount} />
-            <DetailsAttribute label='Content size' value={formatFileSize(this.item.fileSize)} />
+            <DetailsAttribute label='Number of files' value={this.data && this.data.fileCount} />
+            <DetailsAttribute label='Content size' value={formatFileSize(this.data && this.data.fileSize)} />
         </div>;
     }
 }
index b5ebc36e0eab508f048595bd092b397eb59421c9..45afb02b5a4a562b41a90cd94fcca3c8c3c9eb4e 100644 (file)
@@ -4,9 +4,10 @@
 
 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) {}
+    constructor(protected item: T, protected data?: ResourceData) {}
 
     getTitle(): string {
         return this.item.name || 'Projects';
index f0075558dfe20549a808a5ebf47840139381dece..5e5ccefcd37fd6e9df165fa2ea4d7d4ec0c240d2 100644 (file)
@@ -22,6 +22,8 @@ import { EmptyDetails } from "./empty-details";
 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";
 
 type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
 
@@ -57,13 +59,13 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
 });
 
-const getItem = (resource: DetailsResource): DetailsData => {
+const getItem = (resource: DetailsResource, resourceData?: ResourceData): DetailsData => {
     const res = resource || { kind: undefined, name: 'Projects' };
     switch (res.kind) {
         case ResourceKind.PROJECT:
             return new ProjectDetails(res);
         case ResourceKind.COLLECTION:
-            return new CollectionDetails(res);
+            return new CollectionDetails(res, resourceData);
         case ResourceKind.PROCESS:
             return new ProcessDetails(res);
         default:
@@ -71,11 +73,12 @@ const getItem = (resource: DetailsResource): DetailsData => {
     }
 };
 
-const mapStateToProps = ({ detailsPanel, resources }: RootState) => {
+const mapStateToProps = ({ detailsPanel, resources, resourcesData }: RootState) => {
     const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource;
+    const resourceData = getResourceData(detailsPanel.resourceUuid)(resourcesData);
     return {
         isOpened: detailsPanel.isOpened,
-        item: getItem(resource)
+        item: getItem(resource, resourceData)
     };
 };
 
index 7defc072f93205a952688159a2eb6391929edc32..42f7787c66e205e990f6fea108a50f986208aaad 100644 (file)
@@ -23,6 +23,8 @@ import { getResource } from '~/store/resources/resources';
 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";
 
 type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
 
@@ -55,6 +57,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 interface CollectionPanelDataProps {
     item: CollectionResource;
+    data: ResourceData;
 }
 
 type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
@@ -63,14 +66,13 @@ type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
 
 export const CollectionPanel = withStyles(styles)(
     connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
-        const collection = getResource(props.match.params.id)(state.resources);
-        return {
-            item: collection
-        };
+        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 } = this.props;
+                const { classes, item, data } = this.props;
                 return item
                     ? <>
                         <Card className={classes.card}>
@@ -100,9 +102,9 @@ export const CollectionPanel = withStyles(styles)(
                                             </Tooltip>
                                         </DetailsAttribute>
                                         <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                            label='Number of files' value={item && item.fileCount} />
+                                            label='Number of files' value={data && data.fileCount} />
                                         <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                            label='Content size' value={item && formatFileSize(item.fileSize)} />
+                                            label='Content size' value={data && formatFileSize(data.fileSize)} />
                                         <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                                             label='Owner' value={item && item.ownerUuid} />
                                     </Grid>