Merge branch 'master' into 14604-ui-improvements
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Fri, 4 Jan 2019 10:42:17 +0000 (11:42 +0100)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Fri, 4 Jan 2019 10:42:17 +0000 (11:42 +0100)
refs #14604

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

1  2 
src/store/details-panel/details-panel-action.ts
src/store/workbench/workbench-actions.ts
src/views-components/data-explorer/renderers.tsx
src/views/collection-panel/collection-panel.tsx

index e13c84f61f45ac63514ca5f528a15d0f27974076,a5ff58730da0f9d2b718749f7cfe0103a5a93d58..52ea0e785ebb1c15b32ff404fdf19dcb3aaa9f76
@@@ -12,12 -12,13 +12,13 @@@ import { ServiceRepository } from '~/se
  import { TagProperty } from '~/models/tag';
  import { startSubmit, stopSubmit } from 'redux-form';
  import { resourcesActions } from '~/store/resources/resources-actions';
 -import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 +import {snackbarActions, SnackbarKind} from '~/store/snackbar/snackbar-actions';
  
  export const SLIDE_TIMEOUT = 500;
  
  export const detailsPanelActions = unionize({
      TOGGLE_DETAILS_PANEL: ofType<{}>(),
+     OPEN_DETAILS_PANEL: ofType<string>(),
      LOAD_DETAILS_PANEL: ofType<string>()
  });
  
@@@ -28,6 -29,8 +29,8 @@@ export const PROJECT_PROPERTIES_DIALOG_
  
  export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
  
+ export const openDetailsPanel = (uuid: string) => detailsPanelActions.OPEN_DETAILS_PANEL(uuid);
  export const openProjectPropertiesDialog = () =>
      (dispatch: Dispatch) => {
          dispatch<any>(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } }));
@@@ -42,7 -45,7 +45,7 @@@ export const deleteProjectProperty = (k
                  delete project.properties[key];
                  const updatedProject = await services.projectService.update(project.uuid, project);
                  dispatch(resourcesActions.SET_RESOURCES([updatedProject]));
 -                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000 }));
 +                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
              }
          } catch (e) {
              dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_PROPERTIES_FORM_NAME }));
@@@ -60,7 -63,7 +63,7 @@@ export const createProjectProperty = (d
                  project.properties[data.key] = data.value;
                  const updatedProject = await services.projectService.update(project.uuid, project);
                  dispatch(resourcesActions.SET_RESOURCES([updatedProject]));
 -                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully added.", hideDuration: 2000 }));
 +                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully added.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
                  dispatch(stopSubmit(PROJECT_PROPERTIES_FORM_NAME));
              }
              return;
index 46598bde085a5a282e702dcf0ba410a551fbe441,8f5bb605179045b21570b8ad7495a42d4cd27954..2bac55af40e998c7e4a41bce9861d902b731877a
@@@ -5,21 -5,37 +5,37 @@@
  import { Dispatch } from 'redux';
  import { RootState } from "~/store/store";
  import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
- import { snackbarActions } from '~/store/snackbar/snackbar-actions';
- import { loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
- import { openProjectPanel, projectPanelActions, setIsProjectPanelTrashed } from '~/store/project-panel/project-panel-action';
- import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '~/store/side-panel-tree/side-panel-tree-actions';
+ import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+ import { favoritePanelActions, loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
+ import {
+     getProjectPanelCurrentUuid,
+     openProjectPanel,
+     projectPanelActions,
+     setIsProjectPanelTrashed
+ } from '~/store/project-panel/project-panel-action';
+ import {
+     activateSidePanelTreeItem,
+     initSidePanelTree,
+     loadSidePanelTreeProjects,
+     SidePanelTreeCategory
+ } from '~/store/side-panel-tree/side-panel-tree-actions';
  import { loadResource, updateResources } from '~/store/resources/resources-actions';
- import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
  import { projectPanelColumns } from '~/views/project-panel/project-panel';
  import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
  import { matchRootRoute } from '~/routes/routes';
- import { setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs, setTrashBreadcrumbs, setBreadcrumbs, setGroupDetailsBreadcrumbs, setGroupsBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+ import {
+     setBreadcrumbs,
+     setGroupDetailsBreadcrumbs,
+     setGroupsBreadcrumbs,
+     setProcessBreadcrumbs,
+     setSharedWithMeBreadcrumbs,
+     setSidePanelBreadcrumbs,
+     setTrashBreadcrumbs
+ } from '~/store/breadcrumbs/breadcrumbs-actions';
  import { navigateToProject } from '~/store/navigation/navigation-action';
  import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
  import { ServiceRepository } from '~/services/services';
  import { getResource } from '~/store/resources/resources';
- import { getProjectPanelCurrentUuid } from '~/store/project-panel/project-panel-action';
  import * as projectCreateActions from '~/store/projects/project-create-actions';
  import * as projectMoveActions from '~/store/projects/project-move-actions';
  import * as projectUpdateActions from '~/store/projects/project-update-actions';
@@@ -35,8 -51,10 +51,10 @@@ import { trashPanelColumns } from "~/vi
  import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
  import { initProcessLogsPanel } from '~/store/process-logs-panel/process-logs-panel-actions';
  import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
- import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
- import { loadSharedWithMePanel } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
+ import {
+     loadSharedWithMePanel,
+     sharedWithMePanelActions
+ } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
  import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
  import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
  import { loadSshKeysPanel } from '~/store/auth/auth-action-ssh';
@@@ -45,31 -63,35 +63,35 @@@ import { loadSiteManagerPanel } from '~
  import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel-view';
  import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
  import { getProgressIndicator } from '~/store/progress-indicator/progress-indicator-reducer';
- import { ResourceKind, extractUuidKind } from '~/models/resource';
+ import { extractUuidKind, ResourceKind } from '~/models/resource';
  import { FilterBuilder } from '~/services/api/filter-builder';
  import { GroupContentsResource } from '~/services/groups-service/groups-service';
- import { unionize, ofType, UnionOf, MatchCases } from '~/common/unionize';
+ import { MatchCases, ofType, unionize, UnionOf } from '~/common/unionize';
  import { loadRunProcessPanel } from '~/store/run-process-panel/run-process-panel-actions';
  import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
- import { SnackbarKind } from '~/store/snackbar/snackbar-actions';
  import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
  import { CollectionResource } from "~/models/collection";
- import { searchResultsPanelActions, loadSearchResultsPanel } from '~/store/search-results-panel/search-results-panel-actions';
+ import {
+     loadSearchResultsPanel,
+     searchResultsPanelActions
+ } from '~/store/search-results-panel/search-results-panel-actions';
  import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view';
  import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions';
  import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
  import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions';
  import { loadUsersPanel, userBindedActions } from '~/store/users/users-actions';
- import { loadLinkPanel, linkPanelActions } from '~/store/link-panel/link-panel-actions';
- import { loadComputeNodesPanel, computeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
+ import { linkPanelActions, loadLinkPanel } from '~/store/link-panel/link-panel-actions';
+ import { computeNodesActions, loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions';
  import { linkPanelColumns } from '~/views/link-panel/link-panel-root';
  import { userPanelColumns } from '~/views/user-panel/user-panel';
  import { computeNodePanelColumns } from '~/views/compute-node-panel/compute-node-panel-root';
- import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+ import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from '~/store/api-client-authorizations/api-client-authorizations-actions';
+ import { apiClientAuthorizationPanelColumns } from '~/views/api-client-authorization-panel/api-client-authorization-panel-root';
  import * as groupPanelActions from '~/store/groups-panel/groups-panel-actions';
  import { groupsPanelColumns } from '~/views/groups-panel/groups-panel';
  import * as groupDetailsPanelActions from '~/store/group-details-panel/group-details-panel-actions';
  import { groupDetailsPanelColumns } from '~/views/group-details-panel/group-details-panel';
+ import { DataTableFetchMode } from "~/components/data-table/data-table";
  
  export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
  
@@@ -102,12 -124,15 +124,15 @@@ export const loadWorkbench = () =
                  dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
                  dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
                  dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
+                 dispatch(searchResultsPanelActions.SET_FETCH_MODE({ fetchMode: DataTableFetchMode.INFINITE }));
                  dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
                  dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
                  dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
                  dispatch(groupDetailsPanelActions.GroupDetailsPanelActions.SET_COLUMNS({columns: groupDetailsPanelColumns}));
                  dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
                  dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
+                 dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
                  dispatch<any>(initSidePanelTree());
                  if (router.location) {
                      const match = matchRootRoute(router.location.pathname);
@@@ -182,8 -207,7 +207,8 @@@ export const createProject = (data: pro
          if (newProject) {
              dispatch(snackbarActions.OPEN_SNACKBAR({
                  message: "Project has been successfully created.",
 -                hideDuration: 2000
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS
              }));
              await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
              dispatch<any>(reloadProjectMatchingUuid([newProject.ownerUuid]));
@@@ -197,14 -221,14 +222,14 @@@ export const moveProject = (data: MoveT
              const oldOwnerUuid = oldProject ? oldProject.ownerUuid : '';
              const movedProject = await dispatch<any>(projectMoveActions.moveProject(data));
              if (movedProject) {
 -                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Project has been moved', hideDuration: 2000 }));
 +                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Project has been moved', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
                  if (oldProject) {
                      await dispatch<any>(loadSidePanelTreeProjects(oldProject.ownerUuid));
                  }
                  dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
              }
          } catch (e) {
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
 +            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
          }
      };
  
@@@ -214,8 -238,7 +239,8 @@@ export const updateProject = (data: pro
          if (updatedProject) {
              dispatch(snackbarActions.OPEN_SNACKBAR({
                  message: "Project has been successfully updated.",
 -                hideDuration: 2000
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS
              }));
              await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
              dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
@@@ -261,8 -284,7 +286,8 @@@ export const createCollection = (data: 
          if (collection) {
              dispatch(snackbarActions.OPEN_SNACKBAR({
                  message: "Collection has been successfully created.",
 -                hideDuration: 2000
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS
              }));
              dispatch<any>(updateResources([collection]));
              dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
@@@ -275,8 -297,7 +300,8 @@@ export const updateCollection = (data: 
          if (collection) {
              dispatch(snackbarActions.OPEN_SNACKBAR({
                  message: "Collection has been successfully updated.",
 -                hideDuration: 2000
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS
              }));
              dispatch<any>(updateResources([collection]));
              dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
@@@ -310,7 -331,7 +335,7 @@@ export const moveCollection = (data: Mo
              dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
              dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
          } catch (e) {
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
 +            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
          }
      };
  
@@@ -331,14 -352,13 +356,14 @@@ export const updateProcess = (data: pro
              if (process) {
                  dispatch(snackbarActions.OPEN_SNACKBAR({
                      message: "Process has been successfully updated.",
 -                    hideDuration: 2000
 +                    hideDuration: 2000,
 +                    kind: SnackbarKind.SUCCESS
                  }));
                  dispatch<any>(updateResources([process]));
                  dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
              }
          } catch (e) {
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
 +            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
          }
      };
  
@@@ -348,9 -368,9 +373,9 @@@ export const moveProcess = (data: MoveT
              const process = await dispatch<any>(processMoveActions.moveProcess(data));
              dispatch<any>(updateResources([process]));
              dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been moved.', hideDuration: 2000 }));
 +            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been moved.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
          } catch (e) {
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
 +            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
          }
      };
  
@@@ -360,9 -380,9 +385,9 @@@ export const copyProcess = (data: CopyF
              const process = await dispatch<any>(processCopyActions.copyProcess(data));
              dispatch<any>(updateResources([process]));
              dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000 }));
 +            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
          } catch (e) {
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
 +            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
          }
      };
  
@@@ -377,18 -397,15 +402,18 @@@ export const loadProcessLog = (uuid: st
  
  export const resourceIsNotLoaded = (uuid: string) =>
      snackbarActions.OPEN_SNACKBAR({
 -        message: `Resource identified by ${uuid} is not loaded.`
 +        message: `Resource identified by ${uuid} is not loaded.`,
 +        kind: SnackbarKind.ERROR
      });
  
  export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
 -    message: 'User is not authenticated'
 +    message: 'User is not authenticated',
 +    kind: SnackbarKind.ERROR
  });
  
  export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
 -    message: 'Could not load user'
 +    message: 'Could not load user',
 +    kind: SnackbarKind.ERROR
  });
  
  export const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
index bb3f4e10ed1ab5b2b7fe5c305befa9bf920735c5,6905e96084083861c25d4842599039721d772c0c..be0fc793a3cb8716d879f1e8400c5acfacdd3e78
@@@ -33,7 -33,7 +33,7 @@@ const renderName = (item: { name: strin
              {renderIcon(item)}
          </Grid>
          <Grid item>
 -            <Typography color="primary" style={{ width: '450px' }}>
 +            <Typography color="primary" style={{ width: 'auto' }}>
                  {item.name}
              </Typography>
          </Grid>
@@@ -81,7 -81,7 +81,7 @@@ const renderWorkflowName = (item: { nam
          </Grid>
      </Grid>;
  
- export const RosurceWorkflowName = connect(
+ export const ResourceWorkflowName = connect(
      (state: RootState, props: { uuid: string }) => {
          const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
          return resource || { name: '', uuid: '', kind: '', ownerUuid: '' };
@@@ -191,33 -191,75 +191,75 @@@ export const ResourceUsername = connect
          return resource || { username: '' };
      })(renderUsername);
  
- // Compute Node Resources
- const renderNodeDate = (date: string) =>
+ // Common methods
+ const renderCommonData = (data: string) =>
+     <Typography noWrap>{data}</Typography>;
+ const renderCommonDate = (date: string) =>
      <Typography noWrap>{formatDate(date)}</Typography>;
  
- const renderNodeData = (data: string) => {
-     return <Typography noWrap>{data}</Typography>;
- };
+ export const CommonUuid = withResourceData('uuid', renderCommonData);
+ // Api Client Authorizations
+ export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
+ export const TokenApiToken = withResourceData('apiToken', renderCommonData);
+ export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
+ export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
+ export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
  
+ export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
+ export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
+ export const TokenScopes = withResourceData('scopes', renderCommonData);
+ export const TokenUserId = withResourceData('userId', renderCommonData);
+ // Compute Node Resources
  const renderNodeInfo = (data: string) => {
      return <Typography>{JSON.stringify(data, null, 4)}</Typography>;
  };
  
- export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
+ const clusterColors = [
+     ['#f44336', '#fff'],
+     ['#2196f3', '#fff'],
+     ['#009688', '#fff'],
+     ['#cddc39', '#fff'],
+     ['#ff9800', '#fff']
+ ];
+ export const ResourceCluster = (props: { uuid: string }) => {
+     const CLUSTER_ID_LENGTH = 5;
+     const pos = props.uuid.indexOf('-');
+     const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substr(0, pos) : '';
+     const ci = pos >= CLUSTER_ID_LENGTH ? (props.uuid.charCodeAt(0) + props.uuid.charCodeAt(1)) % clusterColors.length : 0;
+     return <Typography>
+         <div style={{
+             backgroundColor: clusterColors[ci][0],
+             color: clusterColors[ci][1],
+             padding: "2px 7px",
+             borderRadius: 3
+         }}>{clusterId}</div>
+     </Typography>;
+ };
  
- export const ComputeNodeUuid = withResourceData('uuid', renderNodeData);
+ export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
  
- export const ComputeNodeDomain = withResourceData('domain', renderNodeData);
+ export const ComputeNodeDomain = withResourceData('domain', renderCommonData);
  
- export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderNodeDate);
+ export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderCommonDate);
  
- export const ComputeNodeHostname = withResourceData('hostname', renderNodeData);
+ export const ComputeNodeHostname = withResourceData('hostname', renderCommonData);
  
- export const ComputeNodeIpAddress = withResourceData('ipAddress', renderNodeData);
+ export const ComputeNodeIpAddress = withResourceData('ipAddress', renderCommonData);
  
- export const ComputeNodeJobUuid = withResourceData('jobUuid', renderNodeData);
+ export const ComputeNodeJobUuid = withResourceData('jobUuid', renderCommonData);
  
- export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderNodeDate);
+ export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderCommonDate);
  
  // Links Resources
  const renderLinkName = (item: { name: string }) =>
index 4124344d3f3311129d1320541352221a11416df1,4ec39ca84debcf2fe6b3e4c8ede9b738f90422e3..3557afe574cb8c064488f146f650e5c737e49dce
@@@ -18,13 -18,14 +18,14 @@@ import { CollectionPanelFiles } from '~
  import * as CopyToClipboard from 'react-copy-to-clipboard';
  import { CollectionTagForm } from './collection-tag-form';
  import { deleteCollectionTag, navigateToProcess } from '~/store/collection-panel/collection-panel-action';
 -import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 +import {snackbarActions, SnackbarKind} from '~/store/snackbar/snackbar-actions';
  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";
+ import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
  
  type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value' | 'link';
  
@@@ -77,13 -78,18 +78,18 @@@ export const CollectionPanel = withStyl
          return { item, data };
      })(
          class extends React.Component<CollectionPanelProps> {
              render() {
                  const { classes, item, data, dispatch } = this.props;
                  return item
                      ? <>
                          <Card className={classes.card}>
                              <CardHeader
-                                 avatar={<CollectionIcon className={classes.iconHeader} />}
+                                 avatar={
+                                     <IconButton onClick={this.openCollectionDetails}>
+                                         <CollectionIcon className={classes.iconHeader} />
+                                     </IconButton>
+                                 }
                                  action={
                                      <Tooltip title="More options" disableFocusListener>
                                          <IconButton
                                      </Tooltip>
                                  }
                                  title={item && item.name}
-                                 subheader={item && item.description} />
+                                 titleTypographyProps={this.titleProps}
+                                 subheader={item && item.description}
+                                 subheaderTypographyProps={this.titleProps} />
                              <CardContent>
                                  <Grid container direction="column">
                                      <Grid item xs={6}>
                                              label='Content size' value={data && formatFileSize(data.fileSize)} />
                                          <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                                              label='Owner' value={item && item.ownerUuid} />
-                                         <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request  || item.properties.containerRequest))}>
+                                         <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
                                              <DetailsAttribute classLabel={classes.link} label='Link to process' />
                                          </span>
                                      </Grid>
              onCopy = () => {
                  this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
                      message: "Uuid has been copied",
 -                    hideDuration: 2000
 +                    hideDuration: 2000,
 +                    kind: SnackbarKind.SUCCESS
                  }));
              }
+             openCollectionDetails = () => {
+                 const { item } = this.props;
+                 if (item) {
+                     this.props.dispatch(openDetailsPanel(item.uuid));
+                 }
+             }
+             titleProps = {
+                 onClick: this.openCollectionDetails
+             };
          }
      )
  );