diff --git a/openbis_ng_ui/src/js/common/consts/ids.js b/openbis_ng_ui/src/js/common/consts/ids.js
index 8bbfa70cf03c1c79c66eb5bce49b541ab68a9469..3e38633948da0d059d40b2d9305d2919bec3619c 100644
--- a/openbis_ng_ui/src/js/common/consts/ids.js
+++ b/openbis_ng_ui/src/js/common/consts/ids.js
@@ -19,6 +19,7 @@ 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'
+const PERSONAL_ACCESS_TOKEN_GRID_ID = 'personal_access_token_grid'
 
 export default {
   WEB_APP_ID,
@@ -41,5 +42,6 @@ export default {
   ENTITY_VALIDATION_PLUGINS_GRID_ID,
   QUERIES_GRID_ID,
   HISTORY_OF_DELETION_GRID_ID,
-  HISTORY_OF_FREEZING_GRID_ID
+  HISTORY_OF_FREEZING_GRID_ID,
+  PERSONAL_ACCESS_TOKEN_GRID_ID
 }
diff --git a/openbis_ng_ui/src/js/common/messages.js b/openbis_ng_ui/src/js/common/messages.js
index 22f4c57b821f1f20f8bedb4cd86f2a6040ea19d7..d34bded6f16867c83be5c08751d84c0c90db2970 100644
--- a/openbis_ng_ui/src/js/common/messages.js
+++ b/openbis_ng_ui/src/js/common/messages.js
@@ -10,6 +10,7 @@ const keys = {
   ADD_ROLE: 'ADD_ROLE',
   ADD_SECTION: 'ADD_SECTION',
   ADD_TERM: 'ADD_TERM',
+  ADD_TOKEN: 'ADD_TOKEN',
   ADD_USER: 'ADD_USER',
   ALL: 'ALL',
   ALL_COLUMNS: 'ALL_COLUMNS',
@@ -143,6 +144,7 @@ const keys = {
   PARAMETERS: 'PARAMETERS',
   PARENTS: 'PARENTS',
   PASSWORD: 'PASSWORD',
+  PERSONAL_ACCESS_TOKEN: 'PERSONAL_ACCESS_TOKEN',
   PERSONAL_ACCESS_TOKENS: 'PERSONAL_ACCESS_TOKENS',
   PLAIN_TEXT: 'PLAIN_TEXT',
   PLUGIN: 'PLUGIN',
@@ -172,6 +174,7 @@ const keys = {
   REGISTRATOR: 'REGISTRATOR',
   REMOVE: 'REMOVE',
   REMOVE_TERM: 'REMOVE_TERM',
+  REMOVE_TOKEN: 'REMOVE_TOKEN',
   RESULT: 'RESULT',
   RESULTS: 'RESULTS',
   RESULTS_RANGE: 'RESULTS_RANGE',
@@ -193,6 +196,7 @@ const keys = {
   SECTION_IS_USED: 'SECTION_IS_USED',
   SELECTED_ROWS: 'SELECTED_ROWS',
   SELECTED_ROWS_NOT_VISIBLE_DUE_TO_FILTERING_AND_PAGING: 'SELECTED_ROWS_NOT_VISIBLE_DUE_TO_FILTERING_AND_PAGING',
+  SESSION_NAME: 'SESSION_NAME',
   SHOW: 'SHOW',
   SHOW_CONTAINER: 'SHOW_CONTAINER',
   SHOW_DETAILS: 'SHOW_DETAILS',
@@ -248,6 +252,7 @@ const messages_en = {
   [keys.ADD_ROLE]: 'Add Role',
   [keys.ADD_SECTION]: 'Add Section',
   [keys.ADD_TERM]: 'Add Term',
+  [keys.ADD_TOKEN]: 'Add Token',
   [keys.ADD_USER]: 'Add User',
   [keys.ALL]: 'All',
   [keys.ALL_COLUMNS]: 'All Columns',
@@ -381,6 +386,7 @@ const messages_en = {
   [keys.PARAMETERS]: 'Parameters',
   [keys.PARENTS]: 'Parents',
   [keys.PASSWORD]: 'Password',
+  [keys.PERSONAL_ACCESS_TOKEN]: 'Personal Access Token',
   [keys.PERSONAL_ACCESS_TOKENS]: 'Personal Access Tokens',
   [keys.PLAIN_TEXT]: 'Plain Text',
   [keys.PLUGIN]: 'Plugin',
@@ -410,6 +416,7 @@ const messages_en = {
   [keys.REGISTRATOR]: 'Registrator',
   [keys.REMOVE]: 'Remove',
   [keys.REMOVE_TERM]: 'Remove Term',
+  [keys.REMOVE_TOKEN]: 'Remove Token',
   [keys.RESULTS]: 'Results',
   [keys.RESULTS_RANGE]: '${0} of ${1}',
   [keys.RESULT]: 'Result',
@@ -431,6 +438,7 @@ const messages_en = {
   [keys.SECTION_IS_USED]: 'This section contains property assignments which are already used by existing entities of "${0}" type. Removing it is also going to remove ${1} existing property value(s) - data will be lost! Are you sure you want to proceed?',
   [keys.SELECTED_ROWS]: 'Selected Rows',
   [keys.SELECTED_ROWS_NOT_VISIBLE_DUE_TO_FILTERING_AND_PAGING]: 'Some selected rows are not visible due to the chosen filtering and paging.',
+  [keys.SESSION_NAME]: 'Session name',
   [keys.SHOW]: 'show',
   [keys.SHOW_CONTAINER]: 'Show Container',
   [keys.SHOW_DETAILS]: 'Show details',
diff --git a/openbis_ng_ui/src/js/components/tools/Tools.jsx b/openbis_ng_ui/src/js/components/tools/Tools.jsx
index 54c4e47cef8530bd9e3b9ce253fb8b1b5a4c4442..22cf0d0febe14e60ea0d068aa364e6e8acc79b68 100644
--- a/openbis_ng_ui/src/js/components/tools/Tools.jsx
+++ b/openbis_ng_ui/src/js/components/tools/Tools.jsx
@@ -63,7 +63,7 @@ class Tools extends React.PureComponent {
       return <ToolSearch searchText={object.id} />
     } else if (object.type === objectType.OVERVIEW) {
       if (object.id === objectType.PERSONAL_ACCESS_TOKEN) {
-        return <PersonalAccessTokenForm />
+        return <PersonalAccessTokenForm object={object} />
       } else {
         return <ToolSearch objectType={object.id} />
       }
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenForm.jsx b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenForm.jsx
index 317a2a5c3e94b08292688aee459beae0a7b6d07b..6fb7dfe811b82e19ce6d4987a19e738248299557 100644
--- a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenForm.jsx
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenForm.jsx
@@ -1,8 +1,139 @@
 import React from 'react'
+import autoBind from 'auto-bind'
+import ComponentContext from '@src/js/components/common/ComponentContext.js'
+import PageWithTwoPanels from '@src/js/components/common/page/PageWithTwoPanels.jsx'
+import GridWithSettings from '@src/js/components/common/grid/GridWithSettings.jsx'
+import GridContainer from '@src/js/components/common/grid/GridContainer.jsx'
+import PersonalAccessTokenFormController from '@src/js/components/tools/form/pat/PersonalAccessTokenFormController.js'
+import PersonalAccessTokenFormFacade from '@src/js/components/tools/form/pat/PersonalAccessTokenFormFacade.js'
+import PersonalAccessTokenFormParameters from '@src/js/components/tools/form/pat/PersonalAccessTokenFormParameters.jsx'
+import PersonalAccessTokenFormButtons from '@src/js/components/tools/form/pat/PersonalAccessTokenFormButtons.jsx'
+import ids from '@src/js/common/consts/ids.js'
+import messages from '@src/js/common/messages.js'
+import logger from '@src/js/common/logger.js'
+
+const columns = [
+  {
+    name: 'sessionName',
+    label: messages.get(messages.SESSION_NAME),
+    getValue: ({ row }) => row.sessionName.value
+  }
+]
 
 class PersonalAccessTokenForm extends React.PureComponent {
+  constructor(props) {
+    super(props)
+    autoBind(this)
+
+    this.state = {}
+
+    if (this.props.controller) {
+      this.controller = this.props.controller
+    } else {
+      this.controller = new PersonalAccessTokenFormController(
+        new PersonalAccessTokenFormFacade()
+      )
+    }
+
+    this.controller.init(new ComponentContext(this))
+  }
+
+  componentDidMount() {
+    this.controller.load()
+  }
+
+  handleClickContainer() {
+    this.controller.handleSelectionChange()
+  }
+
+  handleSelectedRowChange(row) {
+    const { controller } = this
+    if (row) {
+      controller.handleSelectionChange({
+        id: row.id
+      })
+    } else {
+      controller.handleSelectionChange()
+    }
+  }
+
+  handleGridControllerRef(gridController) {
+    this.controller.gridController = gridController
+  }
+
   render() {
-    return <div>Personal Access Token Form</div>
+    logger.log(logger.DEBUG, 'PersonalAccessTokenForm.render')
+
+    const { loadId, loading, loaded } = this.state
+
+    return (
+      <PageWithTwoPanels
+        key={loadId}
+        loading={loading}
+        loaded={loaded}
+        object={{}}
+        renderMainPanel={() => this.renderMainPanel()}
+        renderAdditionalPanel={() => this.renderAdditionalPanel()}
+        renderButtons={() => this.renderButtons()}
+      />
+    )
+  }
+
+  renderMainPanel() {
+    const { pats, selection } = this.state
+
+    return (
+      <GridContainer onClick={this.handleClickContainer}>
+        <GridWithSettings
+          id={ids.PERSONAL_ACCESS_TOKEN_GRID_ID}
+          controllerRef={this.handleGridControllerRef}
+          header={messages.get(messages.PERSONAL_ACCESS_TOKENS)}
+          columns={columns}
+          rows={pats}
+          sort='sessionName'
+          selectable={true}
+          selectedRowId={selection ? selection.params.id : null}
+          onSelectedRowChange={this.handleSelectedRowChange}
+        />
+      </GridContainer>
+    )
+  }
+
+  renderAdditionalPanel() {
+    const { controller } = this
+    const { pats, selection, selectedRow, mode } = this.state
+
+    return (
+      <PersonalAccessTokenFormParameters
+        controller={controller}
+        pats={pats}
+        selection={selection}
+        selectedRow={selectedRow}
+        mode={mode}
+        onChange={controller.handleChange}
+        onSelectionChange={controller.handleSelectionChange}
+        onBlur={controller.handleBlur}
+      />
+    )
+  }
+
+  renderButtons() {
+    const { controller } = this
+    const { pats, selection, changed, mode } = this.state
+
+    return (
+      <PersonalAccessTokenFormButtons
+        pats={pats}
+        selection={selection}
+        changed={changed}
+        mode={mode}
+        onEdit={controller.handleEdit}
+        onSave={controller.handleSave}
+        onCancel={controller.handleCancel}
+        onAdd={controller.handleAdd}
+        onRemove={controller.handleRemove}
+      />
+    )
   }
 }
 
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormButtons.jsx b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormButtons.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..4ec9adaa026c9a7a7862b29e2ac306b31180990b
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormButtons.jsx
@@ -0,0 +1,57 @@
+import React from 'react'
+import PageMode from '@src/js/components/common/page/PageMode.js'
+import PageButtons from '@src/js/components/common/page/PageButtons.jsx'
+import Button from '@src/js/components/common/form/Button.jsx'
+import messages from '@src/js/common/messages.js'
+import logger from '@src/js/common/logger.js'
+
+class PersonalAccessTokenFormButtons extends React.PureComponent {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    logger.log(logger.DEBUG, 'PersonalAccessTokenFormButtons.render')
+
+    const { mode, onEdit, onSave, onCancel, changed } = this.props
+
+    return (
+      <PageButtons
+        mode={mode}
+        changed={changed}
+        onEdit={onEdit}
+        onSave={onSave}
+        onCancel={onCancel}
+        renderAdditionalButtons={params => this.renderAdditionalButtons(params)}
+      />
+    )
+  }
+
+  renderAdditionalButtons({ mode, classes }) {
+    if (mode === PageMode.EDIT) {
+      const { selection, onAdd, onRemove } = this.props
+
+      return (
+        <React.Fragment>
+          <Button
+            name='addToken'
+            label={messages.get(messages.ADD_TOKEN)}
+            styles={{ root: classes.button }}
+            onClick={onAdd}
+          />
+          <Button
+            name='removeToken'
+            label={messages.get(messages.REMOVE_TOKEN)}
+            styles={{ root: classes.button }}
+            disabled={!selection}
+            onClick={onRemove}
+          />
+        </React.Fragment>
+      )
+    } else {
+      return null
+    }
+  }
+}
+
+export default PersonalAccessTokenFormButtons
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormController.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormController.js
new file mode 100644
index 0000000000000000000000000000000000000000..4ed059c0d3b9aa35351fa38860b0e83036540526
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormController.js
@@ -0,0 +1,88 @@
+import autoBind from 'auto-bind'
+import PageControllerChanged from '@src/js/components/common/page/PageControllerChanged.js'
+import PageControllerEdit from '@src/js/components/common/page/PageControllerEdit.js'
+import PageControllerCancel from '@src/js/components/common/page/PageControllerCancel.js'
+import PersonalAccessTokenFormControllerLoad from '@src/js/components/tools/form/pat/PersonalAccessTokenFormControllerLoad.js'
+import PersonalAccessTokenFormControllerAdd from '@src/js/components/tools/form/pat/PersonalAccessTokenFormControllerAdd.js'
+import PersonalAccessTokenFormControllerRemove from '@src/js/components/tools/form/pat/PersonalAccessTokenFormControllerRemove.js'
+import PersonalAccessTokenFormControllerValidate from '@src/js/components/tools/form/pat/PersonalAccessTokenFormControllerValidate.js'
+import PersonalAccessTokenFormControllerChange from '@src/js/components/tools/form/pat/PersonalAccessTokenFormControllerChange.js'
+import PersonalAccessTokenFormControllerSelectionChange from '@src/js/components/tools/form/pat/PersonalAccessTokenFormControllerSelectionChange.js'
+import PersonalAccessTokenFormControllerSave from '@src/js/components/tools/form/pat/PersonalAccessTokenFormControllerSave.js'
+import pages from '@src/js/common/consts/pages.js'
+
+export default class PersonalAccessTokenFormController {
+  constructor(facade) {
+    autoBind(this)
+    this.facade = facade
+  }
+
+  getPage() {
+    return pages.TOOLS
+  }
+
+  init(context) {
+    this.context = context
+    this.object = context.getProps().object
+  }
+
+  load() {
+    return new PersonalAccessTokenFormControllerLoad(this).execute()
+  }
+
+  validate(autofocus) {
+    return new PersonalAccessTokenFormControllerValidate(this).execute(
+      autofocus
+    )
+  }
+
+  changed(changed) {
+    return new PageControllerChanged(this).execute(changed)
+  }
+
+  handleEdit() {
+    return new PageControllerEdit(this).execute()
+  }
+
+  handleCancel() {
+    return new PageControllerCancel(this).execute()
+  }
+
+  handleAdd() {
+    return new PersonalAccessTokenFormControllerAdd(this).execute()
+  }
+
+  handleRemove() {
+    return new PersonalAccessTokenFormControllerRemove(this).execute()
+  }
+
+  handleChange(params) {
+    return new PersonalAccessTokenFormControllerChange(this).execute(params)
+  }
+
+  handleBlur() {
+    return this.validate()
+  }
+
+  handleSelectionChange(params) {
+    return new PersonalAccessTokenFormControllerSelectionChange(this).execute(
+      params
+    )
+  }
+
+  handleSave() {
+    return new PersonalAccessTokenFormControllerSave(this).execute()
+  }
+
+  getFacade() {
+    return this.facade
+  }
+
+  getContext() {
+    return this.context
+  }
+
+  getObject() {
+    return this.object
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerAdd.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerAdd.js
new file mode 100644
index 0000000000000000000000000000000000000000..fe03b8eed43bde12a12e846f43f2175bfd74a8d6
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerAdd.js
@@ -0,0 +1,41 @@
+import _ from 'lodash'
+import FormUtil from '@src/js/components/common/form/FormUtil.js'
+
+export default class PersonalAccessTokenFormControllerAdd {
+  constructor(controller) {
+    this.controller = controller
+    this.context = controller.context
+    this.gridController = controller.gridController
+  }
+
+  async execute() {
+    let { pats } = this.context.getState()
+
+    const newPat = {
+      id: _.uniqueId('pat-'),
+      sessionName: FormUtil.createField({})
+    }
+
+    const newPats = Array.from(pats)
+    newPats.push(newPat)
+
+    await this.context.setState(state => ({
+      ...state,
+      pats: newPats,
+      selection: {
+        params: {
+          id: newPat.id,
+          part: 'code'
+        }
+      }
+    }))
+
+    if (this.gridController) {
+      await this.gridController.load()
+      await this.gridController.selectRow(newPat.id)
+      await this.gridController.showRow(newPat.id)
+    }
+
+    await this.controller.changed(true)
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerChange.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerChange.js
new file mode 100644
index 0000000000000000000000000000000000000000..07dbd338588e2fd6ada87f0f6ba982bcfdd01860
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerChange.js
@@ -0,0 +1,30 @@
+import FormUtil from '@src/js/components/common/form/FormUtil.js'
+
+export default class PersonalAccessTokenFormControllerChange {
+  constructor(controller) {
+    this.controller = controller
+    this.context = controller.context
+    this.gridController = controller.gridController
+  }
+
+  async execute(params) {
+    await this.context.setState(state => {
+      const { newCollection } = FormUtil.changeCollectionItemField(
+        state.pats,
+        params.id,
+        params.field,
+        params.value
+      )
+      return {
+        pats: newCollection
+      }
+    })
+
+    if (this.gridController) {
+      await this.gridController.load()
+      await this.gridController.showRow(params.id)
+    }
+
+    await this.controller.changed(true)
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerLoad.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerLoad.js
new file mode 100644
index 0000000000000000000000000000000000000000..c3d18d3d9459e470f45daffb4df91e162e6cc227
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerLoad.js
@@ -0,0 +1,65 @@
+import _ from 'lodash'
+import PageMode from '@src/js/components/common/page/PageMode.js'
+import FormValidator from '@src/js/components/common/form/FormValidator.js'
+import FormUtil from '@src/js/components/common/form/FormUtil.js'
+import AppController from '@src/js/components/AppController.js'
+
+export default class PersonalAccessTokenFormControllerLoad {
+  constructor(controller) {
+    this.controller = controller
+    this.context = controller.context
+    this.facade = controller.facade
+  }
+
+  async execute() {
+    await this.context.setState({
+      loading: true,
+      mode: PageMode.VIEW,
+      validate: FormValidator.MODE_BASIC
+    })
+
+    try {
+      const loadedPats = await this.facade.loadPats()
+      const pats = loadedPats.map(loadedPat => this._createPat(loadedPat))
+      const selection = this._createSelection(pats)
+
+      return this.context.setState({
+        pats,
+        selection,
+        original: {
+          pats: pats.map(pat => pat.original)
+        }
+      })
+    } catch (error) {
+      AppController.getInstance().errorChange(error)
+    } finally {
+      this.controller.changed(false)
+      this.context.setState({
+        loadId: _.uniqueId('load'),
+        loaded: true,
+        loading: false
+      })
+    }
+  }
+
+  _createPat(loadedPat) {
+    const pat = {
+      id: _.get(loadedPat, 'hash'),
+      hash: FormUtil.createField({
+        value: _.get(loadedPat, 'hash', null),
+        enabled: false
+      }),
+      sessionName: FormUtil.createField({
+        value: _.get(loadedPat, 'sessionName', null),
+        enabled: false
+      }),
+      original: _.cloneDeep(loadedPat)
+    }
+    return pat
+  }
+
+  _createSelection() {
+    // TODO
+    return null
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerRemove.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerRemove.js
new file mode 100644
index 0000000000000000000000000000000000000000..5bbc05df7267925ea31c661950ad9dcd20ef3d19
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerRemove.js
@@ -0,0 +1,29 @@
+export default class PersonalAccessTokenFormControllerRemove {
+  constructor(controller) {
+    this.controller = controller
+    this.context = controller.context
+    this.gridController = controller.gridController
+  }
+
+  async execute() {
+    const { selection, pats } = this.context.getState()
+
+    const patIndex = pats.findIndex(pat => pat.id === selection.params.id)
+
+    const newPats = Array.from(pats)
+    newPats.splice(patIndex, 1)
+
+    await this.context.setState(state => ({
+      ...state,
+      pats: newPats,
+      selection: null
+    }))
+
+    if (this.gridController) {
+      await this.gridController.selectRow(null)
+      await this.gridController.load()
+    }
+
+    await this.controller.changed(true)
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerSave.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerSave.js
new file mode 100644
index 0000000000000000000000000000000000000000..5da75b2d4e89db72593ec0dfe77022580cb05d58
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerSave.js
@@ -0,0 +1,74 @@
+import _ from 'lodash'
+import FormValidator from '@src/js/components/common/form/FormValidator.js'
+import FormUtil from '@src/js/components/common/form/FormUtil.js'
+import AppController from '@src/js/components/AppController.js'
+import openbis from '@src/js/services/openbis.js'
+
+export default class PersonalAccessTokenFormControllerSave {
+  constructor(controller) {
+    this.controller = controller
+    this.context = controller.context
+    this.facade = controller.facade
+  }
+
+  async execute() {
+    try {
+      await this.context.setState({
+        validate: FormValidator.MODE_FULL
+      })
+
+      const valid = await this.controller.validate(true)
+      if (!valid) {
+        return
+      }
+
+      await this.context.setState({
+        loading: true
+      })
+
+      const state = this.context.getState()
+      const pats = this._preparePats(state.pats)
+      const operations = []
+
+      state.original.pats.forEach(originalPat => {
+        const pat = _.find(pats, ['id', originalPat.id])
+        if (!pat) {
+          operations.push(this._deletePatOperation(originalPat))
+        }
+      })
+
+      pats.forEach(pat => {
+        if (!pat.original) {
+          operations.push(this._createPatOperation(pat))
+        }
+      })
+
+      const options = new openbis.SynchronousOperationExecutionOptions()
+      options.setExecuteInOrder(true)
+      await this.facade.executeOperations(operations, options)
+    } catch (error) {
+      AppController.getInstance().errorChange(error)
+    } finally {
+      this.context.setState({
+        loading: false
+      })
+    }
+  }
+
+  _preparePats(pats) {
+    return pats.map(pat => FormUtil.trimFields(pat))
+  }
+
+  _createPatOperation(pat) {
+    const creation = new openbis.PersonalAccessTokenCreation()
+    creation.setSessionName(pat.sessionName.value)
+    return new openbis.CreatePersonalAccessTokensOperation([creation])
+  }
+
+  _deletePatOperation(pat) {
+    const patId = new openbis.PersonalAccessTokenPermId(pat.hash.value)
+    const options = new openbis.PersonalAccessTokenDeletionOptions()
+    options.setReason('deleted via ng_ui')
+    return new openbis.DeletePersonalAccessTokensOperation([patId], options)
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerSelectionChange.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerSelectionChange.js
new file mode 100644
index 0000000000000000000000000000000000000000..35f5a61a30bafd7430e36b9bc57227f3c753e1b6
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerSelectionChange.js
@@ -0,0 +1,30 @@
+export default class PersonalAccessTokenFormControllerSelectionChange {
+  constructor(controller) {
+    this.context = controller.context
+    this.gridController = controller.gridController
+  }
+
+  async execute(params) {
+    let selection = null
+
+    if (params) {
+      selection = {
+        params
+      }
+    }
+
+    this.context.setState(state => ({
+      ...state,
+      selection
+    }))
+
+    if (this.gridController) {
+      await this.gridController.selectRow(
+        selection ? selection.params.id : null
+      )
+      await this.context.setState({
+        selectedRow: this.gridController.getSelectedRow()
+      })
+    }
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerValidate.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerValidate.js
new file mode 100644
index 0000000000000000000000000000000000000000..18b66fff2f90ca86afd0aec8f144681eef79898b
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormControllerValidate.js
@@ -0,0 +1,46 @@
+import PageControllerValidate from '@src/js/components/common/page/PageConrollerValidate.js'
+import messages from '@src/js/common/messages.js'
+
+export default class PersonalAccessTokenFormControllerValidate extends PageControllerValidate {
+  validate(validator) {
+    const { pats } = this.context.getState()
+
+    const newPats = this._validatePats(validator, pats)
+
+    return {
+      pats: newPats
+    }
+  }
+
+  async select(firstError) {
+    const { pats } = this.context.getState()
+
+    if (pats.includes(firstError.object)) {
+      await this.setSelection({
+        params: {
+          id: firstError.object.id,
+          part: firstError.name
+        }
+      })
+
+      if (this.controller.gridController) {
+        await this.controller.gridController.showRow(firstError.object.id)
+      }
+    }
+  }
+
+  _validatePats(validator, pats) {
+    pats.forEach(pat => {
+      this._validatePat(validator, pat)
+    })
+    return validator.withErrors(pats)
+  }
+
+  _validatePat(validator, pat) {
+    validator.validateNotEmpty(
+      pat,
+      'sessionName',
+      messages.get(messages.SESSION_NAME)
+    )
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormFacade.js b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormFacade.js
new file mode 100644
index 0000000000000000000000000000000000000000..464d5a42fff1c0a99d98c57e8f749bd31df49333
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormFacade.js
@@ -0,0 +1,18 @@
+import openbis from '@src/js/services/openbis.js'
+
+export default class PersonalAccessTokenFormFacade {
+  async loadPats() {
+    const criteria = new openbis.PersonalAccessTokenSearchCriteria()
+    const fo = new openbis.PersonalAccessTokenFetchOptions()
+    fo.withOwner()
+    fo.withRegistrator()
+    fo.withModifier()
+    return openbis.searchPersonalAccessTokens(criteria, fo).then(result => {
+      return result.getObjects()
+    })
+  }
+
+  async executeOperations(operations, options) {
+    return openbis.executeOperations(operations, options)
+  }
+}
diff --git a/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormParameters.jsx b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormParameters.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5eab0c91e734a29cc228addee3b406511aa9910c
--- /dev/null
+++ b/openbis_ng_ui/src/js/components/tools/form/pat/PersonalAccessTokenFormParameters.jsx
@@ -0,0 +1,149 @@
+import React from 'react'
+import { withStyles } from '@material-ui/core/styles'
+import Container from '@src/js/components/common/form/Container.jsx'
+import Header from '@src/js/components/common/form/Header.jsx'
+import TextField from '@src/js/components/common/form/TextField.jsx'
+import Message from '@src/js/components/common/form/Message.jsx'
+import messages from '@src/js/common/messages.js'
+import logger from '@src/js/common/logger.js'
+
+const styles = theme => ({
+  field: {
+    paddingBottom: theme.spacing(1)
+  }
+})
+
+class PersonalAccessTokenFormParameters extends React.PureComponent {
+  constructor(props) {
+    super(props)
+    this.state = {}
+    this.references = {
+      sessionName: React.createRef()
+    }
+    this.handleChange = this.handleChange.bind(this)
+    this.handleFocus = this.handleFocus.bind(this)
+    this.handleBlur = this.handleBlur.bind(this)
+  }
+
+  componentDidMount() {
+    this.focus()
+  }
+
+  componentDidUpdate(prevProps) {
+    const prevSelection = prevProps.selection
+    const selection = this.props.selection
+
+    if (prevSelection !== selection) {
+      this.focus()
+    }
+  }
+
+  focus() {
+    const pat = this.getPat(this.props)
+    if (pat && this.props.selection) {
+      const { part } = this.props.selection.params
+      if (part) {
+        const reference = this.references[part]
+        if (reference && reference.current) {
+          reference.current.focus()
+        }
+      }
+    }
+  }
+
+  handleChange(event) {
+    const pat = this.getPat(this.props)
+    this.props.onChange({
+      id: pat.id,
+      field: event.target.name,
+      value: event.target.value
+    })
+  }
+
+  handleFocus(event) {
+    const pat = this.getPat(this.props)
+    this.props.onSelectionChange({
+      id: pat.id,
+      part: event.target.name
+    })
+  }
+
+  handleBlur() {
+    this.props.onBlur()
+  }
+
+  render() {
+    logger.log(logger.DEBUG, 'PersonalAccessTokenFormParameters.render')
+
+    const pat = this.getPat(this.props)
+    if (!pat) {
+      return null
+    }
+
+    return (
+      <Container>
+        <Header>{messages.get(messages.PERSONAL_ACCESS_TOKEN)}</Header>
+        {this.renderMessageVisible(pat)}
+        {this.renderSessionName(pat)}
+      </Container>
+    )
+  }
+
+  renderMessageVisible() {
+    const { classes, selectedRow } = this.props
+
+    if (selectedRow && !selectedRow.visible) {
+      return (
+        <div className={classes.field}>
+          <Message type='warning'>
+            {messages.get(
+              messages.OBJECT_NOT_VISIBLE_DUE_TO_FILTERING_AND_PAGING
+            )}
+          </Message>
+        </div>
+      )
+    } else {
+      return null
+    }
+  }
+
+  renderSessionName(pat) {
+    const { visible, enabled, error, value } = { ...pat.sessionName }
+
+    if (!visible) {
+      return null
+    }
+
+    const { mode, classes } = this.props
+    return (
+      <div className={classes.field}>
+        <TextField
+          reference={this.references.sessionName}
+          label={messages.get(messages.SESSION_NAME)}
+          name='sessionName'
+          mandatory={true}
+          error={error}
+          disabled={!enabled}
+          value={value}
+          mode={mode}
+          onChange={this.handleChange}
+          onFocus={this.handleFocus}
+          onBlur={this.handleBlur}
+        />
+      </div>
+    )
+  }
+
+  getPat(props) {
+    let { pats, selection } = props
+
+    if (selection) {
+      let [pat] = pats.filter(pat => pat.id === selection.params.id)
+      return pat
+    } else {
+      return null
+    }
+  }
+}
+
+export default withStyles(styles)(PersonalAccessTokenFormParameters)
diff --git a/openbis_ng_ui/src/js/services/openbis/api.js b/openbis_ng_ui/src/js/services/openbis/api.js
index 4ff7c613e4ab7d636971343d5f84d3f5f2ab5023..8e2afeb60c014003472257c34e4ce3987fdcbd63 100644
--- a/openbis_ng_ui/src/js/services/openbis/api.js
+++ b/openbis_ng_ui/src/js/services/openbis/api.js
@@ -82,6 +82,10 @@ class Facade {
     return this.promise(this.v3.searchPlugins(criteria, fo))
   }
 
+  searchPersonalAccessTokens(criteria, fo) {
+    return this.promise(this.v3.searchPersonalAccessTokens(criteria, fo))
+  }
+
   searchQueries(criteria, fo) {
     return this.promise(this.v3.searchQueries(criteria, fo))
   }
diff --git a/openbis_ng_ui/src/js/services/openbis/dto.js b/openbis_ng_ui/src/js/services/openbis/dto.js
index da6325faa8c20f1becc3826da0a6da9e0c997c77..436387484239f041f8f67635c23b7e6c4dafe0ce 100644
--- a/openbis_ng_ui/src/js/services/openbis/dto.js
+++ b/openbis_ng_ui/src/js/services/openbis/dto.js
@@ -53,6 +53,14 @@ const CLASS_FULL_NAMES = [
   'as/dto/material/update/MaterialTypeUpdate',
   'as/dto/material/update/UpdateMaterialTypesOperation',
   'as/dto/operation/SynchronousOperationExecutionOptions',
+  'as/dto/pat/create/CreatePersonalAccessTokensOperation',
+  'as/dto/pat/create/PersonalAccessTokenCreation',
+  'as/dto/pat/delete/DeletePersonalAccessTokensOperation',
+  'as/dto/pat/delete/PersonalAccessTokenDeletionOptions',
+  'as/dto/pat/fetchoptions/PersonalAccessTokenFetchOptions',
+  'as/dto/pat/fetchoptions/PersonalAccessTokenFetchOptions',
+  'as/dto/pat/id/PersonalAccessTokenPermId',
+  'as/dto/pat/search/PersonalAccessTokenSearchCriteria',
   'as/dto/person/create/CreatePersonsOperation',
   'as/dto/person/create/PersonCreation',
   'as/dto/person/delete/PersonDeletionOptions',