13494: Merge branch 'master' into 13494-collection-version-browser
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 12 Nov 2020 19:51:00 +0000 (16:51 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 12 Nov 2020 19:51:00 +0000 (16:51 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

18 files changed:
src/common/formatters.ts
src/components/collection-panel-files/collection-panel-files.tsx
src/components/data-explorer/data-explorer.tsx
src/components/search-input/search-input.tsx
src/services/common-service/common-service.ts
src/services/groups-service/groups-service.ts
src/store/collections-content-address-panel/collections-content-address-middleware-service.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-components/details-panel/process-details.tsx
src/views-components/details-panel/project-details.tsx
src/views/collection-content-address-panel/collection-content-address-panel.tsx
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/public-favorites-panel/public-favorites-panel.tsx

index 55fb050738af1d670984b9c6b38cc3f8bafb7ddc..17917127f1c26ac18221f8b34a2efcfd6413da5f 100644 (file)
@@ -22,6 +22,8 @@ export const formatDate = (isoDate?: string | null, utc: boolean = false) => {
 
 export const formatFileSize = (size?: number) => {
     if (typeof size === "number") {
+        if (size === 0) { return "0 B"; }
+
         for (const { base, unit } of FILE_SIZES) {
             if (size >= base) {
                 return `${(size / base).toFixed()} ${unit}`;
index a89c3a92c89425c7dac55759f63715d3513ffee6..bf551b9ffab15834d07c0052a70b193b3a95afc5 100644 (file)
@@ -84,6 +84,7 @@ export const CollectionPanelFilesComponent = ({ onItemMenuOpen, onSearchChange,
                     <span className={classes.cardHeaderContentTitle}>Files</span>
                     <SearchInput
                         value={searchValue}
+                        label='Search files'
                         onSearch={setSearchValue} />
                 </div>
             }
index 7107bd70823526226e45aa16687bdbcb5609b38a..28ae86cdfb9d76350092c3e05013c105e46c5841 100644 (file)
@@ -47,6 +47,7 @@ interface DataExplorerDataProps<T> {
     items: T[];
     itemsAvailable: number;
     columns: DataColumns<T>;
+    searchLabel?: string;
     searchValue: string;
     rowsPerPage: number;
     rowsPerPageOptions: number[];
@@ -90,7 +91,7 @@ export const DataExplorer = withStyles(styles)(
         render() {
             const {
                 columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
-                rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
+                rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
                 paperKey, fetchMode, currentItemUuid, title
@@ -101,6 +102,7 @@ export const DataExplorer = withStyles(styles)(
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                         <div className={classes.searchBox}>
                             {!hideSearchInput && <SearchInput
+                                label={searchLabel}
                                 value={searchValue}
                                 onSearch={onSearch} />}
                         </div>
index 3b4ab35a1f669e388740fc325bdfede2185a7d2d..02c193c2d34e3f867d541a9a3ee41ea6eb126649 100644 (file)
@@ -34,6 +34,7 @@ const styles: StyleRulesCallback<CssRules> = theme => {
 
 interface SearchInputDataProps {
     value: string;
+    label?: string;
 }
 
 interface SearchInputActionProps {
@@ -45,6 +46,7 @@ type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyl
 
 interface SearchInputState {
     value: string;
+    label: string;
 }
 
 export const DEFAULT_SEARCH_DEBOUNCE = 1000;
@@ -52,7 +54,8 @@ export const DEFAULT_SEARCH_DEBOUNCE = 1000;
 export const SearchInput = withStyles(styles)(
     class extends React.Component<SearchInputProps> {
         state: SearchInputState = {
-            value: ""
+            value: "",
+            label: ""
         };
 
         timeout: number;
@@ -60,14 +63,14 @@ export const SearchInput = withStyles(styles)(
         render() {
             return <form onSubmit={this.handleSubmit}>
                 <FormControl>
-                    <InputLabel>Search files</InputLabel>
+                    <InputLabel>{this.state.label}</InputLabel>
                     <Input
                         type="text"
                         value={this.state.value}
                         onChange={this.handleChange}
                         endAdornment={
                             <InputAdornment position="end">
-                                <Tooltip title='Search files'>
+                                <Tooltip title='Search'>
                                     <IconButton
                                         onClick={this.handleSubmit}>
                                         <SearchIcon />
@@ -80,7 +83,10 @@ export const SearchInput = withStyles(styles)(
         }
 
         componentDidMount() {
-            this.setState({ value: this.props.value });
+            this.setState({
+                value: this.props.value,
+                label: this.props.label || 'Search'
+            });
         }
 
         componentWillReceiveProps(nextProps: SearchInputProps) {
index d605611f46143d8a044379fc56237b2ee7f52fa9..8e00c4ad1abd42897f84cc1634d34a22b5bd48f0 100644 (file)
@@ -22,6 +22,7 @@ export interface ListArguments {
     select?: string[];
     distinct?: boolean;
     count?: string;
+    includeOldVersions?: boolean;
 }
 
 export interface ListResults<T> {
@@ -116,7 +117,7 @@ export class CommonService<T> {
     list(args: ListArguments = {}): Promise<ListResults<T>> {
         const { filters, order, ...other } = args;
         const params = {
-            ...other,
+            ...CommonService.mapKeys(_.snakeCase)(other),
             filters: filters ? `[${filters}]` : undefined,
             order: order ? order : undefined
         };
index 281aa92152abaaa07f46b169b90ffef27308edaf..f61b9eff05ab2d7b420d9ef764be8cdaf3cb215b 100644 (file)
@@ -9,6 +9,7 @@ import { AxiosInstance, AxiosRequestConfig } from "axios";
 import { CollectionResource } from "~/models/collection";
 import { ProjectResource } from "~/models/project";
 import { ProcessResource } from "~/models/process";
+import { WorkflowResource } from "~/models/workflow";
 import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
 import { ApiActions } from "~/services/api/api-actions";
 import { GroupResource } from "~/models/group";
@@ -31,7 +32,8 @@ export interface SharedArguments extends ListArguments {
 export type GroupContentsResource =
     CollectionResource |
     ProjectResource |
-    ProcessResource;
+    ProcessResource |
+    WorkflowResource;
 
 export class GroupsService<T extends GroupResource = GroupResource> extends TrashableResourceService<T> {
 
@@ -73,5 +75,6 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Tras
 export enum GroupContentsResourcePrefix {
     COLLECTION = "collections",
     PROJECT = "groups",
-    PROCESS = "container_requests"
+    PROCESS = "container_requests",
+    WORKFLOW = "workflows"
 }
index a68d13bdec52aa3c6c7ddaf0d5d84593202ab47d..e18922a75ea0aa8a31c5aedb2f2a2b7be9625ef4 100644 (file)
@@ -59,7 +59,8 @@ export class CollectionsWithSameContentAddressMiddlewareService extends DataExpl
                     filters: new FilterBuilder()
                         .addEqual('portable_data_hash', contentAddress)
                         .addILike("name", dataExplorer.searchValue)
-                        .getFilters()
+                        .getFilters(),
+                    includeOldVersions: true
                 });
                 const userUuids = response.items.map(it => {
                     if (extractUuidKind(it.ownerUuid) === ResourceKind.USER) {
index 8afa45325ee7ba211950ab3015fbf4fb76fe603a..138c76efd24a9ba03cc66a71e51bacc775c6c601 100644 (file)
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
 import { ResourceKind, TrashableResource } from '~/models/resource';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, WorkflowIcon, ShareIcon } from '~/components/icon/icon';
+import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon } from '~/components/icon/icon';
 import { formatDate, formatFileSize, formatTime } from '~/common/formatters';
 import { resourceLabel } from '~/common/labels';
 import { connect, DispatchProp } from 'react-redux';
@@ -28,10 +28,10 @@ import { withResourceData } from '~/views-components/data-explorer/with-resource
 import { CollectionResource } from '~/models/collection';
 import { IllegalNamingWarning } from '~/components/warning/warning';
 
-const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind: string }) =>
+const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
-            {renderIcon(item.kind)}
+            {renderIcon(item)}
         </Grid>
         <Grid item>
             <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
@@ -52,15 +52,18 @@ const renderName = (dispatch: Dispatch, item: { name: string; uuid: string, kind
 export const ResourceName = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
-        return resource || { name: '', uuid: '', kind: '' };
-    })((resource: { name: string; uuid: string, kind: string } & DispatchProp<any>) => renderName(resource.dispatch, resource));
+        return resource;
+    })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
 
-const renderIcon = (kind: string) => {
-    switch (kind) {
+const renderIcon = (item: GroupContentsResource) => {
+    switch (item.kind) {
         case ResourceKind.PROJECT:
             return <ProjectIcon />;
         case ResourceKind.COLLECTION:
-            return <CollectionIcon />;
+            if (item.uuid === item.currentVersionUuid) {
+                return <CollectionIcon />;
+            }
+            return <CollectionOldVersionIcon />;
         case ResourceKind.PROCESS:
             return <ProcessIcon />;
         case ResourceKind.WORKFLOW:
@@ -74,10 +77,10 @@ const renderDate = (date?: string) => {
     return <Typography noWrap style={{ minWidth: '100px' }}>{formatDate(date)}</Typography>;
 };
 
-const renderWorkflowName = (item: { name: string; uuid: string, kind: string, ownerUuid: string }) =>
+const renderWorkflowName = (item: WorkflowResource) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
-            {renderIcon(item.kind)}
+            {renderIcon(item)}
         </Grid>
         <Grid item>
             <Typography color="primary" style={{ width: '100px' }}>
@@ -89,7 +92,7 @@ const renderWorkflowName = (item: { name: string; uuid: string, kind: string, ow
 export const ResourceWorkflowName = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
-        return resource || { name: '', uuid: '', kind: '', ownerUuid: '' };
+        return resource;
     })(renderWorkflowName);
 
 const getPublicUuid = (uuidPrefix: string) => {
index 625d8405f0d2f34748b8cd6ffcc4e681fb25c23b..d2457559e4d8207189df0c0818a9b698e60643c1 100644 (file)
@@ -14,7 +14,26 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
         return <CollectionIcon className={className} />;
     }
 
-    getDetails() {
-        return <CollectionDetailsAttributes item={this.item} twoCol={false} />;
+    getTabLabels() {
+        return ['Details', 'Versions'];
+    }
+
+    getDetails(tabNumber: number) {
+        switch (tabNumber) {
+            case 0:
+                return this.getCollectionInfo();
+            case 1:
+                return this.getVersionBrowser();
+            default:
+                return <div />;
+        }
+    }
+
+    private getCollectionInfo() {
+        return <CollectionDetailsAttributes twoCol={false} item={this.item} />;
+    }
+
+    private getVersionBrowser() {
+        return <div />;
     }
 }
index ca8e2cd7dd86d661af91500515add9b809dcaf13..68aa5787fa7ac637359467738b06283a8437ad67 100644 (file)
@@ -12,10 +12,10 @@ export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
         return this.item.name || 'Projects';
     }
 
-    abstract getIcon(className?: string): React.ReactElement<any>;
-    abstract getDetails(): React.ReactElement<any>;
-
-    getActivity(): React.ReactElement<any> {
-        return <div />;
+    getTabLabels(): string[] {
+        return ['Details'];
     }
+
+    abstract getIcon(className?: string): React.ReactElement<any>;
+    abstract getDetails(tabNr?: number): React.ReactElement<any>;
 }
index b6b0cdf10b8078c099f981faa2ae2a22ea9bdfec..bf6e9a4eba3bbe0b4ff1ccc991033aa5100c581d 100644 (file)
@@ -162,14 +162,15 @@ export const DetailsPanel = withStyles(styles)(
                     </Grid>
                     <Grid item>
                         <Tabs value={tabsValue} onChange={this.handleChange}>
-                            <Tab disableRipple label="Details" />
-                            <Tab disableRipple label="Activity" disabled />
+                            { item.getTabLabels().map((tabLabel, idx) =>
+                                <Tab key={`tab-label-${idx}`} disableRipple label={tabLabel} />)
+                            }
                         </Tabs>
                     </Grid>
                     <Grid item xs className={this.props.classes.tabContainer} >
-                        {tabsValue === 0
-                            ? item.getDetails()
-                            : null}
+                    {tabsValue !== undefined
+                        ? item.getDetails(tabsValue)
+                        : null}
                     </Grid>
                 </Grid >;
             }
index 2fbdd31363afb88634f594f5fc53c9dd87b90acb..aa1b3a1d73532de76b07398bc177da7f0d57ea1f 100644 (file)
@@ -20,14 +20,11 @@ export class ProcessDetails extends DetailsData<ProcessResource> {
     getDetails() {
         return <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
-            <DetailsAttribute label='Size' value='---' />
             <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} value={this.item.ownerUuid} />
 
-            {/* Missing attr */}
             <DetailsAttribute label='Status' value={this.item.state} />
             <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
 
-            {/* Missing attrs */}
             <DetailsAttribute label='Started at' value={formatDate(this.item.createdAt)} />
             <DetailsAttribute label='Finished at' value={formatDate(this.item.expiresAt)} />
 
index 1be04b00ee8d31e31a94530cb12754df2aee2084..b901abce8ba7a9d497ab17873152fae196b4c990 100644 (file)
@@ -59,14 +59,10 @@ const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
     withStyles(styles)(
         ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
-            {/* Missing attr */}
-            <DetailsAttribute label='Size' value='---' />
             <DetailsAttribute label='Owner' linkToUuid={project.ownerUuid} lowercaseValue={true} />
             <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
             <DetailsAttribute label='Created at' value={formatDate(project.createdAt)} />
             <DetailsAttribute label='Project UUID' linkToUuid={project.uuid} value={project.uuid} />
-            {/* Missing attr */}
-            {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
             <DetailsAttribute label='Description'>
                 {project.description ?
                     <RichTextEditorLink
index b652b502886653bcf43695cf80eeaefd8d9e86bf..038fea2fd44eb4bb3de3d060a3cf163fedd9b6bb 100644 (file)
@@ -20,7 +20,7 @@ import { navigateTo } from '~/store/navigation/navigation-action';
 import { DataColumns } from '~/components/data-table/data-table';
 import { SortDirection } from '~/components/data-table/data-column';
 import { createTree } from '~/models/tree';
-import { ResourceName, ResourceOwnerName, ResourceLastModifiedDate } from '~/views-components/data-explorer/renderers';
+import { ResourceName, ResourceOwnerName, ResourceLastModifiedDate, ResourceStatus } from '~/views-components/data-explorer/renderers';
 
 type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
 
@@ -59,6 +59,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 enum CollectionContentAddressPanelColumnNames {
     COLLECTION_WITH_THIS_ADDRESS = "Collection with this address",
+    STATUS = "Status",
     LOCATION = "Location",
     LAST_MODIFIED = "Last modified"
 }
@@ -72,6 +73,13 @@ export const collectionContentAddressPanelColumns: DataColumns<string> = [
         filters: createTree(),
         render: uuid => <ResourceName uuid={uuid} />
     },
+    {
+        name: CollectionContentAddressPanelColumnNames.STATUS,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceStatus uuid={uuid} />
+    },
     {
         name: CollectionContentAddressPanelColumnNames.LOCATION,
         selected: true,
@@ -137,6 +145,7 @@ export const CollectionsContentAddressPanel = withStyles(styles)(
                     </Button>
                     <DataExplorer
                         id={COLLECTIONS_CONTENT_ADDRESS_PANEL_ID}
+                        hideSearchInput
                         onRowClick={this.props.onItemClick}
                         onRowDoubleClick={this.props.onItemDoubleClick}
                         onContextMenu={this.props.onContextMenu}
index 4cdd8c55f0d231ed5925a147ff94ba6482b94145..feade60c1d500c86b9655f84beff95793dd434bd 100644 (file)
@@ -262,9 +262,10 @@ export const CollectionPanel = withStyles(styles)(
                 this.props.dispatch<any>(deleteCollectionTag(key, value));
             }
 
-            openCollectionDetails = () => {
+            openCollectionDetails = (e: React.MouseEvent<HTMLElement>) => {
                 const { item } = this.props;
                 if (item) {
+                    e.stopPropagation();
                     this.props.dispatch(openDetailsPanel(item.uuid));
                 }
             }
index 9c906eef76df9bfc31e20692e5f1b5ff4f1e940e..6b5cd4c353a81ec3528a3711fd31a28f0725d4a2 100644 (file)
@@ -84,7 +84,7 @@ export const favoritePanelColumns: DataColumns<string> = [
     },
     {
         name: FavoritePanelColumnNames.OWNER,
-        selected: true,
+        selected: false,
         configurable: true,
         filters: createTree(),
         render: uuid => <ResourceOwner uuid={uuid} />
index 687e17dfaebad737ff6f8fe2d3b6eb2c5e688415..d79b98cf0d5ab2f93fd3b48b8fc4079be7fe134d 100644 (file)
@@ -15,7 +15,13 @@ import { DataTableFilterItem } from '~/components/data-table-filters/data-table-
 import { ContainerRequestState } from '~/models/container-request';
 import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind, Resource, EditableResource } from '~/models/resource';
-import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
+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, getResourceWithEditableStatus } from '~/store/resources/resources';
@@ -82,7 +88,7 @@ export const projectPanelColumns: DataColumns<string> = [
     },
     {
         name: ProjectPanelColumnNames.OWNER,
-        selected: true,
+        selected: false,
         configurable: true,
         filters: createTree(),
         render: uuid => <ResourceOwner uuid={uuid} />
index ab423a6e1b6c10d3106586e8e25cedf1a7235487..635ac6213c2122c1be60973525ea0e912481c3f3 100644 (file)
@@ -17,7 +17,8 @@ import {
     ResourceFileSize,
     ResourceLastModifiedDate,
     ResourceType,
-    ResourceName
+    ResourceName,
+    ResourceOwner
 } from '~/views-components/data-explorer/renderers';
 import { PublicFavoriteIcon } from '~/components/icon/icon';
 import { Dispatch } from 'redux';
@@ -81,6 +82,13 @@ export const publicFavoritePanelColumns: DataColumns<string> = [
         filters: getSimpleObjectTypeFilters(),
         render: uuid => <ResourceType uuid={uuid} />
     },
+    {
+        name: PublicFavoritePanelColumnNames.OWNER,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceOwner uuid={uuid} />
+    },
     {
         name: PublicFavoritePanelColumnNames.FILE_SIZE,
         selected: true,