You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2018/09/21 10:15:17 UTC

[06/70] [abbrv] [partial] jena git commit: JENA-1597: separate jena-fuseki-webapp module

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/controllers/upload-controller.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/controllers/upload-controller.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/controllers/upload-controller.js
new file mode 100644
index 0000000..ace2547
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/controllers/upload-controller.js
@@ -0,0 +1,42 @@
+/** Controller for the file uploader component */
+
+define(
+  function( require ) {
+    var Marionette = require( "marionette" ),
+        Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        sprintf = require("sprintf"),
+        pageUtils = require( "app/util/page-utils" ),
+        fui = require( "app/fui" ),
+        FileUploadView = require( "app/views/file-upload" );
+
+    var UploadController = function() {
+      this.initialize();
+    };
+
+    _.extend( UploadController.prototype, {
+
+      /** Initialize the controler */
+      initialize: function() {
+        if (fui.models.fusekiServer && fui.models.fusekiServer.get( "ready" )) {
+          this.onServerModelReady();
+        }
+        else {
+          _.bindAll( this, "onServerModelReady" );
+          fui.vent.on( "models.fuseki-server.ready", this.onServerModelReady );
+        }
+
+      },
+
+      /** When the fuseki server is ready, we can set up the initial view */
+      onServerModelReady: function( event ) {
+        var fusekiServer = fui.models.fusekiServer;
+
+        fui.views.fileUploadView = new FileUploadView();
+        fui.views.fileUploadView.render();
+      },
+    } );
+
+    return UploadController;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/controllers/validation-controller.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/controllers/validation-controller.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/controllers/validation-controller.js
new file mode 100644
index 0000000..43ed8ce
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/controllers/validation-controller.js
@@ -0,0 +1,38 @@
+/** Controller for the main index.html page */
+define(
+  function( require ) {
+    var Marionette = require( "marionette" ),
+        Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        ValidationOptions = require( "app/views/validation-options" ),
+        ValidationService = require( "app/services/validation-service" );
+
+    var ValidationController = function() {
+      this.initServices();
+      this.initEvents();
+    };
+
+    // add the behaviours defined on the controller
+    _.extend( ValidationController.prototype, {
+      initEvents: function() {
+        fui.vent.on( "models.validation-options.ready", this.onValidationOptionsModelReady );
+        $(".validation").on( "click", "a.perform-validation", function( event ) {
+          fui.services.validation.performValidation( fui.views.validationOptions.model );
+        } );
+      },
+
+      onValidationOptionsModelReady: function( e ) {
+        fui.views.validationOptions = new ValidationOptions( {model: fui.models.validationOptions} );
+      },
+
+      initServices: function() {
+        fui.services.validation = new ValidationService( "#query-edit-cm", "#validation-output-cm" );
+        fui.services.validation.init();
+      }
+
+    } );
+
+    return ValidationController;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/fui.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/fui.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/fui.js
new file mode 100644
index 0000000..e8bc41d
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/fui.js
@@ -0,0 +1,33 @@
+/**
+ * Top-level application code module for Fuseki UI
+ */
+
+define( ['require', 'backbone', 'marionette'],
+  function( require, Backbone, Marionette ) {
+    // define the application object, and add it to the global namespace
+    var fui = new Marionette.Application();
+
+    // define some Marionette modules, because they have a lifecycle component
+    // see https://github.com/marionettejs/backbone.marionette/wiki/AMD-Modules-vs-Marionette%27s-Modules
+    fui.module( "models" );
+    fui.module( "views" );
+    fui.module( "layouts" );
+    fui.module( "controllers" );
+    fui.module( "services" );
+
+    // define top-level regions where our layouts will go
+    fui.addRegions({
+    });
+
+    fui.on('initialize:before', function( options ) {
+    });
+
+    fui.on('initialize:after', function( options ) {
+      // Backbone.history.start();
+      this.initialized = true;
+    });
+
+
+    return fui;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.dataset.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.dataset.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.dataset.js
new file mode 100644
index 0000000..50ba27b
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.dataset.js
@@ -0,0 +1,31 @@
+/** RequireJS dependency configuration for dataset.html page */
+
+define( ['require', '../common-config'],
+  function( require ) {
+    require(
+      ['underscore', 'jquery', 'backbone', 'marionette', 'app/fui', 'app/controllers/dataset-controller',
+       'sprintf',
+       'bootstrap-select.min',
+       'app/controllers/query-controller',
+       'app/controllers/upload-controller',
+       'app/models/fuseki-server',
+       'app/models/dataset',
+       'app/views/dataset-selector',
+       'app/views/tabbed-view-manager',
+       'app/services/ping-service',
+       'jquery.xdomainrequest',
+       'jquery.form',
+       'jquery.fileupload'
+      ],
+      function( _, $, Backbone, Marionette, fui, DatasetController ) {
+          var options = { };
+
+        // initialise the backbone application
+        fui.controllers.datasetController = new DatasetController();
+        fui.start( options );
+
+        // additional services
+        require( 'app/services/ping-service' ).start();
+      });
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.index.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.index.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.index.js
new file mode 100644
index 0000000..56c8903
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.index.js
@@ -0,0 +1,24 @@
+
+define( ['require', '../common-config'],
+  function( require ) {
+    require(
+      ['underscore', 'jquery', 'backbone', 'marionette',
+       'app/fui', 'app/controllers/index-controller',
+       'sprintf', 'bootstrap',
+       'app/models/fuseki-server',
+       'app/models/dataset',
+       'app/views/dataset-selection-list',
+       'app/services/ping-service'
+      ],
+      function( _, $, Backbone, Marionette, fui, IndexController ) {
+        var options = { };
+
+        // initialise the backbone application
+        fui.controllers.indexController = new IndexController();
+        fui.start( options );
+
+        // additional services
+        require( 'app/services/ping-service' ).start();
+      });
+  }
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.manage.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.manage.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.manage.js
new file mode 100644
index 0000000..312972c
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.manage.js
@@ -0,0 +1,27 @@
+
+define( ['require', '../common-config'],
+  function( require ) {
+    require(
+      ['underscore', 'jquery', 'backbone', 'marionette',
+       'app/fui', 'app/controllers/manage-controller',
+       'sprintf', 'bootstrap',
+       'app/models/fuseki-server',
+       'app/models/dataset',
+       'app/models/task',
+       'app/views/dataset-management',
+       'app/services/ping-service',
+       'jquery.xdomainrequest'
+      ],
+      function( _, $, Backbone, Marionette, fui, ManageController ) {
+
+        var options = { } ;
+
+        // initialise the backbone application
+        fui.controllers.manageController = new ManageController();
+        fui.start( options );
+
+        // additional services
+        require( 'app/services/ping-service' ).start();
+      });
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.validation.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.validation.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.validation.js
new file mode 100644
index 0000000..0e30fad
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/main.validation.js
@@ -0,0 +1,24 @@
+
+define( ['require', '../common-config'],
+  function( require ) {
+    require(
+      ['underscore', 'jquery', 'backbone', 'marionette',
+       'app/fui', 'app/controllers/validation-controller',
+       'sprintf', 'bootstrap',
+       'app/models/validation-options',
+       'app/services/ping-service',
+       'app/services/validation-service',
+       'jquery.xdomainrequest'
+      ],
+      function( _, $, Backbone, Marionette, fui, ValidationController ) {
+        var options = { } ;
+
+        // initialise the backbone application
+        fui.controllers.validationController = new ValidationController();
+        fui.start( options );
+
+        // additional services
+//        require( 'services/ping-service' ).start(); TODO restore
+      });
+  }
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/dataset-stats.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/dataset-stats.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/dataset-stats.js
new file mode 100644
index 0000000..35527c7
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/dataset-stats.js
@@ -0,0 +1,102 @@
+/**
+ * Backbone model denoting statistics on a dataset
+ */
+define(
+  function( require ) {
+    "use strict";
+
+    var Marionette = require( "marionette" ),
+        Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        sprintf = require( "sprintf" );
+
+    /**
+     * This model represents the statistics available on a one or more datasets
+     */
+    var DatasetStats = Backbone.Model.extend( {
+      initialize: function( dataset, stats ) {
+        this.set( {dataset: dataset, stats: stats} );
+      },
+
+      /** Return the number of datasets we have statistics for */
+      size: function() {
+        return _.keys( datasets() ).length;
+      },
+
+      toJSON: function() {
+        return this.asTable();
+      },
+
+      /** Return a table of the statistics we have, one row per dataset */
+      asTable: function() {
+        var ds = this.datasets();
+        var endpoints = this.collectEndpoints( ds );
+        var rows = [];
+
+        _.each( ds, function( d, dsName ) {
+          var row = [dsName, d.Requests, d.RequestsGood, d.RequestsBad];
+          var es = d.endpoints;
+
+          _.each( endpoints, function( e ) {
+            if (es[e.key]) {
+              var servStats = es[e.key];
+
+              if (servStats.Requests === 0) {
+                row.push( "0" );
+              }
+              else {
+                row.push( sprintf( "%d (%d bad)", servStats.Requests, servStats.RequestsBad ))
+              }
+            }
+            else {
+              row.push( "–" );
+            }
+          } );
+
+          rows.push( row );
+        } );
+
+        return {headings: this.columnHeadings( endpoints ), rows: rows};
+      },
+
+      stats: function() {
+        return this.get( "stats" );
+      },
+
+      datasets: function() {
+        return this.stats().datasets;
+      },
+
+      /** Reload the numbers from the server */
+      refresh: function() {
+        var self = this;
+
+        this.get( "dataset" )
+            .statistics()
+            .done( function( data ) {
+              self.set( "stats", data );
+            } );
+      },
+
+      // internal methods
+
+      collectEndpoints: function( ds ) {
+        var endpoints = [];
+        _.each( ds, function( d ) {
+          var ep = _.each( d.endpoints, function( v, k ) {
+            endpoints.push( {key: k, label: v.description} );
+          } );
+        } );
+
+        return _.uniq( endpoints ).sort();
+      },
+
+      columnHeadings: function( services ) {
+        return ["Name", "Overall", "Overall good", "Overall bad"].concat( _.pluck( services, 'label' ) );
+      }
+    } );
+
+    return DatasetStats;
+  }
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/dataset.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/dataset.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/dataset.js
new file mode 100644
index 0000000..4fc192f
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/dataset.js
@@ -0,0 +1,262 @@
+/**
+ * Backbone model denoting the remote Fuseki server.
+ */
+define(
+  function( require ) {
+    "use strict";
+
+    var Marionette = require( "marionette" ),
+        Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        sprintf = require( "sprintf" ),
+        Task = require( "app/models/task" );
+
+    /**
+     * This model represents the core representation of the remote Fuseki
+     * server. Individual datasets have their own model.
+     */
+    var Dataset = Backbone.Model.extend( {
+      initialize: function( datasetDescription, baseURL, mgmtURL ) {
+        this.set( datasetDescription );
+        this.set( {
+                    baseURL: baseURL,
+                    mgmtURL: mgmtURL,
+                    counts: {},
+                    countPerformed: false,
+                    counting: false,
+                    statistics: false
+                  } );
+      },
+
+      /* x is the empty object if baseURL is "" 
+       * Ensure it is always a string.
+       */
+      getStr: function(key) {
+        var x = this.get( key );
+        return jQuery.isEmptyObject(x) ? "" : x ;
+      },
+  
+      baseURL: function() {
+        return this.getStr( "baseURL" );
+      },
+
+      mgmtURL: function() {
+        return this.getStr( "mgmtURL" );
+      },
+
+      mgmtActionURL: function() {
+        return this.get( "mgmtURL" ) + this.name();
+      },
+
+      statisticsURL: function() {
+        return sprintf( "%s/$/stats%s", this.baseURL(), this.name() );
+      },
+
+      name: function() {
+        return this.get( "ds.name" );
+      },
+
+      services: function() {
+        return this.get( "ds.services" );
+      },
+
+      countPerformed: function() {
+        return this.get( "countPerformed" );
+      },
+
+      counts: function() {
+        return this.get( "counts" );
+      },
+
+      serviceTypes: function() {
+        return _.map( this.services(), function( s ) {return s["srv.type"];} );
+      },
+
+      /** Return a descriptive data-structure listing all this datasets services */
+      servicesDescription: function() {
+        var description = [];
+        var self = this;
+
+        _.each( this.services(), function( s ) {
+          _.each( s["srv.endpoints"], function( e ) {
+            description.push( {label: s["srv.description"],
+                               url: self.datasetEndpointURL( e )
+                              } );
+          } );
+        } );
+
+        description.sort( function( d0, d1 ) {
+          return (d0.label < d1.label) ? -1 : (d0.label > d1.label ? 1 : 0);
+        } );
+
+        return description;
+      },
+
+      /** Return the first service that has the given type */
+      serviceOfType: function( serviceType ) {
+        return _.find( this.services(), function( s ) {
+          return s["srv.type"] === serviceType;
+        } );
+      },
+
+      /** Return the first endpoint of the first service that has the given type */
+      endpointOfType: function( serviceType ) {
+        var service = this.serviceOfType( serviceType );
+        return service && _.first( service["srv.endpoints"] );
+      },
+
+      /* Return URL for a service of a given type or null, if no such service */
+      endpointURL: function( serviceType ) {
+        var endpoint = this.endpointOfType( serviceType );
+        return endpoint ? this.datasetEndpointURL( endpoint ) : null;
+      },
+
+      /** Return the URL for the given endpoint */
+      datasetEndpointURL: function( endpoint ) {
+        return sprintf( "%s%s/%s", this.baseURL(), this.name(), endpoint );
+      },
+
+      /** Return the sparql query URL for this dataset, if it has one, or null */
+      queryURL: function() {
+        return this.endpointURL( "Query" ) ;
+      },
+
+      /** Return the sparql query URL for this dataset, if it has one, or null */
+      quadsURL: function() {
+        return this.endpointURL( "Quads" ) ;
+      },
+
+      /** Return the sparql update URL for this dataset, if it has one, or null */
+      updateURL: function() {
+        return this.endpointURL( "Update" ) ;
+      },
+
+      /** Return the GSP write URL for this dataset, if it has one, or null */
+      graphStoreProtocolURL: function() {
+        if ( this.endpointURL( "GSP" ) )
+            // Old name
+            return this.endpointURL( "GSP" ) ;
+        return this.endpointURL( "GSP_RW" ) ;
+      },
+
+      /** Return the GSP read URL for this dataset, if it has one, or null */
+      graphStoreProtocolReadURL: function() {
+        return this.endpointURL( "GSP_R" ) ;
+      },
+
+      /** Return the upload URL for this dataset, if it has one, or null */
+      uploadURL: function( graphName ) {
+        if (this.graphStoreProtocolURL() !== null) {
+          return sprintf( "%s%s", this.graphStoreProtocolURL(), (graphName === "default" ? "" : ("?graph=" + graphName) ));
+        }
+        else {
+          return null;
+        }
+      },
+
+      /** Perform the action to delete the dataset. Returns the Ajax deferred object */
+      deleteDataset: function() {
+        return $.ajax( {
+          url: this.mgmtActionURL(),
+          type: 'DELETE'
+        } );
+      },
+
+      /** Perform the action of taking a backup of this dataset */
+      backupDataset: function() {
+        var backupURL = sprintf( "%s/$/backup%s", this.baseURL(), this.name() );
+        var ds = this;
+
+        return $.ajax( {
+          url: backupURL,
+          type: 'POST'
+        } )
+          .done( function( taskDescription ) {
+            new Task( ds, "backup", taskDescription );
+          } );
+      },
+
+      /**
+       * Request the statistics for this dataset, and return the promise object for the callback.
+       * @param keep If truthy, and we have existing statistics, re-use the existing stats
+       * */
+      statistics: function( keep ) {
+        var self = this;
+        var currentStats = this.get( "statistics" );
+
+        if (currentStats && keep) {
+          return $.Deferred().resolveWith( null, currentStats );
+        }
+        else {
+          return $.getJSON( this.statisticsURL() )
+                  .then( function( data ) {
+                    self.set( "statistics", data );
+                    return data;
+                  } );
+        }
+      },
+
+      /** Perform a count query to determine the size of the dataset. Changes the size property when done,
+       * but also returns the JQuery promise object used to monitor the query response */
+      count: function() {
+        var self = this;
+        var query1 = sprintf( "select (count(*) as ?count) {?s ?p ?o}" );
+        var query2 = sprintf( "select ?g (count(*) as ?count) {graph ?g {?s ?p ?o}} group by ?g" );
+
+        self.set( "counting", true );
+
+        var updateCount = function( model, result, graph ) {
+          var n = parseInt( result.count.value );
+          var counts = _.extend( {}, model.get( "counts" ) );
+          counts[graph] = n;
+          model.set( "counts", counts );
+        };
+
+        $.getJSON( self.queryURL(), { query: query1 } )
+         .done( function( data ) {
+           updateCount( self, data.results.bindings[0], "default graph" );
+
+           $.getJSON( self.queryURL(), { query: query2 } )
+            .done( function( data ) {
+              _.each( data.results.bindings, function( binding ) {
+                if (binding.g) {
+                  updateCount( self, binding, binding.g.value );
+                }
+              } );
+            } );
+
+           self.set( {countPerformed: true, counting: false} );
+         } );
+      },
+
+      /**
+       * Fetch the content of the given graph as Turtle. Return the jQuery promise
+       * object for the Ajax call.
+       */
+      fetchGraph: function( graphName ) {
+        return $.ajax( this.graphStoreProtocolReadURL(),
+                       {method: "get",
+                        headers: {Accept : "text/turtle; charset=utf-8"},
+                        dataType: "html",
+                        data: {graph: graphName}
+                       } );
+      },
+
+      /**
+       * Put the given Turtle content back to the server using the given graph name
+       */
+      putGraph: function( turtle, graphName ) {
+        return $.ajax( sprintf( "%s?graph=%s", this.graphStoreProtocolURL(), graphName ),
+                       {method: "put",
+                        dataType: "json",
+                        data: turtle,
+                        contentType: "text/turtle; charset=uft-8"
+                       } );
+      }
+
+    } );
+
+    return Dataset;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/fuseki-server.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/fuseki-server.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/fuseki-server.js
new file mode 100644
index 0000000..336d0c7
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/fuseki-server.js
@@ -0,0 +1,180 @@
+/**
+ * Backbone model denoting the remote Fuseki server.
+ */
+
+define(
+  function( require ) {
+    "use strict";
+
+    var Marionette = require( "marionette" ),
+        Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        sprintf = require( "sprintf" ),
+        Dataset = require( "app/models/dataset" ),
+        PageUtils = require( "app/util/page-utils" );
+
+    var DATASETS_MANAGEMENT_PATH = "/$/datasets";
+
+    /**
+     * This model represents the core representation of the remote Fuseki
+     * server. Individual datasets have their own model.
+     */
+    var FusekiServer = Backbone.Model.extend( {
+      /** This initializer occurs when the module starts, not when the constructor is invoked */
+      init: function( options ) {
+        this._baseURL = this.currentRootPath();
+        this._managementURL = null;
+        this.set( "selectedDatasetName", PageUtils.queryParam( "ds" ) )
+      },
+
+      baseURL: function() {
+        return this._baseURL;
+      },
+
+      /** Return the URL from which we extract the details of the current server */
+      serverDetailsURL: function() {
+        return sprintf( "%s/$/server", this.baseURL() );
+      },
+
+      /** Return the URL for issuing commands to the management API, or null if no API defined */
+      managementURL: function() {
+        return this._managementURL;
+      },
+
+      /** Return the URL for getting the stats for all datasets */
+      statsURL: function() {
+        return sprintf( "%s/$/stats", this.managementURL() );
+      },
+
+      /** Return the list of datasets that this server knows about. Each dataset will be a Dataset model object */
+      datasets: function() {
+        return this.get( "datasets" );
+      },
+
+      /** Return the dataset with the given name */
+      dataset: function( dsName ) {
+        return _.find( this.datasets(), function( ds ) {return dsName === ds.name();} )
+      },
+
+      /** Return the name of the currently selected dataset, if known */
+      selectedDatasetName: function() {
+        return this.get( "selectedDatasetName" );
+      },
+
+      /** Return the dataset that is currently selected, or null */
+      selectedDataset: function() {
+        var dsName = this.selectedDatasetName();
+        return dsName && this.dataset( dsName );
+      },
+
+      /** Load and cache the remote server description. Trigger change event when done */
+      loadServerDescription: function() {
+          var self = this;
+          return this.getJSON( this.serverDetailsURL() )
+              .done( function( data ) {
+                  self.saveServerDescription( data );
+              } )
+              .then( function() {
+                  fui.vent.trigger( "models.fuseki-server.ready" );
+              });
+      },
+
+      /** Store the server description in this model */
+      saveServerDescription: function( serverDesc ) {
+        // wrap each dataset JSON description as a dataset model
+        var bURL = this.baseURL();
+        var mgmtURL = bURL;
+
+        if (serverDesc.admin) {
+	       // This is too simple.  window.location.port may be empty and matches protocol.
+	       //mgmtURL = bURL.replace( ":" + window.location.port, ":" + serverDesc.admin.port );
+           //console.log("managementURL -- s/"+window.location.port+"/"+serverDesc.admin.port+"/") ;
+	       var path = window.location.pathname.replace( /\/[^/]*$/, "" ) ;
+ 	       mgmtURL = sprintf( "%s//%s:%s%s",  window.location.protocol, window.location.hostname, serverDesc.admin, path );
+        }
+	    this._managementURL = mgmtURL ;
+	
+        var datasets = _.map( serverDesc.datasets, function( d ) {
+          return new Dataset( d, bURL, mgmtURL + DATASETS_MANAGEMENT_PATH );
+        } );
+
+        datasets.sort( function( ds0, ds1 ) {
+          if (ds0.name() > ds1.name()) {
+            return 1;
+          }
+          else if (ds0.name() < ds1.name()) {
+            return -1;
+          }
+          else
+            return 0;
+        } );
+
+        this.set( {
+          serverDescription: serverDesc,
+          datasets: datasets,
+          ready: true
+        } );
+      },
+
+      /**
+       * Get the given relative path from the server, and return a promise object which will
+       * complete with the JSON object denoted by the path.
+       */
+      getJSON: function( path, data ) {
+        return $.getJSON( path, data );
+      },
+
+      /** Update or create a dataset by posting to its endpoint */
+      updateOrCreateDataset: function( datasetId, data ) {
+        var url = sprintf( "%s/$/datasets%s", this.managementURL(),
+                           datasetId ? ("/" + datasetId) : ""
+                         );
+
+        return $.ajax( url,
+                       { data: data,
+                         method: "post"
+                       }
+                     );
+      },
+
+      /** Extract the server root path from the current window href
+       * This is the path, from /, without protocol, host or port.
+       * Then the browser adds protocol, host or port.
+       * Sometimes, the app does not know URL the browser used.
+       * For example, in docker, the port may have been remapped,
+       * or with a reverse proxy, https may have been terminated.
+       */
+      currentRootPath: function() {
+        var path = window.location.pathname.replace( /\/[^/]*$/, "" );
+          /*
+            console.log("window.location="+window.location) ;
+            console.log("window.location.href="+window.location.href) ;
+            
+            console.log("window.location.protocol="+window.location.protocol) ;
+            console.log("window.location.host="+window.location.host) ;
+            console.log("window.location.hostname="+window.location.hostname) ;
+            console.log("window.location.port="+window.location.port) ;
+            console.log("window.location.pathname="+window.location.pathname) ;
+            
+            console.log("window.location.origin="+window.location.origin) ;
+            console.log("window.location.hash="+window.location.hash) ;
+            console.log("window.location.search="+window.location.search) ;
+            console.log("path='"+path+"'") ;
+          */
+        return path ;
+      }
+    } );
+
+    // when the models module starts, automatically load the server description
+    fui.models.addInitializer( function( options ) {
+      var fusekiServer = new FusekiServer();
+      fui.models.fusekiServer = fusekiServer;
+
+      fusekiServer.init( options );
+      fusekiServer.loadServerDescription();
+    } );
+
+    return FusekiServer;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/task.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/task.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/task.js
new file mode 100644
index 0000000..10c0563
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/task.js
@@ -0,0 +1,105 @@
+/**
+ * A long-running task, which periodically pings the server for its task status
+ */
+
+define(
+  function( require ) {
+    "use strict";
+
+    var Marionette = require( "marionette" ),
+        Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        sprintf = require( "sprintf" );
+
+    /* Constants */
+
+    var MS = 1000;
+    var MAX_DELAY = 10 * MS;
+
+    /**
+     * This model represents a long running task process
+     */
+    var Task = function( ds, operationType, taskDescription ) {
+      this.taskDescription = taskDescription;
+      this.ds = ds;
+      this.operationType = operationType;
+      this.delay = 500;
+
+      _.bindAll( this, "checkTaskStatus", "onCurrentTaskStatusFail", "onCurrentTaskStatus" );
+
+      this.checkTaskStatus();
+    };
+
+    _.extend( Task.prototype, {
+      /** Return the unique ID (on this server) of the task */
+      taskId: function() {
+        return this.taskDescription.taskId;
+      },
+
+      /** Return the URL for the task's API */
+      taskURL: function() {
+        return sprintf( "%s/$/tasks/%s", this.ds.baseURL(), this.taskId() );
+      },
+
+      /** Test the current status of the task */
+      checkTaskStatus: function() {
+        $.getJSON( this.taskURL() )
+         .done( this.onCurrentTaskStatus )
+         .fail( this.onCurrentTaskStatusFail )
+      },
+
+      /** Successful result from checking the task */
+      onCurrentTaskStatus: function( taskDescription ) {
+        this.taskDescription = taskDescription;
+
+        var status = {
+            task: this,
+            dsId: this.ds.name(),
+            finished: this.taskFinished()
+        };
+
+        fui.vent.trigger( "task.status", status );
+
+        this.queueTaskStatusCheck();
+      },
+
+      /** Failed to check the task */
+      onCurrentTaskStatusFail: function( jqxhr, msg, err ) {
+        var status = {
+            task: this,
+            dsId: this.ds.name(),
+            errorMessage: err || msg
+        };
+
+        fui.vent.trigger( "task.failed", status );
+      },
+
+      /** Re-queue the status check if the task is not yet complete */
+      queueTaskStatusCheck: function() {
+        if (!this.taskFinished()) {
+          _.delay( this.checkTaskStatus, this.statusDelay() );
+        }
+      },
+
+      /** Return the completion time if the task has been fid, otherwise null */
+      taskFinished: function() {
+        return this.taskDescription.finished;
+      },
+
+      /** Return the delay in ms until the next status check is due. */
+      statusDelay: function() {
+        var t = this.delay;
+
+        if (t < MAX_DELAY) {
+          this.delay = t * 2;
+        }
+
+        return t;
+      }
+    } );
+
+    return Task;
+  }
+);
+

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/validation-options.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/validation-options.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/validation-options.js
new file mode 100644
index 0000000..b114cf9
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/models/validation-options.js
@@ -0,0 +1,85 @@
+/**
+ * Backbone model denoting the remote Fuseki server.
+ */
+define(
+  function( require ) {
+    "use strict";
+
+    var Marionette = require( "marionette" ),
+        Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        sprintf = require( "sprintf" );
+
+    /**
+     * This model represents the users current choice of options to the
+     * validation service.
+     */
+    var ValidationOptions = Backbone.Model.extend( {
+      initialize: function() {
+        this.set( {validateAs: "sparql"} );
+        this.set( {outputFormat: "algebra"} );
+      },
+
+      validateAs: function() {
+        return this.get( "validateAs" );
+      },
+
+      validateAsQuery: function() {
+        return this.validateAs() === "sparql" || this.validateAs() === "arq";
+      },
+
+      setValidateAs: function( va ) {
+        this.set( "validateAs", va );
+        console.log( JSON.stringify( this.toJSON() ));
+        console.log( "----" );
+      },
+
+      outputFormat: function() {
+        return this.get( "outputFormat" );
+      },
+
+      setOutputFormat: function( of ) {
+        this.set( "outputFormat", of );
+      },
+
+      validationURL: function() {
+        switch (this.get( "validateAs" )) {
+        case "sparql":  return "/validate/query";
+        case "arq":  return "/validate/query";
+        case "Turtle": return "/validate/data";
+        case "TriG": return "/validate/data";
+        case "N-Triples": return "/validate/data";
+        case "N-Quads": return "/validate/data";
+        }
+      },
+
+      payloadParam: function() {
+        return this.validateAsQuery() ? "query" : "data";
+      },
+
+      toJSON: function() {
+        var json = {
+          languageSyntax: this.validateAs(),
+          lineNumbers: true
+        };
+
+        if (this.validateAsQuery()) {
+          json.outputFormat = this.outputFormat();
+        }
+
+        return json;
+      }
+
+    } );
+
+    // when the models module starts, create the model
+    fui.models.addInitializer( function( options ) {
+      fui.models.validationOptions = new ValidationOptions();
+      fui.vent.trigger( "models.validation-options.ready" );
+    } );
+
+
+    return ValidationOptions;
+  }
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/qonsole-config.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/qonsole-config.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/qonsole-config.js
new file mode 100644
index 0000000..0c48273
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/qonsole-config.js
@@ -0,0 +1,27 @@
+/** Standalone configuration for qonsole on index page */
+
+define( [], function() {
+  return {
+    prefixes: {
+      "rdf":      "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+      "rdfs":     "http://www.w3.org/2000/01/rdf-schema#",
+      "owl":      "http://www.w3.org/2002/07/owl#",
+      "xsd":      "http://www.w3.org/2001/XMLSchema#"
+    },
+    queries: [
+      { "name": "Selection of triples",
+        "query": "SELECT ?subject ?predicate ?object\nWHERE {\n" +
+                 "  ?subject ?predicate ?object\n}\n" +
+                 "LIMIT 25"
+      },
+      { "name": "Selection of classes",
+        "query": "SELECT DISTINCT ?class ?label ?description\nWHERE {\n" +
+                 "  ?class a owl:Class.\n" +
+                 "  OPTIONAL { ?class rdfs:label ?label}\n" +
+                 "  OPTIONAL { ?class rdfs:comment ?description}\n}\n" +
+                 "LIMIT 25",
+        "prefixes": ["owl", "rdfs"]
+      }
+    ]
+  };
+} );

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/services/ping-service.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/services/ping-service.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/services/ping-service.js
new file mode 100644
index 0000000..75ef6a4
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/services/ping-service.js
@@ -0,0 +1,54 @@
+/**
+ * The ping service checks the status of the attached server and sets the light in the
+ * control bar accordingly.
+ */
+define( ['jquery', 'underscore', 'sprintf'],
+  function( $, _, sprintf ) {
+
+    var PING_URL = "$/ping"
+    var DEFAULT_PING_TIME = 500000;  // TODO slowed down during debugging phase
+    var _startTime = 0;
+
+    var onBeforeSend = function() {
+      _startTime = new Date().getTime();
+    };
+
+    var duration = function() {
+      return new Date().getTime() - _startTime;
+    };
+
+    var onPingSuccess = function( ) {
+      setPingStatus( "server-up", sprintf( "Last ping returned OK in %dms", duration() ) );
+    };
+
+    var onPingFail = function( jqXHR, msg, errorThrown ) {
+      setPingStatus( "server-down", sprintf( "Last ping returned '%s' in %dms", errorThrown || msg, duration() ) );
+    };
+
+    var setPingStatus = function( lampClass, statusText ) {
+      $( "a#server-status-light span").removeClass()
+                                      .addClass( lampClass )
+                                      .attr( "title", statusText );
+    };
+
+    /** Return a cache-defeating ping URL */
+    var ping_url = function() {
+      return PING_URL + "?_=" + Math.random();
+    };
+
+    var start = function( period ) {
+      ping( period || DEFAULT_PING_TIME );
+    };
+
+    var ping = function( period ) {
+      onBeforeSend();
+      $.get( ping_url() ).done( onPingSuccess )
+                         .fail( onPingFail );
+      setTimeout( function() {ping( period );}, period );
+    };
+
+    return {
+      start: start
+    }
+  }
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/services/validation-service.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/services/validation-service.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/services/validation-service.js
new file mode 100644
index 0000000..1a5d919
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/services/validation-service.js
@@ -0,0 +1,98 @@
+define( ['underscore', 'jquery', 'fui',
+         'lib/codemirror', 'mode/javascript/javascript', 'mode/sparql/sparql'],
+  function( _, $, fui, CodeMirror ) {
+
+    var ValidationService = function( editor_el, output_el ) {
+      this.editor_el = editor_el;
+      this.output_el = output_el;
+    };
+
+    _.extend( ValidationService.prototype, {
+      init: function() {
+        _.bindAll( this, "handleValidationOutput", "handleJsonValidationOutput" );
+        this.editorElement();
+      },
+
+      /** Return the DOM node representing the query editor */
+      editorElement: function() {
+        if (!this._editor) {
+          this._editor = new CodeMirror( $(this.editor_el).get(0), {
+            lineNumbers: true,
+            mode: "text"
+          } );
+        }
+        return this._editor;
+      },
+
+      /** Return the DOM node representing the output editor */
+      outputElement: function( mode, lineNumbers, data ) {
+        $(this.output_el).empty();
+
+        var cm = new CodeMirror( $(this.output_el).get(0), {
+          lineNumbers: lineNumbers,
+          mode: mode || "text",
+          readOnly: true,
+          value: data
+        } );
+
+        return cm;
+      },
+
+      /** Return the current code editor contents */
+      editorContent: function() {
+        return this.editorElement().getValue();
+      },
+
+      /** Perform the given action to validate the current content */
+      performValidation: function( optionsModel ) {
+        var context = {optionsModel: optionsModel};
+        var self = this;
+
+        var content = {};
+        content[optionsModel.payloadParam()] = this.editorContent();
+
+        var options = {
+            data: _.extend( optionsModel.toJSON(), content ),
+            type: "POST"
+        };
+
+        $.ajax( optionsModel.validationURL(), options )
+         .done( function( data, status, xhr ) {
+           self.handleValidationOutput( data, status, xhr, context );
+         } );
+      },
+
+      /** Respond to validation output from the server */
+      handleValidationOutput: function( data, status, xhr, context ) {
+        var ct = xhr.getResponseHeader("content-type") || "";
+        if (ct.match( /json/ )) {
+          this.handleJsonValidationOutput( data, context );
+        }
+        else {
+          // in HTML output, we look for a .error div
+          var errors = $(data).find( "div.error" ).text();
+          this.outputElement( "text", true, errors || "No warnings or errors reported." );
+        }
+      },
+
+      handleJsonValidationOutput: function( json, context ) {
+        var outputFormat = context.optionsModel.outputFormat();
+        console.log( "output format = " + outputFormat );
+        var jsonString = null;
+
+        if (outputFormat && json[outputFormat]) {
+          jsonString = json[outputFormat];
+        }
+        else {
+          jsonString = JSON.stringify( json, null, '  ' );
+        }
+
+        this.outputElement( "application/json", false, jsonString );
+      }
+
+    } );
+
+
+    return ValidationService;
+  }
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-edit.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-edit.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-edit.tpl
new file mode 100644
index 0000000..8202831
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-edit.tpl
@@ -0,0 +1,58 @@
+<div class="row">
+  <div class="col-md-4">
+    <div class="bordered-box">
+      <span class="pull-right">
+        <button class="btn btn-sm btn-info action list-graphs">list current graphs</button>
+      </span>
+      <h3>Available graphs</h3>
+
+      <% if (countPerformed()) { %>
+        <ul class="nav nav-pills nav-stacked graphs">
+          <% _.each( counts(), function( n, g ) { %>
+            <li class="">
+              <a href="#" class="select-dataset" data-graph-name="<%= g %>" data-graph-size="<%= n %>">
+                <%= g %> (<%= n %> triples)
+              </a>
+            </li>
+          <% } ); %>
+        </ul>
+      <% } else { %>
+        <p class="text-muted text-sm">Click to list current graphs</p>
+      <% } %>
+    </div> <!-- /.bordered-box -->
+  </div> <!-- /.col-md-4 -->
+
+  <div class="col-md-8">
+    <div class="row">
+      <div class="col-md-12">
+        <div class="form-group">
+          <div class="input-group">
+            <div class="input-group-addon">graph:</div>
+            <input class="form-control graph-name" type="text" placeholder="">
+          </div>
+        </div>
+      </div>
+    </div> <!-- /.row -->
+
+    <div class="row">
+      <div class="col-md-12">
+        <div id="graph-editor" class="bordered-box"></div>
+      </div>
+    </div> <!-- /.row -->
+
+    <div class="row">
+      <div class="col-md-12">
+        <p class="feedback"></p>
+      </div>
+    </div> <!-- /.row -->
+
+    <div class="row">
+      <div class="col-md-12">
+        <span class="pull-right">
+          <button class="btn btn-default action cancel-edit"><i class="fa fa-times"></i> discard changes</button>
+          <button class="btn btn-info action save-edit"><i class="fa fa-check"></i> save</button>
+        </span>
+      </div>
+    </div> <!-- /.row -->
+  </div> <!-- /.col-md-8 -->
+</div>

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-info.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-info.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-info.tpl
new file mode 100644
index 0000000..c2c6891
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-info.tpl
@@ -0,0 +1,40 @@
+<h2>Available services</h2>
+
+<dl class="dl-horizontal">
+  <% _.each( servicesDescription(), function( serviceDescription ) { %>
+    <dt>
+      <%= serviceDescription.label %>:
+    </dt>
+    <dd>
+      <a href="<%= serviceDescription.url %>"><%= serviceDescription.url %></a>
+    </dd>
+  <% } ); %>
+</dl>
+
+<h2>Statistics</h2>
+<div id="statistics"></div>
+
+<h2>Dataset size</h2>
+<p>
+<strong>Note</strong> this may be slow and impose a significant load on large datasets:
+<button href="#" class="action count-graphs btn btn-primary">count triples in all graphs</button>
+</p>
+<% if (countPerformed()) { %>
+<dl class="dl-horizontal">
+  <dt><span class="heading">graph name:</span></dt><dd><span class="heading">triples:</span></dd>
+  <% _.each( counts(), function( n, g ) { %>
+    <dt class="font-weight-normal">
+      <%= g %>
+    </dt>
+    <dd>
+      <div class="numeric"><%= n %></div>
+    </dd>
+  <% } ); %>
+</dl>
+
+<% } %>
+
+<h2>Ongoing operations</h2>
+
+<p><em>TBD. Will list any long-lasting operations that are ongoing or recently completed,
+e.g. backups.</em></p>

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-management.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-management.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-management.tpl
new file mode 100644
index 0000000..9a01812
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-management.tpl
@@ -0,0 +1,62 @@
+<% if (datasets.length === 0) { %>
+  <p>No datasets have been created yet.
+    <a class="btn btn-sm btn-primary" href="?tab=new-dataset">add one</a>
+  </p>
+<% } else { %>
+  <div class="row">
+    <div class="col-md-12">
+      <table class='table'>
+        <tr class="headings">
+          <th>Name</th>
+          <!-- JENA-867 <th>Active?</th> -->
+          <th></th>
+        </tr>
+        <% _.each( datasets, function( ds ) { %>
+          <tr>
+            <td>
+              <%= ds.name() %>
+            </td>
+            <!-- JENA-867 temporarily disable non-functional checkbox
+            <td>
+              <input type='checkbox' class='checkbox' checked />
+            </td>
+            -->
+            <td>
+              <div>
+                <!-- JENA-869 Disable download button until it works again -->
+                <a class="btn btn-sm action remove btn-primary" data-ds-id='<%= ds.name() %>'><i class='fa fa-times-circle'></i> remove</a>
+                <a class="btn btn-sm action backup btn-primary" data-ds-id='<%= ds.name() %>'><i class='fa fa-download'></i> backup</a>
+                <a class="btn btn-sm action add-data btn-primary" href="dataset.html?tab=upload&ds=<%= ds.name() %>"><i class='fa fa-upload'></i> upload data</a>
+              </div>
+              <div class="action feedback"></a>
+            </td>
+          </tr>
+        <% }) %>
+
+      </table>
+    </div>
+  </div>
+<% } %>
+
+<!-- Modal dialogs -->
+
+<div class="modal fade" id="actionConfirmModal" tabindex="-1" role="dialog" aria-labelledby="actionConfirmModalLabel" aria-hidden="true">
+  <div class="modal-dialog">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <h4 class="modal-title" id="actionConfirmModalLabel">Confirm action</h4>
+      </div>
+      <div class="modal-body">
+        <p></p>
+      </div>
+      <div class="modal-footer">
+        <button type="button" class="btn btn-default" data-dismiss="modal"><i class="fa fa-icon-remove"></i> Cancel</button>
+        <button type="button" class="btn btn-primary action confirm">
+          <i class="fa fa-icon-confirm"></i>
+          Confirm <span class="action-label">action</span>
+        </button>
+      </div>
+    </div><!-- /.modal-content -->
+  </div><!-- /.modal-dialog -->
+</div><!-- /.modal -->

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-selection-list.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-selection-list.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-selection-list.tpl
new file mode 100644
index 0000000..7eda02d
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-selection-list.tpl
@@ -0,0 +1,22 @@
+<div class="col-md-span-12">
+  <% if (datasets.length > 0) { %>
+    <table class='table ijd'>
+      <tr class="headings"><th>dataset name</th><th>actions</th></tr>
+      <% _.each( datasets, function( ds ) { %>
+        <tr>
+          <td>
+            <%= ds.name() %>
+          </td>
+          <td>
+            <a class="btn btn-sm action remove btn-primary" href="dataset.html?tab=query&ds=<%= ds.name() %>"><i class='fa fa-question-circle'></i> query</a>
+            <a class="btn btn-sm action remove btn-primary" href="dataset.html?tab=upload&ds=<%= ds.name() %>"><i class='fa fa-upload'></i> add data</a>
+            <a class="btn btn-sm action configure btn-primary" href="dataset.html?tab=info&ds=<%= ds.name() %>"><i class='fa fa-dashboard'></i> info</a>
+          </td>
+        </tr>
+      <% }) %>
+
+    </table>
+   <% } else { %>
+    <p>There are no datasets on this server yet. <a href="manage.html?tab=new-dataset">Add one.</a></p>
+   <% } %>
+</div>

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-selector.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-selector.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-selector.tpl
new file mode 100644
index 0000000..d684995
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-selector.tpl
@@ -0,0 +1,15 @@
+<div class='dataset-selector'>
+  <% if (datasets.length == 0) { %>
+  <% } else { %>
+    <label class="">
+      <div class="select-picker-label">Dataset:</div>
+      <select class='selectpicker show-tick'>
+        <% _.each( datasets, function( ds ) { %>
+          <option <%= (ds.name() === selectedDatasetName) ? "selected" : "" %>>
+            <%= ds.name() %>
+          </option>
+        <% } ); %>
+      </select>
+    </label>
+  <% } %>
+</div>

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-simple-create.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-simple-create.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-simple-create.tpl
new file mode 100644
index 0000000..3819bdc
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-simple-create.tpl
@@ -0,0 +1,85 @@
+<div class="row">
+  <div class="col-md-12">
+      <div class="" id="simple-edit">
+        <form class="form-horizontal" role="form">
+          <div class="form-group">
+            <label for="datasetName" class="col-sm-2 control-label">Dataset name</label>
+            <div class="col-sm-10">
+              <div class="validation-warning dbNameValidation">A name for the dataset is required</div>
+              <input type="text" class="form-control" name="dbName" placeholder="dataset name" />
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="col-sm-2 control-label">Dataset type</label>
+            <div class="col-sm-10">
+              <div class="radio">
+                <label>
+                  <input type="radio" name="dbType" value="mem" checked>
+                  In-memory &ndash; dataset will be recreated when Fuseki restarts, but contents will be lost
+                </label>
+              </div>
+              <div class="radio">
+                <label>
+                  <input type="radio" name="dbType" value="tdb">
+                  Persistent &ndash; dataset will persist across Fuseki restarts
+                </label>
+              </div>
+              <div class="radio">
+                <label>
+                  <input type="radio" name="dbType" value="tdb2">
+                  Persistent (TDB2) &ndash; dataset will persist across Fuseki restarts
+                </label>
+              </div>
+            </div>
+          </div>
+
+          <div class="row">
+            <div class="col-md-12">
+              <div class="errorOutput bg-warning"></div>
+            </div>
+          </div>
+
+          <div class="row controls">
+            <div class="col-md-3 col-md-offset-9">
+              <a href="#" class="btn btn-sm btn-primary action commit simple"><i class="fa fa-check"></i> create dataset</a>
+            </div>
+          </div>
+        </form>
+      </div>
+
+      <!--
+      <div class="tab-pane" id="upload">
+        <p>&nbsp;</p>
+        <div class="row">
+          <p class="col-sm-12">If you have a Fuseki config file (i.e. a Jena assembler description),
+          you can upload it here:</p>
+        </div>
+        <div class="row controls">
+          <form id="uploadForm" method="post" action="$/datasets" class="form-horizontal col-sm-12">
+            <div class="form-group">
+              <label for="assemblerFile" class="col-sm-2 control-label">Configuration file</label>
+              <div class="col-sm-10">
+                <div class="validation-warning assemblerFileValidation">A file name is required</div>
+                <input type="file" class="form-control" name="assemblerFile" />
+              </div>
+            </div>
+          </form>
+        </div>
+
+        <div class="row">
+          <div class="errorOutput col-sm-12"></div>
+        </div>
+
+        <div class="row">
+          <div class="col-sm-12">
+            <a href="admin-data-management.html" class="btn btn-sm btn-default"><i class="fa fa-mail-reply"></i> cancel</a>
+            <a href="#" class="btn btn-sm btn-primary action upload"><i class="fa fa-upload"></i> upload config file</a>
+          </div>
+        </div>
+      </div>
+      -->
+
+  </div><!-- /.col-md-12 -->
+</div><!-- /.row -->
+
+

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-stats.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-stats.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-stats.tpl
new file mode 100644
index 0000000..fc3d7d9
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/dataset-stats.tpl
@@ -0,0 +1,14 @@
+<table class="table">
+  <tr>
+    <% _.each( headings, function( h ) { %>
+      <th class="text-right"><%= h %></th>
+    <% } ); %>
+  </tr>
+  <% _.each( rows, function( row ) { %>
+    <tr>
+      <% _.each( row, function( cell ) { %>
+        <td class="text-right"><%= cell %></td>
+      <% } ); %>
+    </tr>
+  <% } ) %>
+</table>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/file-upload.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/file-upload.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/file-upload.tpl
new file mode 100644
index 0000000..03d7dcc
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/file-upload.tpl
@@ -0,0 +1,46 @@
+<div class="col-md-12 ful">
+  <h2>Upload files</h2>
+  <p class="text-muted">Load data into the default graph of the currently selected dataset,
+    or the given named graph.
+    You may upload any RDF format, such as Turtle, RDF/XML or TRiG.
+  </p>
+
+  <div class="row">
+      <div class="form-group2 graph-label">
+        <label for="uploadGraphName" class="col-sm-3 control-label">Destination graph name</label>
+        <span class="col-sm-9">
+          <p>
+            <input type="text" name="graph" id="graphName" placeholder="Leave blank for default graph" class="form-control">
+          </p>
+        </span>
+      </div> <!-- /.form-group -->
+  </div>
+
+  <div class="row">
+      <form id="fileuploadForm" class="form-horizontal" role="form" method="POST" enctype="multipart/form-data">
+        <div class="form-group2">
+          <label class="col-sm-3 control-label">Files to upload</label>
+          <div class="col-sm-9">
+            <span>
+              <span class="btn btn-success fileinput-button">
+                  <i class="fa fa-plus"></i>
+                  <span>select files...</span>
+                  <input id="fileupload" type="file" name="files[]" multiple>
+              </span>
+              <button type="submit" class="btn btn-primary start action-upload-all" disabled>
+                  <i class="fa fa-upload"></i>
+                  <span>upload all</span>
+              </button>
+            </span>
+          </div>
+        </div> <!-- /.form-group -->
+
+        <div class="form-group2">
+          <div class="col-sm-9 col-sm-offset-3">
+            <ul class="list-unstyled">
+            </ul>
+          </div>
+        </div> <!-- /.form-group -->
+      </form>
+  </div>
+ </div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/uploadable-file.tpl
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/uploadable-file.tpl b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/uploadable-file.tpl
new file mode 100644
index 0000000..83b8449
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/templates/uploadable-file.tpl
@@ -0,0 +1,23 @@
+<div class="row file-description">
+  <div class="col-sm-3">
+    <%= file.name %>
+  </div>
+  <div class="col-sm-3">
+    <em>
+      <%= file.readableFileSize %>
+    </em>
+  </div>
+  <div class="col-sm-6">
+    <button class="btn btn-sm btn-default action action-upload-file"><i class="fa fa-upload"></i> upload now</button>
+    <button class="btn btn-sm btn-default action action-remove-upload"><i class="fa fa-minus-circle"></i> remove</button>
+  </div>
+  <div class="col-sm-12">
+    <div class="result"></div>
+  </div>
+  <div class="col-sm-12">
+    <div class="progress">
+      <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
+    </div>
+  </div>
+
+</div>

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/util/page-utils.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/util/page-utils.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/util/page-utils.js
new file mode 100644
index 0000000..45b4ffc
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/util/page-utils.js
@@ -0,0 +1,33 @@
+/** Utilities for managing HTML pages */
+
+define(
+  function( require ) {
+    "use strict";
+
+    var _ = require( "underscore" );
+
+    /** Return true if a given query parameter is defined, otherwise null */
+    var hasQueryParam = function( param ) {
+      return !!queryParam( param );
+    };
+
+    /** Return the value of a query parameter, or null */
+    var queryParam = function( param ) {
+      var p = param && queryParams()[param];
+      return p ? p : null;
+    };
+
+    /** Return the current query params as a map */
+    var queryParams = function() {
+      return _.chain( document.location.search.slice(1).split('&') )
+              .invoke('split', '=')
+              .object()
+              .value();
+    };
+
+    return {
+      hasQueryParam: hasQueryParam,
+      queryParam: queryParam
+    };
+  }
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-edit.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-edit.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-edit.js
new file mode 100644
index 0000000..017e097
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-edit.js
@@ -0,0 +1,205 @@
+/** Component for showing detailed information about a dataset */
+
+define(
+  function( require ) {
+    var Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        DatasetEditTpl = require( "plugins/text!app/templates/dataset-edit.tpl" ),
+        CodeMirror = require( "lib/codemirror" ),
+        CodeMirrorTurtle = require( "mode/turtle/turtle" );
+
+    var MAX_EDITABLE_SIZE = 10000;
+
+    var DatasetEdit = Backbone.Marionette.ItemView.extend( {
+
+      initialize: function() {
+        _.bindAll( this, "onCountGraphs", "onModelChanged", "onSelectDataset",
+                         "onShownTab", "onShownEditTab", "onGraphContent",
+                         "onSaveEdit", "onCancelEdit" );
+
+        this.model.on( "change", this.onModelChanged );
+        this._editor = null;
+
+        fui.vent.on( "shown.bs.tab", this.onShownTab );
+      },
+
+      template: _.template( DatasetEditTpl ),
+
+      ui: {
+        listGraphs: ".action.list-graphs",
+        editor: "#graph-editor",
+        graphName: "input.graph-name",
+        saveButton: "button.save-edit",
+        cancelButton: "button.cancel-edit"
+      },
+
+      el: "#edit .with-dataset",
+
+      events: {
+        "click .list-graphs": "onCountGraphs",
+        "click .select-dataset": "onSelectDataset",
+        "click .save-edit": "onSaveEdit",
+        "click .cancel-edit": "onCancelEdit"
+      },
+
+      templateHelpers: {
+      },
+
+      serializeData: function() {
+        return this.model;
+      },
+
+      /** Alias for the model */
+      dataset: function() {
+        return this.model;
+      },
+
+      // event handlers
+
+      onModelChanged: function() {
+        if (!this.model.counting) {
+          this.render();
+        }
+      },
+
+      onCountGraphs: function( e ) {
+        e.preventDefault();
+        this.model.count();
+      },
+
+      /** Event that triggers when any tab is shown */
+      onShownTab: function( tab ) {
+        if (tab.attr("href") === "#edit") {
+          this.onShownEditTab();
+        }
+      },
+
+      /** When the tab is show, ensure the editor element is initialised */
+      onShownEditTab: function() {
+        this.showEditor();
+      },
+
+      /** When rendering, only show the code mirror editor if the tab is visible */
+      onRender: function() {
+        this.showEditor();
+      },
+
+      /** Ensure the code mirror element is visible */
+      showEditor: function() {
+        if (this.editorElementVisible() && this.editorNotYetInitialised()) {
+          this._editor = null;
+          this.editorElement();
+        }
+      },
+
+      /** Return true if the editor container element is visible */
+      editorElementVisible: function()  {
+        return this.ui.editor.is( ":visible" );
+      },
+
+      /** Return true if the CodeMirror element has not yet been initialised */
+      editorNotYetInitialised: function() {
+        return this.ui.editor.is( ":not(:has(.CodeMirror))" );
+      },
+
+      /** User has (attempted to) select a dataset */
+      onSelectDataset: function( e ) {
+        e.preventDefault();
+        var self = this;
+        var elem = $(e.currentTarget);
+        var graphName = elem.data( "graph-name" );
+        var graphSize = parseInt( elem.data( "graph-size" ));
+
+        if (graphSize > MAX_EDITABLE_SIZE) {
+          alert( "Sorry, that dataset is too large to load into the editor" );
+        }
+        else {
+          if (this.dirtyCheck()) {
+            $(".nav.graphs").find( ".active" ).removeClass( "active" );
+            elem.parent().addClass( "active" );
+
+            var gn = this.setGraphName( graphName );
+            this.dataset()
+                .fetchGraph( gn )
+                .done( self.onGraphContent );
+          }
+        }
+      },
+
+      /** Return true if the edit buffer is not dirty, or if the user says OK */
+      dirtyCheck: function() {
+        return true; // TODO
+      },
+
+      /** Return the DOM node representing the query editor */
+      editorElement: function() {
+        if (!this._editor) {
+          this._editor = new CodeMirror( this.ui.editor.get(0), {
+            lineNumbers: true,
+            mode: "turtle"
+          } );
+        }
+        return this._editor;
+      },
+
+      /** Set the graph name, return the actual name used */
+      setGraphName: function( name ) {
+        var text = (name === "default" || name === "default graph") ? "default" : name;
+
+        this.ui.graphName.val( text );
+
+        return text;
+      },
+
+      /** Get the graph name */
+      graphName: function() {
+        return this.ui.graphName.val();
+      },
+
+      /** Server has sent the content of the graph encoded as turtle */
+      onGraphContent: function( data ) {
+        this.editorElement().setValue( data );
+      },
+
+      /** User wants to save changes */
+      onSaveEdit: function( e ) {
+        e.preventDefault();
+        var self = this;
+
+        var turtle = this.editorElement().getValue();
+        this.dataset()
+            .putGraph( turtle, this.graphName() )
+            .done( function( data ) {
+              var nq = parseInt( data.quadCount );
+              var nt = parseInt( data.tripleCount );
+              var typ = (nq > nt) ? "quad" : "triple";
+              var s = (nq + nt) === 1 ? "" : "s";
+              var msg = sprintf( "Added %d %s%s", nq + nt, typ, s );
+
+              self.showFeedback( msg, "" );
+            } )
+            .error( function( jqhxr, msg, err ) {
+              self.showFeedback( err || msg, "text-warning" );
+            } );
+      },
+
+      /** User wants to discard changes */
+      onCancelEdit: function( e ) {
+        e.preventDefault();
+        this.ui.graphName.val( "" );
+        this.editorElement().setValue( "" );
+      },
+
+      /** Show feedback from operations */
+      showFeedback: function( msg, cls ) {
+        $(".feedback").html( sprintf( "<span class='%s'>%s</span>", cls, msg ) );
+      }
+
+
+    });
+
+
+    return DatasetEdit;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-info.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-info.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-info.js
new file mode 100644
index 0000000..665725f
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-info.js
@@ -0,0 +1,76 @@
+/** Component for showing detailed information about a dataset */
+
+define(
+  function( require ) {
+    var Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        DatasetInfoTpl = require( "plugins/text!app/templates/dataset-info.tpl" ),
+        DatasetStatsView = require( "app/views/dataset-stats" ),
+        DatasetStatsModel = require( "app/models/dataset-stats" );
+
+    var DatasetInfo = Backbone.Marionette.ItemView.extend( {
+
+      initialize: function() {
+        _.bindAll( this, "onModelChanged", "onCountGraphs" );
+
+        this.showStatistics( true );
+        this.model.on( "change", this.onModelChanged );
+      },
+
+      template: _.template( DatasetInfoTpl ),
+
+      ui: {
+        stats: "#statistics",
+        count: ".count-graphs"
+      },
+
+      el: "#info .with-dataset",
+
+      events: {
+        "click .count-graphs": "onCountGraphs"
+      },
+
+      templateHelpers: {
+      },
+
+      serializeData: function() {
+        return this.model;
+      },
+
+      /** Alias for the model */
+      dataset: function() {
+        return this.model;
+      },
+
+      // event handlers
+
+      onModelChanged: function() {
+        if (!this.model.counting) {
+          this.render();
+          this.showStatistics( false );
+        }
+      },
+
+      onCountGraphs: function( e ) {
+        e.preventDefault();
+        this.model.count();
+      },
+
+      showStatistics: function( keep ) {
+        var self = this;
+
+        this.model
+            .statistics( keep )
+            .done( function( data ) {
+                     var statsModel = new DatasetStatsModel( self.dataset(), data );
+                     new DatasetStatsView( {model: statsModel} ).render();
+                   } );
+      }
+
+    });
+
+
+    return DatasetInfo;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-management.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-management.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-management.js
new file mode 100644
index 0000000..ca3af74
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-management.js
@@ -0,0 +1,173 @@
+define(
+  function( require ) {
+    var Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        datasetManagementViewTpl = require( "plugins/text!app/templates/dataset-management.tpl" );
+
+    var DataManagementView = Backbone.Marionette.ItemView.extend( {
+
+      initialize: function(){
+        _.bindAll( this, "onRemoveDataset", "onConfirmAction",
+                         "onDatasetRemoveSuccess", "onDatasetRemoveFail",
+                         "onTaskStatus", "onTaskFailed", "cleanup" );
+
+        this.listenTo( this.model, "change", this.onModelChange, this );
+
+        fui.vent.on( "action.delete.confirm", this.onConfirmRemoveDataset, this );
+        fui.vent.on( "action.backup.confirm", this.onConfirmBackupDataset, this );
+
+        fui.vent.on( "task.status", this.onTaskStatus, this );
+        fui.vent.on( "task.failed", this.onTaskFailed, this );
+      },
+
+      template: _.template( datasetManagementViewTpl ),
+
+      ui: {
+        actionConfirmModal: "#actionConfirmModal"
+      },
+
+      el: "#dataset-management",
+
+      events: {
+        "click .action.remove": "onRemoveDataset",
+        "click .action.confirm": "onConfirmAction",
+        "click .action.backup": "onBackupDataset"
+      },
+
+      templateHelpers: {
+      },
+
+      cleanup: function() {
+        this.unbind();
+        this.undelegateEvents();
+        this.model.unbind( 'change', this.onModelChange, this ); 
+        fui.vent.unbind( 'action.delete.confirm', this.onConfirmRemoveDataset, this );
+      },
+
+      /** If the model changes, update the summary */
+      onModelChange: function( event ) {
+         this.cleanup();
+         this.render();
+      },
+
+      /** User has requested a dataset be removed */
+      onRemoveDataset: function( e ) {
+        e.preventDefault();
+        var elem = $(e.currentTarget);
+        var dsId = elem.data( "ds-id" );
+        var msg = sprintf( "Are you sure you want to delete dataset <code>%s</code>? This action cannot be reversed.", dsId );
+
+        this.showConfirmationModal( msg, dsId, "action.delete.confirm" );
+      },
+
+      /** User has requested a dataset be backed up */
+      onBackupDataset: function( e ) {
+        e.preventDefault();
+        var elem = $(e.currentTarget);
+        var dsId = elem.data( "ds-id" );
+        var msg = sprintf( "Are you sure you want to create a backup of dataset <code>%s</code>? This action may take some time to complete", dsId );
+
+        this.showConfirmationModal( msg, dsId, "action.backup.confirm" );
+      },
+
+      /** Show a generic modal confirmation */
+      showConfirmationModal: function( msg, dsId, eventId ) {
+        this.ui.actionConfirmModal
+               .find( ".modal-body p" )
+               .html( msg );
+
+        this.ui.actionConfirmModal
+               .find( ".action.confirm" )
+               .data( "ds-id", dsId )
+               .data( "event-id", eventId );
+
+        this.clearFeedback();
+        this.ui.actionConfirmModal.modal( 'show' );
+      },
+
+      /** Generic response to confirming the current modal dialogue */
+      onConfirmAction: function( e ) {
+        e.preventDefault();
+        var elem = $(e.currentTarget);
+        var dsId = elem.data( "ds-id" );
+        var eventId = elem.data( "event-id" );
+
+        //this.ui.actionConfirmModal.modal( 'hide' );
+        $('.modal.in').modal('hide');
+        $('body').removeClass('modal-open');
+        $('.modal-backdrop').remove();
+        _.delay( function() {
+          fui.vent.trigger( eventId, dsId );
+        }, 100 );
+      },
+
+      /** User has confirmed that the dataset can be deleted */
+      onConfirmRemoveDataset: function( dsId ) {
+        var self = this;
+
+        fui.models
+           .fusekiServer
+           .dataset( dsId )
+           .deleteDataset()
+           .done( function( data ) {self.onDatasetRemoveSuccess( data, dsId );} )
+           .error( function( jqxhr, msg, err ) {self.onDatasetRemoveFail( jqxhr, msg, err, dsId );} );
+      },
+
+      /** Callback after successfully removing a dataset */
+      onDatasetRemoveSuccess: function( data, dsId ) {
+        this.model.loadServerDescription();
+      },
+
+      /** Removing the dataset did not work: notify the user */
+      onDatasetRemoveFail: function( jqxhr, msg, err, dsId ) {
+        this.feedbackArea( dsId )
+            .html( sprintf( "<p class='text-warning'>Sorry, removing dataset %s did not work, because: '%s'</p>", dsId, err || msg ) );
+      },
+
+      /** User has confirmed backing up the dataset */
+      onConfirmBackupDataset: function( dsId ) {
+        var self = this;
+
+        fui.models
+           .fusekiServer
+           .dataset( dsId )
+           .backupDataset();
+      },
+
+      /** Remove any current feedback content */
+      clearFeedback: function() {
+        $(".action.feedback").empty();
+      },
+
+      /** Long running task has updated status */
+      onTaskStatus: function( status ) {
+        var task = status.task;
+        var msg = sprintf( "<p>Task <strong>%s</strong> started at %s%s</p>",
+                           task.operationType,
+                           task.taskDescription.started,
+                           status.finished ? sprintf( ", finished at %s", status.finished ) : " &ndash; ongoing" );
+
+        this.feedbackArea( status.dsId )
+            .html( msg );
+      },
+
+      /** Long running task has failed to start */
+      onTaskFailed: function( status ) {
+        this.feedbackArea( status.dsId )
+            .html( sprintf( "<p class='text-danger'>Task %s failed: '%s'</p>", task.operationType, task.errorMessage ));
+      },
+
+      /** Find the feedback area for a particular dataset */
+      feedbackArea: function( dsId ) {
+        return $(sprintf( ".action[data-ds-id='%s']", dsId ) )
+               .parent()
+               .siblings(".action.feedback");
+      }
+
+    });
+
+
+    return DataManagementView;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-selection-list.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-selection-list.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-selection-list.js
new file mode 100644
index 0000000..ccad19d
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-selection-list.js
@@ -0,0 +1,58 @@
+/**
+ * This view presents a list of the available datasets for the user to interact
+ * with.
+ */
+
+define(
+  function( require ) {
+    var Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        datasetSelectionListTemplate = require( "plugins/text!app/templates/dataset-selection-list.tpl" );
+
+    var DatasetSelectionListView = Backbone.Marionette.ItemView.extend( {
+      initialize: function(){
+//        _.bindAll(this, "onFilter", "onModelChange");
+        this.listenTo( this.model, "change", this.onModelChange, this );
+      },
+
+      template: _.template( datasetSelectionListTemplate ),
+
+      el: "#dataset-selection-list",
+
+      ui: {
+      },
+
+      events: {
+//        "change #independent-variable-selection": "selectVariable",
+//        "click a.action.filter": "onFilter"
+      },
+
+      templateHelpers: {
+      },
+
+//      /** Update the model when the user changes the selection */
+//      selectVariable: function( event ) {
+//        this.model.set( "independentVarSelection", this.ui.variableSelection.val() );
+//      },
+//
+//      /** User wants to open the filter dialog */
+//      onFilter: function( event ) {
+//        var varModel = bgViz.models.variablesConfig.independentVar();
+//        var rangeType = varModel.component.range().rangeType();
+//        var viewName = rangeType.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
+//
+//        bgViz.layouts.filterDialog.showFilter( viewName, varModel );
+//      },
+
+      /** If the model changes, update the summary */
+      onModelChange: function( event ) {
+//        this.ui.summary.html( this.model.independentVar().component.range().summarise() );
+      }
+
+    });
+
+
+    return DatasetSelectionListView;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-selector.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-selector.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-selector.js
new file mode 100644
index 0000000..f14a747
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-selector.js
@@ -0,0 +1,84 @@
+/**
+ * Reusable component that encapsulates selecting a dataset to work on in a given page.
+ * Takes the FusekiServer as a model, and populates a select control to choose one of the
+ * current datasets. If the dataset changes, this view will update the `selectedDatasetName`
+ * on the model, and trigger the event `dataset.changed`.
+ **/
+
+define(
+  function( require ) {
+    "use strict";
+
+    var Marionette = require( "marionette" ),
+        Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        sprintf = require( "sprintf" ),
+        datasetSelectorTemplate = require( "plugins/text!app/templates/dataset-selector.tpl" );
+
+    var DatasetSelectorView = Backbone.Marionette.ItemView.extend( {
+
+      initialize: function(){
+        this.listenTo( this.model, "change", this.render, this );
+      },
+
+      template: _.template( datasetSelectorTemplate ),
+
+      el: ".dataset-selector-container",
+
+      ui: {
+        select: ".dataset-selector select"
+      },
+
+      events: {
+        "change .dataset-selector select": "onChangeDataset"
+      },
+
+      /**
+       * After rendering, set up the dataset picker and notify the rest of the
+       * app if the default dataset name is known.
+       */
+      onRender: function() {
+        var selector = $('.selectpicker');
+        selector.selectpicker('refresh');
+
+        if (selector.val()) {
+          this.unHideDatasetElements();
+          this.onChangeDataset();
+        }
+      },
+
+      /**
+       * Respond to a change in the dataset name selection by updating
+       * the underlying model. TODO: should also update the application
+       * URL.
+       */
+      onChangeDataset: function( e ) {
+        var newDatasetName = this.ui.select.val();
+        this.model.set( "selectedDatasetName", newDatasetName );
+        this.notifyDatasetName( newDatasetName );
+      },
+
+      /**
+       * Ensure that elements that should be visible when a dataset is known
+       * are not hidden, and vice-versa.
+       */
+      unHideDatasetElements: function() {
+        $(".no-dataset").addClass( "hidden" );
+        $(".with-dataset").removeClass( "hidden" );
+      },
+
+      /** Trigger an event to notify other components that the dataset
+       * name has been selected.
+       */
+      notifyDatasetName: function( dsName ) {
+        fui.vent.trigger( "dataset.changed", dsName || this.ui.select.val() );
+      }
+
+
+    });
+
+
+    return DatasetSelectorView;
+  }
+);

http://git-wip-us.apache.org/repos/asf/jena/blob/e8abcbb6/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-simple-create.js
----------------------------------------------------------------------
diff --git a/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-simple-create.js b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-simple-create.js
new file mode 100644
index 0000000..b7a591b
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-webapp/src/main/webapp/js/app/views/dataset-simple-create.js
@@ -0,0 +1,102 @@
+/** Component for creating a new dataset with a few simple options */
+
+define(
+  function( require ) {
+    var Backbone = require( "backbone" ),
+        _ = require( "underscore" ),
+        fui = require( "app/fui" ),
+        DatasetSimpleCreateTpl = require( "plugins/text!app/templates/dataset-simple-create.tpl" );
+
+    var DatasetSimpleCreate = Backbone.Marionette.ItemView.extend( {
+
+      initialize: function() {
+        _.bindAll( this, "onCommitSimple", "clearWarnings" );
+      },
+
+      template: _.template( DatasetSimpleCreateTpl ),
+
+      ui: {
+      },
+
+      el: "#dataset-simple-create",
+
+      events: {
+        "click a.action.commit.simple": "onCommitSimple",
+        "submit form": "onCommitSimple",
+        "keydown input[name=dbName]": "clearWarnings"
+      },
+
+      templateHelpers: {
+      },
+
+      serializeData: function() {
+        return this.model;
+      },
+
+      // event handlers
+
+      onCommitSimple: function( e ) {
+        e.preventDefault();
+
+        if (this.validateSimpleForm()) {
+          var datasetName = $("input[name=dbName]").val().trim();
+          $("input[name=dbName]").val(datasetName);
+          var options = $("#simple-edit form").serializeArray();
+          fui.models.fusekiServer.updateOrCreateDataset( null, options )
+                                 .done( this.showDataManagementPage )
+                                 .fail( this.showFailureMessage );
+        }
+      },
+
+//      onCommitUpload: function( e ) {
+//        e.preventDefault();
+//
+//        if (this.validateUploadForm()) {
+//          $("#uploadForm").ajaxSubmit( {
+//                            success: this.showDataManagementPage,
+//                            error: this.showFailureMessage
+//                           });
+//        }
+//      },
+//
+      showDataManagementPage: function( e ) {
+        location = "?tab=datasets";
+      },
+
+      /** Todo: need to do a better job of responding to errors */
+      showFailureMessage: function( jqXHR, textStatus, errorThrown ) {
+        $(".errorOutput").html( sprintf( "<p class='has-error'>Sorry, that didn't work because:</p><pre>%s</pre>", errorThrown || textStatus ) );
+      },
+
+      /** Clear current warning states */
+      clearWarnings: function() {
+        this.clearValidation();
+        $(".errorOutput").empty();
+      },
+
+      // validation
+
+      validateSimpleForm: function() {
+        this.clearValidation();
+
+        if (! $("input[name=dbName]").val() || 0 === $("input[name=dbName]").val().trim().length) {
+          $(".dbNameValidation").removeClass("hidden")
+                                .parents(".form-group" )
+                                .addClass( "has-error" );
+          return false;
+        }
+
+        return true;
+      },
+
+      clearValidation: function() {
+        $(".has-error").removeClass( "has-error" );
+        $(".has-warning").removeClass( "has-warning" );
+      }
+
+    });
+
+
+    return DatasetSimpleCreate;
+  }
+);