diff --git a/samples/spring-flo-si/LICENSE.txt b/samples/spring-flo-si/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/samples/spring-flo-si/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/samples/spring-flo-si/README.adoc b/samples/spring-flo-si/README.adoc new file mode 100644 index 0000000..f02deac --- /dev/null +++ b/samples/spring-flo-si/README.adoc @@ -0,0 +1,42 @@ +# A sample app using spring-flo to visualize Spring Integration applications + +This sample uses flo as a live viewer for Spring Integration applications. + +This https://spring.io/blog/2016/04/26/spring-integration-4-3-m2-is-available[blog] discusses +how to activate the new endpoint in a Spring Integration application. When +the endpoint is active in your SI application, just enter that into the spring +flo viewer. You should then see something like this: + +image::imgs/basicGraph.png[width="800"] + +# Running the sample + +A basic Spring Boot app is used to serve the sample. Launch it with: + + mvn spring-boot:run + +then open `http://localhost:8082`. In the `Spring Integration Graph Endpoint` +field enter the url for the spring integration data, for example: `http://localhost:8080/integration` +and the graph should load. + + +# Using the application + +Once the graph is loaded you can drag nodes around to adjust the layout. (Press the `Read-Only` +button to prevent moving nodes around). Hovering over a node will show a tool tip with more +information for that element. If you hover over a channel you will see many stats about +traffic flowing over that channel: + +image::imgs/tooltip.png[width="500"] + +It is possible to select one of those stats of interest and have it shown directly on the graph. +Simply select what you are interested in and enter the name of that stat in the `Link label path` +field at the top. The values for that stat will then be shown on the links between graph +elements: + +image::imgs/numbersGraph.png[width="800"] + +If you enter a Refresh rate (minimal allowed is 250ms) then that stat will actually +update on the graph at that rate with a small animation indicating where on the graph changes +in value are occurring. + diff --git a/samples/spring-flo-si/imgs/basicGraph.png b/samples/spring-flo-si/imgs/basicGraph.png new file mode 100644 index 0000000..66104fe Binary files /dev/null and b/samples/spring-flo-si/imgs/basicGraph.png differ diff --git a/samples/spring-flo-si/imgs/numbersGraph.png b/samples/spring-flo-si/imgs/numbersGraph.png new file mode 100644 index 0000000..e9a0fac Binary files /dev/null and b/samples/spring-flo-si/imgs/numbersGraph.png differ diff --git a/samples/spring-flo-si/imgs/tooltip.png b/samples/spring-flo-si/imgs/tooltip.png new file mode 100644 index 0000000..f0ee064 Binary files /dev/null and b/samples/spring-flo-si/imgs/tooltip.png differ diff --git a/samples/spring-flo-si/pom.xml b/samples/spring-flo-si/pom.xml new file mode 100755 index 0000000..cfaecbb --- /dev/null +++ b/samples/spring-flo-si/pom.xml @@ -0,0 +1,115 @@ + + + 4.0.0 + + org.springframework + spring-flo-sample-si + 0.0.1.BUILD-SNAPSHOT + jar + + spring-flo-sample + Spring Flo Sample + + + org.springframework.boot + spring-boot-starter-parent + 1.3.2.RELEASE + + + + + + + + org.webjars.bower + codemirror + 5.1.0 + + + org.webjars.bower + angular + 1.3.8 + + + org.webjars.bower + jshint + 2.8.0 + + + org.webjars.bower + requirejs + 2.1.18 + + + org.webjars.bower + jquery + 2.2.0 + + + org.webjars.bower + lodash + 3.10.1 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.webjars + webjars-locator + 0.31 + + + org.webjars.bower + requirejs-domready + 2.0.1 + + + org.webjars.bower + requirejs-text + 2.0.15 + + + org.webjars.bower + jointjs + 0.9.7 + + + org.webjars.bower + json5 + 0.4.0 + + + org.webjars.bower + spring-flo + 0.5.0 + + + org.webjars.bower + joint + + + + + + + UTF-8 + org.springframework.flo.Application + 1.7 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/samples/spring-flo-si/src/main/java/org/springframework/flo/Application.java b/samples/spring-flo-si/src/main/java/org/springframework/flo/Application.java new file mode 100755 index 0000000..df6784f --- /dev/null +++ b/samples/spring-flo-si/src/main/java/org/springframework/flo/Application.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.flo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * + * @author Andy Clement + * @author Alex Boyko + */ +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +// @Bean +// public WebMvcConfigurer corsConfigurer() { +// return new WebMvcConfigurerAdapter() { +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// registry.addMapping("/").allowedOrigins("http://localhost:9000"); +// } +// }; +// } +} diff --git a/samples/spring-flo-si/src/main/resources/application.properties b/samples/spring-flo-si/src/main/resources/application.properties new file mode 100644 index 0000000..8d51d0c --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8082 \ No newline at end of file diff --git a/samples/spring-flo-si/src/main/resources/static/css/flosi.css b/samples/spring-flo-si/src/main/resources/static/css/flosi.css new file mode 100644 index 0000000..dcf5a95 --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/css/flosi.css @@ -0,0 +1,189 @@ + +.header { + font-weight: 400; + font-family: "Roboto",sans-serif; + font-size: 36px; + color: #ffffff; + padding: 2px; + background-color: #283E49; + border: none; +/* border-top: 4px solid #6db33f; */ + z-index: 1; +} + +.input-label-div { + font-family: "Roboto",sans-serif; + font-size: 18px; + color: #ffffff; + display: inline-block; +} + +#labelpath { + width: 200px; +} + +#refreshrate { + width: 100px; +} + +.inputfield { + font-family: "Roboto",sans-serif; + font-size: 18px; +} + +#endpoint { + font-family: "Roboto",sans-serif; + font-size: 18px; + width: 400px; +} + +#endpoint-button { + font-family: "Roboto",sans-serif; + font-size: 18px; + height:80px; +} + +body { + background-color: #283E49; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.control-button { + width: 16px; + height: 16px; +} + +.header-small { + font: 300 24px "Helvetica Neue"; +} + +pre { + font-size: 18px; +} + +.border-selected { + stroke: #34302d; + stroke-width: 3; +} + +.controls { + border-radius: 2px; + border: solid; + border-color: #283E49; + padding: 5px; + margin-top: 3px; + background-color: #283E49; + border-width: 1px; +} + +.button { + color: #ffffff; + background-image: none; + border-radius: 2px; + background-color: #00B0A7; + font-size: 18px; + line-height: 14px; + font-family: "Roboto",sans-serif; + border: 2px solid #00B0A7; + padding: 5px 20px; + text-shadow: none; +} + +.button span { + background-color: #34302d; + background-image: none; + border-radius: 2px; + color: #f1f1f1; + font-size: 14px; + line-height: 14px; + font-family: Montserrat,sans-serif; + border: 2px solid #6db33f; + padding: 5px 20px; + text-shadow: none; +} + +.button input { + background-color: #34302d; + background-image: none; + color: #f1f1f1; + font-size: 14px; + font-family: Montserrat,sans-serif; + text-shadow: none; + border: 0px; + text-align:right; +} + +button.off { + background-color: #00B0A7; + color: #283E49; +} + +.flow-definition-container { + display: none; + border: 1px solid; + border-color: #283E49; + border-radius: 2px; + margin-top: 3px; + background-color: #ffffff; + font-family: monospace; + z-index: 2; + width:100%; + height:100px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +textarea:input { + outline: none; + border: 1px solid #6db33f; +} + +textarea:input:focus { + outline: none; + border: 1px solid #000000; +} +.canvas { + border-color: #283E49; +} +.flow-definition { + border: 5px; + height:100%; + width:100%; + font-size: 16px; + resize: none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +/* The text label on the nodes */ + +.label { + font-family: 'Ubuntu Mono'; + font-size: 12px; + color: black; +} + + +/* The class for the 'icon/unicode_char' on the nodes */ +.label2 { + font-size: 18px; +} + +[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { + display: none !important; +} + +.box { + stroke: #283e49; +} + + +.node-tooltip-option-name { + font-family: 'Ubuntu Mono', monospace; +} diff --git a/samples/spring-flo-si/src/main/resources/static/icons/delete.svg b/samples/spring-flo-si/src/main/resources/static/icons/delete.svg new file mode 100644 index 0000000..5b3f988 --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/icons/delete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/spring-flo-si/src/main/resources/static/icons/error.svg b/samples/spring-flo-si/src/main/resources/static/icons/error.svg new file mode 100644 index 0000000..c6c8388 --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/icons/error.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/samples/spring-flo-si/src/main/resources/static/icons/rotate.svg b/samples/spring-flo-si/src/main/resources/static/icons/rotate.svg new file mode 100644 index 0000000..9248e1b --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/icons/rotate.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/samples/spring-flo-si/src/main/resources/static/index.html b/samples/spring-flo-si/src/main/resources/static/index.html new file mode 100755 index 0000000..2f3f8a7 --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/index.html @@ -0,0 +1,41 @@ + + + + Spring Integration Viewer + + + + + + + + + + +
+ Spring Integration Graph endpoint: + + +     Link label path: + +     Refresh rate (ms): + +
+
+ +
+ + +
+
+ + +
+
+
+ + diff --git a/samples/spring-flo-si/src/main/resources/static/js/editor-service.js b/samples/spring-flo-si/src/main/resources/static/js/editor-service.js new file mode 100644 index 0000000..0fa4e11 --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/js/editor-service.js @@ -0,0 +1,450 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author Alex Boyko + * @author Andy Clement + */ +define(function(require) { + 'use strict'; + + var joint = require('joint'); + + return ['$log', function($log) { + + function createHandles(flo, createHandle, element) { + var bbox = element.getBBox(); + var pt = bbox.origin().offset(bbox.width + 3, bbox.height + 3); + createHandle(element, 'remove', flo.deleteSelectedNode, pt); + } + + function validatePort(/*flo, cellView, magnet*/) { + return true; + } + + function validateLink(flo, cellViewS, magnetS, cellViewT, magnetT/*, end, linkView*/) { + // Prevent linking from input ports. + if (magnetS && magnetS.getAttribute('type') === 'input') { + return false; + } + // Prevent linking from output ports to input ports within one element. + if (cellViewS === cellViewT) { + return false; + } + // Prevent linking to input ports. + if (magnetT && magnetT.getAttribute('type') === 'output') { + return false; + } + return cellViewS.model && cellViewT.model && !(cellViewS.model instanceof joint.shapes.flo.ErrorDecoration) && !(cellViewT.model instanceof joint.shapes.flo.ErrorDecoration); + } + + function preDelete(flo, cell) { + repairDamage(flo, cell); + } + + function handleNodeDropping(flo, dragDescriptor) { + // this is a viewer not an editor, so do not adjust the graph structure + } + + function calculateDragDescriptor(flo, draggedView, targetUnderMouse, point, context) { + // check if it's a tap being dragged + var source = draggedView.model; + if ((targetUnderMouse instanceof joint.dia.Element) && source.attr('metadata/name') === 'tap') { // jshint ignore:line + return { + context: context, + source: { + cell: draggedView.model, + }, + target: { + cell: targetUnderMouse, + } + }; + } + + // Find closest port + var range = 30; + var graph = flo.getGraph(); + var paper = flo.getPaper(); + var closestData; + var minDistance = Number.MAX_VALUE; + var maxIcomingLinks = draggedView.model.attr('metadata/constraints/maxIncomingLinksNumber'); + var maxOutgoingLinks = draggedView.model.attr('metadata/constraints/maxOutgoingLinksNumber'); + var hasIncomingPort = typeof maxIcomingLinks !== 'number' || maxIcomingLinks > 0; + var hasOutgoingPort = typeof maxOutgoingLinks !== 'number' || maxOutgoingLinks > 0; + if (!hasIncomingPort && !hasOutgoingPort) { + return; + } + var elements = graph.findModelsInArea(joint.g.rect(point.x - range, point.y - range, 2 * range, 2 * range)); // jshint ignore:line + if (Array.isArray(elements)) { + elements.forEach(function(model) { + var view = paper.findViewByModel(model); + if (view && view !== draggedView && model instanceof joint.dia.Element) { // jshint ignore:line + var targetMaxIcomingLinks = view.model.attr('metadata/constraints/maxIncomingLinksNumber'); + var targetMaxOutgoingLinks = view.model.attr('metadata/constraints/maxOutgoingLinksNumber'); + var targetHasIncomingPort = typeof targetMaxIcomingLinks !== 'number' || targetMaxIcomingLinks > 0; + var targetHasOutgoingPort = typeof targetMaxOutgoingLinks !== 'number' || targetMaxOutgoingLinks > 0; + if (view.model.attr('metadata/constraints/xorSourceSink')) { + if (targetHasIncomingPort) { + targetHasIncomingPort = targetHasIncomingPort && graph.getConnectedLinks(view.model, { outbound: true }).length === 0; + } + if (targetHasOutgoingPort) { + targetHasOutgoingPort = targetHasOutgoingPort && graph.getConnectedLinks(view.model, { inbound: true }).length === 0; + } + } + if (draggedView.model.attr('metadata/constraints/xorSourceSink')) { + if (hasIncomingPort) { + targetHasOutgoingPort = targetHasOutgoingPort && graph.getConnectedLinks(view.model, { outbound: true }).length === 0; + } + if (hasOutgoingPort) { + targetHasIncomingPort = targetHasIncomingPort && graph.getConnectedLinks(view.model, { inbound: true }).length === 0; + } + } + view.$('[magnet]').each(function(index, magnet) { + var type = magnet.getAttribute('type'); + if ((type === 'input' && targetHasIncomingPort && hasOutgoingPort) || (type === 'output' && targetHasOutgoingPort && hasIncomingPort)) { + var bbox = joint.V(magnet).bbox(false, paper.viewport); // jshint ignore:line + var distance = point.distance({ + x: bbox.x + bbox.width / 2, + y: bbox.y + bbox.height / 2 + }); + if (distance < range && distance < minDistance) { + minDistance = distance; + closestData = { + context: context, + source: { + cell: draggedView.model, + selector: type === 'output' ? '.input-port' : '.output-port' + }, + target: { + cell: model, + selector: '.' + type+'-port' + }, + range: minDistance + }; + } + } + }); + } + }); + } + if (closestData) { + return closestData; + } + + // Check if drop on a link is allowed + if (targetUnderMouse instanceof joint.dia.Link && !(source.attr('metadata/constraints/xorSourceSink') || source.attr('metadata/constraints/maxOutgoingLinksNumber') === 0 || source.attr('metadata/constraints/maxIncomingLinksNumber') === 0) && graph.getConnectedLinks(source).length === 0) { // jshint ignore:line + return { + context: context, + source: { + cell: source + }, + target: { + cell: targetUnderMouse + } + }; + } + + return { + context: context, + source: { + cell: source + }, + }; + } + + function validateNode(flo, element) { + var errors = []; + var graph = flo.getGraph(); + var constraints = element.attr('metadata/constraints'); + if (constraints) { + var incoming = graph.getConnectedLinks(element, {inbound: true}); + var outgoing = graph.getConnectedLinks(element, {outbound: true}); + if (typeof constraints.maxIncomingLinksNumber === 'number' || typeof constraints.minIncomingLinksNumber === 'number') { + if (typeof constraints.maxIncomingLinksNumber === 'number' && constraints.maxIncomingLinksNumber < incoming.length) { + if (constraints.maxIncomingLinksNumber === 0) { + errors.push({ + message: 'Sources must appear at the start of a stream', + range: element.attr('range') + }); + } else { + errors.push({ + message: 'Max allowed number of incoming links is ' + constraints.maxIncomingLinksNumber, + range: element.attr('range') + }); + } + } + if (typeof constraints.minIncomingLinksNumber === 'number' && constraints.minIncomingLinksNumber > incoming.length) { + errors.push({ + message: 'Min allowed number of incoming links is ' + constraints.minIncomingLinksNumber, + range: element.attr('range') + }); + } + } + if (typeof constraints.maxOutgoingLinksNumber === 'number' || typeof constraints.minOutgoingLinksNumber === 'number') { + if (typeof constraints.maxOutgoingLinksNumber === 'number' && constraints.maxOutgoingLinksNumber < outgoing.length) { + if (constraints.maxOutgoingLinksNumber === 0) { + errors.push({ + message: 'Sinks must appear at the end of a stream', + range: element.attr('range') + }); + } else { + errors.push({ + message: 'Max allowed number of outgoing links is ' + constraints.maxOutgoingLinksNumber, + range: element.attr('range') + }); + } + } + if (typeof constraints.minOutgoingLinksNumber === 'number' && constraints.minOutgoingLinksNumber > outgoing.length) { + errors.push({ + message: 'Min allowed number of outgoing links is ' + constraints.minOutgoingLinksNumber, + range: element.attr('range') + }); + } + } + if (constraints.xorSourceSink && incoming.length && outgoing.length) { + errors.push({ + message: 'Node can either have incoming or outgoing links, but not both', + range: element.attr('range') + }); + } + } + if (!element.attr('metadata') || element.attr('metadata/unresolved')) { + var msg = 'Unknown element \'' + element.attr('metadata/name') + '\''; + if (element.attr('metadata/group')) { + msg += ' from group \'' + element.attr('metadata/group') + '\'.'; + } + errors.push({ + message: msg, + range: element.attr('range') + }); + } + + // If possible, verify the properties specified match those allowed on this type of element + // propertiesRanges are the ranges for each property included the entire '--name=value'. + // The format of a range is {'start':{'ch':NNNN,'line':NNNN},'end':{'ch':NNNN,'line':NNNN}} + var propertiesRanges = element.attr('propertiesranges'); + if (propertiesRanges) { + var moduleSchema = element.attr('metadata'); + // Grab the list of supported properties for this module type + moduleSchema.get('properties').then(function(moduleSchemaProperties) { + if (!moduleSchemaProperties) { + moduleSchemaProperties = {}; + } + // Example moduleSchemaProperties: + // {"host":{"name":"host","type":"String","description":"the hostname of the mail server","defaultValue":"localhost","hidden":false}, + // "password":{"name":"password","type":"String","description":"the password to use to connect to the mail server ","defaultValue":null,"hidden":false} + var specifiedProperties = element.attr('props'); + Object.keys(specifiedProperties).forEach(function(propertyName) { + if (!moduleSchemaProperties[propertyName]) { + // The schema does not mention that property + var propertyRange = propertiesRanges[propertyName]; + if (propertyRange) { + errors.push({ + message: 'unrecognized option \''+propertyName+'\' for module \''+element.attr('metadata/name')+'\'', + range: propertyRange + }); + } + } + }); + }); + } + + return errors; + } + + function moveNodeOnNode(flo, node, pivotNode, side, shouldRepairDamage) { + side = side || 'left'; + if (canSwap(flo, node, pivotNode, side)) { + var link; + var i; + if (side === 'left') { + var sources = []; + if (shouldRepairDamage) { + /* + * Commented out because it doesn't prevent cycles. + */ +// if (graph.getConnectedLinks(pivotNode, {inbound: true}).length > 0 || graph.getConnectedLinks(node, {outbound: true}).length > 0) { + repairDamage(flo, node); +// } + } + var pivotTargetLinks = flo.getGraph().getConnectedLinks(pivotNode, {inbound: true}); + for (i = 0; i < pivotTargetLinks.length; i++) { + link = pivotTargetLinks[i]; + sources.push(link.get('source').id); + link.remove(); + } + for (i = 0; i < sources.length; i++) { + flo.createLink({ + 'id': sources[i], + 'selector': '.output-port' + }, { + 'id': node.id, + 'selector': '.input-port' + }); + } + flo.createLink({ + 'id': node.id, + 'selector': '.output-port' + }, { + 'id': pivotNode.id, + 'selector': '.input-port' + }); + } else if (side === 'right') { + var targets = []; + if (shouldRepairDamage) { + /* + * Commented out because it doesn't prevent cycles. + */ +// if (graph.getConnectedLinks(pivotNode, {outbound: true}).length > 0 || graph.getConnectedLinks(node, {inbound: true}).length > 0) { + repairDamage(flo, node); +// } + } + var pivotSourceLinks = flo.getGraph().getConnectedLinks(pivotNode, {outbound: true}); + for (i = 0; i < pivotSourceLinks.length; i++) { + link = pivotSourceLinks[i]; + targets.push(link.get('target').id); + link.remove(); + } + for (i = 0; i < targets.length; i++) { + flo.createLink({ + 'id': node.id, + 'selector': '.output-port' + }, { + 'id': targets[i], + 'selector': '.input-port' + }); + } + flo.createLink({ + 'id': pivotNode.id, + 'selector': '.output-port' + }, { + 'id': node.id, + 'selector': '.input-port' + }); + } + } + } + + function moveNodeOnLink(flo, node, link, shouldRepairDamage) { + var source = link.get('source').id; + var target = link.get('target').id; + + if (shouldRepairDamage) { + repairDamage(flo, node); + } + link.remove(); + + if (source) { + flo.createLink({ + 'id': source, + 'selector': '.output-port' + }, { + 'id': node.id, + 'selector': '.input-port' + }); + } + if (target) { + flo.createLink({ + 'id': node.id, + 'selector': '.output-port' + }, { + 'id': target, + 'selector': '.input-port' + }); + } + } + + function repairDamage(flo, node) { + /* + * remove incoming, outgoing links and cache their sources and targets not equal to current node + */ + var sources = []; + var targets = []; + var i = 0; + var links = flo.getGraph().getConnectedLinks(node); + for (i = 0; i < links.length; i++) { + var targetId = links[i].get('target').id; + var sourceId = links[i].get('source').id; + if (targetId === node.id) { + links[i].remove(); + sources.push(sourceId); + } else if (sourceId === node.id) { + links[i].remove(); + targets.push(targetId); + } + } + /* + * best attempt to connect source and targets bypassing the node + */ + if (sources.length === 1) { + var source = sources[0]; + for (i = 0; i < targets.length; i++) { + flo.createLink({ + 'id': source, + 'selector': '.output-port' + }, { + 'id': targets[i], + 'selector': '.input-port' + }); + } + } else if (targets.length === 1) { + var target = targets[0]; + for (i = 0; i < sources.length; i++) { + flo.createLink({ + 'id': sources[i], + 'selector': '.output-port' + }, { + 'id': target, + 'selector': '.input-port' + }); + } + } + } + + /** + * Check if node being dropped and drop target node next to each other such that they won't be swapped by the drop + */ + function canSwap(flo, dropee, target, side) { + var i, targetId, sourceId, noSwap = (dropee.id === target.id); + if (dropee === target) { + $log.debug('What!??? Dragged == Dropped!!! id = ' + target); + } + var links = flo.getGraph().getConnectedLinks(dropee); + for (i = 0; i < links.length && !noSwap; i++) { + targetId = links[i].get('target').id; + sourceId = links[i].get('source').id; + noSwap = (side === 'left' && targetId === target.id && sourceId === dropee.id) || (side === 'right' && targetId === dropee.id && sourceId === target.id); + } + return !noSwap; + } + + return { + 'createHandles': createHandles, + 'validatePort': validatePort, + 'validateLink': validateLink, + 'calculateDragDescriptor': calculateDragDescriptor, + 'handleNodeDropping': handleNodeDropping, + 'validateNode': validateNode, + 'preDelete': preDelete, + 'interactive': { + 'vertexAdd': false + }, + 'allowLinkVertexEdit': false + }; + + }]; + +}); diff --git a/samples/spring-flo-si/src/main/resources/static/js/flosi-app.js b/samples/spring-flo-si/src/main/resources/static/js/flosi-app.js new file mode 100644 index 0000000..25278fa --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/js/flosi-app.js @@ -0,0 +1,104 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author Alex Boyko + * @author Andy Clement + */ +define(function(require) { + 'use strict'; + var angular = require('angular'); + require('flo'); + var app = angular.module('floSiApp', [ 'spring.flo' ]); + app.factory('SampleMetamodelService', require('metamodel-service')); + app.factory('SampleRenderService', require('render-service')); + app.factory('SampleEditorService', require('editor-service')); + + app.controller('SiController', ['$scope', '$http', 'SampleMetamodelService', function($scope, $http, metamodelService) { + $scope.endpoint = 'http://localhost:8080/integration'; + $scope.labelpath = "stats.sendcount"; + $scope.refreshrate=0; + + var refreshTimer; + + $scope.load = function(endpoint) { + console.log("Loading from endpoint: '"+endpoint+"'"); + $scope.endpoint = endpoint; + // Load the graph from the endpoint + $http.get(endpoint, { }).success(function(json) { + // console.log("JSON is "+json); + // console.log("it is "+$('#endpoint').val()); + // $('#flow-definition').val('foo'); + $scope.definition.text = JSON.stringify(json); + $scope.flo.updateGraphRepresentation(); + }).error(function(err) { + console.log(err); + }); + }; + + $scope.updateRefreshRate = function(newRefreshRate) { + console.log("Update refresh rate: '"+newRefreshRate+"'"); + $scope.refreshrate=newRefreshRate; + if (refreshTimer) { + clearTimeout(refreshTimer); + } + if (newRefreshRate >0) { + if (newRefreshRate < 250) { + $scope.refreshrate = 250; + } + var refresher = function() { + refresh(); + refreshTimer = setTimeout(function() { refresher() }, $scope.refreshrate); + } + refreshTimer = setTimeout(refresher, $scope.refreshrate); + } else { + $scope.refreshrate=0; + } + } + + function refresh() { + $http.get($scope.endpoint, { }).success(function(json) { + metamodelService.updateGraphLabels($scope.flo, JSON.stringify(json), $scope.labelpath); + }).error(function(err) { + console.log(err); + }); + } + + $scope.updateLabelPath = function(newLabelPath) { + console.log("Update label path: '"+newLabelPath+"'"); + $scope.labelpath = newLabelPath; + // Update the graph from the endpoint + $http.get($scope.endpoint, { }).success(function(json) { + metamodelService.updateGraphLabels($scope.flo, JSON.stringify(json), newLabelPath); + }).error(function(err) { + console.log(err); + }); + }; + + }]).directive('ngEnter', function() { + return function(scope, element, attrs) { + element.bind("keydown keypress", function(event) { + if(event.which === 13) { + scope.$apply(function(){ + scope.$eval(attrs.ngEnter); + }); + event.preventDefault(); + } + }); + }; + }); + return app; +}); diff --git a/samples/spring-flo-si/src/main/resources/static/js/graph-to-text.js b/samples/spring-flo-si/src/main/resources/static/js/graph-to-text.js new file mode 100644 index 0000000..cb375bb --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/js/graph-to-text.js @@ -0,0 +1,179 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Convert a graph to a text representation. + * + * @author Alex Boyko + * @author Andy Clement + */ +define(function() { + 'use strict'; + + // Graph + var g; + + // Number of Links left to visit + var numberOfLinksToVisit; + + // Number of nodes left to visit + var numberOfNodesToVisit; + + // Map of links left to visit indexed by id + var linksToVisit; + + // Map of nodes left to visit indexed by id + var nodesToVisit; + + // Map of nodes incoming non-visited links degrees index by node id + var nodesInDegrees; + + // Priority: + // 1. find links whose source has no other links pointing at it + // 2. find links whose source has already been processed (not currently needed in sample DSL since + // can't create graphs like that due to metamodel constraints) + // 3. find remaining links + function nextLink() { + var indegree = Number.MAX_INT; + var currentBest; + for (var id in linksToVisit) { + var link = g.getCell(id); + var source = g.getCell(link.get('source').id); + var currentInDegree = nodesInDegrees[source.get('id')]; + if (currentInDegree === 0) { + return visit(link); + } else if (indegree > currentInDegree) { + indegree = currentInDegree; + currentBest = link; + } + } + if (currentBest) { + return visit(currentBest); + } + } + + function visit(e) { + if (e.isLink()) { + delete linksToVisit[e.get('id')]; + nodesInDegrees[e.get('target').id]--; + numberOfLinksToVisit--; + } else { + delete nodesToVisit[e.get('id')]; + numberOfNodesToVisit--; + } + return e; + } + + function init(graph) { + numberOfLinksToVisit = 0; + numberOfNodesToVisit = 0; + linksToVisit = {}; + nodesToVisit = {}; + nodesInDegrees = {}; + g = graph; + g.getElements().forEach(function(element) { + if (element.attr('metadata/name')) { // is it a node? + nodesToVisit[element.get('id')] = element; + var indegree = 0; + g.getConnectedLinks(element, {inbound: true}).forEach(function(link) { + if (link.get('source') && link.get('source').id && g.getCell(link.get('source').id) && + g.getCell(link.get('source').id).attr('metadata/name')) { + linksToVisit[link.get('id')] = link; + numberOfLinksToVisit++; + indegree++; + } + }); + nodesInDegrees[element.get('id')] = indegree; + numberOfNodesToVisit++; + } + }); + } + + /** + * Starts at a link and proceeds down a chain. Converts each node to + * text and then joins them with a ' > '. + */ + function chainToText(link) { + var text = ''; + var source = g.getCell(link.get('source').id); + text += nodeToText(source, true); + while (link) { + var target = g.getCell(link.get('target').id); + text += ' > '; + text += nodeToText(target, false); + + // Find next not visited link to follow + link = null; + var outgoingLinks = g.getConnectedLinks(target, {outbound: true}); + for (var i = 0; i < outgoingLinks.length && !link; i++) { + if (linksToVisit[outgoingLinks[i].get('id')]) { + source = target; + link = visit(outgoingLinks[i]); + } + } + } + return text; + } + + /** + * Very basic format. From a node to the text: + * "name --key=value --key=value" + */ + function nodeToText(element) { + var text = ''; + var props = element.attr('props'); + if (!element) { + return; + } + text += element.attr('metadata/name'); + if (props) { + Object.keys(props).forEach(function(propertyName) { + text += ' --' + propertyName + '=' + props[propertyName]; + }); + } + visit(element); + return text; + } + + function appendChainText(text, chainText) { + if (chainText) { + if (text) { + text += '\n'; + } + text += chainText; + } + return text; + } + + // Translate a graph into a basic string + return function(g) { + var text = ''; + var chainText; + var id; + init(g); + while (numberOfLinksToVisit) { + chainText = chainToText(nextLink()); + text = appendChainText(text, chainText); + } + // Visit all disconnected nodes + for (id in nodesToVisit) { + chainText = nodeToText(nodesToVisit[id], true); + text = appendChainText(text, chainText); + } + return text; + }; + +}); \ No newline at end of file diff --git a/samples/spring-flo-si/src/main/resources/static/js/main.js b/samples/spring-flo-si/src/main/resources/static/js/main.js new file mode 100755 index 0000000..24733bd --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/js/main.js @@ -0,0 +1,83 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author Alex Boyko + * @author Andy Clement + */ +requirejs.config({ + baseUrl:'js', + paths: { + joint: '/webjars/jointjs/dist/joint', + backbone: '/webjars/backbone/backbone', + domReady: '/webjars/requirejs-domready/domReady', + angular: '/webjars/angular/angular', + jquery: '/webjars/jquery/dist/jquery', + bootstrap:'/webjars/bootstrap/bootstrap', + lodash: '/webjars/lodash/lodash', // lodash.compat + dagre: '/webjars/dagre/dist/dagre.core', + graphlib: '/webjars/graphlib/graphlib.core', + text : '/webjars/requirejs-text/text', + flo : '/webjars/spring-flo/dist/spring-flo', + json5 : '/webjars/json5/json5' + }, + map: { + '*': { + // Backbone requires underscore. This forces requireJS to load lodash instead: + 'underscore': 'lodash' + } + }, + packages: [ + { + name: 'codemirror', + location: '../lib/codemirror', + main: 'lib/codemirror' + } + ], + shim: { + angular: { + deps: ['bootstrap'], + exports: 'angular' + }, + bootstrap: { + deps: ['jquery'] + }, + graphlib: { + deps: ['underscore'] + }, + dagre: { + deps: ['graphlib', 'underscore'] + }, + joint: { + deps: ['jquery', 'underscore', 'backbone'], + }, + underscore: { + exports: '_' + }, + 'flo': { + deps: ['angular', 'jquery', 'joint', 'underscore'] + } + } +}); + +define(['require','angular'], function (require, angular) { + 'use strict'; + require(['domReady!', 'flosi-app'], + function (document) { + angular.bootstrap(document, ['floSiApp']); + } + ); +}); diff --git a/samples/spring-flo-si/src/main/resources/static/js/metamodel-sample.json b/samples/spring-flo-si/src/main/resources/static/js/metamodel-sample.json new file mode 100644 index 0000000..4313f82 --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/js/metamodel-sample.json @@ -0,0 +1,90 @@ +[ +{ + 'name':'general', 'group':'general', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'service-activator', 'group':'messaging-endpoints', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'message-handler', 'group':'messaging-endpoints', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'gateway', 'group':'messaging-endpoints', 'description':'Gateway', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'splitter', 'group':'routing', 'description':'Splitter', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } + } +, +{ + 'name':'router', 'group':'routing', 'description':'Router', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'transformer', 'group':'transformation', 'description':'Transformer', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'bridge', 'group':'routing', 'description':'Bridge', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'channel', 'group':'connectors', 'description':'Channel', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'publish-subscribe-channel', 'group':'connectors', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'test-producer', 'group':'producers', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'BareHandler', 'group':'producers', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +}, +{ + 'name':'filter', 'group':'routing', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'aggregator', 'group':'routing', 'description':'Produce an output message after correlating some set of incoming messages', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'logging-channel-adapter', 'group':'connectors', 'description':'?', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'chain', 'group':'messaging-endpoints', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'inbound-channel-adapter', 'group':'messaging-endpoints', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +, +{ + 'name':'outbound-channel-adapter', 'group':'messaging-endpoints', 'description':'', + 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 } +} +] \ No newline at end of file diff --git a/samples/spring-flo-si/src/main/resources/static/js/metamodel-service.js b/samples/spring-flo-si/src/main/resources/static/js/metamodel-service.js new file mode 100644 index 0000000..4f5d7b3 --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/js/metamodel-service.js @@ -0,0 +1,161 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author Alex Boyko + * @author Andy Clement + */ +define(function(require) { + 'use strict'; + + require('json5'); + + var convertGraphToText = require('graph-to-text'); + var convertTextToGraph = require('text-to-graph'); + var updateGraph = require('update-graph'); + + return ['$http', '$q', '$timeout', '$log', 'MetamodelUtils', function($http, $q, $timeout, $log, metamodelUtils) { + + var metamodel; + + // Internally stored metamodel load promise + var request; + + var statsProperties = [ + {'name':'name','default':'?', 'description':'name'}, + {'name':'id','default':'?', 'description':'node id'}, + {'name':'componentType','default':'','description':'Detailed component type'}, + {'name':'stats.loggingEnabled', 'default': '?', 'description':'?' }, + {'name':'stats.statsEnabled', 'default': '?', 'description':'?' }, + {'name':'stats.countsEnabled', 'default': '?', 'description':'?' }, + {'name':'stats.sendRate.count', 'default': '?', 'description':'?' }, + {'name':'stats.sendRate.min', 'default': '?', 'description':'?' }, + {'name':'stats.sendRate.max', 'default': '?', 'description':'?' }, + {'name':'stats.sendRate.mean', 'default': '?', 'description':'?' }, + {'name':'stats.sendRate.standardDeviation', 'default': '?', 'description':'?' }, + {'name':'stats.sendRate.countLong', 'default': '?', 'description':'?' }, + {'name':'stats.errorRate.count', 'default': '?', 'description':'?' }, + {'name':'stats.errorRate.min', 'default': '?', 'description':'?' }, + {'name':'stats.errorRate.max', 'default': '?', 'description':'?' }, + {'name':'stats.errorRate.mean', 'default': '?', 'description':'?' }, + {'name':'stats.errorRate.standardDeviation', 'default': '?', 'description':'?' }, + {'name':'stats.errorRate.countLong', 'default': '?', 'description':'?' }, + {'name':'stats.sendCount', 'default': '?', 'description':'?' }, + {'name':'stats.sendErrorCount', 'default': '?', 'description':'?' }, + {'name':'stats.timeSinceLastSend', 'default': '?', 'description':'?' }, + {'name':'stats.meanSendRate', 'default': '?', 'description':'?' }, + {'name':'stats.meanErrorRate', 'default': '?', 'description':'?' }, + {'name':'stats.meanErrorRatio', 'default': '?', 'description':'?' }, + {'name':'stats.meanSendDuration', 'default': '?', 'description':'?' }, + {'name':'stats.minSendDuration', 'default': '?', 'description':'?' }, + {'name':'stats.maxSendDuration', 'default': '?', 'description':'?' }, + {'name':'stats.standardDeviationSendDuration', 'default': '?', 'description':'?' }, + {'name':'stats.sendDuration.count', 'default': '?', 'description':'?' }, + {'name':'stats.sendDuration.min', 'default': '?', 'description':'?' }, + {'name':'stats.sendDuration.max', 'default': '?', 'description':'?' }, + {'name':'stats.sendDuration.mean', 'default': '?', 'description':'?' }, + {'name':'stats.sendDuration.standardDeviation', 'default': '?', 'description':'?' }, + {'name':'stats.sendDuration.countLong', 'default': '?', 'description':'?' }]; + + /** + * Helper that goes from basic JSON to a lazy getter structure. Useful when the + * metamodel is 'cheap' to build. If it is costly to discover the actual properties + * the getter may be more complex (e.g. make a REST request). + */ + function createMetadata(entry) { + var props = {}; + if (!entry.properties) { + // use the default stats properties + entry.properties = JSON.parse(JSON.stringify(statsProperties)); + } + if (Array.isArray(entry.properties)) { + entry.properties.forEach(function(property) { + if (!property.id) { + property.id = property.name; + } + props[property.id] = property; + }); + } + entry.properties = props; + return { + name: entry.name, + group: entry.group, + icon: entry.icon, + constraints: entry.constraints, + description: entry.description, + metadata: entry.metadata, + properties: entry.properties, + get: function(property) { + var deferred = $q.defer(); + if (entry.hasOwnProperty(property)) { + deferred.resolve(entry[property]); + } else { + deferred.reject(); + } + return deferred.promise; + } + }; + } + + function load() { + // COULDDO: to cache the result here, check result before doing this processing + // and simply return it if it is set. If doing that may want to override refresh + // in this service + var metamodelData = JSON5.parse(require('text!metamodel-sample.json')); + var deferred = $q.defer(); + var newData = {}; + metamodelData.forEach(function(data) { + var metadata = createMetadata(data); + if (!newData[metadata.group]) { + newData[metadata.group] = {}; + } + newData[metadata.group][metadata.name] = metadata; + }); + metamodel = newData; + deferred.resolve(metamodel); + request = deferred.promise; + return request; + } + + function graphToText(flo, definition) { + definition.text = convertGraphToText(flo.getGraph()); + } + + function updateGraphLabels(flo, text, labelpath) { + updateGraph(text, flo.getGraph(), labelpath); + } + + function textToGraph(flo, definition) { + // TODO perhaps push these flo operations into the 'caller' to make this simpler + flo.getGraph().clear(); + load().then(function(metamodel) { + convertTextToGraph(definition.text, flo, metamodel, metamodelUtils); + updateGraph(definition.text,flo.getGraph(),'stats.sendcount'); + flo.performLayout(); + flo.fitToPage(); + }); + } + + return { + 'load': load, + 'textToGraph': textToGraph, + 'updateGraphLabels': updateGraphLabels, + 'graphToText': graphToText + }; + + }]; + +}); diff --git a/samples/spring-flo-si/src/main/resources/static/js/render-service.js b/samples/spring-flo-si/src/main/resources/static/js/render-service.js new file mode 100644 index 0000000..e54d5af --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/js/render-service.js @@ -0,0 +1,746 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author Alex Boyko + * @author Andy Clement + */ +define(function(require) { + 'use strict'; + + var joint = require('joint'); + + var dagre = require('dagre'); + + var HANDLE_ICON_MAP = { + 'remove': 'icons/delete.svg', + }; + + var DECORATION_ICON_MAP = { + 'error': 'icons/error.svg' + }; + + var IMAGE_W = 120, + IMAGE_H = 40; + + var HORIZONTAL_PADDING = 10; + + joint.shapes.si = {}; + + joint.routers.floforintegration = (function() { + + // expands a box by specific value + function expand(bbox, val) { + return joint.g.rect(bbox).moveAndExpand({ x: -val, y: -val, width: 2 * val, height: 2 * val }); + } + + function routeAround(exp, ref, anchor, opt) { + + var anchorSide = exp.sideNearestToPoint(anchor); + var expAnchor = exp.pointNearestToPoint(anchor); + var line = joint.g.line(ref, anchor); + var center = exp.center(); + var intersection; + var pts = []; + + if (anchorSide !== 'top') { + intersection = line.intersection(joint.g.line(exp.origin(), exp.topRight())); + if (intersection) { + if (anchorSide === 'bottom') { + if (intersection.x - exp.x + expAnchor.x - exp.x <= exp.width) { + pts = [exp.origin(), exp.bottomLeft()]; + } else { + pts = [exp.topRight(), exp.corner()]; + } + } else { + if (anchorSide === 'left') { + pts = [exp.origin()]; + } else { + pts = [exp.topRight()]; + } + } + if (opt.includeExtraPoints) { + pts.push(expAnchor); + } + return pts; + } + } + + if (anchorSide !== 'bottom') { + intersection = line.intersection(joint.g.line(exp.corner(), exp.bottomLeft())); + if (intersection) { + if (anchorSide === 'top') { + if (intersection.x - exp.x + expAnchor.x - exp.x <= exp.width) { + pts = [exp.bottomLeft(), exp.origin()]; + } else { + pts = [exp.corner(), exp.topRight()]; + } + } else { + if (anchorSide === 'left') { + pts = [exp.bottomLeft()]; + } else { + pts = [exp.corner()]; + } + } + if (opt.includeExtraPoints) { + pts.push(expAnchor); + } + return pts; + } + } + + if (anchorSide !== 'left') { + intersection = line.intersection(joint.g.line(exp.origin(), exp.bottomLeft())); + if (intersection) { + if (anchorSide === 'right') { + if (intersection.y - exp.y + expAnchor.y - exp.y <= exp.height) { + pts = [exp.origin(), exp.topRight()]; + } else { + pts = [exp.bottomLeft(), exp.corner()]; + } + } else { + if (anchorSide === 'top') { + pts = [exp.origin()]; + } else { + pts = [exp.bottomLeft()]; + } + } + if (opt.includeExtraPoints) { + pts.push(expAnchor); + } + return pts; + } + } + + if (anchorSide !== 'right') { + intersection = line.intersection(joint.g.line(exp.topRight(), exp.corner())); + if (intersection) { + if (anchorSide === 'left') { + if (intersection.y - exp.y + expAnchor.y - exp.y <= exp.height) { + pts = [exp.topRight(), exp.origin()]; + } else { + pts = [exp.corner(), exp.bottomLeft()]; + } + } else { + if (anchorSide === 'top') { + pts = [exp.topRight()]; + } else { + pts = [exp.corner()]; + } + } + if (opt.includeExtraPoints) { + pts.push(expAnchor); + } + return pts; + } + } + + return pts; + } + + function findRoute(vx, opt, linkView) { + + var vertices = opt.metro ? joint.routers.metro(vx, opt, linkView) : vx; + var sourceRoute = [], targetRoute = []; + + var paper = linkView.paper; + var reference = vertices.length ? vertices[0] : joint.g.rect(linkView.targetBBox).center(); + var sourceAnchorPt = paper.options.linkConnectionPoint(linkView, linkView.sourceView, linkView.sourceMagnet, reference); + var targetAnchorPt = paper.options.linkConnectionPoint(linkView, linkView.targetView, linkView.targetMagnet, sourceAnchorPt); + var padding = opt.elementPadding || 20; + + if (linkView.sourceView) { + var expSource = expand(linkView.sourceView.model.getBBox(), padding); + while (vertices.length && expSource.containsPoint(vertices[0])) { + vertices.splice(0, 1); + } + var sourceRef = vertices.length ? joint.g.point(vertices[0]) : targetAnchorPt; + sourceRoute = routeAround(expSource, sourceRef, sourceAnchorPt, opt).reverse(); + } + if (linkView.targetView) { + var expTarget = expand(linkView.targetView.model.getBBox(), padding); + while (vertices.length && expTarget.containsPoint(vertices[vertices.length - 1])) { + vertices.splice(vertices.length - 1, 1); + } + var targetRef = vertices.length ? joint.g.point(vertices[vertices.length - 1]) : sourceAnchorPt; + + targetRoute = routeAround(expTarget, targetRef, targetAnchorPt, opt); + } + + return sourceRoute.concat(vertices).concat(targetRoute); + }; + + return findRoute; + + })(); + + joint.shapes.si.Channel = joint.shapes.basic.Generic.extend({ + + markup: + ''+ + '' + + ''+ + ''+ + ''+ + ''+ + '' + +// ''+ + ''+ + ''+ + '', + + defaults: joint.util.deepSupplement({ + type: 'channel',//joint.shapes.flo.NODE_TYPE, + position: {x: 0, y: 0}, + size: { width: 100, height: 40 }, + attrs: { + '.': { + magnet: false, + }, + '.the_shape': { + 'stroke':'#000000' + }, + // rounded edges around image +// '.border': { +// width: IMAGE_W, +// height: IMAGE_H, +// rx: 2, +// ry: 2, +// 'fill-opacity':0, // see through +// stroke: '#eeeeee', +// 'stroke-width': 0, +// }, +// '.box': { +// width: IMAGE_W, +// height: IMAGE_H, +// rx: 2, +// ry: 2, +// //'fill-opacity':0, // see through +// stroke: '#6db33f', +// fill: '#eeeeee', +// 'stroke-width': 2, +// }, + '.input-port': { + type: 'input', + port: 'input', + r:4, + height: 4, width: 4, + magnet: true, + fill: '#eeeeee', + transform: 'translate(' + -2 + ',' + ((20/2)-2+10) + ')', + stroke: '#34302d', + 'stroke-width': 1, + }, + '.output-port': { + type: 'output', + port: 'output', + r:4, + height: 4, width: 4, + magnet: true, + fill: '#eeeeee', + transform: 'translate(' + (100+8-2) + ',' + ((20/2)-2+10) + ')', + stroke: '#34302d', + 'stroke-width': 1, + }, +// '.tap-port': { +// type: 'output', +// port: 'tap', +// r: 4, +// magnet: true, +// fill: '#eeeeee', +// 'ref-x': 0.5, +// 'ref-y': 0.99999999, +// ref: '.border', +// stroke: '#34302D' +// }, + '.label': { + 'ref-x': 0.5, // jointjs specific: relative position to ref'd element + 'ref-y': 0.525, + 'y-alignment': 'middle', + 'x-alignment' : 'middle', + ref: '.the_shape', // jointjs specific: element for ref-x, ref-y + fill: 'black', + 'stroke': 'black', + 'font-size': '12px', + 'font-family': 'Ubuntu Mono', + 'color': 'black' + }, + '.label2': { + 'y-alignment': 'middle', + 'ref-x': HORIZONTAL_PADDING+2, // jointjs specific: relative position to ref'd element + 'ref-y': 0.55, // jointjs specific: relative position to ref'd element + ref: '.border', // jointjs specific: element for ref-x, ref-y + fill: 'black', + 'font-size': 20 + }, +// '.stream-label': { +// 'x-alignment': 'middle', +// 'y-alignment': -0.999999, +// 'ref-x': 0.5, // jointjs specific: relative position to ref'd element +// 'ref-y': 0, // jointjs specific: relative position to ref'd element +// ref: '.border', // jointjs specific: element for ref-x, ref-y +// fill: '#AAAAAA', +// 'font-size': 15 +// }, +// '.shape': { +// } + } + }, joint.shapes.basic.Generic.prototype.defaults) + }); + + + joint.shapes.si.Node = joint.shapes.basic.Generic.extend({ + markup: + ''+ + '' + + '' + + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '', + + defaults: joint.util.deepSupplement({ + + type: 'node',//joint.shapes.flo.NODE_TYPE, + position: {x: 0, y: 0}, + size: { width: IMAGE_W, height: IMAGE_H }, + attrs: { + '.': { magnet: false }, + // rounded edges around image + '.border': { + width: IMAGE_W, + height: IMAGE_H, + rx: 3, + ry: 3, + 'fill-opacity':0, // see through + stroke: '#eeeeee', + 'stroke-width': 0 + }, + + '.box': { + width: IMAGE_W, + height: IMAGE_H, + rx: 3, + ry: 3, + //'fill-opacity':0, // see through + stroke: '#6db33f', + fill: '#eeeeee', + 'stroke-width': 1 + }, + '.input-port': { + type: 'input', + height: 4, width: 4, + magnet: true, + fill: '#eeeeee', + transform: 'translate(' + -2 + ',' + ((IMAGE_H/2)-2) + ')', + stroke: '#34302d', + 'stroke-width': 1 + }, + '.output-port': { + type: 'output', + height: 4, width: 4, + magnet: true, + fill: '#eeeeee', + transform: 'translate(' + (IMAGE_W-2) + ',' + ((IMAGE_H/2)-2) + ')', + stroke: '#34302d', + 'stroke-width': 1 + }, + '.error-port': { + type: 'output', + height: 4, width: 4, + magnet: true, + fill: '#ff0000', + transform: 'translate(' + (IMAGE_W/2-2) + ',' + ((IMAGE_H)-2) + ')', + stroke: '#34302d', + 'stroke-width': 1 + }, + '.label': { + 'text-anchor': 'middle', + 'ref-x': 0.5, // jointjs specific: relative position to ref'd element + // 'ref-y': -12, // jointjs specific: relative position to ref'd element + 'ref-y': 0.3, + ref: '.border', // jointjs specific: element for ref-x, ref-y + fill: 'black', + 'font-size': 14 + }, + '.label2': { + 'text': '\u21d2', + 'text-anchor': 'middle', + 'ref-x': 0.15, // jointjs specific: relative position to ref'd element + 'ref-y': 0.15, // jointjs specific: relative position to ref'd element + ref: '.border', // jointjs specific: element for ref-x, ref-y + transform: 'translate(' + (IMAGE_W/2) + ',' + (IMAGE_H/2) + ')', + fill: 'black', + 'font-size': 24 + }, + '.shape': { + }, + '.image': { + width: IMAGE_W, + height: IMAGE_H + } + } + }, joint.shapes.basic.Generic.prototype.defaults) + }); + + joint.shapes.si.ServiceActivator = joint.shapes.basic.Generic.extend({ + markup: + ''+ + '' + + '' + + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '' + , + + + defaults: joint.util.deepSupplement({ + + type: 'node',//joint.shapes.flo.NODE_TYPE, + position: {x: 0, y: 0}, + size: { width: IMAGE_W, height: IMAGE_H }, + attrs: { + '.': { magnet: false }, + // rounded edges around image + '.border': { + width: IMAGE_W, + height: IMAGE_H, + rx: 3, + ry: 3, + 'fill-opacity':0, // see through + stroke: '#eeeeee', + 'stroke-width': 0 + }, + '.arrowGray': { + 'stroke':'#aaaaaa', + 'stroke-width':'2', + 'fill': '#aaaaaa' + }, + '.arrowBlack': { + 'stroke':'black', + 'stroke-width':'2', + 'fill': 'black' + }, + '.blockSolid': { + 'stroke':'#000000', + 'stroke-width':'2', + 'fill': 'black' + }, + '.blockEmpty': { + 'stroke':'#000000', + 'stroke-width':'2', + 'fill': 'white' + }, + '.box': { + width: IMAGE_W, + height: IMAGE_H, + rx: 3, + ry: 3, + //'fill-opacity':0, // see through + stroke: '#6db33f', + fill: '#eeeeee', + 'stroke-width': 1 + }, + '.input-port': { + type: 'input', + height: 4, width: 4, + magnet: true, + fill: '#eeeeee', + transform: 'translate(' + -2 + ',' + ((IMAGE_H/2)-2) + ')', + stroke: '#34302d', + 'stroke-width': 1 + }, + '.output-port': { + type: 'output', + height: 4, width: 4, + magnet: true, + fill: '#eeeeee', + transform: 'translate(' + (IMAGE_W-2) + ',' + ((IMAGE_H/2)-2) + ')', + stroke: '#34302d', + 'stroke-width': 1 + }, + '.error-port': { + type: 'output', + height: 4, width: 4, + magnet: true, + fill: '#ff0000', + transform: 'translate(' + (IMAGE_W/2-2) + ',' + ((IMAGE_H)-2) + ')', + stroke: '#34302d', + 'stroke-width': 1 + }, + '.label': { + 'text-anchor': 'middle', + 'ref-x': 0.5, // jointjs specific: relative position to ref'd element +// 'ref-y': -12, // jointjs specific: relative position to ref'd element + 'ref-y': 0.3, + ref: '.border', // jointjs specific: element for ref-x, ref-y + fill: 'black', + 'font-size': 14 + }, + '.label2': { + 'text': '\u21d2', + 'text-anchor': 'middle', + 'ref-x': 0.15, // jointjs specific: relative position to ref'd element + 'ref-y': 0.15, // jointjs specific: relative position to ref'd element + ref: '.border', // jointjs specific: element for ref-x, ref-y + transform: 'translate(' + (IMAGE_W/2) + ',' + (IMAGE_H/2) + ')', + fill: 'black', + 'font-size': 24 + }, + '.shape': { + }, + '.image': { + width: IMAGE_W, + height: IMAGE_H + } + } + }, joint.shapes.basic.Generic.prototype.defaults) + }); + + + return ['$log', function($log) { + + function createHandle(kind) { + return new joint.shapes.flo.ErrorDecoration({ + size: {width: 10, height: 10}, + attrs: { + 'image': { + 'xlink:href': HANDLE_ICON_MAP[kind] + } + } + }); + } + + function createDecoration(kind) { + return new joint.shapes.flo.ErrorDecoration({ + size: {width: 16, height: 16}, + attrs: { + 'image': { + 'xlink:href': DECORATION_ICON_MAP[kind] + } + } + }); + } + + function createNode(metadata, props) { + if (metadata.name === 'channel' || metadata.name === 'publish-subscribe-channel') { + return new joint.shapes.si.Channel(); + } else if (metadata.name === 'service-activator') { + return new joint.shapes.si.ServiceActivator(); + } else { + return new joint.shapes.si.Node(); + } + } + + function initializeNewNode(node, context) { + var metadata = node.attr('metadata'); + if (metadata) { + node.attr('.label/text', node.attr('metadata/name')); + if (node.attr('metadata/constraints/maxIncomingLinksNumber') === 0) { + node.attr('.input-port/display','none'); + } + if (node.attr('metadata/constraints/maxOutgoingLinksNumber') === 0) { + node.attr('.output-port/display','none'); + } + + var type = node.attr('metadata/name'); + if (type === 'tap') { + if (!node.attr('props/channel')) { + node.attr('props/channel', 'tap:stream:STREAM'); + } + refreshVisuals(node, 'props/channel', context.paper); + } else if (type === 'named-channel') { + // Default channel for named channel is 'queue:default' + if (!node.attr('props/channel')) { + node.attr('props/channel', 'queue:default'); + } + refreshVisuals(node, 'props/channel', context.paper); + } + } + node.attr('.label2/text',''); + + } + + function validateNode(flo, node) { + return []; + } + + function fitLabel(paper, node, labelPath) { + var label = node.attr(labelPath); + if (label && label.length<9) { + return; + } + var view = paper.findViewByModel(node); + if (view && label) { + var textView = view.findBySelector(labelPath.substr(0, labelPath.indexOf('/')))[0]; + var offset = 0; + if (node.attr('.label2/text')) { + var label2View = view.findBySelector('.label2')[0]; + if (label2View) { + var box = joint.V(label2View).bbox(false, paper.viewport); + offset = HORIZONTAL_PADDING + box.width; + } + } + var width = joint.V(textView).bbox(false, paper.viewport).width; + var threshold = IMAGE_W - HORIZONTAL_PADDING - HORIZONTAL_PADDING - offset; + if (offset) { + node.attr('.label1/ref-x', Math.max((offset + HORIZONTAL_PADDING + width / 2) / IMAGE_W, 0.5), { silent: true }); + } + // Trim package prefix + + if (!label.endsWith('?')) { +// console.log("modifying label "+label); + // Sample name: com.foo.method(a.b.c.Order) + var openParen = label.indexOf('('); + if (openParen !== -1) { + label = label.substring(0,openParen); + } + width = joint.V(textView).bbox(false, paper.viewport).width; + if (width > threshold) { + var lastDot = label.lastIndexOf('.'); + if (lastDot !== -1) { + label = label.substring(lastDot+1); + } + console.log('driving label change'); + node.attr(labelPath, label, { silent: true }); + view.update(); + width = joint.V(textView).bbox(false, paper.viewport).width; + for (var i = 1; i < label.length && width > threshold; i++) { + node.attr(labelPath, label.substr(0, label.length - i) + '\u2026', { silent: true }); + view.update(); + width = joint.V(textView).bbox(false, paper.viewport).width; + if (offset) { + node.attr('.label1/ref-x', Math.max((offset + HORIZONTAL_PADDING + width / 2) / IMAGE_W, 0.5), { silent: true }); + } + } + } + } +// view.update(); + } + } + + function createLink() { + var link = new joint.shapes.flo.Link(joint.util.deepSupplement({ + router: { name: 'floforintegration', args: {elementPadding: 20/*, metro: true*/} }, + connector: { name: 'smooth' }, + attrs: { + '.': { + //filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } } + }, + '.connection': { 'stroke-width': 3, 'stroke': 'black', 'stroke-linecap': 'round' }, + '.marker-arrowheads': { display: 'none' }, + '.tool-options': { display: 'none' }, + 'stroke':'red' // TODO necessary? + }, + }, joint.shapes.flo.Link.prototype.defaults)); + return link; + } + + function isSemanticProperty(propertyPath) { + return propertyPath === '.label/text'; + } + + function refreshVisuals(element, changedPropertyPath, paper) { + fitLabel(paper, element, '.label/text'); + } + + function layout(paper) { + var graph = paper.model; + var i; + var g = new dagre.graphlib.Graph(); + g.setGraph({}); + g.setDefaultEdgeLabel(function() {return{};}); + + var nodes = graph.getElements(); + for (i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (node.get('type') === joint.shapes.flo.NODE_TYPE) { + g.setNode(node.id, node.get('size')); + } + } + + var links = graph.getLinks(); + for (i = 0; i < links.length; i++) { + var link = links[i]; + if (link.get('type') === joint.shapes.flo.LINK_TYPE) { + var options = { + minlen: 1.5 + }; +// if (link.get('labels') && link.get('labels').length > 0) { +// options.minlen = 1 + link.get('labels').length * 0.5; +// } + g.setEdge(link.get('source').id, link.get('target').id, options); + link.set('vertices', []); + } + } + + g.graph().rankdir = 'LR'; + dagre.layout(g); + g.nodes().forEach(function(v) { + var node = graph.getCell(v); + if (node) { + var bbox = node.getBBox(); + node.translate(g.node(v).x - bbox.x, g.node(v).y - bbox.y); + } + }); + } + + function getLinkAnchorPoint(linkView, view, magnet, reference) { + if (magnet) { + var cssClass = magnet.getAttribute('class'); + var bbox = joint.V(magnet).bbox(false, linkView.paper.viewport); + var rect = joint.g.rect(bbox); + if (cssClass.indexOf('input-port') !== -1) { + return joint.g.point(rect.x, rect.y + rect.height / 2); + } else if (cssClass.indexOf('error-port') !== -1) { + return joint.g.point(rect.x + rect.width / 2, rect.y + rect.height); + } else { + return joint.g.point(rect.x + rect.width, rect.y + rect.height / 2); + } + } else { + $log.debug('No magnet!'); + return reference; + } + } + + return { + 'createHandle': createHandle, + 'createDecoration': createDecoration, + 'createNode': createNode, + 'createLink': createLink, + 'initializeNewNode': initializeNewNode, + 'isSemanticProperty': isSemanticProperty, + 'refreshVisuals': refreshVisuals, + 'layout': layout, + 'getLinkAnchorPoint': getLinkAnchorPoint + }; + + }]; + +}); diff --git a/samples/spring-flo-si/src/main/resources/static/js/text-to-graph.js b/samples/spring-flo-si/src/main/resources/static/js/text-to-graph.js new file mode 100644 index 0000000..8a7bf91 --- /dev/null +++ b/samples/spring-flo-si/src/main/resources/static/js/text-to-graph.js @@ -0,0 +1,273 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Convert a text representation to a graph. + * + * @author Alex Boyko + * @author Andy Clement + */ +define(function(require) { + 'use strict'; + var joint = require('joint'); + + function collapseOneLevel(prefix, obj, collector) { + var type = typeof obj; + if (obj == null) { + collector[prefix] = null; + return; + } + if (type === 'object') { + Object.keys(obj).forEach(function(key) { + collapseOneLevel(prefix.length==0?key:prefix+'.'+key,obj[key],collector); + }); + } else if (type === 'array') { + for (var i=0;i0.975) p = 0; + setTimeout(function() {animate(link,p)},25); + } + } + + + return function(input, flo, metamodel, metamodelUtils) { + // input is a string like this (3 nodes: foo, goo and hoo): foo --a=b --c=d > goo --d=e --f=g>hoo + var trimmed = input.trim(); + if (trimmed.length===0) { + return; + } + var getMetadata = function(type) { + var group = metamodelUtils.matchGroup(metamodel, type, 1, 1); + var md = metamodelUtils.getMetadata(metamodel, type, group); + if (!md || md.unresolved) { + var secondAttempt; + // Examples: mail:outbound-channel-adapter or file:inbound-channel-adapter + if (type.indexOf("inbound-channel-adapter")!=-1) { + type = "inbound-channel-adapter"; + group = metamodelUtils.matchGroup(metamodel, type, 1, 1); + secondAttempt = metamodelUtils.getMetadata(metamodel, type, group); + if (secondAttempt && !secondAttempt.unresolved) { + md = secondAttempt; + } + } else if (type.indexOf("outbound-channel-adapter")!=-1) { + type = "outbound-channel-adapter"; + group = metamodelUtils.matchGroup(metamodel, type, 1, 1); + secondAttempt = metamodelUtils.getMetadata(metamodel, type, group); + if (secondAttempt && !secondAttempt.unresolved) { + md = secondAttempt; + } + } else { + // use the general one - this will ensure validation is OK and tooltips work but + // we aren't really sure what type it is. + type = 'general'; + group = metamodelUtils.matchGroup(metamodel, type, 1, 1); + secondAttempt = metamodelUtils.getMetadata(metamodel, type, group); + if (secondAttempt && !secondAttempt.unresolved) { + md = secondAttempt; + } + } + } + return md; + } + var integrationGraph = JSON.parse(input); + var nodes = integrationGraph.nodes; + var nodesMap = {}; + for (var i=0;i goo --d=e --f=g>hoo + var trimmed = input.trim(); + if (trimmed.length===0) { + return; + } + var integrationGraph = JSON.parse(input); + var nodes = integrationGraph.nodes; + var graphNodes = graph.getElements(); + var map = {}; + var linksToVisit = graph.getLinks(); + graphNodes.forEach(function(element) { + if (element.attr('metadata/name')) { // is it a node? +// if (!element.get('source')) { + map[element.attr('props/id')] = element; + } else { + linksToVisit.push(element); + } + }); + function toLabel(text) { + var string = text.toString(); + if (string.length>5) { + string = string.substring(0,5); + if (string.endsWith('.')) { + string = string.substring(0,4); + } + return string; + } + return text; + } + // Go through nodes, updating properties + for (var i=0;i