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:
committed by
Oliver Gierke
parent
71cfe235ad
commit
be67f3e5b0
@@ -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>
|
||||
|
||||
@@ -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">×</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"> </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">×</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">×</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>
|
||||
@@ -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 |
Reference in New Issue
Block a user