New sample showing usage of spring-flo to visualize Spring Integration apps

This commit is contained in:
Andy Clement
2016-10-14 10:01:12 -07:00
parent 38b3d8f5a3
commit 5b15f2c84c
22 changed files with 2985 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

115
samples/spring-flo-si/pom.xml Executable file
View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>spring-flo-sample-si</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-flo-sample</name>
<description>Spring Flo Sample</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<!-- TODO maybe adjust flos dependencies so this configuration isn't so complicated -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>codemirror</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>angular</artifactId>
<version>1.3.8</version> <!-- flo wants 1.3.5 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jshint</artifactId>
<version>2.8.0</version> <!-- flo wants 2.6.3 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>requirejs</artifactId>
<version>2.1.18</version> <!-- flo wants 2.1.15 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jquery</artifactId>
<version>2.2.0</version> <!-- jointjs wants 2.0.3 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>lodash</artifactId>
<version>3.10.1</version> <!-- jointjs -> graphlib -> wants 3.10.1-amd -->
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>0.31</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>requirejs-domready</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>requirejs-text</artifactId>
<version>2.0.15</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jointjs</artifactId>
<version>0.9.7</version> <!-- flo wants 0.9.6 -->
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>json5</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>spring-flo</artifactId>
<version>0.5.0</version>
<exclusions>
<exclusion>
<groupId>org.webjars.bower</groupId>
<artifactId>joint</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>org.springframework.flo.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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");
// }
// };
// }
}

View File

@@ -0,0 +1 @@
server.port=8082

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="collapse-handle" viewBox="0 0 24 24">
<line x1="4" y1="4" x2="20" y2="20" stroke="black" stroke-width="8" stroke-linecap="round"/>
<line x1="20" y1="4" x2="4" y2="20" stroke="black" stroke-width="8" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="27.963px" height="27.963px" viewBox="0 0 27.963 27.963" style="enable-background:new 0 0 27.963 27.963;"
xml:space="preserve">
<g>
<circle cx="13.9815" cy="13.9815" r="12.9815" stroke="black" stroke-width="1" fill="red" />
<polygon style="fill:white;" points="15.578,17.158 16.19,4.579 11.803,4.579 12.413,17.158 "/>
<path style="fill:white;" d="M13.997,18.546c-1.471,0-2.5,1.029-2.5,2.526c0,1.443,0.999,2.528,2.444,2.528h0.056
c1.499,0,2.469-1.085,2.469-2.528C16.44,19.575,15.467,18.546,13.997,18.546z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="1000px" height="1000px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<g>
<path d="M139.2,264.6c-47.5,71.7-72.7,155.2-72.7,241.5c0,59,11.6,116.3,34.4,170.3c22,52.1,53.6,98.9,93.7,139
c40.1,40.1,86.9,71.7,139,93.7c54,22.8,111.3,34.4,170.3,34.4s116.3-11.6,170.3-34.4c52.1-22,98.9-53.6,139-93.7
c40.1-40.1,71.7-86.9,93.7-139c22.8-54,34.4-111.3,34.4-170.3c0-50.4-8.5-99.8-25.3-146.9c-16.2-45.5-39.8-87.8-70.1-125.7
c-30-37.5-65.6-69.7-106-95.6c-41.1-26.4-86-45.6-133.4-57l-39.8,165.3c57.3,13.8,109.3,46.9,146.3,93.4
c38.1,47.8,58.3,105.4,58.3,166.7c0,71.4-27.8,138.6-78.3,189.1c-50.5,50.5-117.7,78.3-189.1,78.3s-138.6-27.8-189.1-78.3
c-50.5-50.5-78.3-117.7-78.3-189.1c0-52.8,15.3-103.8,44.3-147.6c6.5-9.9,13.7-19.2,21.4-28.1L399.1,456L508,70.1l-406.4,0.1
l95.6,124C175.7,215.5,156.2,239,139.2,264.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,41 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Spring Integration Viewer</title>
<link href="http://fonts.googleapis.com/css?family=Open+Sans|Montserrat:400,700" rel="stylesheet" type="text/css" />
<link href="http://fonts.googleapis.com/css?family=Ubuntu+Mono|Montserrat:400,700" rel="stylesheet" type="text/css" />
<link href="http://fonts.googleapis.com/css?family=Roboto|Montserrat:400,700" rel="stylesheet" type="text/css" />
<link href="/webjars/spring-flo/dist/spring-flo.css" rel="stylesheet" />
<link href="css/flosi.css" rel="stylesheet" />
<script data-main="js/main" src="/webjars/requirejs/require.js"></script>
</head>
<body>
<div id="header" class="header">
Spring Integration Viewer
</div>
<div class="input-label-div" ng-controller="SiController">
Spring Integration Graph endpoint:
<input type="text" class="inputfield" name="endpoint" id="endpoint" ng-model="endpoint" ng-enter="load(endpoint)">
<input type="submit" class="button" value="Load" ng-click="load(endpoint)">
&nbsp;&nbsp;&nbsp;&nbsp;Link label path:
<input type="text" class="inputfield" name="labelpath" id="labelpath" ng-model="labelpath" ng-enter="updateLabelPath(labelpath)">
&nbsp;&nbsp;&nbsp;&nbsp;Refresh rate (ms):
<input type="text" class="inputfield" name="refreshrate" id="refreshrate" ng-model="refreshrate" ng-enter="updateRefreshRate(refreshrate)">
</div>
<div id="flo-container">
<flo-editor ng-cloak metamodel-service-name="SampleMetamodelService" render-service-name="SampleRenderService" editor-service-name="SampleEditorService" palette-size="170"
min-zoom="10" max-zoom="300" zoom-step="10" ng-init="canvasControls={zoom:true};flo.noPalette=true;">
<div id="controls" class="controls">
<button class="button" id="performLayout" ng-click="flo.performLayout(); flo.fitToPage();">Reset Layout</button>
<button class="button" id="readOnly" ng-click="flo.readOnlyCanvas(!flo.readOnlyCanvas())" ng-class="{off:!flo.readOnlyCanvas()}">Read-Only</button>
</div>
<div class="flow-definition-container">
<textarea ng-if="editor" dsl-editor="true" id="flow-definition" class="flow-definition"></textarea>
<textarea ng-if="!editor" id="flow-definition" class="flow-definition" placeholder="Enter stream definition..."
ng-model="definition.text" ng-keyup="flo.scheduleUpdateGraphRepresentation(); $event.stopPropagation();" ng-blur="flo.enableSyncing(true)" ng-focus="flo.enableSyncing(false)"></textarea>
</div>
</flo-editor>
</div>
</body>
</html>

View File

@@ -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
};
}];
});

View File

@@ -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;
});

View File

@@ -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;
};
});

View File

@@ -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']);
}
);
});

View File

@@ -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 }
}
]

View File

@@ -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
};
}];
});

View File

@@ -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:
'<g class="shape">'+
'<rect class="border"/>' +
'<path class="the_shape" d="M 0 10 H 100 A 8 10 0 0 1 100 30 H 0"/>'+
'<ellipse class="the_shape" cx="0" cy="20" rx="8" ry="10"/>'+
'<text class="label"/>'+
'<text class="label2"/>'+
'</g>' +
// '<text class="stream-label"/>'+
'<rect class="input-port" />'+
'<rect class="output-port"/>'+
'<circle class="tap-port"/>',
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:
'<g class="shape"><image class="image" /></g>'+
'<rect class="border-white"/>' +
'<rect class="border"/>' +
'<rect class="box"/>'+
'<text class="label"/>'+
'<text class="label2"></text>'+
'<rect class="input-port" />'+
'<rect class="error-port" />'+
'<rect class="output-port"/>'+
'<rect class="output-port-cover"/>',
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:
'<g class="shape"><image class="image" /></g>'+
'<rect class="border-white"/>' +
'<rect class="border"/>' +
'<rect class="box"/>'+
'<text class="label"/>'+
'<text class="label2"></text>'+
'<rect class="input-port" />'+
'<rect class="error-port" />'+
'<rect class="output-port"/>'+
'<rect class="output-port-cover"/>'+
'<g transform="scale(0.4)">'+
'<path class="blockSolid" d="M 43 20 l 6 6 l 6 -6 l -6 -6 l -6 6"/>'+
'<path class="blockEmpty" d="M 86 20 l 6 6 l 6 -6 l -6 -6 l -6 6"/>'+
'<path class="arrowGray" d="M 0 20 h 33 l 0 -5 l 7 5 l -7 5 l 0 -5"/>'+
'<path class="arrowBlack" d="M 52 20 h 26 l 0 -5 l 7 5 l -7 5 l 0 -5"/>'+
'</g>'
,
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
};
}];
});

View File

@@ -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;i<obj.length;i++) {
collapseOneLevel(prefix.length==0?key:prefix+'.'+i,obj[i],collector);
}
} else {
collector[prefix] = obj;
}
}
function collapse(obj, prefix) {
if (!prefix) {
prefix = '';
}
var retval = {};
collapseOneLevel(prefix,obj,retval);
console.log("collapsed = "+JSON.stringify(retval));
return retval;
}
var MAGNITUDE_NUMBERS = [ 1000000000, 1000000, 1000];
var MAGNITUDE_LITERALS = ['B', 'M', 'K'];
var rateLabel = function() {
var postFix, division, index = -1, fixed = 3;
do {
division = this.rate / MAGNITUDE_NUMBERS[++index];
} while (!Math.floor(division) && index < MAGNITUDE_NUMBERS.length);
if (index === MAGNITUDE_NUMBERS.length) {
postFix = '';
division = this.rate;
} else {
postFix = MAGNITUDE_LITERALS[index];
}
for (var decimal = 1; decimal <= 100 && Math.floor(division / decimal); decimal*=10) {
fixed--;
}
return division.toFixed(fixed) + postFix;
};
function animate(link,p) {
// console.log("moving label on "+link.id+" to "+p);
if (!link.label(1)) {
console.log("No label1 on this link??");
} else {
link.label(1,{position: p})
p+=0.025
if (p>0.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<nodes.length;i++) {
var node = nodes[i];
var stats = node.stats;
var props = collapse(node.stats,'stats');
var props2 = collapse(node.properties,'properties');
for (var attrname in props2) { props[attrname] = props2[attrname]; }
props.name = node.name;
props.id = node.nodeId;
var newNode = flo.createNode(getMetadata(node.componentType), props);
var nodeName = node.name;
var metadataName = newNode.attr('metadata').name;
if (metadataName === 'splitter' && nodeName.endsWith('.splitter')) {
nodeName = nodeName.substring(0,nodeName.length-'.splitter'.length);
} else if (metadataName === 'aggregator' && nodeName.endsWith('.aggregator')) {
nodeName = nodeName.substring(0,nodeName.length-'.aggregator'.length);
} else if (metadataName === 'service-activator' && nodeName.endsWith('serviceActivator')) {
nodeName = nodeName.substring(0,nodeName.length-'.serviceActivator'.length);
}
if (node.name.indexOf('ConsumerEndpointFactoryBean')!==-1) {
if (metadataName === 'router' && props['properties.expression']) {
nodeName = props['properties.expression']+'?';
} else if (metadataName != 'general') {
nodeName = metadataName;
}
}
newNode.attr('props/componentType',node.componentType);
// if (nodeName != node.componentType) {
// // Don't lose the componentType. For example the nodeName might end up as mailOut but
// // componentType is mail:outbound-channel-adapter
// }
newNode.attr('.label/text',nodeName);
nodesMap[node.nodeId] = newNode;
}
var links = integrationGraph.links;
for (var i=0;i<links.length;i++) {
var link = links[i];
var isErrorLink = false;
var fromPort = '.output-port';
var toName = nodesMap[link.to].attr('.label/text');
var fromName = nodesMap[link.from].attr('.label/text');
if (link.type == 'error') {
fromPort = '.error-port';
isErrorLink=true;
}
var jointLink = flo.createLink({'id': nodesMap[link.from].id,'selector': fromPort},
{'id': nodesMap[link.to].id, 'selector': '.input-port'});
if (isErrorLink) {
jointLink.attr('.connection/stroke','red');
} else {
if (nodes[link.from-1].stats && nodes[link.from-1].stats.hasOwnProperty('sendCount')) {
// jointLink.label(0, {
// position: 15,
// type: 'outgoing-rate',
// // rate: sourceRates.outgoingRate,
// attrs: {
// text: {
// transform: 'translate(0, -8)',
// //text: '{{rateLabel()}}',
// text: nodes[link.from-1].stats.sendCount,
// 'fill': 'black',
// 'stroke': 'none',
// 'font-size': '12'
// },
// rect: {
// display: 'none'
//// transform: 'translate(0, -5)',
//// stroke: 'black',
//// rx:1,ry:1,
//// 'border-width': '2px',
//// 'stroke-width': 1,
//// fill: '#00B0A7'
// }
// }
// });
// jointLink.label(1, {
// position: 0.5,
// type: 'message',
// // rate: sourceRates.outgoingRate,
// attrs: {
// text: {
// transform: 'translate(0, 0)',
// //text: '{{rateLabel()}}',
// text: ' ',
// 'fill': 'black',
// 'stroke': 'black',
// 'font-size': '2'
// },
// rect: {
// transform: 'translate(0, 0)',
// stroke: '#ffffff',
// rx:1,ry:1,
// 'border-width': '3px',
// 'stroke-width': 2,
// fill: '#ffffff'
// }
// }
// });
// console.log("Label 1 on link id "+jointLink.id+" = "+jointLink.label(1));
// setTimeout(function() {animate(this,0.0)}.bind(jointLink),1000);
// jointLink.transition('labels/1/position',1,{valueFunction: joint.util.interpolate.unit, timingFunction: joint.util.timing.bounce});
}
}
}
// var lines = trimmed.split('\n');
// for (var l=0;l<lines.length;l++) {
// var line = lines[l];
// var elements = line.split('>');
// var lastNode = null;
// for (var e=0;e<elements.length;e++) {
// var element = elements[e].trim();
// // Has properties?
// var startOfProps = element.indexOf(' ');
// var name = element;
// var properties = {};
// if (startOfProps !== -1) {
// name = element.substring(0,startOfProps);
// var propValues = element.substring(startOfProps+1).trim().split(' ');
// for (var p=0;p<propValues.length;p++) {
// var propValue = propValues[p].trim();
// if (propValue.length===0) {
// // allows for multiple spaces between options
// continue;
// }
// var equalsIndex = propValue.indexOf('=');
// // The 2 skips the '--'
// var key = propValue.substring(2,equalsIndex);
// var value = propValue.substring(equalsIndex+1);
// properties[key] = value;
// }
// }
// var group = metamodelUtils.matchGroup(metamodel, name, 1, 1);
// var newNode = flo.createNode(metamodelUtils.getMetadata(metamodel,name,group),properties);
// newNode.attr('.label/text',name);
// if (lastNode) {
// flo.createLink({'id': lastNode.id,'selector': '.output-port'},
// {'id': newNode.id,'selector': '.input-port'});
// }
// lastNode = newNode;
// }
// }
};
});

View File

@@ -0,0 +1,232 @@
/*
* 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.
*/
/**
* Update the labels on links on the graph based on a fresh copy of the graph data.
* This assumes no nodes have changed, no links have changed - purely the counter
* stats on those elements.
*
* @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;i<obj.length;i++) {
collapseOneLevel(prefix.length==0?key:prefix+'.'+i,obj[i],collector);
}
} else {
collector[prefix] = obj;
}
}
function collapse(obj, prefix) {
if (!prefix) {
prefix = '';
}
var retval = {};
collapseOneLevel(prefix,obj,retval);
// console.log("collapsed = "+JSON.stringify(retval));
return retval;
}
var MAGNITUDE_NUMBERS = [ 1000000000, 1000000, 1000];
var MAGNITUDE_LITERALS = ['B', 'M', 'K'];
var rateLabel = function() {
var postFix, division, index = -1, fixed = 3;
do {
division = this.rate / MAGNITUDE_NUMBERS[++index];
} while (!Math.floor(division) && index < MAGNITUDE_NUMBERS.length);
if (index === MAGNITUDE_NUMBERS.length) {
postFix = '';
division = this.rate;
} else {
postFix = MAGNITUDE_LITERALS[index];
}
for (var decimal = 1; decimal <= 100 && Math.floor(division / decimal); decimal*=10) {
fixed--;
}
return division.toFixed(fixed) + postFix;
};
function animate(link,p) {
// console.log("moving label on "+link.id+" to "+p);
if (!link.label(1)) {
console.log("No label1 on this link??");
} else {
var label = link.label(1);
if (label.timer) {
clearTimeout(label.timer);
}
link.label(1,{position: p})
p+=0.06
if (p<1) {
label.timer = setTimeout(function() {animate(link,p)},15);
} else {
link.label(1, {
position: 0.0,
type: 'blip',
// rate: sourceRates.outgoingRate,
attrs: {
text: {
transform: 'translate(0, 0)',
//text: '{{rateLabel()}}',
text: '',
'fill': 'black',
'stroke': 'black',
'font-size': '4'
},
rect: {
transform: 'translate(0, 0)',
stroke: '#00ffff',
rx:1,ry:1,
'border-width': '3px',
'stroke-width': 4,
fill: '#00ffff'
}
}
});
// link.label(1,{text:{text:''}});
// label.attr('rect/display','none');
// label.attr('text/display','none');
// label.attrs.text.display = 'none';
// label.attrs.rect.display = 'none';
}
}
}
return function(input, graph, labelpath) { //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 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<nodes.length;i++) {
var inputNode = nodes[i];
var props = collapse(inputNode.stats,'stats');
var props2 = collapse(inputNode.properties,'properties');
for (var attrname in props2) { props[attrname] = props2[attrname]; }
map[inputNode.nodeId].attr('props',props);
}
for (var i=0;i<linksToVisit.length;i++) {
var link = linksToVisit[i];
var sourceId = link.get('source').id;
var sourceElement = graph.getCell(sourceId);
var rate;
var props = sourceElement.attr('props');
Object.keys(props).forEach(function(key) {
if (key.toLowerCase() === labelpath.toLowerCase()) {
rate = props[key];
}
});
var existingLabel = link.label(0);
var existingValue;
var animateLink = false;
if (existingLabel) {
existingValue = existingLabel.attrs.text.text;
if (existingValue !== toLabel(rate)) {
animateLink = true;
}
}
if (rate) {
link.label(0, {
position: 15,
type: 'outgoing-rate',
attrs: {
text: { transform: 'translate(0, -8)', text: toLabel(rate), 'fill': 'black', 'stroke': 'none', 'font-size': '12' },
rect: {
display: 'none'
// transform: 'translate(0, -5)',
// stroke: 'black',
// rx:1,ry:1,
// 'border-width': '2px',
// 'stroke-width': 1,
// fill: '#00B0A7'
}
}
});
if (animateLink) {
link.label(1, {
position: 0.0,
type: 'blip',
// rate: sourceRates.outgoingRate,
attrs: {
text: {
transform: 'translate(0, 0)',
//text: '{{rateLabel()}}',
text: ' ',
'fill': '#00ffff',
'stroke': 'black',
'font-size': '10'
},
rect: {
transform: 'translate(0, 0)',
// stroke: '#00B0A7',
'stroke': 'black',
rx:2,ry:2,
'border-width': 2,
'stroke-width': 3,
// fill: '#00B0A7'
'fill': 'black'
}
}
});
link.label(1).timer = setTimeout(function() {animate(this,0.0)}.bind(link),0);
}
}
}
};
});