diff --git a/search_api.links.action.yml b/search_api.links.action.yml
index 7d85b506..5b99f87f 100644
--- a/search_api.links.action.yml
+++ b/search_api.links.action.yml
@@ -13,8 +13,3 @@ search_api.execute_tasks:
   title: 'Execute pending tasks'
   appears_on:
     - search_api.overview
-entity.search_api_index.add_fields:
-  route_name: entity.search_api_index.add_fields
-  title: 'Add fields'
-  appears_on:
-    - entity.search_api_index.fields
diff --git a/search_api.routing.yml b/search_api.routing.yml
index 34c34480..4c8c4e35 100644
--- a/search_api.routing.yml
+++ b/search_api.routing.yml
@@ -124,7 +124,19 @@ entity.search_api_index.fields:
     _entity_access: 'search_api_index.fields'
 
 entity.search_api_index.add_fields:
-  path: '/admin/config/search/search-api/index/{search_api_index}/fields/add'
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/add/nojs'
+  options:
+    parameters:
+      search_api_index:
+        tempstore: TRUE
+        type: 'entity:search_api_index'
+  defaults:
+    _entity_form: 'search_api_index.add_fields'
+  requirements:
+    _entity_access: 'search_api_index.fields'
+
+entity.search_api_index.add_fields_ajax:
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/add/ajax'
   options:
     parameters:
       search_api_index:
@@ -136,7 +148,7 @@ entity.search_api_index.add_fields:
     _entity_access: 'search_api_index.fields'
 
 entity.search_api_index.field_config:
-  path: '/admin/config/search/search-api/index/{search_api_index}/fields/{field_id}/edit'
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/edit/{field_id}'
   options:
     parameters:
       search_api_index:
@@ -149,7 +161,7 @@ entity.search_api_index.field_config:
     _entity_access: 'search_api_index.fields'
 
 entity.search_api_index.remove_field:
-  path: '/admin/config/search/search-api/index/{search_api_index}/fields/{field_id}/remove'
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/remove/{field_id}'
   options:
     parameters:
       search_api_index:
diff --git a/search_api.theme.inc b/search_api.theme.inc
index ebb0bd7e..bb315a7d 100644
--- a/search_api.theme.inc
+++ b/search_api.theme.inc
@@ -35,15 +35,14 @@ function theme_search_api_admin_fields_table($variables) {
           $row[] = $cell;
         }
       }
-      if (empty($form['fields'][$name]['description']['#value'])) {
-        $rows[] = Utility::deepCopy($row);
-      }
-      else {
-        $rows[] = array(
-          'data' => $row,
-          'title' => strip_tags($form['fields'][$name]['description']['#value']),
-        );
+      $row = array(
+        'data' => $row,
+        'data-field-row-id' => $name,
+      );
+      if (!empty($form['fields'][$name]['description']['#value'])) {
+        $row['title'] = strip_tags($form['fields'][$name]['description']['#value']);
       }
+      $rows[] = $row;
     }
   }
 
@@ -60,6 +59,7 @@ function theme_search_api_admin_fields_table($variables) {
     '#theme' => 'table',
     '#header' => $form['#header'],
     '#rows' => $rows,
+    '#empty' => t('No fields have been added for this datasource.'),
   );
 
   $output .= render($build);
@@ -160,6 +160,9 @@ function theme_search_api_form_item_list(array $variables) {
   $build = array(
     '#theme' => 'item_list',
   );
+  if (!empty($element['#title'])) {
+    $build['#title'] = $element['#title'];
+  }
   foreach (Element::children($element) as $key) {
     $build['#items'][$key] = $element[$key];
   }
diff --git a/src/Controller/IndexController.php b/src/Controller/IndexController.php
index 07bdebdc..a1d9c487 100644
--- a/src/Controller/IndexController.php
+++ b/src/Controller/IndexController.php
@@ -3,9 +3,14 @@
 namespace Drupal\search_api\Controller;
 
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\RemoveCommand;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\SearchApiException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
@@ -14,6 +19,57 @@
 class IndexController extends ControllerBase {
 
   /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack|null
+   */
+  protected $requestStack;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    /** @var static $controller */
+    $controller = parent::create($container);
+
+    $controller->setRequestStack($container->get('request_stack'));
+
+    return $controller;
+  }
+
+  /**
+   * Retrieves the request stack.
+   *
+   * @return \Symfony\Component\HttpFoundation\RequestStack
+   *   The request stack.
+   */
+  public function getRequestStack() {
+    return $this->requestStack ?: \Drupal::service('request_stack');
+  }
+
+  /**
+   * Retrieves the current request.
+   *
+   * @return \Symfony\Component\HttpFoundation\Request|null
+   */
+  public function getRequest() {
+    return $this->getRequestStack()->getCurrentRequest();
+  }
+
+  /**
+   * Sets the request stack.
+   *
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The new request stack.
+   *
+   * @return $this
+   */
+  public function setRequestStack(RequestStack $request_stack) {
+    $this->requestStack = $request_stack;
+    return $this;
+  }
+
+  /**
    * Displays information about a search index.
    *
    * @param \Drupal\search_api\IndexInterface $search_api_index
@@ -96,10 +152,12 @@ public function indexBypassEnable(IndexInterface $search_api_index) {
    */
   public function removeField(IndexInterface $search_api_index, $field_id) {
     $fields = $search_api_index->getFields();
+    $success = FALSE;
     if (isset($fields[$field_id])) {
       try {
         $search_api_index->removeField($field_id);
         $search_api_index->save();
+        $success = TRUE;
       }
       catch (SearchApiException $e) {
         $args['%field'] = $fields[$field_id]->getLabel();
@@ -110,7 +168,13 @@ public function removeField(IndexInterface $search_api_index, $field_id) {
       throw new NotFoundHttpException();
     }
 
-    // Redirect to the index's "View" page.
+    // If this is an AJAX request, just remove the row in question.
+    if ($success && $this->getRequest()->request->get(AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER)) {
+      $response = new AjaxResponse();
+      $response->addCommand(new RemoveCommand("tr[data-field-row-id='$field_id']"));
+      return $response;
+    }
+    // Redirect to the index's "Fields" page.
     $url = $search_api_index->toUrl('fields');
     return $this->redirect($url->getRouteName(), $url->getRouteParameters());
   }
diff --git a/src/Entity/Index.php b/src/Entity/Index.php
index bbe53e78..69fe5192 100644
--- a/src/Entity/Index.php
+++ b/src/Entity/Index.php
@@ -78,7 +78,8 @@
  *     "add-form" = "/admin/config/search/search-api/add-index",
  *     "edit-form" = "/admin/config/search/search-api/index/{search_api_index}/edit",
  *     "fields" = "/admin/config/search/search-api/index/{search_api_index}/fields",
- *     "add-fields" = "/admin/config/search/search-api/index/{search_api_index}/fields/add",
+ *     "add-fields" = "/admin/config/search/search-api/index/{search_api_index}/fields/add/nojs",
+ *     "add-fields-ajax" = "/admin/config/search/search-api/index/{search_api_index}/fields/add/ajax",
  *     "break-lock-form" = "/admin/config/search/search-api/index/{search_api_index}/fields/break-lock",
  *     "processors" = "/admin/config/search/search-api/index/{search_api_index}/processors",
  *     "delete-form" = "/admin/config/search/search-api/index/{search_api_index}/delete",
diff --git a/src/Form/FieldConfigurationForm.php b/src/Form/FieldConfigurationForm.php
index c81912f5..ccb12f41 100644
--- a/src/Form/FieldConfigurationForm.php
+++ b/src/Form/FieldConfigurationForm.php
@@ -2,9 +2,12 @@
 
 namespace Drupal\search_api\Form;
 
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\search_api\Processor\ConfigurablePropertyInterface;
@@ -33,6 +36,13 @@ class FieldConfigurationForm extends EntityForm {
   protected $field;
 
   /**
+   * The "id" attribute of the generated form.
+   *
+   * @var string
+   */
+  protected $formIdAttribute;
+
+  /**
    * {@inheritdoc}
    */
   public function getFormId() {
@@ -76,9 +86,6 @@ public static function create(ContainerInterface $container) {
   public function buildForm(array $form, FormStateInterface $form_state) {
     $field = $this->getField();
 
-    $args['%field'] = $field->getLabel();
-    $form['#title'] = $this->t('Edit field %field', $args);
-
     if (!$field) {
       $args['@id'] = $this->getRequest()->attributes->get('field_id');
       $form['message'] = array(
@@ -87,6 +94,21 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       return $form;
     }
 
+    $args['%field'] = $field->getLabel();
+    $form['#title'] = $this->t('Edit field %field', $args);
+
+    if ($this->getRequest()->query->get('modal_redirect')) {
+      $form['title']['#markup'] = new FormattableMarkup('<h2>@title</h2>', ['@title' => $form['#title']]);
+      Html::setIsAjax(TRUE);
+    }
+
+    $this->formIdAttribute = Html::getUniqueId($this->getFormId());
+    $form['#id'] = $this->formIdAttribute;
+
+    $form['messages'] = [
+      '#type' => 'status_messages',
+    ];
+
     $property = $field->getDataDefinition();
     if (!($property instanceof ConfigurablePropertyInterface)) {
       $args['%field'] = $field->getLabel();
@@ -123,11 +145,16 @@ protected function actions(array $form, FormStateInterface $form_state) {
     $actions = parent::actions($form, $form_state);
     unset($actions['delete']);
 
-    $actions['cancel'] = array(
-      '#type' => 'link',
-      '#title' => $this->t('Cancel'),
-      '#url' => $this->entity->toUrl('fields'),
-    );
+    if ($this->getRequest()->query->get('modal_redirect')) {
+      $actions['submit']['#ajax']['wrapper'] = $this->formIdAttribute;
+    }
+    else {
+      $actions['cancel'] = array(
+        '#type' => 'link',
+        '#title' => $this->t('Cancel'),
+        '#url' => $this->entity->toUrl('fields'),
+      );
+    }
 
     return $actions;
   }
@@ -152,7 +179,16 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $property->submitConfigurationForm($field, $form, $form_state);
 
     drupal_set_message($this->t('The field configuration was successfully saved.'));
-    $form_state->setRedirectUrl($this->entity->toUrl('fields'));
+    if ($this->getRequest()->query->get('modal_redirect')) {
+      $url = $this->entity->toUrl('add-fields-ajax')
+        ->setOption('query', [
+          MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
+        ]);
+      $form_state->setRedirectUrl($url);
+    }
+    else {
+      $form_state->setRedirectUrl($this->entity->toUrl('fields'));
+    }
   }
 
   /**
diff --git a/src/Form/IndexAddFieldsForm.php b/src/Form/IndexAddFieldsForm.php
index ec1f0087..6e599b24 100644
--- a/src/Form/IndexAddFieldsForm.php
+++ b/src/Form/IndexAddFieldsForm.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Entity\EntityForm;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
@@ -67,6 +68,13 @@ class IndexAddFieldsForm extends EntityForm {
   protected $unmappedFields = array();
 
   /**
+   * The "id" attribute of the generated form.
+   *
+   * @var string
+   */
+  protected $formIdAttribute;
+
+  /**
    * {@inheritdoc}
    */
   public function getFormId() {
@@ -150,15 +158,22 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $args['%index'] = $index->label();
     $form['#title'] = $this->t('Add fields to index %index', $args);
 
-    $form['properties'] = array(
-      '#theme' => 'search_api_form_item_list',
-    );
+    $this->formIdAttribute = Html::getUniqueId($this->getFormId());
+    $form['#id'] = $this->formIdAttribute;
+
+    $form['messages'] = [
+      '#type' => 'status_messages',
+    ];
+
     $datasources = array(
       '' => NULL,
     );
     $datasources += $this->entity->getDatasources();
-    foreach ($datasources as $datasource) {
-      $form['properties'][] = $this->getDatasourceListItem($datasource);
+    foreach ($datasources as $datasource_id => $datasource) {
+      $item = $this->getDatasourceListItem($datasource);
+      if ($item) {
+        $form['datasources']['datasource_' . $datasource_id] = $item;
+      }
     }
 
     $form['actions'] = $this->actionsElement($form, $form_state);
@@ -200,52 +215,25 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    *   attached properties.
    */
   protected function getDatasourceListItem(DatasourceInterface $datasource = NULL) {
-    $item = array(
-      '#type' => 'container',
-      '#attributes' => array(
-        'class' => array('container-inline'),
-      ),
-    );
-
-    $active = FALSE;
-    $datasource_id = $datasource ? $datasource->getPluginId() : '';
-    $active_datasource = $this->getParameter('datasource');
-    if (isset($active_datasource)) {
-      $active = $active_datasource == $datasource_id;
-    }
-
-    $url = $this->entity->toUrl('add-fields');
-    if ($active) {
-      $expand_link = array(
-        '#type' => 'link',
-        '#title' => '(-) ',
-        '#url' => $url,
-      );
-    }
-    else {
-      $url->setOption('query', array('datasource' => $datasource_id));
-      $expand_link = array(
-        '#type' => 'link',
-        '#title' => '(+) ',
-        '#url' => $url,
-      );
-    }
-    $item['expand_link'] = $expand_link;
-
-    $label = $datasource ? Html::escape($datasource->label()) : $this->t('General');
-    $item['label']['#markup'] = $label;
-
-    if ($active) {
-      $properties = $this->entity->getPropertyDefinitions($datasource_id ?: NULL);
-      if ($properties) {
+    $datasource_id = $datasource ? $datasource->getPluginId() : NULL;
+    $datasource_id_param = $datasource_id ?: '';
+    $properties = $this->entity->getPropertyDefinitions($datasource_id);
+    if ($properties) {
+      $active_property_path = '';
+      $active_datasource = $this->getParameter('datasource');
+      if ($active_datasource !== NULL && $active_datasource == $datasource_id_param) {
         $active_property_path = $this->getParameter('property_path', '');
-        $base_url = clone $url;
-        $base_url->setOption('query', array('datasource' => $datasource_id));
-        $item['properties'] = $this->getPropertiesList($properties, $active_property_path, $base_url);
       }
+
+      $base_url = $this->entity->toUrl('add-fields');
+      $base_url->setOption('query', array('datasource' => $datasource_id_param));
+
+      $item = $this->getPropertiesList($properties, $active_property_path, $base_url, $datasource_id);
+      $item['#title'] = $datasource ? $datasource->label() : $this->t('General');
+      return $item;
     }
 
-    return $item;
+    return NULL;
   }
 
   /**
@@ -258,6 +246,9 @@ protected function getDatasourceListItem(DatasourceInterface $datasource = NULL)
    * @param \Drupal\Core\Url $base_url
    *   The base URL to which property path parameters should be added for
    *   the navigation links.
+   * @param string|null $datasource_id
+   *   The datasource ID of the listed properties, or NULL for
+   *   datasource-independent properties.
    * @param string $parent_path
    *   (optional) The common property path prefix of the given properties.
    * @param string $label_prefix
@@ -267,7 +258,7 @@ protected function getDatasourceListItem(DatasourceInterface $datasource = NULL)
    *   A render array representing the given properties and, possibly, nested
    *   properties.
    */
-  protected function getPropertiesList(array $properties, $active_property_path, Url $base_url, $parent_path = '', $label_prefix = '') {
+  protected function getPropertiesList(array $properties, $active_property_path, Url $base_url, $datasource_id, $parent_path = '', $label_prefix = '') {
     $list = array(
       '#theme' => 'search_api_form_item_list',
     );
@@ -340,55 +331,59 @@ protected function getPropertiesList(array $properties, $active_property_path, U
         continue;
       }
 
+      $item = array(
+        '#type' => 'container',
+        '#attributes' => array(
+          'class' => array('container-inline'),
+        ),
+      );
+
       $nested_list = array();
-      $expand_link = array();
       if ($nested_properties) {
         if ($key == $active_item) {
           $link_url = clone $base_url;
           $query_base['property_path'] = $parent_path;
           $link_url->setOption('query', $query_base);
-          $expand_link = array(
+          $item['expand_link'] = array(
             '#type' => 'link',
             '#title' => '(-) ',
             '#url' => $link_url,
+            '#ajax' => array(
+              'wrapper' => $this->formIdAttribute,
+            ),
           );
 
-          $nested_list = $this->getPropertiesList($nested_properties, $active_property_path, $base_url, $this_path, $label_prefix . $label . ' » ');
+          $nested_list = $this->getPropertiesList($nested_properties, $active_property_path, $base_url, $datasource_id, $this_path, $label_prefix . $label . ' » ');
         }
         else {
           $link_url = clone $base_url;
           $query_base['property_path'] = $this_path;
           $link_url->setOption('query', $query_base);
-          $expand_link = array(
+          $item['expand_link'] = array(
             '#type' => 'link',
             '#title' => '(+) ',
             '#url' => $link_url,
+            '#ajax' => array(
+              'wrapper' => $this->formIdAttribute,
+            ),
           );
         }
       }
 
-      $item = array(
-        '#type' => 'container',
-        '#attributes' => array(
-          'class' => array('container-inline'),
-        ),
-      );
-
-      if ($expand_link) {
-        $item['expand_link'] = $expand_link;
-      }
-
       $item['label']['#markup'] = Html::escape($label) . ' ';
 
       if ($can_be_indexed) {
         $item['add'] = array(
           '#type' => 'submit',
-          '#name' => Utility::createCombinedId($this->getParameter('datasource') ?: NULL, $this_path),
+          '#name' => Utility::createCombinedId($datasource_id, $this_path),
           '#value' => $this->t('Add'),
           '#submit' => array('::addField', '::save'),
           '#property' => $property,
           '#prefixed_label' => $label_prefix . $label,
           '#data_type' => $type_mapping[$type],
+          '#ajax' => array(
+            'wrapper' => $this->formIdAttribute,
+          ),
         );
       }
 
@@ -396,7 +391,7 @@ protected function getPropertiesList(array $properties, $active_property_path, U
         $item['properties'] = $nested_list;
       }
 
-      $list[] = $item;
+      $list[$key] = $item;
     }
 
     return $list;
@@ -445,7 +440,15 @@ public function addField(array $form, FormStateInterface $form_state) {
         'search_api_index' => $this->entity->id(),
         'field_id' => $field->getFieldIdentifier(),
       );
-      $form_state->setRedirect('entity.search_api_index.field_config', $parameters);
+      $options = array();
+      $route = $this->getRequest()->attributes->get('_route');
+      if ($route === 'entity.search_api_index.add_fields_ajax') {
+        $options['query'] = [
+          MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
+          'modal_redirect' => 1,
+        ];
+      }
+      $form_state->setRedirect('entity.search_api_index.field_config', $parameters, $options);
     }
 
     $args['%label'] = $field->getLabel();
diff --git a/src/Form/IndexFieldsForm.php b/src/Form/IndexFieldsForm.php
index b0574b12..e7206bbe 100644
--- a/src/Form/IndexFieldsForm.php
+++ b/src/Form/IndexFieldsForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\search_api\Form;
 
+use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\Entity\EntityForm;
@@ -11,6 +12,7 @@
 use Drupal\Core\Url;
 use Drupal\search_api\DataType\DataTypePluginManager;
 use Drupal\search_api\Processor\ConfigurablePropertyInterface;
+use Drupal\search_api\SearchApiException;
 use Drupal\search_api\UnsavedConfigurationInterface;
 use Drupal\search_api\Utility\DataTypeHelperInterface;
 use Drupal\search_api\Utility\FieldsHelperInterface;
@@ -120,6 +122,8 @@ public static function create(ContainerInterface $container) {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
+
     $index = $this->entity;
 
     // Do not allow the form to be cached. See
@@ -132,6 +136,25 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['#title'] = $this->t('Manage fields for search index %label', array('%label' => $index->label()));
     $form['#tree'] = TRUE;
 
+    $form['add-field'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Add fields'),
+      '#url' => $this->entity->toUrl('add-fields'),
+      '#attributes' => [
+        'class' => [
+          'use-ajax',
+          'button',
+          'button-action',
+          'button--primary',
+          'button--small',
+        ],
+        'data-dialog-type' => 'modal',
+        'data-dialog-options' => Json::encode([
+          'width' => 700,
+        ]),
+      ],
+    ];
+
     $form['description']['#markup'] = $this->t('<p>The data type of a field determines how it can be used for searching and filtering. The boost is used to give additional weight to certain fields, for example titles or tags.</p> <p>For information about the data types available for indexing, see the <a href="@url">data types table</a> at the bottom of the page.</p>', array('@url' => '#search-api-data-types-table'));
 
     if ($fields = $index->getFieldsByDatasource(NULL)) {
@@ -199,11 +222,17 @@ protected function buildFieldsTable(array $fields) {
       }
     }
 
-    $fulltext_types = array('text');
+    $fulltext_types = array(
+      array(
+        'value' => 'text',
+      ),
+    );
     // Add all data types with fallback "text" to fulltext types as well.
-    foreach ($this->dataTypePluginManager->getInstances() as $id => $type) {
+    foreach ($this->dataTypePluginManager->getInstances() as $type_id => $type) {
       if ($type->getFallbackType() == 'text') {
-        $fulltext_types[] = $id;
+        $fulltext_types[] = array(
+          'value' => $type_id,
+        );
       }
     }
 
@@ -271,7 +300,6 @@ protected function buildFieldsTable(array $fields) {
         );
       }
 
-      $css_key = '#edit-fields-' . Html::getId($key);
       $build['fields'][$key]['type'] = array(
         '#type' => 'select',
         '#options' => $types,
@@ -285,10 +313,12 @@ protected function buildFieldsTable(array $fields) {
         '#type' => 'select',
         '#options' => $boosts,
         '#default_value' => sprintf('%.1f', $field->getBoost()),
+        '#states' => array(
+          'visible' => array(
+            ':input[name="fields[' . $key . '][type]"]' => $fulltext_types,
+          ),
+        ),
       );
-      foreach ($fulltext_types as $type) {
-        $build['fields'][$key]['boost']['#states']['visible'][$css_key . '-type'][] = array('value' => $type);
-      }
 
       $route_parameters = array(
         'search_api_index' => $this->entity->id(),
@@ -298,12 +328,27 @@ protected function buildFieldsTable(array $fields) {
       // don't break the table structure. (theme_search_api_admin_fields_table()
       // does not add empty cells.)
       $build['fields'][$key]['edit']['#markup'] = '<span></span>';
-      if ($field->getDataDefinition() instanceof ConfigurablePropertyInterface) {
-        $build['fields'][$key]['edit'] = array(
-          '#type' => 'link',
-          '#title' => $this->t('Edit'),
-          '#url' => Url::fromRoute('entity.search_api_index.field_config', $route_parameters),
-        );
+      try {
+        if ($field->getDataDefinition() instanceof ConfigurablePropertyInterface) {
+          $build['fields'][$key]['edit'] = array(
+            '#type' => 'link',
+            '#title' => $this->t('Edit'),
+            '#url' => Url::fromRoute('entity.search_api_index.field_config', $route_parameters),
+            '#attributes' => [
+              'class' => [
+                'use-ajax',
+              ],
+              'data-dialog-type' => 'modal',
+              'data-dialog-options' => Json::encode([
+                'width' => 700,
+              ]),
+            ],
+          );
+        }
+      }
+      catch (SearchApiException $e) {
+        // Could not retrieve data definition. Since this almost certainly means
+        // that the property isn't configurable, we can just ignore it here.
       }
       $build['fields'][$key]['remove']['#markup'] = '<span></span>';
       if (!$field->isIndexedLocked()) {
@@ -311,6 +356,9 @@ protected function buildFieldsTable(array $fields) {
           '#type' => 'link',
           '#title' => $this->t('Remove'),
           '#url' => Url::fromRoute('entity.search_api_index.remove_field', $route_parameters),
+          '#attributes' => array(
+            'class' => array('use-ajax'),
+          ),
         );
       }
     }
diff --git a/tests/src/Functional/IntegrationTest.php b/tests/src/Functional/IntegrationTest.php
index 3bfbd64f..3015bce5 100644
--- a/tests/src/Functional/IntegrationTest.php
+++ b/tests/src/Functional/IntegrationTest.php
@@ -311,7 +311,7 @@ protected function createIndex() {
     $this->assertSession()->pageTextContains('The index was successfully saved.');
 
     // @todo Make this work correctly.
-    // $this->assertUrl($this->getIndexPath('fields/add'), [], 'Correct redirect to index page.');
+    // $this->assertUrl($this->getIndexPath('fields/add/nojs'), [], 'Correct redirect to index page.');
     $this->assertHtmlEscaped($index_name);
 
     $this->drupalGet($this->getIndexPath('edit'));
@@ -733,7 +733,7 @@ protected function checkFieldLabels() {
     ])->save();
 
     $url_options['query']['datasource'] = 'entity:node';
-    $this->drupalGet($this->getIndexPath('fields/add'), $url_options);
+    $this->drupalGet($this->getIndexPath('fields/add/nojs'), $url_options);
     $this->assertHtmlEscaped($field_name);
 
     $this->addField('entity:node', 'field__field_', $field_name);
@@ -759,7 +759,7 @@ protected function checkFieldLabels() {
   protected function addFieldsToIndex() {
     // Make sure that hidden properties are not displayed.
     $url_options['query']['datasource'] = '';
-    $this->drupalGet($this->getIndexPath('fields/add'), $url_options);
+    $this->drupalGet($this->getIndexPath('fields/add/nojs'), $url_options);
     $this->assertSession()->pageTextNotContains('Node access information');
 
     $fields = [
@@ -788,7 +788,7 @@ protected function addFieldsToIndex() {
 
     // Ensure that we aren't offered to index properties of the "Content type"
     // property.
-    $path = $this->getIndexPath('fields/add');
+    $path = $this->getIndexPath('fields/add/nojs');
     $url_options = ['query' => ['datasource' => 'entity:node']];
     $this->drupalGet($path, $url_options);
     $this->assertSession()->responseNotContains('property_path=type');
@@ -887,7 +887,7 @@ protected function checkDataTypesTable() {
    *   (optional) If given, the label to check for in the success message.
    */
   protected function addField($datasource_id, $property_path, $label = NULL) {
-    $path = $this->getIndexPath('fields/add');
+    $path = $this->getIndexPath('fields/add/nojs');
     $url_options = ['query' => ['datasource' => $datasource_id]];
     list($parent_path) = Utility::splitPropertyPath($property_path);
     if ($parent_path) {
@@ -1031,11 +1031,11 @@ protected function checkUnsavedChanges() {
     // everything else.
     $message_parts = explode('@age', $message);
 
-    $this->drupalGet($this->getIndexPath('fields/add'));
+    $this->drupalGet($this->getIndexPath('fields/add/nojs'));
     $this->assertSession()->responseContains($message_parts[0]);
     $this->assertSession()->responseContains($message_parts[1]);
     $this->assertFalse($this->xpath('//input[not(@disabled)]'));
-    $this->drupalGet($this->getIndexPath('fields/rendered_item/edit'));
+    $this->drupalGet($this->getIndexPath('fields/edit/rendered_item'));
     $this->assertSession()->responseContains($message_parts[0]);
     $this->assertSession()->responseContains($message_parts[1]);
     $this->assertFalse($this->xpath('//input[not(@disabled)]'));
diff --git a/tests/src/Functional/ProcessorIntegrationTest.php b/tests/src/Functional/ProcessorIntegrationTest.php
index a8cdb021..ebeee981 100644
--- a/tests/src/Functional/ProcessorIntegrationTest.php
+++ b/tests/src/Functional/ProcessorIntegrationTest.php
@@ -300,14 +300,14 @@ public function checkAggregatedFieldsIntegration() {
     $this->assertTrue($this->loadIndex()->isValidProcessor('aggregated_field'), 'The "Aggregated fields" processor cannot be disabled.');
 
     $options['query']['datasource'] = '';
-    $this->drupalGet($this->getIndexPath('fields/add'), $options);
+    $this->drupalGet($this->getIndexPath('fields/add/nojs'), $options);
 
     // See \Drupal\search_api\Tests\IntegrationTest::addField().
     $this->assertSession()->responseContains('name="aggregated_field"');
     $this->submitForm([], 'aggregated_field');
     $args['%label'] = 'Aggregated field';
     $this->assertSession()->responseContains(new FormattableMarkup('Field %label was added to the index.', $args));
-    $this->assertSession()->addressEquals($this->getIndexPath('fields/aggregated_field/edit'));
+    $this->assertSession()->addressEquals($this->getIndexPath('fields/edit/aggregated_field'));
     $edit = [
       'type' => 'first',
       'fields[entity:node/title]' => 'title',
@@ -493,14 +493,14 @@ public function checkRenderedItemIntegration() {
     $this->assertTrue($this->loadIndex()->isValidProcessor('rendered_item'), 'The "Rendered item" processor cannot be disabled.');
 
     $options['query']['datasource'] = '';
-    $this->drupalGet($this->getIndexPath('fields/add'), $options);
+    $this->drupalGet($this->getIndexPath('fields/add/nojs'), $options);
 
     // See \Drupal\search_api\Tests\IntegrationTest::addField().
     $this->assertSession()->responseContains('name="rendered_item"');
     $this->submitForm([], 'rendered_item');
     $args['%label'] = 'Rendered HTML output';
     $this->assertSession()->responseContains(new FormattableMarkup('Field %label was added to the index.', $args));
-    $this->assertSession()->addressEquals($this->getIndexPath('fields/rendered_item/edit'));
+    $this->assertSession()->addressEquals($this->getIndexPath('fields/edit/rendered_item'));
     $edit = [
       'roles[]' => ['authenticated'],
       'view_mode[entity:node][article]' => 'default',
