DATAREST-627 - HAL Browser now uses JSON Editor view for resources exposing metadata.

In case a link relation's target exposes a profile link (via HTTP HEAD and "Link" response header), we now try to obtain a JSON schema from the target resource and use a custom view based on the JSON Editor library fro non-GET requests.

This creates a customized, user friendly pop-up dialog for data entry with a nice polished look. If there is no profile link, we use a fallback editor for raw JSON input.

Original pull request: #191.
This commit is contained in:
Greg Turnquist
2015-07-30 14:51:49 -05:00
committed by Oliver Gierke
parent 71cfe235ad
commit be67f3e5b0
4 changed files with 520 additions and 4 deletions

View File

@@ -12,7 +12,8 @@
<name>Spring Data REST - HAL Browser</name>
<properties>
<browser.version>b7669f1-1</browser.version>
<browser.version>9f96c74</browser.version>
<json-editor.version>0.7.21</json-editor.version>
</properties>
<dependencies>
@@ -33,7 +34,14 @@
<dependency>
<groupId>org.webjars</groupId>
<artifactId>hal-browser</artifactId>
<version>b7669f1-1</version>
<version>${browser.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>json-editor</artifactId>
<version>${json-editor.version}</version>
<scope>provided</scope>
</dependency>
@@ -49,7 +57,7 @@
<version>2.10</version>
<executions>
<execution>
<id>unpack</id>
<id>unpack-hal-browser</id>
<phase>process-resources</phase>
<goals>
<goal>unpack</goal>
@@ -64,6 +72,22 @@
<outputDirectory>${project.build.directory}/hal-browser</outputDirectory>
</configuration>
</execution>
<execution>
<id>unpack-json-editor</id>
<phase>process-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.webjars</groupId>
<artifactId>json-editor</artifactId>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/json-editor</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
@@ -82,7 +106,10 @@
<move todir="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-browser">
<fileset dir="${project.build.directory}/hal-browser/META-INF/resources/webjars/hal-browser/${browser.version}" />
</move>
<move file="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-browser/browser.html" tofile="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-browser/index.html" />
<move todir="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-browser/vendor/js">
<fileset dir="${project.build.directory}/json-editor/META-INF/resources/webjars/json-editor/${json-editor.version}" />
</move>
<delete file="${project.build.outputDirectory}/META-INF/spring-data-rest/hal-browser/browser.html" />
</target>
</configuration>
</execution>

View File

@@ -0,0 +1,288 @@
<!doctype html>
<head>
<meta charset="utf-8">
<title>The HAL Browser (customized for Spring Data REST)</title>
<link rel="stylesheet" media="screen" href="vendor/css/bootstrap.css" />
<style type="text/css">
body {
padding-top: 60px;
padding-bottom: 40px;
}
.sidebar-nav {
padding: 9px 0;
}
</style>
<link rel="stylesheet" media="screen" href="vendor/css/bootstrap-responsive.css" />
<link rel="stylesheet" media="screen" href="styles.css" />
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="brand">The HAL Browser</a>
<div class="nav-collapse">
<ul class="nav">
<li><a href="#/" id="entryPointLink">Go To Entry Point</a></li>
<li><a href="https://github.com/mikekelly/hal-browser">About The HAL Browser</a></li>
</ul>
</div>
</div>
</div>
</div>
<div id="browser" class="container-fluid"></div>
<script id="location-bar-template" type="text/template">
<form>
<div class="input-append span12 location-bar-container">
<input class="span11" id="appendedInputButton" type="text" value="<%= _.escape(url) %>">
<button class="btn" type="submit">Go!</button>
<span class="ajax-loader"></span>
</div>
</form>
</script>
<script id="links-template" type="text/template">
<h2>Links</h2>
<table class="table">
<thead>
<tr>
<th>rel</th>
<th>title</th>
<th>name / index</th>
<th>docs</th>
<th>GET</th>
<th>NON-GET</th>
</tr>
</thead>
<tbody>
<% _.each(links, function(obj, rel) { %>
<% if ($.isArray(obj)) { %>
<% _.each(obj, function(link, i) { %>
<tr>
<td><strong><%= HAL.truncateIfUrl(rel) %></strong></td>
<td><%= link.title || '' %></td>
<td><%= link.name ? 'name: ' + link.name : 'index: ' + i %></a></td>
<td>
<% if (HAL.isUrl(rel)) { %>
<a class="dox" href="<%= HAL.normalizeUrl(HAL.buildUrl(rel)) %>"><i class="icon-book"></i></a>
<% } %>
</td>
<td>
<% if (link.templated === true) { %>
<a class="query btn btn-success" href="<%= HAL.normalizeUrl(link.href) %>" title="Query URI template"><i class="icon-question-sign"></i></a>
<% } else { %>
<a class="follow btn btn-success" href="<%= HAL.normalizeUrl(link.href) %>" title="Follow link"><i class="icon-arrow-right"></i></a>
<% } %>
</td>
<td>
<a class="non-get btn btn-warning" href="<%= HAL.normalizeUrl(link.href) %>" title="Perform non-GET request">!</a>
</td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td><strong><%= HAL.truncateIfUrl(rel) %></strong></td>
<td><%= obj.title || '' %></td>
<td><%= obj.name || '' %></td>
<td>
<% if (HAL.isUrl(rel)) { %>
<a class="dox" href="<%= HAL.normalizeUrl(HAL.buildUrl(rel)) %>"><i class="icon-book"></i></a>
<% } %>
</td>
<td>
<% if (obj.templated === true) { %>
<a class="query btn btn-success" href="<%= HAL.normalizeUrl(obj.href) %>" title="Query URI template"><i class="icon-question-sign"></i></a>
<% } else { %>
<a class="follow btn btn-success" href="<%= HAL.normalizeUrl(obj.href) %>" title="Follow link"><i class="icon-arrow-right"></i></a>
<% } %>
</td>
<td>
<a class="non-get btn btn-warning" href="<%= HAL.normalizeUrl(obj.href) %>" title="Perform non-GET request">!</a>
</td>
</tr>
<% } %>
<% }) %>
</tbody>
</table>
</script>
<script id="properties-template" type="text/template">
<h2>Properties</h2>
<pre><%= _.escape(JSON.stringify(properties, null, HAL.jsonIndent)) %></pre>
</script>
<script id="request-headers-template" type="text/template">
<h2>Custom Request Headers</h2>
<textarea class="span12"></textarea>
</script>
<script id="response-headers-template" type="text/template">
<h2>Response Headers</h2>
<pre><%= status.code %> <%= status.text %>
<% _.each(headers, function(value, name) {
%><%= _.escape(name) %>: <%
if(HAL.isFollowableHeader(name)) {
%><a href="<%= HAL.normalizeUrl(value) %>" class="follow"><%
}
%><%= _.escape(value)
%><% if(HAL.isFollowableHeader(name)) {
%></a><%
} %>
<% }) %></pre>
</script>
<script id="response-body-template" type="text/template">
<h2>Response Body</h2>
<pre><%= _.escape(body) %></pre>
</script>
<script id="query-uri-template" type="text/template">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Expand URI Template</h3>
</div>
<form id="query" action="<%= href %>">
<div class="modal-body">
<p>URI Template:</p>
<pre><%- href %></pre>
<p>Input (JSON):</p>
<textarea><%= input %></textarea>
<p>Expanded URI:</p>
<pre class="preview">&nbsp;</pre>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Follow URI</button>
</div>
</form>
</script>
<script id="non-safe-request-template" type="text/template">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Make a non-GET request</h3>
</div>
<form class="non-safe" action="<%= href %>">
<div class="modal-body">
<p>Target URI</p>
<input name="url" type="text" class="url" value="<%= href %>" />
<p>Method:</p>
<input name="method" type="text" class="method" value="POST" />
<p>Headers:</p>
<textarea name="headers" class="headers" style="height: 100px">
Content-Type: application/json
<%= user_defined_headers %>
</textarea>
<p>Body:</p>
<textarea name="body" class="body" style="height: 200px">
{
}
</textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Make Request</button>
</div>
</form>
</script>
<script id="dynamic-request-template" type="text/template">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Create/Update</h3>
</div>
<form class="non-safe" action="<%= href %>">
<div class="modal-body" style="padding-top: 0px">
<div id="jsoneditor"></div>
<div class="well well-small" style="padding-bottom: 0px;">
<div class="container-fluid">
<div class="row-fluid">
<div class="control-group">
<label class="control-label" style="display: inline-block; font-weight: bold;">Action:</label>
<input name="method" type="text" class="method controls" style="width: 98%" value="POST" />
<input name="url" type="text" class="url controls" style="width: 98%" value="<%= href %>" />
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Make Request</button>
</div>
</form>
</script>
<script id="embedded-resources-template" type="text/template">
<h2>Embedded Resources</h2>
</script>
<script id="embedded-resource-template" type="text/template">
<div class="accordion-heading">
<a class="accordion-toggle" href="#"><%= resource.identifier %><% if (resource.title) { %>: <span class="embedded-resource-title"><%- resource.title %></span><% } %>
<% if (HAL.isUrl(resource.embed_rel)) { %>
<span class="dox pull-right" data-href="<%= HAL.normalizeUrl(HAL.buildUrl(resource.embed_rel)) %>">
<i class="icon-book"></i>
</span>
<% } %>
</a>
</div>
</script>
<script src="vendor/js/jquery-1.10.2.min.js"></script>
<script src="vendor/js/underscore.js"></script>
<script src="vendor/js/backbone.js"></script>
<script src="vendor/js/uritemplates.js"></script>
<script src="vendor/js/URI.min.js"></script>
<script src="vendor/js/bootstrap.js"></script>
<script src="js/hal.js"></script>
<script src="js/hal/browser.js"></script>
<script src="js/hal/http/client.js"></script>
<script src="js/hal/resource.js"></script>
<script src="js/hal/views/browser.js"></script>
<script src="js/hal/views/explorer.js"></script>
<script src="js/hal/views/inspector.js"></script>
<script src="js/hal/views/navigation.js"></script>
<script src="js/hal/views/location_bar.js"></script>
<script src="js/hal/views/request_headers.js"></script>
<script src="js/hal/views/resource.js"></script>
<script src="js/hal/views/properties.js"></script>
<script src="js/hal/views/links.js"></script>
<script src="js/hal/views/embedded_resources.js"></script>
<script src="js/hal/views/embedded_resource.js"></script>
<script src="js/hal/views/non_safe_request_dialog.js"></script>
<script src="js/hal/views/query_uri_dialog.js"></script>
<script src="js/hal/views/response.js"></script>
<script src="js/hal/views/response_headers.js"></script>
<script src="js/hal/views/response_body.js"></script>
<script src="js/hal/views/documentation.js"></script>
<script src="vendor/js/jsoneditor.js"></script>
<script src="js/CustomPostForm.js"></script>
<script>
var browser = new HAL.Browser({
container: $('#browser'),
entryPoint: '/'
});
Backbone.history.start();
</script>
</body>

View File

@@ -0,0 +1,201 @@
/**
* Custom Backbone view that uses JSON Schema metadata to create pop-up dialog with actual field names instead of
* asking user to input raw JSON.
*
* NOTE: Because JSON Schema lists all properties, including those that are links, they have to be filtered out.
* Links have to be set via a PUT operation with the proper media type.
*
* @author Greg Turnquist
* @since 2.4
* @see DATAREST-627
*/
/* jshint strict: true */
/* globals HAL, Backbone, _, $, window, jqxhr */
'use strict';
var CustomPostForm = Backbone.View.extend({
initialize: function (opts) {
this.href = opts.href.split('{')[0];
this.vent = opts.vent;
_.bindAll(this, 'createNewResource');
},
events: {
'submit form': 'createNewResource'
},
className: 'modal fade',
/**
* Perform a POST/PUT operation on the resource.
*
* @param e
*/
createNewResource: function (e) {
e.preventDefault();
var self = this;
var opts = {
url: this.$('.url').val(),
headers: {'Content-Type': 'application/json'},
method: this.$('.method').val(),
data: this.getNewResourceData()
};
HAL.client.request(opts).done(function (response) {
self.vent.trigger('response', {resource: response, jqxhr: jqxhr});
}).fail(function (e) {
self.vent.trigger('fail-response', {jqxhr: jqxhr});
}).always(function (e) {
self.vent.trigger('response-headers', {jqxhr: jqxhr});
window.location.hash = 'NON-GET:' + opts.url;
});
this.$el.modal('hide');
},
/**
* Draw the dialog after fetching the resource's JSON Schema metadata. If no metadata is available, use the
* fallback editor.
*
* @param opts
* @returns {CustomPostForm}
*/
render: function (opts) {
var self = this;
try {
HAL.client.request({
method: 'HEAD',
url: this.href
}).done(function (message, text, jqXHR) {
self.$el.html(self.template({href: self.href}));
try {
var hal = self.w3cLinksToHalLinks(jqXHR.getResponseHeader('Link'));
HAL.client.request({
method: 'GET',
url: hal._links.profile.href,
headers: {'Accept': 'application/schema+json'}
}).done(function (schema) {
self.loadJsonEditor(schema);
});
} catch (e) {
self.loadFallbackEditor();
}
self.$el.modal();
});
} catch (e) {
self.loadFallbackEditor();
self.$el.modal();
}
return this;
},
/**
* Load the JSON Schema-driven editor.
*
* @see https://github.com/jdorn/json-editor
*/
loadJsonEditor: function (schema) {
var self = this;
/**
* Remove URI-based fields since this dialog doesn't handle relationships.
*/
Object.keys(schema.properties).forEach(function (property) {
if (schema.properties[property].hasOwnProperty('format') &&
schema.properties[property].format === 'uri') {
delete schema.properties[property];
}
});
/**
* See https://github.com/jdorn/json-editor#options for more customizing options.
*/
this.editor = new window.JSONEditor(this.$('#jsoneditor')[0], {
theme: 'bootstrap2',
schema: schema,
disable_collapse: true,
disable_edit_json: true,
disable_properties: true
});
this.getNewResourceData = function() {
return JSON.stringify(self.editor.getValue());
}
},
/**
* Load fallback editor that doesn't depend on any form of metadata.
*/
loadFallbackEditor: function () {
var editor = this.$('#jsoneditor');
editor.append($('<h4>' + this.href + '</h4>'));
var inputBox = $('<textarea name="body" class="body" style="height: 200px">{\n}</textarea>');
editor.append(inputBox);
this.getNewResourceData = function() {
return inputBox.val();
}
},
/**
* Convert a W3C link header into a HAL-based set of _links.
*
* e.g.
* <http://localhost:8080/persons>; rel="persons",<http://localhost:8080/profile/persons>; rel="profile"
* to
* {
* _links: {
* persons: {
* href: http://localhost:8080/persons
* },
* profile: {
* href: http://localhost:8080/profile/persons
* }
* }
* }
*
* @param linkHeader - HTTP Response header containing a list of W3C compliant links with rels.
* @see http://www.w3.org/wiki/LinkHeader
*/
w3cLinksToHalLinks: function (linkHeader) {
var w3cLinks = linkHeader.split(',');
var halLinks = {_links: {}};
w3cLinks.forEach(function (w3cLink) {
var parts = w3cLink.split(';');
var hrefWrappedWithBrackets = parts[0];
var href = hrefWrappedWithBrackets.slice(1, parts[0].length - 1);
var w3cRel = parts[1];
var relWrappedWithQuotes = w3cRel.split('=')[1];
var rel = relWrappedWithQuotes.slice(1, relWrappedWithQuotes.length - 1);
halLinks._links[rel] = { "href": href };
});
return halLinks;
},
/**
* Look up the HTML template.
*/
template: _.template($('#dynamic-request-template').html())
});
/**
* Inject the form into the HAL Browser.
*/
HAL.customPostForm = CustomPostForm;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 67 KiB