diff --git a/api/urls.py b/api/urls.py
index 935b8990a186d720feaca690ba0b897ac0ed8444..7adf2a1dc491d88f2a58f71435974eb1c6ea4054 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -2,8 +2,11 @@ from django.urls import include, path
 
 from rest_framework_nested import routers
 
-from .views import (
+from .views.datasources import (
     DataSourceApiViewset,
+    MetadataItemApiViewset
+)
+from .views.quality import (
     QualityCriterionApiViewset,
     QualityLevelApiViewset,
     QualityRulesetApiViewset
@@ -13,9 +16,12 @@ app_name = 'api'
 
 # Register ViewSets
 router = routers.DefaultRouter()
-router.register('datasources', DataSourceApiViewset)
-
+router.register('datasources', DataSourceApiViewset, base_name='datasource')
 router.register('rulesets', QualityRulesetApiViewset, base_name='rulesets')
+
+datasource_router = routers.NestedSimpleRouter(router, 'datasources', lookup='datasource')
+datasource_router.register('metadata_items', MetadataItemApiViewset, base_name='metadata-item')
+
 ruleset_router = routers.NestedSimpleRouter(router, 'rulesets', lookup='ruleset')
 ruleset_router.register('levels', QualityLevelApiViewset, base_name='levels')
 
@@ -26,6 +32,9 @@ urlpatterns = [
     path('',
          include(router.urls)),
 
+    path('',
+         include(datasource_router.urls)),
+
     path('',
          include(ruleset_router.urls)),
 
diff --git a/api/views/datasources.py b/api/views/datasources.py
index db34262e852d486124022cb8838f119c92a0f630..8ec1c6dfdc438b2cee91b824e060dd485b5d51f2 100644
--- a/api/views/datasources.py
+++ b/api/views/datasources.py
@@ -9,6 +9,7 @@ import typing
 from django.contrib.auth import get_user_model
 from django.db.models import ObjectDoesNotExist
 from django.http import HttpResponse, JsonResponse
+from django.shortcuts import get_object_or_404
 
 from rest_framework import decorators, request, response, viewsets
 from requests.exceptions import HTTPError
@@ -18,6 +19,18 @@ from datasources import models, serializers
 from provenance import models as prov_models
 
 
+class MetadataItemApiViewset(viewsets.ModelViewSet):
+    serializer_class = serializers.MetadataItemSerializer
+    permission_classes = [permissions.IsAdminOrReadOnly]
+
+    def get_queryset(self):
+        return models.MetadataItem.objects.filter(datasource=self.kwargs['datasource_pk'])
+
+    def perform_create(self, serializer):
+        datasource = get_object_or_404(models.DataSource, pk=self.kwargs['datasource_pk'])
+        serializer.save(datasource=datasource)
+
+
 class DataSourceApiViewset(viewsets.ReadOnlyModelViewSet):
     """
     Provides views for:
diff --git a/datasources/serializers.py b/datasources/serializers.py
index 5b923a3ec18ad4a9e745f89ac00d2a8949dbf84b..2d73fc3d5c613a749396bc03a4295fb4ef0ac3ec 100644
--- a/datasources/serializers.py
+++ b/datasources/serializers.py
@@ -16,8 +16,6 @@ class MetadataFieldSerializer(serializers.ModelSerializer):
 
 
 class MetadataItemSerializer(serializers.ModelSerializer):
-    field = MetadataFieldSerializer(read_only=True)
-
     class Meta:
         model = models.MetadataItem
         fields = ['field', 'value']
diff --git a/datasources/static/js/metadata.js b/datasources/static/js/metadata.js
index 517865ae4134c111730f863856f8c9a626dc7d28..887c4a3b06d0faaaed11e1a030cc6a289155798e 100644
--- a/datasources/static/js/metadata.js
+++ b/datasources/static/js/metadata.js
@@ -29,13 +29,31 @@ function getCookie(name) {
     }
 }
 
-function updateMetadata(metadataIdField, valueField) {
+
+/**
+ * Create a new metadata item or update an existing one.
+ *
+ * @param event Form event
+ */
+function submitMetadataForm(event) {
     "use strict";
 
+    const metadataIdField = event.target.getElementsByClassName("fieldMetadataId")[0];
+    const metadataValueIdField = event.target.getElementsByClassName("fieldMetadataValueId")[0];
+    const valueField = event.target.getElementsByClassName("fieldMetadataValue")[0];
+
+    let url = metadataUrl;
+    let method = "POST";
+
+    if (metadataValueIdField && metadataValueIdField.value) {
+        url = metadataUrl + metadataValueIdField.value.toString() + "/";
+        method = "PUT";
+    }
+
     fetch(
-        metadataUrl,
+        url,
         {
-            method: "POST",
+            method: method,
             body: JSON.stringify({
                 field: metadataIdField.value,
                 value: valueField.value
@@ -48,75 +66,69 @@ function updateMetadata(metadataIdField, valueField) {
         }
 
     ).then(function (response) {
-        if (response.status !== 200) {
-            throw new Error("Request failed")
+        if (!response.ok) {
+            throw new Error("Request failed: " + response.statusText);
         }
 
         valueField.dataset.currentValue = valueField.value;
+        window.location.reload();
 
-        return response.json()
+    }).catch(function (error) {
+        console.log(error);
+
+        if (valueField.dataset.currentValue !== null) {
+            valueField.value = valueField.dataset.lastValue;
+        }
 
-    }).then(function (response) {
-        console.log(response);
     });
 }
 
-function deleteMetadata(metadataIdField, valueField) {
+/**
+ * Delete an existing metadata item.
+ *
+ * @param event Form event
+ */
+function deleteMetadataForm(event) {
     "use strict";
 
+    const metadataIdField = event.target.getElementsByClassName("fieldMetadataValueId")[0];
+
     fetch(
-        metadataUrl,
+        metadataUrl + metadataIdField.value.toString() + "/",
         {
             method: "DELETE",
-            body: JSON.stringify({
-                field: metadataIdField.value,
-                value: valueField.dataset.currentValue
-            }),
             headers: {
                 "Accept": "application/json",
-                "Content-Type": "application/json",
                 "X-CSRFToken": getCookie("csrftoken")
             }
         }
 
     ).then(function (response) {
-        if (response.status !== 200) {
-            throw new Error("Request failed");
+        if (!response.ok) {
+            throw new Error("Request failed: " + response.statusText);
         }
 
-        valueField.dataset.currentValue = "";
-
-        return response.json();
-
-    }).catch(
-        error => console.log(error)
-    )
-}
-
-function submitMetadataForm(event) {
-    "use strict";
-
-    const metdataIdField = event.target.getElementsByClassName("fieldMetadataId")[0];
-    const valueField = event.target.getElementsByClassName("fieldMetadataValue")[0];
+        window.location.reload();
 
-    if (valueField.value) {
-        updateMetadata(metdataIdField, valueField);
-    } else {
-        deleteMetadata(metdataIdField, valueField);
-    }
+    }).catch(function (error) {
+        console.log(error);
 
-    updateLevelBadge();
+    });
 }
 
+/**
+ * Update the star rating display.
+ */
 function updateLevelBadge() {
     "use strict";
 
     fetch(ratingUrl).then(
-        response => response.json()
+        (response) => response.json()
 
     ).then(function (data) {
         const levelBadge = document.getElementById("qualityLevelBadge");
 
+        // Clear existing rating
         while (levelBadge.hasChildNodes()) {
             levelBadge.removeChild(levelBadge.lastChild);
         }
diff --git a/datasources/templates/datasources/datasource/detail.html b/datasources/templates/datasources/datasource/detail.html
index 9fb47628180b2b76edf5b0277d9bab17e80b189e..64d16d5cfd49e7da72ab704ae490c3abe4770347 100644
--- a/datasources/templates/datasources/datasource/detail.html
+++ b/datasources/templates/datasources/datasource/detail.html
@@ -73,6 +73,9 @@
                 <a href="{% url 'datasources:datasource.access.manage' pk=datasource.pk %}"
                    class="btn btn-block btn-primary" role="button">Manage Access</a>
 
+                <a href="{% url 'datasources:datasource.metadata' pk=datasource.pk %}"
+                   class="btn btn-block btn-secondary" role="button">Manage Metadata</a>
+
                 <a href="{% url 'datasources:datasource.edit' datasource.id %}"
                    class="btn btn-block btn-success" role="button">Edit</a>
 
diff --git a/datasources/templates/datasources/datasource/metadata.html b/datasources/templates/datasources/datasource/metadata.html
index bce2d60f0c7ce78f645c2c13b1dcda0412f5709f..99e5b8559a72ef6e12deb3a63dc8f9fc7a00384e 100644
--- a/datasources/templates/datasources/datasource/metadata.html
+++ b/datasources/templates/datasources/datasource/metadata.html
@@ -7,7 +7,7 @@
     <script type="application/javascript" src="{% static 'js/metadata.js' %}"></script>
 
     <script type="application/javascript">
-        setMetadataUrl("/datasources/{{ datasource.pk }}/metadata");
+        setMetadataUrl("{% url 'api:metadata-item-list' datasource_pk=datasource.pk %}");
         setRatingUrl("{% url 'api:datasource-quality' pk=datasource.pk %}");
     </script>
 {% endblock %}
@@ -50,47 +50,163 @@
 
     <hr>
 
-    <table id="" class="table">
+    {% load datasource_extras %}
+
+    <h2>Quality Metadata</h2>
+
+    <table class="table">
         <tbody>
-            {% load datasource_extras %}
 
             {% for level in ruleset.levels.all %}
                 <tr>
-                    <th>Level {{ level.level }}</th>
+                    <th class="row">
+                        <div class="col-md-3">
+                            Level {{ level.level }}
+                        </div>
+
+                        <div class="col-md-1">
+                            {{ level.threshold }}
+                        </div>
+                    </th>
                 </tr>
 
                 {% for criterion in level.criteria.all %}
-                    <tr>
-                        <td>
-                            <form class="row"
-                                  onsubmit="submitMetadataForm(event); event.preventDefault();">
-                                <div class="col-md-4 mb-2">
-                                    <input type="hidden" class="fieldDatasourceId" readonly
-                                           value="{{ datasource.id }}">
+                    {% with metadata_items|access:criterion.metadata_field.short_name as metadata_field_group %}
+                        <tr>
+                            <td>
+                                <div class="row">
+                                    <div class="col-md-3">
+                                        <span class="form-text">{{ criterion.metadata_field.name }}</span>
+                                    </div>
+
+                                    <div class="col-md-1">
+                                        {{ criterion.weight }}
+                                    </div>
+
+                                    <div class="col-md-8">
+                                        {% for item in metadata_field_group.list %}
+                                            <form class="row"
+                                                  onsubmit="submitMetadataForm(event); event.preventDefault();"
+                                                  onreset="deleteMetadataForm(event); event.preventDefault();">
+                                                <div class="col-md-8">
+                                                    <input type="hidden" class="fieldDatasourceId" readonly
+                                                           value="{{ datasource.id }}">
+
+                                                    <input type="hidden" class="fieldMetadataId" readonly
+                                                           value="{{ metadata_field_group.field.id }}">
+
+                                                    <input type="hidden" class="fieldMetadataValueId" readonly
+                                                           value="{{ item.id }}">
+
+                                                    <input type="text" class="form-control mb-2 fieldMetadataValue"
+                                                           value="{{ item.value }}" data-last-value="{{ item.value }}">
+                                                </div>
+
+                                                <div class="col-md-2">
+                                                    <button type="submit" class="btn btn-block btn-success mb-2">Save</button>
+                                                </div>
+
+                                                <div class="col-md-2">
+                                                    <button type="reset" class="btn btn-block btn-danger mb-2">Delete</button>
+                                                </div>
+                                            </form>
+                                        {% endfor %}
+
+                                    <form class="row"
+                                          onsubmit="submitMetadataForm(event); event.preventDefault();">
+                                        <div class="col-md-8">
+                                            <input type="hidden" class="fieldDatasourceId" readonly
+                                                   value="{{ datasource.id }}">
+
+                                            <input type="hidden" class="fieldMetadataId" readonly
+                                                   value="{{ criterion.metadata_field.id }}">
+
+                                            <input type="text" class="form-control mb-2 fieldMetadataValue">
+                                        </div>
+
+                                        <div class="col-md-2">
+                                            <button type="submit" class="btn btn-block btn-success mb-2">Save</button>
+                                        </div>
+                                    </form>
+
+                                    </div>
+                                </div>
+                            </td>
+                        </tr>
+                    {% endwith %}
 
-                                    <input type="text" class="form-control-plaintext" readonly
-                                           value="{{ criterion.metadata_field.name }}">
+                {% endfor %}
 
-                                    <input type="hidden" class="fieldMetadataId" readonly
-                                           value="{{ criterion.metadata_field.id }}">
-                                </div>
+            {% endfor %}
+
+        </tbody>
+    </table>
+
+    <hr>
+
+    <h2>All Metadata</h2>
 
-                                <div class="col-md-6 mb-2">
-                                    {% with metadata|access:criterion.metadata_field.short_name|default_if_none:'' as metadata_value %}
-                                        <input id="input-{{ criterion.metadata_field.short_name }}"
-                                               type="text" class="form-control fieldMetadataValue"
-                                               value="{{ metadata_value }}"
-                                               data-current-value="{{ metadata_value }}">
-                                    {% endwith %}
+    <table class="table">
+        <tbody>
+            {% for metadata_field in metadata_fields %}
+                {% with metadata_items|access:metadata_field.short_name as metadata_field_group %}
+                    <tr>
+                        <td>
+                            <div class="row">
+                                <div class="col-md-4">
+                                    <span class="form-text">{{ metadata_field.name }}</span>
                                 </div>
 
-                                <div class="col-md-2 mb-2">
-                                    <button type="submit" class="btn btn-block btn-success">Save</button>
+                                <div class="col-md-8">
+                                    {% for item in metadata_field_group.list %}
+                                        <form class="row"
+                                              onsubmit="submitMetadataForm(event); event.preventDefault();"
+                                              onreset="deleteMetadataForm(event); event.preventDefault();">
+                                            <div class="col-md-8">
+                                                <input type="hidden" class="fieldDatasourceId" readonly
+                                                       value="{{ datasource.id }}">
+
+                                                <input type="hidden" class="fieldMetadataId" readonly
+                                                       value="{{ metadata_field_group.field.id }}">
+
+                                                <input type="hidden" class="fieldMetadataValueId" readonly
+                                                       value="{{ item.id }}">
+
+                                                <input type="text" class="form-control mb-2 fieldMetadataValue"
+                                                       value="{{ item.value }}" data-last-value="{{ item.value }}">
+                                            </div>
+
+                                            <div class="col-md-2">
+                                                <button type="submit" class="btn btn-block btn-success mb-2">Save</button>
+                                            </div>
+
+                                            <div class="col-md-2">
+                                                <button type="reset" class="btn btn-block btn-danger mb-2">Delete</button>
+                                            </div>
+                                        </form>
+                                    {% endfor %}
+
+                                    <form class="row"
+                                          onsubmit="submitMetadataForm(event); event.preventDefault();">
+                                        <div class="col-md-8">
+                                            <input type="hidden" class="fieldDatasourceId" readonly
+                                                   value="{{ datasource.id }}">
+
+                                            <input type="hidden" class="fieldMetadataId" readonly
+                                                   value="{{ metadata_field.id }}">
+
+                                            <input type="text" class="form-control mb-2 fieldMetadataValue">
+                                        </div>
+
+                                        <div class="col-md-2">
+                                            <button type="submit" class="btn btn-block btn-success mb-2">Save</button>
+                                        </div>
+                                    </form>
                                 </div>
-                            </form>
+                            </div>
                         </td>
                     </tr>
-                {% endfor %}
+                {% endwith %}
             {% endfor %}
         </tbody>
     </table>
diff --git a/datasources/templatetags/datasource_extras.py b/datasources/templatetags/datasource_extras.py
index 717e4d16d2ef80f3fe912879a594fc3ce32acb97..997794bf550e95734d2c7d87cf7a9a26fcb44d22 100644
--- a/datasources/templatetags/datasource_extras.py
+++ b/datasources/templatetags/datasource_extras.py
@@ -17,3 +17,11 @@ def access(value, arg):
 
     except KeyError:
         return None
+
+    except TypeError:
+        # Is a GroupedResult not a dictionary
+        for item in value:
+            if item.field.short_name == arg:
+                return item
+
+        return None
diff --git a/datasources/urls.py b/datasources/urls.py
index 0c800d6f7ec786c4dc6a49fb536ac1de11c8d44b..0974dc716b7efeaf8cbf9f67546f2b5d85db3925 100644
--- a/datasources/urls.py
+++ b/datasources/urls.py
@@ -26,12 +26,8 @@ urlpatterns = [
          name='datasource.delete'),
 
     path('<int:pk>/metadata',
-         views.datasource.DataSourceMetadataAjaxView.as_view(),
-         name='datasource.metadata'),
-
-    path('<int:pk>/metadata-dash',
          views.datasource.DataSourceMetadataView.as_view(),
-         name='datasource.metadata.dash'),
+         name='datasource.metadata'),
 
     path('<int:pk>/explorer',
          views.datasource.DataSourceExplorerView.as_view(),
diff --git a/datasources/views/datasource.py b/datasources/views/datasource.py
index 57b223034630ac8941669c476b6de3f067116373..0b2b58eb3b231ea7d9e06668044acb52145d4ac3 100644
--- a/datasources/views/datasource.py
+++ b/datasources/views/datasource.py
@@ -1,3 +1,5 @@
+from collections import namedtuple
+
 from django.contrib import messages
 from django.contrib.auth import get_user_model
 from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -131,64 +133,7 @@ class DataSourceDataSetSearchView(HasPermissionLevelMixin, DetailView):
         return context
 
 
-class DataSourceMetadataAjaxView(OwnerPermissionMixin, APIView):
-    model = models.DataSource
-
-    # Don't redirect to login page if unauthorised
-    raise_exception = True
-
-    class MetadataSerializer(serializers.ModelSerializer):
-        class Meta:
-            model = models.MetadataItem
-            fields = '__all__'
-
-    def get_object(self, pk):
-        return self.model.objects.get(pk=pk)
-
-    def post(self, request, pk, format=None):
-        """
-        Create a new MetadataItem associated with this DataSource.
-        """
-        datasource = self.get_object(pk)
-        if 'datasource' not in request.data:
-            request.data['datasource'] = datasource.pk
-
-        serializer = self.MetadataSerializer(data=request.data)
-
-        if serializer.is_valid():
-            obj = serializer.save()
-
-            return Response({
-                'status': 'success',
-                'data': {
-                    'datasource': datasource.pk,
-                    'field': obj.field.name,
-                    'field_short': obj.field.short_name,
-                    'value': obj.value,
-                }
-            })
-
-        return Response({'status': 'failure'}, status=400)
-
-    def delete(self, request, pk, format=None):
-        """
-        Delete a MetadataItem associated with this DataSource.
-        """
-        datasource = self.get_object(pk)
-        if 'datasource' not in request.data:
-            request.data['datasource'] = datasource.pk
-
-        metadata_item = models.MetadataItem.objects.get(
-            datasource=datasource,
-            field=self.request.data['field'],
-            value=self.request.data['value']
-        )
-
-        metadata_item.delete()
-
-        return Response({
-            'status': 'success'
-        })
+FieldValueSet = namedtuple('FieldValueSet', ['field', 'list'])
 
 
 class DataSourceMetadataView(OwnerPermissionMixin, DetailView):
@@ -200,9 +145,17 @@ class DataSourceMetadataView(OwnerPermissionMixin, DetailView):
         context = super().get_context_data(**kwargs)
 
         context['ruleset'] = get_user_model().get_quality_ruleset()
-        context['metadata'] = {
-            item.field.short_name: item.value for item in self.object.metadata_items.all()
-        }
+        context['metadata_fields'] = models.MetadataField.objects.all()
+
+        items = []
+        present_fields = {item.field for item in self.object.metadata_items.all()}
+        for field in present_fields:
+            items.append(
+                FieldValueSet(field, [
+                    item for item in self.object.metadata_items.all() if item.field == field
+                ])
+            )
+        context['metadata_items'] = items
 
         return context