replicationDesired: number;
replicationConfirmed: number;
replicationConfirmedAt: string;
- fileSize: number;
- fileCount: number;
}
export const getCollectionUrl = (uuid: string) => {
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>(),
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[]) =>
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) {
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) {
}
};
-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>) =>
--- /dev/null
+// 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>;
--- /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];
// 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 };
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' &&
processLogsPanel: processLogsPanelReducer,
properties: propertiesReducer,
resources: resourcesReducer,
+ resourcesData: resourcesDataReducer,
router: routerReducer,
snackbar: snackbarReducer,
treePicker: treePickerReducer,
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}>
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));
<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>;
}
}
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';
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';
},
});
-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:
}
};
-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)
};
};
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';
interface CollectionPanelDataProps {
item: CollectionResource;
+ data: ResourceData;
}
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}>
</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>