From 1bfe394ccea89c51e784d0cd5a6c08c614c36363 Mon Sep 17 00:00:00 2001
From: pkupczyk <piotr.kupczyk@id.ethz.ch>
Date: Fri, 18 Jun 2021 16:56:19 +0200
Subject: [PATCH] Revert "SSDM-10833 : Entity Deletion History - NG_UI - revert
 commits in master to work in a dedicated branch"

This reverts commit 5bae039462415f5e093b4f6eb17ea3e373e074bb.
---
 openbis_ng_ui/src/js/common/consts/ids.js     |   6 +-
 .../src/js/common/consts/objectType.js        |   2 +
 openbis_ng_ui/src/js/common/consts/routes.js  |   9 +
 openbis_ng_ui/src/js/common/date.js           |  22 +++
 openbis_ng_ui/src/js/common/messages.js       |  54 ++++--
 .../components/common/grid/GridController.js  |  37 ++--
 .../src/js/components/common/grid/GridRow.jsx |  17 +-
 .../src/js/components/tools/Tools.jsx         |   7 +-
 .../tools/browser/ToolBrowserController.js    |  29 +++
 .../components/tools/common/HistoryGrid.jsx   | 166 ++++++++++++++++++
 .../components/tools/common/PluginsGrid.jsx   |   5 +-
 .../components/tools/common/QueriesGrid.jsx   |   5 +-
 .../tools/form/history/HistoryForm.jsx        | 127 ++++++++++++++
 .../js/components/types/common/TypesGrid.jsx  |   5 +-
 .../types/common/VocabulariesGrid.jsx         |   5 +-
 .../users/common/UserGroupsGrid.jsx           |   5 +-
 openbis_ng_ui/src/js/services/openbis/api.js  |   4 +
 openbis_ng_ui/src/js/services/openbis/dto.js  |   5 +
 18 files changed, 470 insertions(+), 40 deletions(-)
 create mode 100644 openbis_ng_ui/src/js/common/date.js
 create mode 100644 openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx
 create mode 100644 openbis_ng_ui/src/js/components/tools/form/history/HistoryForm.jsx

diff --git a/openbis_ng_ui/src/js/common/consts/ids.js b/openbis_ng_ui/src/js/common/consts/ids.js
index 7b561933fb2..9256e4b3e18 100644
--- a/openbis_ng_ui/src/js/common/consts/ids.js
+++ b/openbis_ng_ui/src/js/common/consts/ids.js
@@ -16,6 +16,8 @@ const VOCABULARY_TERMS_GRID_ID = 'vocabulary_terms_grid'
 const DYNAMIC_PROPERTY_PLUGINS_GRID_ID = 'dynamic_property_plugins_grid'
 const ENTITY_VALIDATION_PLUGINS_GRID_ID = 'entity_validation_plugins_grid'
 const QUERIES_GRID_ID = 'queries_grid'
+const HISTORY_OF_DELETION_GRID_ID = 'history_of_deletion_grid'
+const HISTORY_OF_FREEZING_GRID_ID = 'history_of_freezing_grid'
 
 export default {
   WEB_APP_ID,
@@ -35,5 +37,7 @@ export default {
   VOCABULARY_TERMS_GRID_ID,
   DYNAMIC_PROPERTY_PLUGINS_GRID_ID,
   ENTITY_VALIDATION_PLUGINS_GRID_ID,
-  QUERIES_GRID_ID
+  QUERIES_GRID_ID,
+  HISTORY_OF_DELETION_GRID_ID,
+  HISTORY_OF_FREEZING_GRID_ID
 }
diff --git a/openbis_ng_ui/src/js/common/consts/objectType.js b/openbis_ng_ui/src/js/common/consts/objectType.js
index 3cf4daf47be..75c3c93e8af 100644
--- a/openbis_ng_ui/src/js/common/consts/objectType.js
+++ b/openbis_ng_ui/src/js/common/consts/objectType.js
@@ -19,6 +19,7 @@ const USER_GROUP = 'userGroup'
 const DYNAMIC_PROPERTY_PLUGIN = 'dynamicPropertyPlugin'
 const ENTITY_VALIDATION_PLUGIN = 'entityValidationPlugin'
 const QUERY = 'query'
+const HISTORY = 'history'
 
 const SEARCH = 'search'
 const OVERVIEW = 'overview'
@@ -44,6 +45,7 @@ export default {
   DYNAMIC_PROPERTY_PLUGIN,
   ENTITY_VALIDATION_PLUGIN,
   QUERY,
+  HISTORY,
   SEARCH,
   OVERVIEW
 }
diff --git a/openbis_ng_ui/src/js/common/consts/routes.js b/openbis_ng_ui/src/js/common/consts/routes.js
index cefa0431ff2..d09d850b0ba 100644
--- a/openbis_ng_ui/src/js/common/consts/routes.js
+++ b/openbis_ng_ui/src/js/common/consts/routes.js
@@ -209,6 +209,15 @@ const routes = {
     type: objectTypes.OVERVIEW,
     id: objectTypes.QUERY
   }),
+  HISTORY: new Route('/history/:id', {
+    page: pages.TOOLS,
+    type: objectTypes.HISTORY
+  }),
+  HISTORY_OVERVIEW: new Route('/history-overview', {
+    page: pages.TOOLS,
+    type: objectTypes.OVERVIEW,
+    id: objectTypes.HISTORY
+  }),
   DEFAULT: new DefaultRoute()
 }
 
diff --git a/openbis_ng_ui/src/js/common/date.js b/openbis_ng_ui/src/js/common/date.js
new file mode 100644
index 00000000000..afda66e7e7c
--- /dev/null
+++ b/openbis_ng_ui/src/js/common/date.js
@@ -0,0 +1,22 @@
+function format(millis) {
+  if (millis === null) {
+    return ''
+  }
+
+  var date = new Date(millis)
+
+  const year = String(date.getFullYear()).padStart(4, '0')
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  const hours = String(date.getHours()).padStart(2, '0')
+  const minutes = String(date.getMinutes()).padStart(2, '0')
+  const seconds = String(date.getSeconds()).padStart(2, '0')
+
+  return (
+    year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
+  )
+}
+
+export default {
+  format
+}
diff --git a/openbis_ng_ui/src/js/common/messages.js b/openbis_ng_ui/src/js/common/messages.js
index 945a0d82234..3e137f09068 100644
--- a/openbis_ng_ui/src/js/common/messages.js
+++ b/openbis_ng_ui/src/js/common/messages.js
@@ -23,6 +23,7 @@ const keys = {
   CONFIRMATION_REMOVE_IT: 'CONFIRMATION_REMOVE_IT',
   CONFIRMATION_UNSAVED_CHANGES: 'CONFIRMATION_UNSAVED_CHANGES',
   CONTAINER: 'CONTAINER',
+  CONTENT: 'CONTENT',
   CONVERTED: 'CONVERTED',
   CRASH: 'CRASH',
   DATABASE: 'DATABASE',
@@ -31,7 +32,9 @@ const keys = {
   DATA_TYPE: 'DATA_TYPE',
   DATA_TYPE_NOT_SELECTED_FOR_PREVIEW: 'DATA_TYPE_NOT_SELECTED_FOR_PREVIEW',
   DATA_TYPE_NOT_SUPPORTED: 'DATA_TYPE_NOT_SUPPORTED',
+  DATE: 'DATE',
   DEACTIVATE_USER: 'DEACTIVATE_USER',
+  DELETIONS: 'DELETIONS',
   DESCRIPTION: 'DESCRIPTION',
   DISALLOW_DELETION: 'DISALLOW_DELETION',
   DYNAMIC_PROPERTY_PLUGIN: 'DYNAMIC_PROPERTY_PLUGIN',
@@ -39,22 +42,31 @@ const keys = {
   EDIT: 'EDIT',
   EMAIL: 'EMAIL',
   ENTITY: 'ENTITY',
+  ENTITY_IDENTIFIER: 'ENTITY_IDENTIFIER',
   ENTITY_KIND: 'ENTITY_KIND',
+  ENTITY_PROJECT: 'ENTITY_PROJECT',
+  ENTITY_REGISTRATION_DATE: 'ENTITY_REGISTRATION_DATE',
+  ENTITY_REGISTRATOR: 'ENTITY_REGISTRATOR',
+  ENTITY_SPACE: 'ENTITY_SPACE',
+  ENTITY_TYPE: 'ENTITY_TYPE',
   ENTITY_TYPE_PATTERN: 'ENTITY_TYPE_PATTERN',
   ENTITY_VALIDATION_PLUGIN: 'ENTITY_VALIDATION_PLUGIN',
   ENTITY_VALIDATION_PLUGINS: 'ENTITY_VALIDATION_PLUGINS',
   ERROR: 'ERROR',
   EVALUATE: 'EVALUATE',
+  EVENT_TYPE: 'EVENT_TYPE',
   EXECUTE: 'EXECUTE',
   FILTER: 'FILTER',
   FIRST_NAME: 'FIRST_NAME',
   FIRST_PAGE: 'FIRST_PAGE',
   FORM_PREVIEW: 'FORM_PREVIEW',
+  FREEZES: 'FREEZES',
   GENERATED_CODE_PREFIX: 'GENERATED_CODE_PREFIX',
   GENERATE_CODES: 'GENERATE_CODES',
   GLOBAL: 'GLOBAL',
   GROUP: 'GROUP',
   GROUPS: 'GROUPS',
+  HISTORY: 'HISTORY',
   HOME_SPACE: 'HOME_SPACE',
   INHERITED_FROM: 'INHERITED_FROM',
   INITIAL_VALUE: 'INITIAL_VALUE',
@@ -118,6 +130,7 @@ const keys = {
   QUERY_HINT: 'QUERY_HINT',
   QUERY_PUBLIC_WARNING: 'QUERY_PUBLIC_WARNING',
   QUERY_TYPE: 'QUERY_TYPE',
+  REASON: 'REASON',
   REGISTRATOR: 'REGISTRATOR',
   REMOVE: 'REMOVE',
   REMOVE_TERM: 'REMOVE_TERM',
@@ -183,49 +196,63 @@ const messages_en = {
   [keys.CANCEL]: 'Cancel',
   [keys.CLOSE]: 'Close',
   [keys.CODE]: 'Code',
-  [keys.COLLECTION_TYPE]: 'Collection Type',
   [keys.COLLECTION_TYPES]: 'Collection Types',
-  [keys.CONFIRM]: 'Confirm',
+  [keys.COLLECTION_TYPE]: 'Collection Type',
   [keys.CONFIRMATION]: 'Confirmation',
   [keys.CONFIRMATION_ACTIVATE_USER]: 'Are you sure you want to activate the user?',
   [keys.CONFIRMATION_DEACTIVATE_USER]: 'Are you sure you want to deactivate the user?',
   [keys.CONFIRMATION_REMOVE]: 'Are you sure you want to remove "${0}"?',
   [keys.CONFIRMATION_REMOVE_IT]: 'Are you sure you want to remove it?',
   [keys.CONFIRMATION_UNSAVED_CHANGES]: 'Are you sure you want to lose the unsaved changes?',
+  [keys.CONFIRM]: 'Confirm',
   [keys.CONTAINER]: 'Container',
+  [keys.CONTENT]: 'Content',
   [keys.CONVERTED]: 'Converted',
   [keys.CRASH]: 'Something went wrong :(',
   [keys.DATABASE]: 'Database',
-  [keys.DATA_SET_TYPE]: 'Data Set Type',
   [keys.DATA_SET_TYPES]: 'Data Set Types',
+  [keys.DATA_SET_TYPE]: 'Data Set Type',
   [keys.DATA_TYPE]: 'Data Type',
   [keys.DATA_TYPE_NOT_SELECTED_FOR_PREVIEW]: 'Please select a data type to display the field preview.',
   [keys.DATA_TYPE_NOT_SUPPORTED]: 'The selected data type is not supported yet.',
+  [keys.DATE]: 'Date',
   [keys.DEACTIVATE_USER]: 'Deactivate user',
+  [keys.DELETIONS]: 'Deletions',
   [keys.DESCRIPTION]: 'Description',
   [keys.DISALLOW_DELETION]: 'Disallow Deletion',
-  [keys.DYNAMIC_PROPERTY_PLUGIN]: 'Dynamic Property Plugin',
   [keys.DYNAMIC_PROPERTY_PLUGINS]: 'Dynamic Property Plugins',
+  [keys.DYNAMIC_PROPERTY_PLUGIN]: 'Dynamic Property Plugin',
   [keys.EDIT]: 'Edit',
   [keys.EMAIL]: 'Email',
   [keys.ENTITY]: 'Entity',
+  [keys.ENTITY_IDENTIFIER]: 'Entity Identifier',
   [keys.ENTITY_KIND]: 'Entity Kind',
+  [keys.ENTITY_PROJECT]: 'Entity Project',
+  [keys.ENTITY_REGISTRATION_DATE]: 'Entity Registration Date',
+  [keys.ENTITY_REGISTRATOR]: 'Entity Registrator',
+  [keys.ENTITY_SPACE]: 'Entity Space',
+  [keys.ENTITY_TYPE]: 'Entity Type',
   [keys.ENTITY_TYPE_PATTERN]: 'Entity Type Pattern',
-  [keys.ENTITY_VALIDATION_PLUGIN]: 'Entity Validation Plugin',
   [keys.ENTITY_VALIDATION_PLUGINS]: 'Entity Validation Plugins',
+  [keys.ENTITY_VALIDATION_PLUGIN]: 'Entity Validation Plugin',
   [keys.ERROR]: 'Error',
   [keys.EVALUATE]: 'Evaluate',
+  [keys.EVENT_TYPE]: 'Event Type',
   [keys.EXECUTE]: 'Execute',
   [keys.FILTER]: 'Filter',
   [keys.FIRST_NAME]: 'First Name',
   [keys.FIRST_PAGE]: 'First Page',
   [keys.FORM_PREVIEW]: 'Form Preview',
+  [keys.FREEZES]: 'Freezes',
   [keys.GENERATED_CODE_PREFIX]: 'Generated code prefix',
   [keys.GENERATE_CODES]: 'Generate Codes',
   [keys.GLOBAL]: 'Global',
-  [keys.GROUP]: 'Group',
   [keys.GROUPS]: 'Groups',
+  [keys.GROUP]: 'Group',
+  [keys.HISTORY]: 'History',
+  [keys.HISTORY]: 'History',
   [keys.HOME_SPACE]: 'Home Space',
+  [keys.IDENTIFIER]: 'Identifier',
   [keys.INHERITED_FROM]: 'Inherited From',
   [keys.INITIAL_VALUE]: 'Initial Value',
   [keys.IS_NEW_ENTITY]: 'Is New Entity',
@@ -239,8 +266,8 @@ const messages_en = {
   [keys.MAIN_DATA_SET_PATH]: 'Main Data Set Path',
   [keys.MAIN_DATA_SET_PATTERN]: 'Main Data Set Pattern',
   [keys.MANDATORY]: 'Mandatory',
-  [keys.MATERIAL_TYPE]: 'Material Type',
   [keys.MATERIAL_TYPES]: 'Material Types',
+  [keys.MATERIAL_TYPE]: 'Material Type',
   [keys.NAME]: 'Name',
   [keys.NEW_COLLECTION_TYPE]: 'New Collection Type',
   [keys.NEW_DATA_SET_TYPE]: 'New Data Set Type',
@@ -256,8 +283,8 @@ const messages_en = {
   [keys.NO_RESULTS_FOUND]: 'No results found',
   [keys.OBJECT_DOES_NOT_EXIST]: 'Object does not exist',
   [keys.OBJECT_NOT_VISIBLE_DUE_TO_FILTERING_AND_PAGING]: 'The selected object is currently not visible in the list due to the chosen filtering and paging.',
-  [keys.OBJECT_TYPE]: 'Object Type',
   [keys.OBJECT_TYPES]: 'Object Types',
+  [keys.OBJECT_TYPE]: 'Object Type',
   [keys.OFFICIAL]: 'Official',
   [keys.OFFICIAL_TERM_HINT]: 'Unofficial (aka ad-hoc) terms can be created by regular users from the non-admin UI. Once verified a term can be made official by an admin. WARNING: Official terms cannot be made unofficial again.',
   [keys.ONLY_FIRST_RESULTS_SHOWN]: 'Showing only the first ${0} results (${1} results found)',
@@ -288,15 +315,16 @@ const messages_en = {
   [keys.QUERY_HINT]: 'A query can contain parameters in the following format: ${parameterName}.',
   [keys.QUERY_PUBLIC_WARNING]: 'Security warning: this query is public (i.e. visible to other users) and is defined for a database that is not assigned to any space. Please make sure the query returns only data that can be seen by every user or the results contain one of the special columns (i.e. experiment_key/sample_key/data_set_key) that will be used for an automatic query results filtering.',
   [keys.QUERY_TYPE]: 'Query Type',
+  [keys.REASON]: 'Reason',
   [keys.REGISTRATOR]: 'Registrator',
   [keys.REMOVE]: 'Remove',
   [keys.REMOVE_TERM]: 'Remove Term',
-  [keys.RESULT]: 'Result',
   [keys.RESULTS]: 'Results',
-  [keys.ROLE]: 'Role',
+  [keys.RESULT]: 'Result',
   [keys.ROLES]: 'Roles',
   [keys.ROLES_OF_GROUPS]: 'Groups\' Roles',
   [keys.ROLES_OF_USERS]: 'Users\' Roles',
+  [keys.ROLE]: 'Role',
   [keys.ROLE_IS_INHERITED]: 'This role is inherited from ${0} group.',
   [keys.ROLE_IS_INSTANCE_ADMIN]: 'This is an instance admin role. It gives an access to the user and master data management functionality.',
   [keys.ROWS_PER_PAGE]: 'Rows per page: ',
@@ -313,16 +341,16 @@ const messages_en = {
   [keys.SPACE]: 'Space',
   [keys.SQL]: 'SQL',
   [keys.SUBCODES_UNIQUE]: 'Unique Subcodes',
-  [keys.TERM]: 'Term',
   [keys.TERMS]: 'Terms',
+  [keys.TERM]: 'Term',
   [keys.TERM_IS_INTERNAL]: 'This is a system internal term. The term parameters cannot be changed. The term cannot be removed.',
   [keys.TESTER]: 'Tester',
   [keys.TOOLS]: 'Tools',
   [keys.TYPES]: 'Types',
   [keys.UNSAVED_CHANGES]: 'You have unsaved changes',
   [keys.URL_TEMPLATE]: 'URL Template',
-  [keys.USER]: 'User',
   [keys.USERS]: 'Users',
+  [keys.USER]: 'User',
   [keys.USER_ID]: 'User Id',
   [keys.VALIDATION_CANNOT_BE_EMPTY]: '${0} cannot be empty',
   [keys.VALIDATION_CODE_PATTERN]: '${0} can only contain A-Z, a-z, 0-9 and _, -, .',
@@ -331,8 +359,8 @@ const messages_en = {
   [keys.VALIDATION_TERM_CODE_PATTERN]: '${0} can only contain A-Z, a-z, 0-9 and _, -, ., :',
   [keys.VALIDATION_USER_CODE_PATTERN]: '${0} can only contain A-Z, a-z, 0-9 and _, -, ., @',
   [keys.VISIBLE]: 'Visible',
-  [keys.VOCABULARY_TYPE]: 'Vocabulary Type',
   [keys.VOCABULARY_TYPES]: 'Vocabulary Types',
+  [keys.VOCABULARY_TYPE]: 'Vocabulary Type',
   [keys.VOCABULARY_TYPE_IS_INTERNAL]: 'This is a system internal vocabulary. The vocabulary parameters cannot be changed.',
   [keys.XML_SCHEMA]: 'XML Schema',
   [keys.XSLT_SCRIPT]: 'XSLT Script',
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridController.js b/openbis_ng_ui/src/js/components/common/grid/GridController.js
index 1f4f7e685c3..20208cc6ac3 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridController.js
+++ b/openbis_ng_ui/src/js/components/common/grid/GridController.js
@@ -33,24 +33,24 @@ export default class GridController {
       }
 
       columns.push(this.initColumn(column))
+    })
 
-      context.initState({
-        loaded: false,
-        filters: {},
-        page: 0,
-        pageSize: 10,
-        columns,
-        allRows: [],
-        filteredRows: [],
-        sortedRows: [],
-        currentRows: [],
-        selectedRow: null,
-        sort: initialSort,
-        sortDirection: initialSortDirection
-      })
-
-      this.context = context
+    context.initState({
+      loaded: false,
+      filters: {},
+      page: 0,
+      pageSize: 10,
+      columns,
+      allRows: [],
+      filteredRows: [],
+      sortedRows: [],
+      currentRows: [],
+      selectedRow: null,
+      sort: initialSort,
+      sortDirection: initialSortDirection
     })
+
+    this.context = context
   }
 
   initColumn(config) {
@@ -61,14 +61,15 @@ export default class GridController {
       name: config.name,
       label: config.label,
       getValue: config.getValue,
-      render: row => {
+      render: (row, classes) => {
         const value = config.getValue({ row, column })
 
         const renderedValue = config.renderValue
           ? config.renderValue({
               value,
               row,
-              column
+              column,
+              classes
             })
           : value
 
diff --git a/openbis_ng_ui/src/js/components/common/grid/GridRow.jsx b/openbis_ng_ui/src/js/components/common/grid/GridRow.jsx
index dfc6d0f39ff..9f0763380b8 100644
--- a/openbis_ng_ui/src/js/components/common/grid/GridRow.jsx
+++ b/openbis_ng_ui/src/js/components/common/grid/GridRow.jsx
@@ -3,6 +3,7 @@ import { withStyles } from '@material-ui/core/styles'
 import TableRow from '@material-ui/core/TableRow'
 import TableCell from '@material-ui/core/TableCell'
 import logger from '@src/js/common/logger.js'
+import util from '@src/js/common/util.js'
 
 const styles = theme => ({
   row: {
@@ -11,6 +12,12 @@ const styles = theme => ({
   cell: {
     padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
     borderColor: theme.palette.border.secondary
+  },
+  wrap: {
+    whiteSpace: 'normal'
+  },
+  nowrap: {
+    whiteSpace: 'nowrap'
   }
 })
 
@@ -51,9 +58,15 @@ class GridRow extends React.PureComponent {
     const { classes } = this.props
 
     if (column.visible) {
-      let rendered = column.render(row)
+      let rendered = column.render(row, {
+        wrap: classes.wrap,
+        nowrap: classes.nowrap
+      })
       return (
-        <TableCell key={column.name} classes={{ root: classes.cell }}>
+        <TableCell
+          key={column.name}
+          classes={{ root: util.classNames(classes.cell, classes.nowrap) }}
+        >
           {rendered ? rendered : <span>&nbsp;</span>}
         </TableCell>
       )
diff --git a/openbis_ng_ui/src/js/components/tools/Tools.jsx b/openbis_ng_ui/src/js/components/tools/Tools.jsx
index 67401b47355..88b84215d7b 100644
--- a/openbis_ng_ui/src/js/components/tools/Tools.jsx
+++ b/openbis_ng_ui/src/js/components/tools/Tools.jsx
@@ -9,6 +9,7 @@ import ToolBrowser from '@src/js/components/tools/browser/ToolBrowser.jsx'
 import ToolSearch from '@src/js/components/tools/search/ToolSearch.jsx'
 import PluginForm from '@src/js/components/tools/form/plugin/PluginForm.jsx'
 import QueryForm from '@src/js/components/tools/form/query/QueryForm.jsx'
+import HistoryForm from '@src/js/components/tools/form/history/HistoryForm.jsx'
 import messages from '@src/js/common/messages.js'
 
 const styles = () => ({
@@ -50,6 +51,8 @@ class Tools extends React.Component {
       object.type === objectType.QUERY
     ) {
       return <QueryForm object={object} />
+    } else if (object.type === objectType.HISTORY) {
+      return <HistoryForm object={object} />
     } else if (object.type === objectType.SEARCH) {
       return <ToolSearch searchText={object.id} />
     } else if (object.type === objectType.OVERVIEW) {
@@ -70,7 +73,8 @@ class Tools extends React.Component {
         [objectType.ENTITY_VALIDATION_PLUGIN]: messages.get(
           messages.ENTITY_VALIDATION_PLUGINS
         ),
-        [objectType.QUERY]: messages.get(messages.QUERIES)
+        [objectType.QUERY]: messages.get(messages.QUERIES),
+        [objectType.HISTORY]: messages.get(messages.HISTORY)
       }
       label = labels[object.id]
     } else {
@@ -85,6 +89,7 @@ class Tools extends React.Component {
         [objectType.ENTITY_VALIDATION_PLUGIN]:
           messages.get(messages.ENTITY_VALIDATION_PLUGIN) + ': ',
         [objectType.QUERY]: messages.get(messages.QUERY) + ': ',
+        [objectType.HISTORY]: messages.get(messages.HISTORY) + ': ',
         [objectType.SEARCH]: messages.get(messages.SEARCH) + ': '
       }
       label = prefixes[object.type] + object.id
diff --git a/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js b/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js
index 89088077066..a765aafc2da 100644
--- a/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js
+++ b/openbis_ng_ui/src/js/components/tools/browser/ToolBrowserController.js
@@ -71,6 +71,29 @@ export default class ToolBrowserController extends BrowserController {
         }
       })
 
+      const historyNodes = [
+        {
+          id: `history/deletion`,
+          text: openbis.EventType.DELETION,
+          object: {
+            type: objectType.HISTORY,
+            id: openbis.EventType.DELETION
+          },
+          canMatchFilter: true,
+          canRemove: false
+        },
+        {
+          id: `history/freeze`,
+          text: openbis.EventType.FREEZING,
+          object: {
+            type: objectType.HISTORY,
+            id: openbis.EventType.FREEZING
+          },
+          canMatchFilter: true,
+          canRemove: false
+        }
+      ]
+
       let nodes = [
         {
           id: 'dynamicPropertyPlugins',
@@ -104,6 +127,12 @@ export default class ToolBrowserController extends BrowserController {
           children: queryNodes,
           childrenType: objectType.NEW_QUERY,
           canAdd: true
+        },
+        {
+          id: 'history',
+          text: messages.get(messages.HISTORY),
+          children: historyNodes,
+          canAdd: false
         }
       ]
 
diff --git a/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx b/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx
new file mode 100644
index 00000000000..139d2db68b2
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx
@@ -0,0 +1,166 @@
+import React from 'react'
+import Grid from '@src/js/components/common/grid/Grid.jsx'
+import UserLink from '@src/js/components/common/link/UserLink.jsx'
+import Collapse from '@material-ui/core/Collapse'
+import Link from '@material-ui/core/Link'
+import openbis from '@src/js/services/openbis.js'
+import messages from '@src/js/common/messages.js'
+import date from '@src/js/common/date.js'
+import ids from '@src/js/common/consts/ids.js'
+import logger from '@src/js/common/logger.js'
+
+class HistoryGrid extends React.PureComponent {
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      shown: {}
+    }
+  }
+
+  handleVisibilityChange(row, fieldName) {
+    const { onRowChange } = this.props
+    if (onRowChange) {
+      onRowChange(row.id, {
+        [fieldName]: {
+          ...row[fieldName],
+          visible: !row[fieldName].visible
+        }
+      })
+    }
+  }
+
+  render() {
+    logger.log(logger.DEBUG, 'HistoryGrid.render')
+
+    const {
+      rows,
+      selectedRowId,
+      onSelectedRowChange,
+      controllerRef
+    } = this.props
+
+    return (
+      <Grid
+        id={this.getId()}
+        header={this.getHeader()}
+        controllerRef={controllerRef}
+        columns={[
+          {
+            name: 'eventType',
+            label: messages.get(messages.EVENT_TYPE),
+            getValue: ({ row }) => row.eventType.value
+          },
+          {
+            name: 'entityType',
+            label: messages.get(messages.ENTITY_TYPE),
+            getValue: ({ row }) => row.entityType.value
+          },
+          {
+            name: 'entityIdentifier',
+            label: messages.get(messages.ENTITY_IDENTIFIER),
+            getValue: ({ row }) => row.identifier.value
+          },
+          {
+            name: 'entitySpace',
+            label: messages.get(messages.ENTITY_SPACE),
+            getValue: ({ row }) => row.entitySpace.value
+          },
+          {
+            name: 'entityProject',
+            label: messages.get(messages.ENTITY_PROJECT),
+            getValue: ({ row }) => row.entityProject.value
+          },
+          {
+            name: 'entityRegistrator',
+            label: messages.get(messages.ENTITY_REGISTRATOR),
+            getValue: ({ row }) => row.entityRegistrator.value
+          },
+          {
+            name: 'entityRegistrationDate',
+            label: messages.get(messages.ENTITY_REGISTRATION_DATE),
+            getValue: ({ row }) => date.format(row.entityRegistrationDate.value)
+          },
+          {
+            name: 'description',
+            label: messages.get(messages.DESCRIPTION),
+            getValue: ({ row }) => row.description.value
+          },
+          {
+            name: 'reason',
+            label: messages.get(messages.REASON),
+            getValue: ({ row }) => row.reason.value
+          },
+          {
+            name: 'content',
+            label: messages.get(messages.CONTENT),
+            getValue: ({ row }) => row.content.value,
+            renderValue: ({ row }) => {
+              const { value, visible } = row.content
+              if (value) {
+                return (
+                  <div>
+                    <Link
+                      onClick={() => {
+                        this.handleVisibilityChange(row, 'content')
+                      }}
+                    >
+                      {visible ? 'hide' : 'show'}
+                    </Link>
+                    <Collapse
+                      in={visible}
+                      mountOnEnter={true}
+                      unmountOnExit={true}
+                    >
+                      <pre>{value}</pre>
+                    </Collapse>
+                  </div>
+                )
+              } else {
+                return null
+              }
+            }
+          },
+          {
+            name: 'registrator',
+            label: messages.get(messages.USER),
+            getValue: ({ row }) => row.registrator.value,
+            renderValue: ({ value }) => {
+              return <UserLink userId={value} />
+            }
+          },
+          {
+            name: 'registrationDate',
+            label: messages.get(messages.DATE),
+            getValue: ({ row }) => date.format(row.registrationDate.value)
+          }
+        ]}
+        rows={rows}
+        selectedRowId={selectedRowId}
+        onSelectedRowChange={onSelectedRowChange}
+      />
+    )
+  }
+
+  getId() {
+    const { eventType } = this.props
+
+    if (eventType === openbis.EventType.DELETION) {
+      return ids.HISTORY_OF_DELETION_GRID_ID
+    } else if (eventType === openbis.EventType.FREEZING) {
+      return ids.HISTORY_OF_FREEZING_GRID_ID
+    }
+  }
+
+  getHeader() {
+    const { eventType } = this.props
+
+    if (eventType === openbis.EventType.DELETION) {
+      return messages.get(messages.DELETIONS)
+    } else if (eventType === openbis.EventType.FREEZING) {
+      return messages.get(messages.FREEZES)
+    }
+  }
+}
+
+export default HistoryGrid
diff --git a/openbis_ng_ui/src/js/components/tools/common/PluginsGrid.jsx b/openbis_ng_ui/src/js/components/tools/common/PluginsGrid.jsx
index 1f96f375ab1..f833b6d2ade 100644
--- a/openbis_ng_ui/src/js/components/tools/common/PluginsGrid.jsx
+++ b/openbis_ng_ui/src/js/components/tools/common/PluginsGrid.jsx
@@ -43,7 +43,10 @@ class PluginsGrid extends React.PureComponent {
           {
             name: 'description',
             label: messages.get(messages.DESCRIPTION),
-            getValue: ({ row }) => row.description.value
+            getValue: ({ row }) => row.description.value,
+            renderValue: ({ value, classes }) => (
+              <span className={classes.wrap}>{value}</span>
+            )
           },
           {
             name: 'pluginKind',
diff --git a/openbis_ng_ui/src/js/components/tools/common/QueriesGrid.jsx b/openbis_ng_ui/src/js/components/tools/common/QueriesGrid.jsx
index 20320dfa19a..10b8c71918e 100644
--- a/openbis_ng_ui/src/js/components/tools/common/QueriesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/tools/common/QueriesGrid.jsx
@@ -36,7 +36,10 @@ class QueriesGrid extends React.PureComponent {
           {
             name: 'description',
             label: messages.get(messages.DESCRIPTION),
-            getValue: ({ row }) => row.description.value
+            getValue: ({ row }) => row.description.value,
+            renderValue: ({ value, classes }) => (
+              <span className={classes.wrap}>{value}</span>
+            )
           },
           {
             name: 'database',
diff --git a/openbis_ng_ui/src/js/components/tools/form/history/HistoryForm.jsx b/openbis_ng_ui/src/js/components/tools/form/history/HistoryForm.jsx
new file mode 100644
index 00000000000..94461d1c751
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/history/HistoryForm.jsx
@@ -0,0 +1,127 @@
+import _ from 'lodash'
+import React from 'react'
+import autoBind from 'auto-bind'
+import { connect } from 'react-redux'
+import { withStyles } from '@material-ui/core/styles'
+import GridContainer from '@src/js/components/common/grid/GridContainer.jsx'
+import HistoryGrid from '@src/js/components/tools/common/HistoryGrid.jsx'
+import FormUtil from '@src/js/components/common/form/FormUtil.js'
+import openbis from '@src/js/services/openbis.js'
+import store from '@src/js/store/store.js'
+import actions from '@src/js/store/actions/actions.js'
+import logger from '@src/js/common/logger.js'
+
+const styles = () => ({})
+
+class HistoryForm extends React.PureComponent {
+  constructor(props) {
+    super(props)
+    autoBind(this)
+
+    this.state = {
+      loaded: false,
+      selection: null
+    }
+  }
+
+  componentDidMount() {
+    this.load()
+  }
+
+  async load() {
+    try {
+      const rows = await this.loadEvents(this.props.object.id)
+      this.setState(() => ({
+        rows,
+        loaded: true
+      }))
+    } catch (error) {
+      store.dispatch(actions.errorChange(error))
+    }
+  }
+
+  async loadEvents(eventType) {
+    const criteria = new openbis.EventSearchCriteria()
+    criteria.withEventType().thatEquals(eventType)
+
+    const fo = new openbis.EventFetchOptions()
+    fo.withRegistrator()
+
+    const result = await openbis.searchEvents(criteria, fo)
+
+    return result.objects.map(event => ({
+      id: _.get(event, 'id'),
+      eventType: FormUtil.createField({ value: _.get(event, 'eventType') }),
+      entityType: FormUtil.createField({ value: _.get(event, 'entityType') }),
+      entitySpace: FormUtil.createField({
+        value: _.get(event, 'entitySpace')
+      }),
+      entityProject: FormUtil.createField({
+        value: _.get(event, 'entityProject')
+      }),
+      entityRegistrator: FormUtil.createField({
+        value: _.get(event, 'entityRegistrator')
+      }),
+      entityRegistrationDate: FormUtil.createField({
+        value: _.get(event, 'entityRegistrationDate')
+      }),
+      identifier: FormUtil.createField({
+        value: _.get(event, 'identifier')
+      }),
+      description: FormUtil.createField({
+        value: _.get(event, 'description')
+      }),
+      reason: FormUtil.createField({
+        value: _.get(event, 'reason')
+      }),
+      content: FormUtil.createField({
+        value: _.get(event, 'content'),
+        visible: false
+      }),
+      registrator: FormUtil.createField({
+        value: _.get(event, 'registrator.userId')
+      }),
+      registrationDate: FormUtil.createField({
+        value: _.get(event, 'registrationDate')
+      })
+    }))
+  }
+
+  handleRowChange(rowId, change) {
+    const rows = this.state.rows
+    this.setState(state => {
+      const index = rows.findIndex(row => row.id === rowId)
+      if (index !== -1) {
+        const row = rows[index]
+        const newRows = Array.from(rows)
+        newRows[index] = {
+          ...row,
+          ...change
+        }
+        return {
+          ...state,
+          rows: newRows
+        }
+      }
+    })
+  }
+
+  render() {
+    logger.log(logger.DEBUG, 'HistoryForm.render')
+
+    const { id } = this.props.object
+    const { rows } = this.state
+
+    return (
+      <GridContainer>
+        <HistoryGrid
+          eventType={id}
+          rows={rows}
+          onRowChange={this.handleRowChange}
+        />
+      </GridContainer>
+    )
+  }
+}
+
+export default _.flow(connect(), withStyles(styles))(HistoryForm)
diff --git a/openbis_ng_ui/src/js/components/types/common/TypesGrid.jsx b/openbis_ng_ui/src/js/components/types/common/TypesGrid.jsx
index 554b96c41f1..15dc0e50eae 100644
--- a/openbis_ng_ui/src/js/components/types/common/TypesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/types/common/TypesGrid.jsx
@@ -62,7 +62,10 @@ class TypesGrid extends React.PureComponent {
     columns.push({
       name: 'description',
       label: messages.get(messages.DESCRIPTION),
-      getValue: ({ row }) => row.description
+      getValue: ({ row }) => row.description,
+      renderValue: ({ value, classes }) => (
+        <span className={classes.wrap}>{value}</span>
+      )
     })
 
     columns.push({
diff --git a/openbis_ng_ui/src/js/components/types/common/VocabulariesGrid.jsx b/openbis_ng_ui/src/js/components/types/common/VocabulariesGrid.jsx
index 962b146b6c8..6d2144e97c7 100644
--- a/openbis_ng_ui/src/js/components/types/common/VocabulariesGrid.jsx
+++ b/openbis_ng_ui/src/js/components/types/common/VocabulariesGrid.jsx
@@ -34,7 +34,10 @@ class VocabulariesGrid extends React.PureComponent {
           {
             name: 'description',
             label: messages.get(messages.DESCRIPTION),
-            getValue: ({ row }) => row.description
+            getValue: ({ row }) => row.description,
+            renderValue: ({ value, classes }) => (
+              <span className={classes.wrap}>{value}</span>
+            )
           },
           {
             name: 'urlTemplate',
diff --git a/openbis_ng_ui/src/js/components/users/common/UserGroupsGrid.jsx b/openbis_ng_ui/src/js/components/users/common/UserGroupsGrid.jsx
index 16055aec359..820ccb7e065 100644
--- a/openbis_ng_ui/src/js/components/users/common/UserGroupsGrid.jsx
+++ b/openbis_ng_ui/src/js/components/users/common/UserGroupsGrid.jsx
@@ -34,7 +34,10 @@ export default class GroupsGrid extends React.PureComponent {
           {
             name: 'description',
             label: messages.get(messages.DESCRIPTION),
-            getValue: ({ row }) => row.description.value
+            getValue: ({ row }) => row.description.value,
+            renderValue: ({ value, classes }) => (
+              <span className={classes.wrap}>{value}</span>
+            )
           }
         ]}
         rows={rows}
diff --git a/openbis_ng_ui/src/js/services/openbis/api.js b/openbis_ng_ui/src/js/services/openbis/api.js
index 6342a38da5b..e503d363b84 100644
--- a/openbis_ng_ui/src/js/services/openbis/api.js
+++ b/openbis_ng_ui/src/js/services/openbis/api.js
@@ -115,6 +115,10 @@ class Facade {
     return this.promise(this.v3.searchPropertyAssignments(criteria, fo))
   }
 
+  searchEvents(criteria, fo) {
+    return this.promise(this.v3.searchEvents(criteria, fo))
+  }
+
   getSampleTypes(ids, fo) {
     return this.promise(this.v3.getSampleTypes(ids, fo))
   }
diff --git a/openbis_ng_ui/src/js/services/openbis/dto.js b/openbis_ng_ui/src/js/services/openbis/dto.js
index ea7cb50f111..e219445ce2f 100644
--- a/openbis_ng_ui/src/js/services/openbis/dto.js
+++ b/openbis_ng_ui/src/js/services/openbis/dto.js
@@ -23,6 +23,11 @@ const CLASS_FULL_NAMES = [
   'as/dto/dataset/update/UpdateDataSetTypesOperation',
   'as/dto/entitytype/EntityKind',
   'as/dto/entitytype/id/EntityTypePermId',
+  'as/dto/event/fetchoptions/EventFetchOptions',
+  'as/dto/event/search/EventSearchCriteria',
+  'as/dto/event/EntityType',
+  'as/dto/event/Event',
+  'as/dto/event/EventType',
   'as/dto/experiment/create/CreateExperimentTypesOperation',
   'as/dto/experiment/create/ExperimentTypeCreation',
   'as/dto/experiment/delete/DeleteExperimentTypesOperation',
-- 
GitLab