diff --git a/ModelCatalogueCorePluginTestApp/.flowconfig b/ModelCatalogueCorePluginTestApp/.flowconfig new file mode 100644 index 0000000000..1fed445333 --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/.flowconfig @@ -0,0 +1,11 @@ +[ignore] + +[include] + +[libs] + +[lints] + +[options] + +[strict] diff --git a/ModelCatalogueCorePluginTestApp/bower.json b/ModelCatalogueCorePluginTestApp/bower.json index 41b0dbca24..bf099abf85 100644 --- a/ModelCatalogueCorePluginTestApp/bower.json +++ b/ModelCatalogueCorePluginTestApp/bower.json @@ -47,7 +47,10 @@ "angular-file-saver": "1.1.1", "vkbeautify": "master", "sly-repeat": "https://github.com/scalyr/angular.git#v1.0.3", - "core.js": "~2.3.0" + "core.js": "~2.3.0", + "validator-js": "^10.2.0", + "d3": "3.0.0", + "underscore": "1.9.0" }, "resolutions": { "angular": "1.4.10" diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/templates/mc/core/ui/states/mc.tpl.html b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/templates/mc/core/ui/states/mc.tpl.html index 6bf132d79d..f6db8ffe0d 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/templates/mc/core/ui/states/mc.tpl.html +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/javascripts/templates/mc/core/ui/states/mc.tpl.html @@ -3,6 +3,11 @@ resizable="{'handles': 'e', 'mirror': '.split-view-right', 'maxWidthPct': 60, 'minWidthPct': 20, 'windowWidthCorrection': 91, 'parentWidthCorrection': 31, 'breakWidth': 768}">
+ + + Basic Data Model View +

+
diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/init.js b/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/init.js new file mode 100644 index 0000000000..e8dde8da51 --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/init.js @@ -0,0 +1,451 @@ +//= require d3/d3.min.js +//= require validator-js/validator.min.js +//= require underscore/underscore-min.js +//= require_self +// @flow + +// exposed variable +var serverUrl = "" + +var initD3 = (function() { + + //// D3 Setup stuff + // dimensions of page + var m = [20, 120, 20, 120], + w = 1280 + 6000 - m[1] - m[3], + h = 800 - m[0] - m[2], + i = 0, // node ids + root; + + // layout generator + var tree = d3.layout.tree() + .size([h, w]); + + // transition stuff + var diagonal = d3.svg.diagonal() + .projection(function(d) { return [d.y, d.x]; }); + + // visualization pane + var vis = d3.select("#body").append("svg:svg") + .attr("width", w + m[1] + m[3]) + .attr("height", h + m[0] + m[2]) + .call(d3.behavior.zoom().on("zoom", function () { + vis.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")") + })) + .append("svg:g") + .attr("transform", "translate(" + (m[3] + 20) + "," + m[0] + ")"); + + // Define the div for the tooltip + var div = d3.select("#body").append("div") + .attr("class", "tooltip") + .style("opacity", 0); + + + //// Link with Grails GSP + + // Parse jsonString to jsonObject + function parseModelToJS(jsonString /*: string */) /*: Object */ { + jsonString=jsonString.replace(/\"/g,'"'); + //jsonString=jsonString.replace(/"/g, '"'); + jsonString=jsonString.replace(/\n/g, ' '); // i.e. \n + jsonString=validator.unescape(jsonString);h // unescape e.g. & + var jsonObject=$.parseJSON(jsonString); + return jsonObject + } + + /*:: + type D3JSON = { + name: string, + id: number, + description: string, + + angularLink: string, + type: string, + + loadedChildren: boolean, + loading: boolean, + + enumerations: ?Object, + + children: ?Array, + + _children: ?Array, + + x: number, + y: number, + x0: number, + y0: number + } + */ + + + //// naming functions + /** return Upper Case first letter + * @param str + * @returns {string} + */ + function ucFirst(str /*: string */) { + return str.charAt(0) .toUpperCase() + str.substr(1) + } + + + function typeAndName(d /*: D3JSON */) { + return "" + ucFirst(d.type) + " \"" + d.name + "\"" + } + + //// Info functions + + /** + * Return HTML for info panel on the right from node data + * @param d node data + * @returns {string} + */ + function info(d /*: D3JSON */) { + return "Name: " + "" + d.name + "" + "
" + + " (Click to see Advanced View)" + "
" + "
" + + "Type: " + ucFirst(d.type) + "
" + + (d.description? "Description: " + d.description + "
" : "") + + (d.enumerations ? enumerate(d.enumerations): "") + } + + /** + * Return HTML for enumeration from Map + * @param map + * @returns {string} + */ + function enumerate(map) { + var ret = "Enumerations:
    " + Object.keys(map).forEach(function(key) { + ret = ret + "
  • " + key + ": " + map[key] + "
  • " + console.log(key, map[key]); + }); + return ret = ret + "
" + } + + function writeMessage(text) { + $('#d3-info-messages').append("
  • " + (new Date().toLocaleString()) + ":
    " + text + "
  • ") + } + + + /** + * Initialize + * @param json + */ + function initD3(json /*: D3JSON */) { + root = json; + root.x0 = h / 2; + root.y0 = 0; + $('#d3-info-data-model').html(info(root)); + + function toggleAll(d) { + if (d.children) { + d.children.forEach(toggleAll); + toggle(d); + } + } + + // Initialize the display to show a few nodes. + // toggle(root.children[1]); + // toggle(root.children[1].children[2]); + // toggle(root.children[9]); + // toggle(root.children[9].children[0]); + + update(root); + }; + + // node colours + var coloursMap = { + "dataModel": "blueviolet", + "dataClass": "blue", + "dataElement": "gold", + "dataType": "green" + } + + + //// path functions + + function pathFromRoot(d) { + var currentNode = d + var pathToRoot = [] + + while (d.parent) { + pathToRoot.push(d) + d = d.parent + } + // !d.parent, so d is root + pathToRoot.push(d) + + return pathToRoot.reverse() + } + + function namesFromPath(path) { + return _.pluck(path, "name") + } + + function angularTreeviewPathString(path) { + var ids = _.pluck(path, "id") + ids.splice(1,0, 'all') + return ids.join('-') + } + + /** + * Update a node (source) + * @param source + */ + function update(source) { + + var duration = d3.event && d3.event.altKey ? 5000 : 500; + + var radius = 7 + var unopenedNodeBorderColour = "orangered" + + + // Compute the new tree layout. + var nodeLayoutData = tree.nodes(root).reverse(); + + // Normalize for fixed-depth. + nodeLayoutData.forEach(function(d) { d.y = d.depth * 180; }); + + // Update the nodes… + var svgNodes = vis.selectAll("g.node") + .data(nodeLayoutData, function(d) { return d.nodeId || (d.nodeId = ++i); }); + + /*:: + type ChildrenData = { + children: Array, + canAccessDataModel: boolean, + caseHandled: boolean + } + */ + + // load children if not loaded + function onNodeClick(d) { + var path = pathFromRoot(d) + console.log("Path from root: " + namesFromPath(path)) + console.log("Angular pathstring: " + angularTreeviewPathString(path)) + + $('#d3-info-element').html(info(d)); // display info + + if (d.loadedChildren && !d.loading) { + toggle(d); + update(d); + } + + else { + + if (!d.loading) { // i.e. !d.loadedChildren + + writeMessage("Loading children for " + typeAndName(d) + "...") + d.loading = true // try to prevent double-loading, although race conditions may still result if you click fast enough. Not really a completely well-thought-out concurrency thing. + $.ajax({ + url: serverUrl + "/dataModel/basicViewChildrenData/" + d.type + "/" + d.id + }).then(function(data /*: ChildrenData */) { + + d.loadedChildren = true; + d.loading = false; + // end loading + + if (data.canAccessDataModel && data.caseHandled) { + d.children = null + if (data.children.length > 0) { + d._children = data.children; + } + writeMessage("Loading children for " + typeAndName(d) + " succeeded!") + toggle(d); + update(d); + } + + else { + if (!data.canAccessDataModel) { + writeMessage("You do not have access to the data model of " + typeAndName(d) + " or it does not exist.") + // TODO: If you can't access the data model because you've been logged out, it's more appropriate that d.loadedChildren remains false, so the user can try to load the children again once logged in. But we need a more fine-grained response from the controller to differentiate the reason for inability to access. + } + else { // !data.caseHandled + writeMessage("Loading children is not handled for this case.") + } + + } + + + }, function(jqXHR, textStatus, errorThrown) { // request failure + writeMessage("Loading children for " + typeAndName(d) + " failed with error message: " + errorThrown) + d.loading = false + // end loading + }) + } + + } + + } + + function tooltipMouseover(d) { + div.transition() + .duration(0) + .style("opacity", 1); + div .html("

    " + d.name + "

    ") + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + infoMouseover(d) + + } + + function tooltipMouseout(d) { + div.transition() + .duration(500) + .style("opacity", 0); + } + + // display info on mouseover + function infoMouseover(d){ + $('#d3-info-element-last-mouseover').html(info(d)); // display info + } + + // ENTER any new nodes at the parent's previous position. + // the g element includes the circle AND the text next to it (the name). So event listeners registered here will apply to both circle and text. + var nodeEnter = svgNodes.enter().append("svg:g") + .attr("class", "node") + .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) + .on("click", onNodeClick) + .on('mousedown', function(d) { + + d3.event.stopImmediatePropagation(); // to stop panning + }) + + + // mouseover to expand the name + .on("mouseover", function(d) { + d3.select(this).select("a").select("text").text(d.name) + infoMouseover(d) + }) + .on("mouseout", function(d) { + d3.select(this).select("a").select("text").text(shortenedNodeText) + }); + + + + // add circles for each node + nodeEnter.append("svg:circle") + .attr("r", 1e-6) + .style("fill", function(d) { return !d.children ? coloursMap[d.type] /*"lightsteelblue"*/ : "#fff"; }) + // Tooltip on mouseover + // .on("mouseover", tooltipMouseover) + // .on("mouseout", tooltipMouseout); + + var maxStringLength = 20; + + function shouldNameBeShortened(d /*: D3JSON */) /*: boolean */ { + // return d.children || d._children + return (d.type == 'dataClass' || d.type == 'dataModel' || d.type == 'dataElement') + } + + function shortenedNodeText(d) { + return (d.name.length >= maxStringLength && shouldNameBeShortened(d)) ? d.name.substring(0,maxStringLength) + "..." : d.name; } + + // Text/link for each node + nodeEnter.append("svg:a") + .attr("xlink:href", function(d) {return d.angularLink}) + .attr("target", "_blank") + + .append("svg:text") + .attr("dy", ".35em") + // text with possibly shortened name + + .attr("x", function(d) { return shouldNameBeShortened(d) ? -(radius + 5): radius + 5; }) + .attr("text-anchor", function(d) { + return shouldNameBeShortened(d) ? "end" : "start"; + }) + .text(shortenedNodeText) + + .style("fill-opacity", 1e-6) + .style("font", "15px sans-serif"); + + + + + // TRANSITION nodes to their new position. + var nodeUpdate = svgNodes.transition() + .duration(duration) + .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); + + nodeUpdate.select("circle") + .attr("r", radius) + .style("stroke", function(d) { + return (d._children || !d.loadedChildren) ? unopenedNodeBorderColour: coloursMap[d.type]; }) + .style("stroke-width", 3) + .style("fill", function(d) { return coloursMap[d.type]}) + ; + + nodeUpdate.select("text") + .style("fill-opacity", 1); + + + + + // TRANSITION EXITING nodes to the parent's new position. + var nodeExit = svgNodes.exit().transition() + .duration(duration) + .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) + .remove(); + + nodeExit.select("circle") + .attr("r", 1e-6); + + nodeExit.select("text") + .style("fill-opacity", 1e-6); + + + + + // Update the LINKS… + var link = vis.selectAll("path.link") + .data(tree.links(nodeLayoutData), function(d) { return d.target.id; }); + + // ENTER any new links at the parent's previous position. + link.enter().insert("svg:path", "g") + .attr("class", "link") + .attr("d", function(d) { + var o = {x: source.x0, y: source.y0}; + return diagonal({source: o, target: o}); + }) + .transition() + .duration(duration) + .attr("d", diagonal); + + // Transition links to their new position. + link.transition() + .duration(duration) + .attr("d", diagonal); + + // Transition exiting links to the parent's new position. + link.exit().transition() + .duration(duration) + .attr("d", function(d) { + var o = {x: source.x, y: source.y}; + return diagonal({source: o, target: o}); + }) + .remove(); + + // Stash the old positions for transition. + nodeLayoutData.forEach(function(d) { + d.x0 = d.x; + d.y0 = d.y; + }); + } + + // Toggle children. + function toggle(d) { + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; + } + } + + return { + "parseModelToJS": parseModelToJS, + "initD3": initD3, + "writeMessage": writeMessage + } +})() diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/style.css b/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/style.css new file mode 100644 index 0000000000..974a1e8bd4 --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/style.css @@ -0,0 +1,215 @@ +#body { + background: url(texture-noise.png); + overflow: scroll; + margin: 0; + font-size: 14px; + font-family: "Helvetica Neue", Helvetica; +} +/* +#chart, #header, #footer { + position: absolute; + top: 0; +} + +#header, #footer { + z-index: 1; + display: block; + font-size: 36px; + font-weight: 300; + text-shadow: 0 1px 0 #fff; +} + +#header.inverted, #footer.inverted { + color: #fff; + text-shadow: 0 1px 4px #000; +} + +#header { + top: 80px; + left: 140px; + width: 1000px; +} + +#footer { + top: 680px; + right: 140px; + text-align: right; +} +*/ + +.column { + float: left; + + height: 1200px; +} + +.column-left { + width: 60%; +} + +.column-right { + width: 20%; + overflow: scroll; + border: solid 3px; +} +.column-right-2 { + width: 20%; + overflow: scroll; + border: solid 3px; +} + +.info-box { + padding: 0.8em; + +} + +.info-box-limited { + + overflow: scroll; + height: 30%; + + padding-bottom: 1.5em; + border-bottom-style: solid; + border-bottom-width: medium; + +} + + +#d3-info-messages { + padding: 1em; +} + + +h1, h2 { + margin-top: 0.2em; + margin-bottom: 0.2em; +} + +div.tooltip { + position: absolute; + text-align: center; + /*width: 60px;*/ + height: 40px; + padding: 5px; + font: 12px sans-serif; + background: lightsteelblue; + border: 0px; + border-radius: 8px; + pointer-events: none; +} + + +rect { + fill: none; + pointer-events: all; +} + +pre { + font-size: 18px; +} + +line { + stroke: #000; + stroke-width: 1.5px; +} + +.string, .regexp { + color: #f39; +} + +.keyword { + color: #00c; +} + +.comment { + color: #777; + font-style: oblique; +} + +.number { + color: #369; +} + +.class, .special { + color: #1181B8; +} + +text a { + fill: navy; +} +text a:visited { + fill:darkpurple; +} +text a:hover, text a:active { + text-decoration: underline; + fill:darkred; +} + +.hint { + position: absolute; + right: 0; + width: 1280px; + font-size: 12px; + color: #999; +} + + +@media only screen and (max-height: 1100px) { + .column { + height: 1100px; + } +} + +@media only screen and (max-height: 1000px) { + .column { + height: 1000px; + } +} + +@media only screen and (max-height: 900px) { + .column { + height: 900px; + } +} + +@media only screen and (max-height: 800px) { + .column { + height: 800px; + } +} + +@media only screen and (max-height: 700px) { + .column { + height: 700px; + } +} + +@media only screen and (max-height: 600px) { + .column { + height: 600px; + } +} + +@media only screen and (max-height: 500px) { + .column { + height: 500px; + } +} + +@media only screen and (max-height: 400px) { + .column { + height: 400px; + } +} + +@media only screen and (max-height: 300px) { + .column { + height: 300px; + } +} + +@media only screen and (max-height: 200px) { + .column { + height: 200px; + } +} diff --git a/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/texture-noise.png b/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/texture-noise.png new file mode 100644 index 0000000000..684f4469ae Binary files /dev/null and b/ModelCatalogueCorePluginTestApp/grails-app/assets/other/d3_data_model_view/texture-noise.png differ diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/Config.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/Config.groovy index d3cb883b8c..bd4d356032 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/conf/Config.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/Config.groovy @@ -530,7 +530,7 @@ grails.doc.copyright = ''// The copyright message to display grails.doc.footer = ''// The footer to use -grails.assets.minifyJs = false +grails.assets.minifyJs = false // placing this here would seem to undo the settings in the production block... modelcatalogue.defaults.relationshiptypes = [ [name: "containment", diff --git a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy index 62337c6595..266f39ae23 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/conf/DataModelUrlMappings.groovy @@ -3,6 +3,8 @@ import org.springframework.http.HttpMethod class DataModelUrlMappings { static mappings = { + "/dataModel/basicView/$id"(controller: 'dataModel', action: 'basicView') + "/dataModel/basicViewChildrenData/$type/$id"(controller: 'dataModel', action: 'basicViewChildrenData') "/dataModel/showAssetInAngular/$id"(controller: 'dataModel', action: 'showAssetInAngular', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'index', method: HttpMethod.GET) "/api/modelCatalogue/core/dataModel"(controller: 'dataModel', action: 'save', method: HttpMethod.POST) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy index b8f422c370..53ffa44bb3 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/controllers/org/modelcatalogue/core/DataModelController.groovy @@ -3,7 +3,11 @@ package org.modelcatalogue.core import grails.gorm.DetachedCriteria import grails.plugin.springsecurity.SpringSecurityUtils import org.modelcatalogue.core.asset.MicrosoftOfficeDocument +import org.modelcatalogue.core.d3viewUtils.ChildrenData +import org.modelcatalogue.core.d3viewUtils.D3ViewUtilsService import org.modelcatalogue.core.persistence.AssetGormService +import org.modelcatalogue.core.persistence.DataClassGormService +import org.modelcatalogue.core.util.MetadataDomain import org.modelcatalogue.core.util.ParamArgs import org.modelcatalogue.core.util.SearchParams import org.modelcatalogue.core.util.lists.ListWithTotalAndTypeImpl @@ -44,6 +48,11 @@ import grails.plugin.springsecurity.SpringSecurityService class DataModelController extends AbstractCatalogueElementController { + static allowedMethods = [ + basicView: "GET", + basicViewChildrenData: "GET", + ] + SessionFactory sessionFactory SpringSecurityService springSecurityService @@ -60,6 +69,10 @@ class DataModelController extends AbstractCatalogueE AssetMetadataService assetMetadataService + D3ViewUtilsService d3ViewUtilsService + + DataClassGormService dataClassGormService + DataModelController() { super(DataModel, false) } @@ -243,6 +256,77 @@ class DataModelController extends AbstractCatalogueE respond instance, [status: OK] } + /** + * Initialize Basic View (D3 view) with just one node (the Data Model itself, no children) + * Returns + * + type DataModelDisplayData = { + dataModelJson: D3JSON, + modelFound: boolean, + dataModelId: long + }; + * @return + */ + def basicView() { + + long dataModelId = params.long('id') + DataModel dataModel = dataModelGormService.findById(dataModelId) + + def dataModelJson = [:] + boolean modelFound = dataModel + + if (modelFound) { + dataModelJson = d3ViewUtilsService.dataModelD3Json(dataModel) // no children + } + + + render(view: 'd3_data_model_view', model: [ + dataModelJson: dataModelJson, + modelFound: modelFound, + dataModelId: dataModelId]) + } + + /** + * + * @return ChildrenData + */ + def basicViewChildrenData() { + + String type = params.get('type').toString() + long id = params.long('id') + + boolean canAccessDataModel = false + boolean caseHandled = true + def children = [] + + if (type == 'dataModel') { + DataModel dataModel = dataModelGormService.findById(id) + canAccessDataModel = dataModel + + if (canAccessDataModel) { + children = d3ViewUtilsService.dataModelD3JsonChildren(dataModel) + } + } + else if (type == 'dataClass') { + DataClass dataClass = dataClassGormService.findById(id) + canAccessDataModel = dataModelAclService.isAdminOrHasReadPermission(dataClass) + + if (canAccessDataModel) { + children = d3ViewUtilsService.dataClassD3JsonChildren(dataClass) + } + } + else { + caseHandled = false + } + + + render(contentType: 'text/json') { + new ChildrenData(children: children, canAccessDataModel: canAccessDataModel, caseHandled: caseHandled) + } + } + + + /** * Check if a data model contains or imports another catalogue element * @param id of data model, other id of item to be checked diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataClassService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataClassService.groovy index fd7bdc2f62..e8531c2cd4 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataClassService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataClassService.groovy @@ -6,6 +6,7 @@ import org.modelcatalogue.core.api.ElementStatus import org.modelcatalogue.core.persistence.DataModelGormService import org.modelcatalogue.core.security.DataModelAclService import org.modelcatalogue.core.util.DataModelFilter +import org.modelcatalogue.core.util.MetadataDomain import org.modelcatalogue.core.util.lists.ListWithTotalAndType import org.modelcatalogue.core.util.lists.Lists @@ -156,6 +157,16 @@ class DataClassService { } } + static List getChildDataClasses(DataClass dataClass) { + List children = dataClass.getOutgoingRelationshipsByType(RelationshipType.hierarchyType)*.destination + children.collect{it as DataClass} + } + + static List getDataElementsIn(DataClass dataClass) { + List dcDEs = dataClass.getOutgoingRelationshipsByType(RelationshipType.containmentType)*.destination + dcDEs.collect{it as DataElement} + } + static int counter = 0; diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataElementService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataElementService.groovy index f954677aa5..e9612b48b6 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataElementService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/DataElementService.groovy @@ -153,7 +153,14 @@ class DataElementService { return sqlQuery.list()[0] } } + static List getDataClassesOf(DataElement dataElement) { + List deDCs = dataElement.getIncomingRelationshipsByType(RelationshipType.containmentType)*.source + deDCs.collect{it as DataClass} + } + static int countDataClassesOf(DataElement dataElement) { + dataElement.getIncomingRelationshipsByType(RelationshipType.containmentType).size() + } private ListWithTotalAndType buildDataElementsList(Map params, String selectElements, String selectCount, String fromQuery, @DelegatesTo(SQLQuery) Closure closure) { QuerySetupListMethods querySetupListMethods = new QuerySetupListMethods(selectElements, selectCount, fromQuery, closure) return Lists.methodNotClosureLazy(params, DataElement, querySetupListMethods) diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy new file mode 100644 index 0000000000..59f47d65da --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/d3viewUtils/D3ViewUtilsService.groovy @@ -0,0 +1,195 @@ +package org.modelcatalogue.core.d3viewUtils + +import com.google.common.collect.ImmutableSet +import grails.transaction.Transactional +import org.codehaus.groovy.grails.commons.GrailsApplication +import org.modelcatalogue.core.DataClass +import org.modelcatalogue.core.DataClassService +import org.modelcatalogue.core.DataElement +import org.modelcatalogue.core.DataElementService +import org.modelcatalogue.core.DataModel +import org.modelcatalogue.core.DataModelService +import org.modelcatalogue.core.DataType +import org.modelcatalogue.core.EnumeratedType +import org.modelcatalogue.core.api.ElementStatus +import org.modelcatalogue.core.util.DataModelFilter +import org.modelcatalogue.core.util.MetadataDomain +import org.modelcatalogue.core.util.lists.ListWithTotalAndType + +@Transactional +/** + * Produces D3 hierarchical layout json augmented with model catalogue specific fields. + * + */ +class D3ViewUtilsService { + + DataModelService dataModelService + DataClassService dataClassService + DataElementService dataElementService + GrailsApplication grailsApplication + + //// Utilities: + + static String lowerCamelCaseDomainName(Class clazz) { + MetadataDomain.lowerCamelCaseDomainName(MetadataDomain.ofClass(clazz)) + } + + String angularLink(Long dataModelId, Long id, Class clazz) { + return "${grailsApplication.config.grails.serverURL}/#/$dataModelId/${lowerCamelCaseDomainName(clazz)}/$id" + } + + + //// Data Models: + + /** + * No children; load them later + * @param dataModel + * @return D3JSON + */ + D3JSON dataModelD3Json(DataModel dataModel) { + + D3JSON dataModelJson = new D3JSON( + name: dataModel.name, + description: dataModel.description, + id: dataModel.id, + angularLink: angularLink(dataModel.id, dataModel.id, DataModel), + type: lowerCamelCaseDomainName(DataModel) + ) + } + + /** + * Get "children" of data model for display (top level data classes and data elements + * @param dataModel + * @return List + */ + List dataModelD3JsonChildren(DataModel dataModel) { + DataModelFilter filter = DataModelFilter.create(ImmutableSet. of(dataModel), ImmutableSet. of()) + Map stats = dataModelService.getStatistics(filter) + + + ListWithTotalAndType dataClasses = dataClassService.getTopLevelDataClasses(filter, [toplevel: true, status: dataModel.status != ElementStatus.DEPRECATED ? 'active' : '']) + + + def dataClassChildrenJson = dataClasses.items.collect {dataClass -> + dataClassD3Json(dataClass) + + } +// List unDataClassedDataElements = DataElement.findAllByDataModel(dataModel).findAll{ +// dataElementService.countDataClassesOf(it) == 0 +// } +// def dataElementChildrenJson = unDataClassedDataElements.collect { +// dataElementD3Json(it) +// } + + + + return dataClassChildrenJson //+ dataElementChildrenJson + + // the dataElementChildrenJson seems to take a long time + // TODO: Handle case where there are just DataTypes listed not connected to any DataElements + + } + + //// Data Classes: + + + /** + * @return DataClass D3JSON (children will be loaded later) + */ + D3JSON dataClassD3Json(DataClass dataClass) { + + + D3JSON ret = new D3JSON( + name: dataClass.name, + id: dataClass.id, + description: dataClass.description, + angularLink: angularLink(dataClass.dataModel.id, dataClass.id, DataClass), + type: lowerCamelCaseDomainName(DataClass), + ) + + + + return ret + } + + List dataClassD3JsonChildren(DataClass dataClass) { + + List dataElementsJson = dataClassService.getDataElementsIn(dataClass).collect{ + dataElementD3Json(it) + } + + List childDataClassesJson = dataClassService.getChildDataClasses(dataClass).collect{ + dataClassD3Json(it) // recursive + } + + + List children = dataElementsJson + childDataClassesJson + + return children + } + + //// Data Elements and Data Types: + + + D3JSON dataElementD3Json(DataElement dataElement) { + D3JSON ret = new D3JSON( + + name: dataElement.name, + id: dataElement.id, + description: dataElement.description, + angularLink: angularLink(dataElement.dataModel.id, dataElement.id, DataElement), + loadedChildren: true, // Just load data type, no laziness + type: lowerCamelCaseDomainName(DataElement) + ) + + if (dataElement.dataType) { + ret.children = [dataTypeD3Json(dataElement.dataType)] + } + + return ret + } + + D3JSON dataTypeD3Json(DataType dataType) { + D3JSON ret = new D3JSON( + name: dataType.name, + id: dataType.id, + description: dataType.description, + angularLink: angularLink(dataType.dataModel.id, dataType.id, DataType), + loadedChildren: true, // No children, so "already loaded children." + type: lowerCamelCaseDomainName(DataType), + ) + + if (dataType instanceof EnumeratedType) { + ret.enumerations = ((EnumeratedType) dataType).enumerations + } + return ret + } +} +/** + * D3 view json recursive format + * + * type is a String from MetadataDomain.lowerCamelCaseDomainName e.g. 'dataClass' + * angularLinkis a String of format e.g. "http://localhost:8080/#/82467/dataClass/82470/", which is a link to the angular application view for data model 82467, data class 82470 + * enumerations is a Map of Strings to Strings for enumerated types. + * + */ +class D3JSON { + String name + long id + String description + + boolean loadedChildren = false // false by default; children will be loaded later. + boolean loading = false + + String angularLink + String type + Map enumerations + List children + +} + +class ChildrenData { + List children + boolean canAccessDataModel + boolean caseHandled +} diff --git a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy index b763b13d02..d3b6d1f8e8 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy +++ b/ModelCatalogueCorePluginTestApp/grails-app/services/org/modelcatalogue/core/security/MetadataSecurityService.groovy @@ -663,6 +663,8 @@ class MetadataSecurityService { ] public static final List DATA_MODEL_MAPPINGS = [ + ["/dataModel/basicView/*", 'isAuthenticated()', HttpMethod.GET], + ["/dataModel/basicViewChildrenData/*/*", 'isAuthenticated()', HttpMethod.GET], ["/dataModel/create", MetadataRoles.ROLE_CURATOR, HttpMethod.GET], ["/dataModel/save", MetadataRoles.ROLE_CURATOR, HttpMethod.POST], ["/dataModel/showAssetInAngular/*", 'isAuthenticated()', HttpMethod.GET], diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dashboard/_dataModelLink.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dashboard/_dataModelLink.gsp index c78be069bf..3a3c411324 100644 --- a/ModelCatalogueCorePluginTestApp/grails-app/views/dashboard/_dataModelLink.gsp +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dashboard/_dataModelLink.gsp @@ -1 +1,5 @@ -${dataModel?.name} \ No newline at end of file +%{--Angular Link:--}% +%{--${dataModel?.name}--}% + +%{--Basic View Link:--}% +${dataModel?.name} diff --git a/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp new file mode 100644 index 0000000000..9aaf0b48fc --- /dev/null +++ b/ModelCatalogueCorePluginTestApp/grails-app/views/dataModel/d3_data_model_view.gsp @@ -0,0 +1,112 @@ +// @flow +<%-- + Created by IntelliJ IDEA. + User: james + Date: 16/05/2018 + Time: 17:11 +--%> +<%@ page import="grails.converters.JSON" %> +<%@ page contentType="text/html;charset=UTF-8" %> + + + + + + + + %{--Javascript at bottom of body--}% + %{--Style--}% + + + + + + + +
    + +
    + +
    + +
    + +
    +

    Data Model:

    +
    +
    + +
    +

    Element (Last Click):

    +
    +
    + +
    +

    Element (Last Mouse-Over):

    +
    +
    + + + +
    + +
    +
    +

    Messages:

    +
      + +
    +
    +
    +
    + + + %{--Inititalize D3--}% + + + + +