first commit
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
node/
|
||||
bower_components/
|
||||
node_modules/
|
||||
lib/
|
||||
target/
|
||||
.idea/
|
||||
.project
|
||||
.DS_Store
|
||||
spring-flo.iml
|
||||
30
.jshintrc
Normal file
30
.jshintrc
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"esnext": true,
|
||||
"bitwise": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"indent": 2,
|
||||
"latedef": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
"regexp": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"strict": true,
|
||||
"trailing": true,
|
||||
"smarttabs": true,
|
||||
"globals": {
|
||||
"angular": false,
|
||||
"requirejs": true,
|
||||
"define": true,
|
||||
"XRegExp": true,
|
||||
"moment": true,
|
||||
"$": true,
|
||||
"_": true
|
||||
}
|
||||
}
|
||||
44
CODE_OF_CONDUCT.adoc
Normal file
44
CODE_OF_CONDUCT.adoc
Normal file
@@ -0,0 +1,44 @@
|
||||
= Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project, and in the interest of fostering an open
|
||||
and welcoming community, we pledge to respect all people who contribute through reporting
|
||||
issues, posting feature requests, updating documentation, submitting pull requests or
|
||||
patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project a harassment-free experience for
|
||||
everyone, regardless of level of experience, gender, gender identity and expression,
|
||||
sexual orientation, disability, personal appearance, body size, race, ethnicity, age,
|
||||
religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery
|
||||
* Personal attacks
|
||||
* Trolling or insulting/derogatory comments
|
||||
* Public or private harassment
|
||||
* Publishing other's private information, such as physical or electronic addresses,
|
||||
without explicit permission
|
||||
* Other unethical or unprofessional conduct
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments,
|
||||
commits, code, wiki edits, issues, and other contributions that are not aligned to this
|
||||
Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors
|
||||
that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
By adopting this Code of Conduct, project maintainers commit themselves to fairly and
|
||||
consistently applying these principles to every aspect of managing this project. Project
|
||||
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
|
||||
from the project team.
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an
|
||||
individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
|
||||
contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will
|
||||
be reviewed and investigated and will result in a response that is deemed necessary and
|
||||
appropriate to the circumstances. Maintainers are obligated to maintain confidentiality
|
||||
with regard to the reporter of an incident.
|
||||
|
||||
This Code of Conduct is adapted from the
|
||||
http://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at
|
||||
http://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]
|
||||
32
CONTRIBUTING.adoc
Normal file
32
CONTRIBUTING.adoc
Normal file
@@ -0,0 +1,32 @@
|
||||
= Contributing to Spring Flo
|
||||
|
||||
Spring Flo is released under the Apache 2.0 license. If you would like to contribute
|
||||
something, or simply want to hack on the code this document should help you get started.
|
||||
|
||||
|
||||
|
||||
== Code of Conduct
|
||||
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of
|
||||
conduct]. By participating, you are expected to uphold this code. Please report
|
||||
unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
|
||||
|
||||
|
||||
== Using GitHub issues
|
||||
We use GitHub issues to track bugs and enhancements - or you can also raise an issue simply
|
||||
to ask questions.
|
||||
|
||||
If you are reporting a bug, please help to speed up problem diagnosis by providing as much
|
||||
information as possible.
|
||||
|
||||
|
||||
|
||||
== Sign the Contributor License Agreement
|
||||
Before we accept a non-trivial patch or pull request we will need you to sign the
|
||||
https://support.springsource.com/spring_committer_signup[contributor's agreement].
|
||||
Signing the contributor's agreement does not grant anyone commit rights to the main
|
||||
repository, but it does mean that we can accept your contributions, and you will get an
|
||||
author credit if we do. Active contributors might be asked to join the core team, and
|
||||
given the ability to merge pull requests. Use '`Andy Clement`' in the
|
||||
project lead field when you complete the form.
|
||||
|
||||
202
LICENSE.txt
Normal file
202
LICENSE.txt
Normal 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.
|
||||
36
README.adoc
Normal file
36
README.adoc
Normal file
@@ -0,0 +1,36 @@
|
||||
# Spring Flo
|
||||
|
||||
Spring Flo is a JavaScript library that offers a basic embeddable HTML5 visual builder for pipelines and simple graphs. This library is used as the basis of the stream builder in Spring Cloud Data Flow.
|
||||
|
||||
image::docs/Flo.png[width="700", link="http://cloud.spring.io/spring-cloud-dataflow/"]
|
||||
|
||||
Here is a https://www.youtube.com/watch?v=78CgV46OstI[youtube video] of Spring Flo in action.
|
||||
|
||||
## Consuming Spring Flo
|
||||
|
||||
Refer to the https://github.com/spring-projects/spring-flo/wiki[wiki] for more information on how to embed it in an application.
|
||||
|
||||
## Build
|
||||
|
||||
Spring Flo is built using Grunt. Grunt commands can be run directly or indirectly through maven. Simplest way to build is via `mvn clean package` to build the project and run the tests. Build results are produced under the __dist__ folder. Contents of the folder are
|
||||
|
||||
* __flo.js__ that contains all JS code from various JS dev modules (produced by development build)
|
||||
* __flo.min.js__ a minified version of __flo.js__ (produced by production build)
|
||||
* __flo.css__ concatenated CSS file for Flo Editor
|
||||
* __flo.min.css__ minified version of the CSS above
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you have any questions, issues, feedback or feature request please https://github.com/spring-projects/spring-flo/issues[raise an issue].
|
||||
|
||||
## Samples
|
||||
|
||||
A small self contained sample usage of Spring Flo is coming soon. The https://github.com/spring-cloud/spring-cloud-dataflow-ui[Spring Cloud Data Flow UI] at github shows a larger scale usage of Spring Flo.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome, but before accepting them we will need you to sign the https://support.springsource.com/spring_committer_signup[Contributors Agreement].
|
||||
|
||||
## License
|
||||
|
||||
Spring Flo is Open Source software released under the http://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license].
|
||||
54
bower.json
Normal file
54
bower.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "spring-flo",
|
||||
"description": "Angular based embeddable graphical component for pipeline/graph editing",
|
||||
"main": [
|
||||
"dist/spring-flo.css",
|
||||
"dist/spring-flo.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"requirejs": "2.1.15",
|
||||
"angular": "1.3.5",
|
||||
"jquery": "2.2.0",
|
||||
"bootstrap": "3.3.4",
|
||||
"backbone": "1.2.1",
|
||||
"joint": "0.9.6",
|
||||
"lodash": "2.4.2",
|
||||
"codemirror": "5.0.0",
|
||||
"jshint": "2.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "1.3.5",
|
||||
"angular-scenario": "1.3.5",
|
||||
"dagre": "0.7.4",
|
||||
"requirejs-text": "2.0.14"
|
||||
},
|
||||
"resolutions": {
|
||||
"lodash": "2.4.2",
|
||||
"jquery": "2.2.0"
|
||||
},
|
||||
"authors": [
|
||||
"Andy Clement <aclement@pivotal.io>",
|
||||
"Alex Boyko <aboyko@pivotal.io"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/spring-projects/spring-flo.git"
|
||||
},
|
||||
"homepage": "https://github.com/spring-projects/spring-flo",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"tests",
|
||||
"karma.conf.js",
|
||||
"CODE_OF_CONDUCT.adoc",
|
||||
"CONTRIBUTING.adoc",
|
||||
"package.json",
|
||||
"pom.xml",
|
||||
"docs",
|
||||
"bower_components",
|
||||
"gruntfile.js",
|
||||
"css",
|
||||
"src"
|
||||
]
|
||||
}
|
||||
366
css/flo.css
Executable file
366
css/flo.css
Executable file
@@ -0,0 +1,366 @@
|
||||
.flo-view {
|
||||
width:100%;
|
||||
height:100%;
|
||||
margin: 0;
|
||||
background-color: #eeeeee;
|
||||
font-family: "Varela Round",sans-serif;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
border: 1px solid;
|
||||
border-color: #6db33f;
|
||||
border-radius: 2px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* Canvas contains the palette on the left and the paper on the right */
|
||||
|
||||
.paper {
|
||||
padding: 0px;
|
||||
background-color: #ffffff;
|
||||
/* height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
*//* margin-left: 400px; */
|
||||
}
|
||||
|
||||
#sidebar-resizer {
|
||||
background-color: #34302d;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
#palette-container {
|
||||
background-color: #EEE;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#paper-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
/* Joint JS paper for drawing palette -> canvas DnD visual feedback START */
|
||||
|
||||
#palette-floater {
|
||||
/* TODO size relative to paper that goes on it? */
|
||||
width:170px;
|
||||
height:60px;
|
||||
opacity: 0.75;
|
||||
/*
|
||||
background-color: #6db33f;
|
||||
*/
|
||||
float:left;
|
||||
position: absolute;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Joint JS paper for drawing palette -> canvas DnD visual feedback END */
|
||||
|
||||
/* Palette START */
|
||||
|
||||
.palette-filter {
|
||||
border: 3px solid #6db33f;
|
||||
}
|
||||
|
||||
.palette-filter-textfield {
|
||||
width: 100%;
|
||||
font-size:24px;
|
||||
/* border: 3px solid #6db33f;
|
||||
*/ font-family: "Varela Round",sans-serif;
|
||||
/* padding: 2px; */
|
||||
}
|
||||
|
||||
.palette-paper {
|
||||
background-color: #eeeeee;
|
||||
/*
|
||||
border-right: 7px solid;
|
||||
*/
|
||||
border-color: #6db33f;
|
||||
/* width: 170px;
|
||||
height:100%;
|
||||
float: left;
|
||||
*/
|
||||
}
|
||||
|
||||
/* Palette END */
|
||||
|
||||
/* Tooltip START */
|
||||
|
||||
.node-tooltip .tooltip-description {
|
||||
margin-top: 5px;
|
||||
margin-left: 0px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.node-tooltip {
|
||||
display:none;
|
||||
position:absolute;
|
||||
border:1px solid #333;
|
||||
background-color:#34302d;/*#161616;*/
|
||||
border-radius:5px;
|
||||
padding:5px;
|
||||
color:#fff;
|
||||
/* font-size:12px Arial;*/
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 19px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tooltip-title-type {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tooltip-title-group {
|
||||
padding-left: 5px;
|
||||
font-size: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.node-tooltip-option-name {
|
||||
font-family: monospace;/*"Varela Round",sans-serif;*/
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
padding-right: 20px;
|
||||
|
||||
}
|
||||
|
||||
.node-tooltip-option-description {
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Tooltip END */
|
||||
|
||||
/* Properties DIV Start */
|
||||
|
||||
.properties td {
|
||||
border-top: 1px solid #34302d;
|
||||
}
|
||||
|
||||
.properties {
|
||||
/* opacity: 0.5; */
|
||||
border: 8px #eeeeee;
|
||||
/* border-radius: 2px;*/
|
||||
/* border: 2px solid;
|
||||
*/ border-color: #6db33f;
|
||||
margin-top: 3px;
|
||||
background-color: #eeeeee;
|
||||
/* height: 115px;
|
||||
*/ font-family: monospace;
|
||||
z-index: 2;
|
||||
/* padding-top:1px; */
|
||||
position: absolute;
|
||||
/* left: 850px;
|
||||
top: 386px;
|
||||
width:360px;
|
||||
height:0px;
|
||||
overflow-y:auto;
|
||||
*/}
|
||||
.properties-node-name {
|
||||
|
||||
width: 100%;
|
||||
/* background-color: #eeeeee; */
|
||||
background: #34302d;
|
||||
color: #ffffff;
|
||||
padding-left:2px;
|
||||
border:0px;
|
||||
font-size: 18px;
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
.properties-node-name-row {
|
||||
/*
|
||||
background: #34302d;
|
||||
color: #ffffff;
|
||||
*/background: #34302d;
|
||||
width: 100%;
|
||||
padding-left:2px;
|
||||
}
|
||||
|
||||
.properties-row-even {
|
||||
width: 100%;
|
||||
border-top: 1px #34302d;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
.properties-row-odd {
|
||||
width: 100%;
|
||||
border-top: 1px #34302d;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.properties-row-text-even {
|
||||
background-color: #ffffff;
|
||||
border-left:0px;
|
||||
border-right:0px;
|
||||
border-bottom:0px;
|
||||
border-top:1px #34302d;
|
||||
}
|
||||
|
||||
.properties-row-text-odd {
|
||||
background-color: #eeeeee;
|
||||
border-left:0px;
|
||||
border-right:0px;
|
||||
border-bottom:0px;
|
||||
border-top:1px #34302d;
|
||||
}
|
||||
|
||||
.properties-input {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
font-family: "Varela Round",sans-serif;
|
||||
}
|
||||
|
||||
.properties-key {
|
||||
width: 30%;
|
||||
padding-left:2px;
|
||||
padding-right:4px;
|
||||
}
|
||||
|
||||
.properties-value {
|
||||
width: 70%;
|
||||
padding-left:2px;
|
||||
padding-right:2px;
|
||||
}
|
||||
|
||||
.properties-table {
|
||||
border: 1px solid #d1d1d1;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.properties-new-property {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
/* Properties DIV END */
|
||||
|
||||
/* Validation Error Marker on Canvas START */
|
||||
|
||||
.error-tooltip p {
|
||||
margin-top: 5px;
|
||||
margin-left: 0px;
|
||||
margin-bottom: 5px;
|
||||
color:#fff;
|
||||
}
|
||||
.error-tooltip {
|
||||
display:none;
|
||||
position:absolute;
|
||||
border:1px solid #333;
|
||||
background-color:red;/*#161616;*/
|
||||
border-radius:5px;
|
||||
padding:5px;
|
||||
color:#fff;
|
||||
/* font-size:12px Arial;*/
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Validation Error Marker on Canvas END */
|
||||
|
||||
/* Controls on Canvas START */
|
||||
|
||||
.canvas-controls-container {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.canvas-control {
|
||||
background: transparent;
|
||||
font-family: "Varela Round",sans-serif;
|
||||
font-size: 11px;
|
||||
vertical-align: middle;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.zoom-canvas-control {
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.zoom-canvas-input {
|
||||
text-align: right;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.zoom-canvas-label {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
/* Controls on Canvas END */
|
||||
|
||||
/* Code Mirror related styles START */
|
||||
|
||||
.CodeMirror {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
}
|
||||
.CodeMirror-hint {
|
||||
max-width: 38em;
|
||||
}
|
||||
|
||||
/* Code Mirror related styles END */
|
||||
|
||||
|
||||
/* START - FLO CANVAS STYLES - override joint js styles */
|
||||
|
||||
.highlighted {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.element.highlighted rect {
|
||||
stroke: #34302d;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.available-magnet {
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.link {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.link-tools .tool-options {
|
||||
display: none; /* by default, we don't display link options tool */
|
||||
}
|
||||
|
||||
/* END - FLO CANVAS STYLES */
|
||||
|
||||
1041
dist/spring-flo.css
vendored
Normal file
1041
dist/spring-flo.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
28699
dist/spring-flo.js
vendored
Normal file
28699
dist/spring-flo.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
dist/spring-flo.min.css
vendored
Normal file
7
dist/spring-flo.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
102
dist/spring-flo.min.js
vendored
Normal file
102
dist/spring-flo.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
docs/Flo.png
Normal file
BIN
docs/Flo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
150
gruntfile.js
Normal file
150
gruntfile.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = function(grunt) {
|
||||
|
||||
// Load grunt tasks automatically
|
||||
require('load-grunt-tasks')(grunt);
|
||||
|
||||
// Time how long tasks take. Can help when optimizing build times
|
||||
require('time-grunt')(grunt);
|
||||
|
||||
var LIB_DIR = './lib';
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
|
||||
// Test settings
|
||||
karma: {
|
||||
options: {
|
||||
browsers: ['PhantomJS'],
|
||||
singleRun: true
|
||||
},
|
||||
unit: {
|
||||
configFile: 'karma.conf.js'
|
||||
}
|
||||
},
|
||||
|
||||
requirejs: {
|
||||
// global config
|
||||
options: {
|
||||
baseUrl: './src',
|
||||
name: 'flo',
|
||||
exclude: [ 'joint', 'angular', 'jquery', 'bootstrap', 'underscore' ],
|
||||
insertRequire: [ 'flo' ],
|
||||
mainConfigFile: './src/main.js'
|
||||
},
|
||||
production: {
|
||||
// overwrites the default config above
|
||||
options: {
|
||||
out: './dist/spring-flo.min.js'
|
||||
}
|
||||
},
|
||||
development: {
|
||||
// overwrites the default config above
|
||||
options: {
|
||||
out: './dist/spring-flo.js',
|
||||
optimize: 'none' // no minification
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Empties folders to start fresh
|
||||
clean: {
|
||||
build: {
|
||||
files: [
|
||||
{
|
||||
dot: true,
|
||||
src: [ './dist']
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Make sure code styles are up to par and there are no obvious mistakes
|
||||
jshint: {
|
||||
options: {
|
||||
jshintrc: '.jshintrc',
|
||||
reporter: require('jshint-stylish')
|
||||
},
|
||||
all: [
|
||||
'Gruntfile.js',
|
||||
'./src/{,**/}*.js'
|
||||
],
|
||||
test: {
|
||||
options: {
|
||||
jshintrc: 'tests/.jshintrc'
|
||||
},
|
||||
src: ['./tests/spec/{,*/}*.js', './tests/resources/{,*/}*.js']
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
copy: {
|
||||
codemirror: {
|
||||
files: [
|
||||
{expand:true,
|
||||
src: ['codemirror/**'],
|
||||
cwd: 'bower_components',
|
||||
dest: LIB_DIR
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
concat: {
|
||||
css: {
|
||||
src: [
|
||||
'./bower_components/codemirror/lib/codemirror.css',
|
||||
'./bower_components/codemirror/lib/codemirror.css',
|
||||
'./bower_components/codemirror/addon/lint/lint.css',
|
||||
'./bower_components/codemirror/addon/hint/show-hint.css',
|
||||
'./lib/joint/joint.css',
|
||||
'./css/flo.css'
|
||||
],
|
||||
dest: 'dist/spring-flo.css'
|
||||
}
|
||||
},
|
||||
|
||||
cssmin: {
|
||||
css:{
|
||||
src: 'dist/spring-flo.css',
|
||||
dest: 'dist/spring-flo.min.css'
|
||||
}
|
||||
},
|
||||
|
||||
// Set bower task's targetDir to use src/main/resources/static/bower_components
|
||||
bower: {
|
||||
options: {
|
||||
targetDir: LIB_DIR,
|
||||
cleanTargetDir: true
|
||||
},
|
||||
// Provide install target
|
||||
install: {}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
grunt.registerTask('css', ['concat:css', 'cssmin:css']);
|
||||
grunt.registerTask('dev', ['clean:build', 'bower:install', 'copy:codemirror', 'requirejs:development', 'concat:css' ]);
|
||||
grunt.registerTask('prod', ['clean:build', 'bower:install', 'copy:codemirror', 'requirejs:production', 'css' ]);
|
||||
grunt.registerTask('build', ['clean:build', 'jshint', 'bower:install', 'copy:codemirror', 'requirejs', 'css' ]);
|
||||
grunt.registerTask('test', ['karma']);
|
||||
grunt.registerTask('default', ['build', 'test']);
|
||||
|
||||
};
|
||||
64
karma.conf.js
Normal file
64
karma.conf.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// Karma configuration
|
||||
// http://karma-runner.github.io/0.10/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
'use strict';
|
||||
config.set({
|
||||
// base path, that will be used to resolve files and exclude
|
||||
basePath: '',
|
||||
|
||||
// testing framework to use (jasmine/mocha/qunit/...)
|
||||
frameworks: ['jasmine', 'requirejs'],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
{pattern: 'src/**/*.js', included: false},
|
||||
{pattern: 'lib/**/*.js', included: false},
|
||||
{pattern: 'tests/spec/**/*.js', included: false},
|
||||
{pattern: 'dist/**/*.js', included: false},
|
||||
{pattern: 'tests/resources/**/*', included: false},
|
||||
'tests/karma-test-main.js'
|
||||
],
|
||||
|
||||
plugins: [
|
||||
'karma-junit-reporter',
|
||||
'karma-chrome-launcher',
|
||||
'karma-firefox-launcher',
|
||||
'karma-jasmine',
|
||||
'karma-phantomjs-launcher',
|
||||
'karma-requirejs',
|
||||
'karma-jasmine-html-reporter',
|
||||
],
|
||||
|
||||
// reporters: [
|
||||
// 'html'
|
||||
// ],
|
||||
|
||||
// list of files / patterns to exclude
|
||||
exclude: [],
|
||||
|
||||
// web server port
|
||||
port: 7070,
|
||||
|
||||
// level of logging
|
||||
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: true,
|
||||
|
||||
// Start these browsers, currently available:
|
||||
// - Chrome
|
||||
// - ChromeCanary
|
||||
// - Firefox
|
||||
// - Opera
|
||||
// - Safari (only Mac)
|
||||
// - PhantomJS
|
||||
// - IE (only Windows)
|
||||
browsers: ['Firefox', 'Chrome'],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, it capture browsers, run tests and exit
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
60
package.json
Normal file
60
package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "Spring-Flo",
|
||||
"version": "0.0.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/spring-projects/spring-flo.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bower": "1.3.12",
|
||||
"grunt": "0.4.5",
|
||||
"grunt-autoprefixer": "1.0.0",
|
||||
"grunt-bower-task": "0.4.0",
|
||||
"grunt-cli": "0.1.13",
|
||||
"grunt-concurrent": "1.0.0",
|
||||
"grunt-contrib-clean": "^0.6.0",
|
||||
"grunt-contrib-concat": "0.5.0",
|
||||
"grunt-contrib-connect": "0.9.0",
|
||||
"grunt-contrib-copy": "^0.7.0",
|
||||
"grunt-contrib-cssmin": "0.10.0",
|
||||
"grunt-contrib-htmlmin": "0.3.0",
|
||||
"grunt-contrib-imagemin": "0.9.2",
|
||||
"grunt-contrib-jshint": "0.11.2",
|
||||
"grunt-contrib-less": "0.12.0",
|
||||
"grunt-contrib-nodeunit": "0.4.1",
|
||||
"grunt-contrib-requirejs": "0.4.4",
|
||||
"grunt-contrib-uglify": "0.6.0",
|
||||
"grunt-contrib-watch": "0.6.1",
|
||||
"load-grunt-tasks": "3.1.0",
|
||||
"time-grunt": "1.1.1",
|
||||
"grunt-karma": "0.9.0",
|
||||
"grunt-newer": "0.8.0",
|
||||
"grunt-ngmin": "0.0.3",
|
||||
"grunt-protractor-runner": "^1.2.1",
|
||||
"grunt-rev": "0.1.0",
|
||||
"grunt-usemin": "2.6.2",
|
||||
"jasmine-reporters": "^2.0.5",
|
||||
"jshint-stylish": "1.0.1",
|
||||
"karma": "0.12.28",
|
||||
"karma-chrome-launcher": "0.1.6",
|
||||
"karma-coffee-preprocessor": "0.2.1",
|
||||
"karma-firefox-launcher": "0.1.3",
|
||||
"karma-html2js-preprocessor": "0.1.0",
|
||||
"karma-jasmine": "0.3.5",
|
||||
"karma-jasmine-html-reporter": "^0.1.6",
|
||||
"karma-junit-reporter": "^0.2.2",
|
||||
"karma-ng-html2js-preprocessor": "0.1.2",
|
||||
"karma-phantomjs-launcher": "0.1.4",
|
||||
"karma-requirejs": "0.2.2",
|
||||
"karma-safari-launcher": "0.1.1",
|
||||
"karma-script-launcher": "0.1.0",
|
||||
"load-grunt-tasks": "1.0.0",
|
||||
"phantomjs": "1.9.13",
|
||||
"protractor": "1.6.0",
|
||||
"requirejs": "2.1.15",
|
||||
"time-grunt": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
}
|
||||
103
pom.xml
Normal file
103
pom.xml
Normal file
@@ -0,0 +1,103 @@
|
||||
<?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.flo</groupId>
|
||||
<artifactId>spring-flo</artifactId>
|
||||
<version>0.0.1.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Spring Flo</name>
|
||||
|
||||
<prerequisites>
|
||||
<maven>3.1.0</maven>
|
||||
</prerequisites>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>0.0.23</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>install node and npm</id>
|
||||
<goals>
|
||||
<goal>install-node-and-npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<nodeVersion>v0.12.2</nodeVersion>
|
||||
<npmVersion>2.9.1</npmVersion>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm install</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>install</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>grunt build</id>
|
||||
<goals>
|
||||
<goal>grunt</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>--no-color</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-clean-plugin</artifactId>
|
||||
<version>2.6.1</version>
|
||||
<configuration>
|
||||
<filesets>
|
||||
<fileset>
|
||||
<directory>node_modules</directory>
|
||||
<followSymlinks>false</followSymlinks>
|
||||
</fileset>
|
||||
<fileset>
|
||||
<directory>node</directory>
|
||||
<followSymlinks>false</followSymlinks>
|
||||
</fileset>
|
||||
<fileset>
|
||||
<directory>node_modules</directory>
|
||||
<followSymlinks>false</followSymlinks>
|
||||
</fileset>
|
||||
<fileset>
|
||||
<directory>bower_components</directory>
|
||||
<followSymlinks>false</followSymlinks>
|
||||
</fileset>
|
||||
<fileset>
|
||||
<directory>.bower_cache</directory>
|
||||
<followSymlinks>false</followSymlinks>
|
||||
</fileset>
|
||||
<fileset>
|
||||
<directory>flo/lib</directory>
|
||||
<followSymlinks>false</followSymlinks>
|
||||
</fileset>
|
||||
</filesets>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
143
src/code-editor.js
Normal file
143
src/code-editor.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Code-editor directive. Connects Code-Mirror editor to angular.
|
||||
* Supports passing language and code text via 2-way scope binding variables or via DOM node attributes.
|
||||
* There is support for text transformation functions, i.e. encode/decode text before modifying the value
|
||||
*
|
||||
* Note: instead of language and code text it's likely the code will be changed to have getters and setters
|
||||
* for the code text which will avoid having encode and decode functions.
|
||||
*
|
||||
* This editor is typically used for properties where the property value is a script/code like value and
|
||||
* benefit from the use of a real editor that can provide features like syntax highlighting and mark
|
||||
* errors/warnings.
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
return ['$scope', function ($scope) {
|
||||
|
||||
var angular = require('angular');
|
||||
var CodeMirror = require('codemirror');
|
||||
|
||||
require('codemirror/mode/meta');
|
||||
require('codemirror/addon/lint/lint');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
require('codemirror/addon/mode/loadmode');
|
||||
require('codemirror/addon/edit/matchbrackets');
|
||||
require('codemirror/addon/edit/closebrackets');
|
||||
|
||||
// languages
|
||||
require('codemirror/mode/groovy/groovy');
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/python/python');
|
||||
require('codemirror/mode/ruby/ruby');
|
||||
require('codemirror/mode/clike/clike');
|
||||
|
||||
// Lint support
|
||||
require('jshint');
|
||||
require('codemirror/addon/lint/javascript-lint');
|
||||
|
||||
CodeMirror.modeURL = 'codemirror/mode/%N/%N';
|
||||
|
||||
var LINT_MAP = {
|
||||
'javascript': true
|
||||
};
|
||||
|
||||
var defaultText = '';
|
||||
var defaultLanguage = 'Plain Text';
|
||||
|
||||
$scope.init = function(textarea, attrs) {
|
||||
|
||||
var doc = CodeMirror.fromTextArea(textarea, {
|
||||
gutters: ['CodeMirror-lint-markers'],
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
matchBrackets: true,
|
||||
autoCloseBrackets: true
|
||||
});
|
||||
|
||||
function getLintOption(modeName) {
|
||||
var lint = LINT_MAP[modeName.toLowerCase()];
|
||||
return lint ? lint : false;
|
||||
}
|
||||
|
||||
function updateScopeText() {
|
||||
var text = doc.getValue();
|
||||
var result;
|
||||
var encodeFunc = $scope.encodeFunction();
|
||||
if (angular.isFunction(encodeFunc)) {
|
||||
result = encodeFunc.call(null, text);
|
||||
}
|
||||
if (typeof result === 'string') {
|
||||
text = result;
|
||||
}
|
||||
if (text === defaultText) {
|
||||
$scope.text = undefined;
|
||||
} else {
|
||||
$scope.text = text;
|
||||
}
|
||||
$scope.$apply();
|
||||
}
|
||||
|
||||
function updateMode() {
|
||||
var language = $scope.language && typeof $scope.language === 'string' ? $scope.language : defaultLanguage;
|
||||
if (language) {
|
||||
var info = CodeMirror.findModeByName(language);
|
||||
|
||||
// Set proper editor mode
|
||||
doc.setOption('mode', info.mime);
|
||||
CodeMirror.autoLoadMode(doc, info.mode);
|
||||
|
||||
// Set proper Lint mode
|
||||
doc.setOption('lint', getLintOption(info.name));
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.defaultText) {
|
||||
defaultText = attrs.defaultText;
|
||||
}
|
||||
|
||||
if (attrs.defaultLanguage) {
|
||||
defaultLanguage = attrs.defaultLanguage.toLowerCase();
|
||||
}
|
||||
|
||||
updateMode();
|
||||
|
||||
var text = $scope.text ? $scope.text : defaultText;
|
||||
var result;
|
||||
var decodeFunc = $scope.decodeFunction();
|
||||
if (angular.isFunction(decodeFunc)) {
|
||||
result = decodeFunc.call(this, text);
|
||||
}
|
||||
if (typeof result === 'string') {
|
||||
text = result;
|
||||
}
|
||||
doc.setValue(text);
|
||||
|
||||
doc.on('changes', function () {
|
||||
updateScopeText();
|
||||
});
|
||||
doc.on('blur', function () {
|
||||
updateScopeText();
|
||||
});
|
||||
$scope.$watch('language', function() {
|
||||
updateMode();
|
||||
});
|
||||
};
|
||||
|
||||
}];});
|
||||
169
src/dsl-editor.js
Normal file
169
src/dsl-editor.js
Normal file
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var angular = require('angular');
|
||||
|
||||
return ['$scope', '$http', '$injector', '$log', function ($scope, $http, $injector, $log) {
|
||||
|
||||
var CodeMirror = require('codemirror');
|
||||
var enableTextToGraphSyncing = false;
|
||||
|
||||
var doc;
|
||||
|
||||
require('codemirror/addon/lint/lint');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
|
||||
/**
|
||||
* Control graph-to-text syncing. When it is active the graph will be automatically
|
||||
* updated as the text is modified.
|
||||
*/
|
||||
function enableGraphToTextSyncing(enable) {
|
||||
$scope.flo.enableSyncing(enable);
|
||||
enableTextToGraphSyncing = !enable;
|
||||
}
|
||||
|
||||
//TODO: using a controller to setup codemirror is probably not the 'nice'
|
||||
// way to do that. (angular docs say that dom manipulations are not the job of a controller
|
||||
// so probably this should be a directive rather than a controller.
|
||||
|
||||
// A bit dirty, we store the callback for codemirror linter here.
|
||||
// that way we can update markers each time error objects are
|
||||
// changed.
|
||||
var updateLinting;
|
||||
|
||||
/**
|
||||
* If new parse errors are discovered, this will create markers against
|
||||
* the editor text for them and call code mirror to update those markers.
|
||||
*/
|
||||
function refreshMarkers() {
|
||||
var markers = [];
|
||||
var parseErrors = $scope.definition.parseError;
|
||||
if (parseErrors && parseErrors.length) {
|
||||
for (var i = 0; i < parseErrors.length; i++) {
|
||||
var parseError = parseErrors[i];
|
||||
if (parseError.message && parseError.range) {
|
||||
var range = parseError.range;
|
||||
markers.push({
|
||||
from: range.start,
|
||||
to: range.end,
|
||||
message: parseError.message.split(/\r?\n/)[0],
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
updateLinting(doc, markers);
|
||||
}
|
||||
|
||||
function isDelimiter(c) {
|
||||
return c && (/\s|\|/).test(c);
|
||||
}
|
||||
|
||||
function findLast(string, predicate, start) {
|
||||
var pos = start || string.length-1;
|
||||
while (pos>=0 && !predicate(string[pos])) {
|
||||
pos--;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* The suggestions provided by rest api are very long and include the whole command typed
|
||||
* from the start of the line. This function determines the start of the 'interesting' part
|
||||
* at the end of the prefix, so that we can use it to chop-off the suggestion there.
|
||||
*/
|
||||
function interestingPrefixStart(prefix, completions) {
|
||||
var cursor = prefix.length;
|
||||
if (completions.every(function (completion) { return isDelimiter(completion[cursor]);})) {
|
||||
return cursor;
|
||||
}
|
||||
return findLast(prefix, isDelimiter);
|
||||
}
|
||||
|
||||
function contentAssist(doc, callback) {
|
||||
var cursor = doc.getCursor();
|
||||
var startOfLine = {line: cursor.line, ch: 0};
|
||||
var prefix = doc.getRange(startOfLine, cursor);
|
||||
|
||||
if ($scope.contentAssistServiceName) {
|
||||
var caService = $injector.get($scope.contentAssistServiceName);
|
||||
if (caService && angular.isFunction(caService.getProposals)) {
|
||||
return caService.getProposals(prefix).then(function(completions) {
|
||||
var chopAt = interestingPrefixStart(prefix, completions);
|
||||
return callback({
|
||||
list: completions.map(function (longCompletion) {
|
||||
var text = typeof longCompletion === 'string' ? longCompletion : longCompletion.text;
|
||||
return text.substring(chopAt);
|
||||
}),
|
||||
from: {line: startOfLine.line, ch:chopAt},
|
||||
to: cursor
|
||||
});
|
||||
}, function(err) {
|
||||
$log.error('Cannot get content assist: ' + err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.init = function(textarea) {
|
||||
contentAssist.async = true;
|
||||
doc = CodeMirror.fromTextArea(textarea, {
|
||||
gutters: ['CodeMirror-lint-markers'],
|
||||
lint: {
|
||||
async: true,
|
||||
getAnnotations: function (text, updateFun) {
|
||||
if (!updateLinting) {
|
||||
updateLinting = updateFun;
|
||||
$scope.$watch('definition.parseError', refreshMarkers);
|
||||
}
|
||||
}
|
||||
},
|
||||
extraKeys: {'Ctrl-Space': 'autocomplete'},
|
||||
hintOptions: {
|
||||
async: 'true',
|
||||
hint: contentAssist
|
||||
},
|
||||
lineNumbers: true,
|
||||
lineWrapping: true
|
||||
});
|
||||
doc.on('change', function () {
|
||||
if (enableTextToGraphSyncing) {
|
||||
$scope.definition.text = doc.getValue();
|
||||
$scope.flo.scheduleUpdateGraphRepresentation();
|
||||
}
|
||||
});
|
||||
doc.on('focus', function () {
|
||||
enableGraphToTextSyncing(false);
|
||||
});
|
||||
doc.on('blur', function () {
|
||||
enableGraphToTextSyncing(true);
|
||||
});
|
||||
$scope.$watch('definition.text', function (newValue) {
|
||||
if (newValue!==doc.getValue()) {
|
||||
var cursorPosition = doc.getCursor();
|
||||
doc.setValue(newValue);
|
||||
doc.setCursor(cursorPosition);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
}];
|
||||
|
||||
});
|
||||
1649
src/editor-manager.js
Executable file
1649
src/editor-manager.js
Executable file
File diff suppressed because it is too large
Load Diff
60
src/event-manager.js
Normal file
60
src/event-manager.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
return function() {
|
||||
|
||||
// TODO remove this home grown eventing
|
||||
var triggerFns = {};
|
||||
|
||||
function on(triggerEvent, fn) {
|
||||
var triggers = triggerFns[triggerEvent];
|
||||
if (!triggers) {
|
||||
triggers = [];
|
||||
triggerFns[triggerEvent] = triggers;
|
||||
}
|
||||
triggers.push(fn);
|
||||
}
|
||||
|
||||
function off(triggerEvent, fn) {
|
||||
var triggers = triggerFns[triggerEvent];
|
||||
if (triggers) {
|
||||
var index = triggers.indexOf(fn);
|
||||
if (index >= 0) {
|
||||
triggers.splice(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fireEvent(event, paramsObject) {
|
||||
var triggers = triggerFns[event];
|
||||
if (triggers && Array.isArray(triggers)) {
|
||||
triggers.forEach(function(trigger) {
|
||||
trigger(paramsObject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
on: on,
|
||||
off: off,
|
||||
fireEvent: fireEvent
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
301
src/flo.js
Normal file
301
src/flo.js
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
require('angular');
|
||||
|
||||
var app = angular.module('spring.flo', []);
|
||||
|
||||
app.directive('dslEditor', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: true,
|
||||
controller: require('dsl-editor'),
|
||||
link: function(scope, element, attrs) {
|
||||
if (attrs.contentAssistServiceName) {
|
||||
scope.contentAssistServiceName = attrs.contentAssistServiceName;
|
||||
}
|
||||
scope.init(element.context);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.directive('codeEditor', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
controller: require('code-editor'),
|
||||
link: function(scope, element, attrs) {
|
||||
scope.init(element.context, attrs);
|
||||
},
|
||||
scope: {
|
||||
language: '=codeLanguage',
|
||||
text: '=codeText',
|
||||
decodeFunction: '&',
|
||||
encodeFunction: '&'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.directive('floPalette', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
link: function(scope, element, attrs) {
|
||||
if (attrs.metamodelServiceName) {
|
||||
scope.metamodelServiceName = attrs.metamodelServiceName;
|
||||
}
|
||||
scope.init(element.context, attrs);
|
||||
},
|
||||
controller: require('palette-manager'),
|
||||
controllerAs: 'floPalette',
|
||||
template:
|
||||
'<div id="palette-filter" class="palette-filter">' +
|
||||
'<input type="text" id="palette-filter-textfield" ng-model="filterText" ng-change="filterTextUpdated()" class="palette-filter-textfield" ng-model-options="{ debounce: 500 }"/>' +
|
||||
'</div>' +
|
||||
'<div id="palette-paper-container" style="height:calc(100% - 46px); width:100%; overflow:auto;">' +
|
||||
'<div id="palette-paper" class="palette-paper" style="overflow:hidden;"></div>' +
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
// Div for resizing the palette
|
||||
app.directive('resizer', function($document) {
|
||||
|
||||
return function($scope, $element, $attrs) {
|
||||
|
||||
function resize(size) {
|
||||
if ($attrs.resizer === 'vertical') {
|
||||
// Handle vertical resizer
|
||||
var x = size;
|
||||
|
||||
if ($attrs.resizerMax && x > $attrs.resizerMax) {
|
||||
x = parseInt($attrs.resizerMax);
|
||||
}
|
||||
|
||||
$element.css({
|
||||
left: x + 'px'
|
||||
});
|
||||
|
||||
$($attrs.resizerLeft).css({
|
||||
width: x + 'px'
|
||||
});
|
||||
$($attrs.resizerRight).css({
|
||||
left: (x + parseInt($attrs.resizerWidth)) + 'px'
|
||||
});
|
||||
} else {
|
||||
// Handle horizontal resizer
|
||||
var y = size;
|
||||
|
||||
$element.css({
|
||||
bottom: y + 'px'
|
||||
});
|
||||
|
||||
$($attrs.resizerTop).css({
|
||||
bottom: (y + parseInt($attrs.resizerHeight)) + 'px'
|
||||
});
|
||||
$($attrs.resizerBottom).css({
|
||||
height: y + 'px'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mousemove(event) {
|
||||
var size;
|
||||
if ($attrs.resizer === 'vertical') {
|
||||
// Handle vertical resizer. Calculate new size relative to palette container DOM node
|
||||
size = event.pageX - $($attrs.resizerLeft).offset().left;
|
||||
} else {
|
||||
// Handle horizontal resizer Calculate new size relative to palette container DOM node
|
||||
size = window.innerHeight - event.pageY - $($attrs.resizerTop).offset().top;
|
||||
}
|
||||
$scope.flo.paletteSize = size;
|
||||
// Apply changes to the scope such that various watchers can react to palette size change
|
||||
$scope.$apply();
|
||||
}
|
||||
|
||||
function mouseup() {
|
||||
$document.unbind('mousemove', mousemove);
|
||||
$document.unbind('mouseup', mouseup);
|
||||
}
|
||||
|
||||
$element.on('mousedown', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
$document.on('mousemove', mousemove);
|
||||
$document.on('mouseup', mouseup);
|
||||
});
|
||||
|
||||
// Adjust DOM node sizes based on palette size model value
|
||||
$scope.$watch(function() {
|
||||
return $scope.flo.paletteSize;
|
||||
}, function(newValue) {
|
||||
resize(newValue);
|
||||
});
|
||||
|
||||
if ($scope.flo.paletteSize) {
|
||||
resize($scope.flo.paletteSize);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.directive('floEditor', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, element, attrs) {
|
||||
if (attrs.metamodelServiceName) {
|
||||
scope.metamodelServiceName = attrs.metamodelServiceName;
|
||||
}
|
||||
|
||||
if (attrs.renderServiceName) {
|
||||
scope.renderServiceName = attrs.renderServiceName;
|
||||
}
|
||||
|
||||
if (attrs.editorServiceName) {
|
||||
scope.editorServiceName = attrs.editorServiceName;
|
||||
}
|
||||
|
||||
if (attrs.paletteSize) {
|
||||
scope.flo.paletteSize = Number(attrs.paletteSize);
|
||||
}
|
||||
|
||||
if (attrs.paperPadding) {
|
||||
scope.paperPadding = Number(attrs.paperPadding);
|
||||
}
|
||||
|
||||
scope.init(element.context, attrs);
|
||||
element.find('#paper').bind('keydown', function(e) {
|
||||
if (e.which === 8 || e.which === 46) {
|
||||
if (!scope.flo.readOnly() /*&& scope.flo.getSelection()*/) {
|
||||
scope.flo.deleteSelectedNode();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
controller: require('editor-manager'),
|
||||
controllerAs: 'floModel',
|
||||
transclude: true,
|
||||
template:
|
||||
'<ng-transclude></ng-transclude>' +
|
||||
'<div id="flow-view" class="flow-view" style="position:relative;">' +
|
||||
'<div id="canvas" class="canvas" style="position:relative; display: block; width: 100%; height: 100%;">' +
|
||||
'<div id="palette-container" class="palette-container" ng-if="!flo.noPalette" style="overflow:hidden;">' +
|
||||
'<flo-palette></flo-palette>' +
|
||||
'</div>' +
|
||||
'<div id="sidebar-resizer" ng-if="!flo.noPalette"' +
|
||||
'resizer="vertical"' +
|
||||
'resizer-width="6"' +
|
||||
'resizer-left="#palette-container"' +
|
||||
'resizer-right="#paper-container">' +
|
||||
'</div>' +
|
||||
|
||||
'<div id="paper-container">' +
|
||||
'<div id="paper" class="paper" tabindex="0" style="overflow: hidden; position: absolute; display: block; height:100%; width:100%; overflow:auto;"></div>' +
|
||||
'<span class="canvas-controls-container" ng-if="canvasControls">' +
|
||||
'<table ng-if="canvasControls.zoom" class="canvas-control zoom-canvas-control"><tbody><tr>' +
|
||||
'<td><input class="zoom-canvas-input canvas-control zoom-canvas-control" type="text" data-inline="true" ng-model="flo.zoomPercent" ng-model-options="{ getterSetter: true, updateOn: \'blur change\' }" size="3"></input>' +
|
||||
'<label class="canvas-control zoom-canvas-label">%</label></td>' +
|
||||
'<td><input type="range" data-inline="true" ng-model="flo.zoomPercent" ng-model-options="{ getterSetter: true }" step="{{flo.getZoomStep()}}" max="{{flo.getMaxZoom()}}" min="{{flo.getMinZoom()}}" data-type="range" name="range" class="canvas-control zoom-canvas-control"></input></td>' +
|
||||
'</tr></tbody></table>' +
|
||||
'</span>' +
|
||||
'<div id="properties" class="properties" style="position:absolute; bottom:0; width:100%; overflow:auto;"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="hook"></div>' +
|
||||
'<div id="dialogs"></div>' +
|
||||
'</div>'
|
||||
};
|
||||
});
|
||||
|
||||
app.factory('MetamodelUtils', ['$q', function($q) {
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Return the metadata for a particular palette entry in a particular group.
|
||||
* @param {String} name - name of the palette entry
|
||||
* @param {string} group - group in which the palette entry should exist (e.g. sinks)
|
||||
* @return {{name:string,group:string,unresolved:Boolean}}
|
||||
*/
|
||||
getMetadata: function(metamodel, name, group) {
|
||||
if (name && group && metamodel[group][name]) {
|
||||
return metamodel[group][name];
|
||||
} else {
|
||||
return {
|
||||
name: name,
|
||||
group: group,
|
||||
unresolved: true,
|
||||
get: function() {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve();
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
matchGroup: function(metamodel, type, incoming, outgoing) {
|
||||
incoming = typeof incoming === 'number' ? incoming : 0;
|
||||
outgoing = typeof outgoing === 'number' ? outgoing : 0;
|
||||
var matches = [];
|
||||
var i;
|
||||
if (type) {
|
||||
for (i in metamodel) {
|
||||
if (metamodel[i][type]) {
|
||||
matches.push(metamodel[i][type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
var group;
|
||||
var score = Number.MIN_VALUE;
|
||||
for (i = 0; i < matches.length; i++) {
|
||||
var constraints = matches[i].constraints;
|
||||
if (constraints) {
|
||||
var failedConstraintsNumber = 0;
|
||||
if (typeof constraints.maxOutgoingLinksNumber === 'number' && constraints.maxOutgoingLinksNumber < outgoing) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
if (typeof constraints.minOutgoingLinksNumber === 'number' && constraints.minOutgoingLinksNumber > outgoing) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
if (typeof constraints.maxIncomingLinksNumber === 'number' && constraints.maxIncomingLinksNumber < incoming) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
if (typeof constraints.minIncomingLinksNumber === 'number' && constraints.minIncomingLinksNumber > incoming) {
|
||||
failedConstraintsNumber++;
|
||||
}
|
||||
|
||||
if (failedConstraintsNumber === 0) {
|
||||
return matches[i].group;
|
||||
} else if (failedConstraintsNumber > score) {
|
||||
score = failedConstraintsNumber;
|
||||
group = matches[i].group;
|
||||
}
|
||||
} else {
|
||||
return matches[i].group;
|
||||
}
|
||||
}
|
||||
return group;
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
return app;
|
||||
|
||||
});
|
||||
58
src/main.js
Executable file
58
src/main.js
Executable file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
requirejs.config({
|
||||
paths: {
|
||||
joint: '../lib/joint/joint',
|
||||
backbone: '../lib/backbone/backbone',
|
||||
angular: '../lib/angular/angular',
|
||||
jquery: '../lib/jquery/jquery',
|
||||
bootstrap: '../lib/bootstrap/bootstrap',
|
||||
lodash: '../lib/lodash/lodash.compat',
|
||||
jshint: '../lib/jshint/dist/jshint'
|
||||
},
|
||||
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']
|
||||
},
|
||||
joint: {
|
||||
deps: ['jquery', 'underscore', 'backbone'],
|
||||
},
|
||||
underscore: {
|
||||
exports: '_'
|
||||
},
|
||||
jshint: {
|
||||
deps: ['lodash']
|
||||
}
|
||||
}
|
||||
});
|
||||
547
src/palette-manager.js
Executable file
547
src/palette-manager.js
Executable file
@@ -0,0 +1,547 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var $ = require('jquery');
|
||||
var joint = require('joint');
|
||||
var shapesFactory = require('shapes-factory');
|
||||
var angular = require('angular');
|
||||
|
||||
// TODO move this node into custom nodes and links to centralize
|
||||
if (!joint.shapes.flo) {
|
||||
joint.shapes.flo = {};
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/23960312/can-i-add-new-attributes-in-jointjs-element
|
||||
joint.shapes.flo.PaletteGroupHeader = joint.shapes.basic.Generic.extend({
|
||||
// The path is the open/close arrow, defaults to vertical (open)
|
||||
markup: '<g class="scalable"><rect/></g><text/><g class="rotatable"><path d="m 10 10 l 5 8.7 l 5 -8.7 z"/></g>',
|
||||
defaults: joint.util.deepSupplement({
|
||||
type: 'palette.groupheader',
|
||||
size:{width:170,height:30},
|
||||
position:{x:0,y:0},
|
||||
attrs: {
|
||||
'rect': { fill: '#34302d', 'stroke-width': 1, stroke: '#6db33f', 'follow-scale':true, width:80, height:40 },
|
||||
'text': {
|
||||
text:'',
|
||||
fill: '#eeeeee',
|
||||
'ref-x': 0.5,
|
||||
'ref-y': 7,
|
||||
'x-alignment':'middle',
|
||||
'font-size': 18/*, 'font-weight': 'bold', 'font-variant': 'small-caps', 'text-transform': 'capitalize'*/
|
||||
},
|
||||
'path': { fill: 'white', 'stroke-width': 2, stroke: 'white'/*,transform:'rotate(90,15,15)'*/}
|
||||
},
|
||||
// custom properties
|
||||
isOpen:true
|
||||
}, joint.shapes.basic.Generic.prototype.defaults)
|
||||
});
|
||||
|
||||
return ['$scope', '$timeout', '$log', '$injector', function($scope, $timeout, $log, $injector) {
|
||||
|
||||
var domContext;
|
||||
|
||||
var metamodelService;
|
||||
|
||||
var renderService;
|
||||
|
||||
var paletteGraph = new joint.dia.Graph();
|
||||
paletteGraph.attributes.type = joint.shapes.flo.PALETTE_TYPE;
|
||||
|
||||
var palette;
|
||||
|
||||
/**
|
||||
* The names of any groups in the palette that have been deliberately closed (the arrow clicked on)
|
||||
* @type {String[]}
|
||||
*/
|
||||
var closedGroups = [];
|
||||
|
||||
/**
|
||||
* Model of the clicked element
|
||||
*/
|
||||
var clickedElement;
|
||||
|
||||
var viewBeingDragged = null;
|
||||
|
||||
function trigger(triggerEvent,paramsObject) {
|
||||
if ($scope.paletteObservers) {
|
||||
$scope.paletteObservers.fireEvent(triggerEvent, paramsObject);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrag(event) {
|
||||
// TODO offsetX/Y not on firefox
|
||||
//$log.debug("tracking move: x="+event.pageX+",y="+event.pageY);
|
||||
if (clickedElement && clickedElement.attr('metadata')) {
|
||||
if (!viewBeingDragged) {
|
||||
var dataOfClickedElement = clickedElement.attr('metadata');
|
||||
// custom div if not already built.
|
||||
$('<div>', {
|
||||
id: 'palette-floater'
|
||||
}).appendTo($('body'));
|
||||
var floatergraph = new joint.dia.Graph();
|
||||
floatergraph.attributes.type = joint.shapes.flo.FEEDBACK_TYPE;
|
||||
var floaterpaper = new joint.dia.Paper({
|
||||
el: $('#palette-floater'),
|
||||
gridSize:10,
|
||||
model: floatergraph,
|
||||
height: 400,
|
||||
width: 200,
|
||||
validateMagnet: function() {
|
||||
return false;
|
||||
},
|
||||
validateConnection: function() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
// TODO float thing needs to be bigger otherwise icon label is missing
|
||||
// Initiative drag and drop - create draggable element
|
||||
var floaternode = shapesFactory.createNode({
|
||||
'renderService': renderService,
|
||||
'paper': floaterpaper,
|
||||
'graph': floatergraph,
|
||||
'metadata': dataOfClickedElement
|
||||
});
|
||||
var box = floaterpaper.findViewByModel(floaternode).getBBox();
|
||||
var size = floaternode.get('size');
|
||||
// Account for node real size including ports
|
||||
floaternode.translate(box.width - size.width, box.height - size.height);
|
||||
viewBeingDragged = floaterpaper.findViewByModel(floaternode);
|
||||
$('#palette-floater').offset({left:event.pageX+5,top:event.pageY+5});
|
||||
// trigger('dragStarted',{'dragged':viewBeingDragged,'x':x,'y':y});
|
||||
} else {
|
||||
$('#palette-floater').offset({left:event.pageX+5,top:event.pageY+5});
|
||||
trigger('drag',{'dragged':viewBeingDragged,'evt':event});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO lock to grid on drag? (if grid is on)
|
||||
|
||||
var ConstraintElementView = joint.dia.ElementView.extend({
|
||||
pointerdown: function(/*evt, x, y*/) {
|
||||
// Remove the tooltip
|
||||
$('.node-tooltip').remove();
|
||||
// TODO move metadata to the right place (not inside attrs I think)
|
||||
clickedElement = this.model;
|
||||
if (clickedElement.attr('metadata')) {
|
||||
$(document).on('mousemove', handleDrag);
|
||||
}
|
||||
},
|
||||
pointermove: function(/*evt, x, y*/) {
|
||||
// Nothing to prevent move within the palette canvas
|
||||
},
|
||||
events: {
|
||||
// Tooltips on the palette elements
|
||||
'mouseenter': function(evt) {
|
||||
|
||||
// Ignore 'mouseenter' if any other buttons are pressed
|
||||
if (evt.buttons) {
|
||||
return;
|
||||
}
|
||||
|
||||
var model = this.model;
|
||||
var metadata = model.attr('metadata');
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showTooltip(evt.pageX, evt.pageY);
|
||||
},
|
||||
// TODO bug here - if the call to get the info takes a while, the tooltip may appear after the pointer has left the cell
|
||||
'mouseleave': function(/*evt, x,y*/) {
|
||||
this.hideTooltip();
|
||||
},
|
||||
'mousemove': function(evt) {
|
||||
this.moveTooltip(evt.pageX, evt.pageY);
|
||||
}
|
||||
},
|
||||
|
||||
showTooltip: function(x, y) {
|
||||
var model = this.model;
|
||||
var metadata = model.attr('metadata');
|
||||
// TODO refactor to use tooltip module
|
||||
var nodeTooltip = document.createElement('div');
|
||||
$(nodeTooltip).addClass('node-tooltip');
|
||||
$(nodeTooltip).appendTo($('body')).fadeIn('fast');
|
||||
var nodeDescription = document.createElement('div');
|
||||
$(nodeTooltip).addClass('tooltip-description');
|
||||
$(nodeTooltip).append(nodeDescription);
|
||||
|
||||
metadata.get('description').then(function(description) {
|
||||
$(nodeDescription).text(description ? description : model.attr('metadata/name'));
|
||||
}, function() {
|
||||
$(nodeDescription).text(model.attr('metadata/name'));
|
||||
});
|
||||
|
||||
if (!metadata.metadata || !metadata.metadata['hide-tooltip-options']) {
|
||||
metadata.get('properties').then(function(metaProps) {
|
||||
if (metaProps) {
|
||||
Object.keys(metaProps).sort().forEach(function(propertyName) {
|
||||
var optionRow = document.createElement('div');
|
||||
var optionName = document.createElement('span');
|
||||
var optionDescription = document.createElement('span');
|
||||
$(optionName).addClass('node-tooltip-option-name');
|
||||
$(optionDescription).addClass('node-tooltip-option-description');
|
||||
$(optionName).text(metaProps[propertyName].name);
|
||||
$(optionDescription).text(metaProps[propertyName].description);
|
||||
$(optionRow).append(optionName);
|
||||
$(optionRow).append(optionDescription);
|
||||
$(nodeTooltip).append(optionRow);
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
if (error) {
|
||||
$log.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var mousex = x + 10;
|
||||
var mousey = y + 10;
|
||||
$('.node-tooltip').css({ top: mousey, left: mousex });
|
||||
},
|
||||
|
||||
hideTooltip: function() {
|
||||
$('.node-tooltip').remove();
|
||||
},
|
||||
|
||||
moveTooltip: function(x, y) {
|
||||
var mousex = x + 10; // Get X coordinates
|
||||
var mousey = y + 10; // Get Y coordinates
|
||||
$('.node-tooltip').css({ top: mousey, left: mousex });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function createPaletteGroup(title, isOpen) {
|
||||
var newGroupHeader = new joint.shapes.flo.PaletteGroupHeader({attrs:{text:{text:title}}});
|
||||
newGroupHeader.set('header',title);
|
||||
if (!isOpen) {
|
||||
newGroupHeader.attr({'path':{'transform':'rotate(-90,15,13)'}});
|
||||
newGroupHeader.set('isOpen',false);
|
||||
}
|
||||
paletteGraph.addCell(newGroupHeader);
|
||||
return newGroupHeader;
|
||||
}
|
||||
|
||||
function createPaletteEntry(title, metadata) {
|
||||
return shapesFactory.createNode({
|
||||
'renderService': renderService,
|
||||
'paper': palette,
|
||||
'metadata': metadata
|
||||
});
|
||||
}
|
||||
|
||||
function buildPalette(metamodel) {
|
||||
var startTime = new Date().getTime();
|
||||
paletteGraph.clear();
|
||||
|
||||
var filterText = $scope.filterText;
|
||||
if (filterText) {
|
||||
filterText = filterText.toLowerCase();
|
||||
}
|
||||
|
||||
var paletteNodes = [];
|
||||
var groupAdded = {};
|
||||
|
||||
var parentWidth = $(domContext.parentNode).width();
|
||||
|
||||
// The field closedGroups tells us which should not be shown
|
||||
// Work out the list of active groups/nodes based on the filter text
|
||||
Object.keys(metamodel).sort(function(g1, g2) {
|
||||
var order = ['source', 'processor', 'sink', 'other', 'job'];
|
||||
var idx1 = order.indexOf(g1);
|
||||
var idx2 = order.indexOf(g2);
|
||||
if (idx1 === -1) {
|
||||
if (idx2 === -1) {
|
||||
return g1.localeCompare(g2);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
if (idx2 === -1) {
|
||||
return -1;
|
||||
} else {
|
||||
return idx1 - idx2;
|
||||
}
|
||||
}
|
||||
}).forEach(function(group) {
|
||||
Object.keys(metamodel[group]).sort().forEach(function(name) {
|
||||
var node = metamodel[group][name];
|
||||
var nodeActive = !node.noPaletteEntry;
|
||||
if (nodeActive && filterText) {
|
||||
nodeActive = false;
|
||||
if (name.toLowerCase().indexOf(filterText) !== -1) {
|
||||
nodeActive = true;
|
||||
}
|
||||
else if (group.toLowerCase().indexOf(filterText) !== -1) {
|
||||
nodeActive = true;
|
||||
}
|
||||
else if (node.description && node.description.toLowerCase().indexOf(filterText) !== -1) {
|
||||
nodeActive = true;
|
||||
}
|
||||
else if (node.properties) {
|
||||
Object.keys(node.properties).sort().forEach(function(propertyName) {
|
||||
if (propertyName.toLowerCase().indexOf(filterText) !== -1 ||
|
||||
(node.properties[propertyName].description &&
|
||||
node.properties[propertyName].description.toLowerCase().indexOf(filterText) !== -1)) {
|
||||
nodeActive=true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (nodeActive) {
|
||||
if (!groupAdded[group]) {
|
||||
var header = createPaletteGroup(group, !_.contains(closedGroups,group));
|
||||
header.set('size', {width: parentWidth, height: 30});
|
||||
paletteNodes.push(header);
|
||||
groupAdded[group] = true;
|
||||
}
|
||||
if (!_.contains(closedGroups,group)) {
|
||||
paletteNodes.push(createPaletteEntry(name, node));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var cellWidth = 0, cellHeight = 0;
|
||||
// Determine the size of the palette entry cell (width and height)
|
||||
paletteNodes.forEach(function(pnode) {
|
||||
if (pnode.attr('metadata/name')) {
|
||||
var dimension = {
|
||||
width: pnode.get('size').width,
|
||||
height: pnode.get('size').height
|
||||
};
|
||||
if (cellWidth < dimension.width) {
|
||||
cellWidth = dimension.width;
|
||||
}
|
||||
if (cellHeight < dimension.height) {
|
||||
cellHeight = dimension.height;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Adjust the palette entry cell size with paddings.
|
||||
cellWidth += 2 * $scope.flo.paletteEntryPadding.x;
|
||||
cellHeight += 2 * $scope.flo.paletteEntryPadding.y;
|
||||
|
||||
// Align palette entries row to be at the center
|
||||
var startX = parentWidth >= cellWidth ? (parentWidth - Math.floor(parentWidth / cellWidth) * cellWidth) / 2 : 0;
|
||||
var xpos = startX;
|
||||
var ypos= 0;
|
||||
var prevNode;
|
||||
|
||||
// Layout palette entry nodes
|
||||
paletteNodes.forEach(function(pnode) {
|
||||
var dimension = {
|
||||
width: pnode.get('size').width,
|
||||
height: pnode.get('size').height
|
||||
};
|
||||
if (pnode.get('header')) { //attributes.attrs.header) {
|
||||
// Palette entry header
|
||||
xpos = startX;
|
||||
pnode.set('position',{x:0, y:ypos});
|
||||
ypos += dimension.height + 5;
|
||||
} else {
|
||||
// Palette entry element
|
||||
if (xpos + cellWidth > parentWidth) {
|
||||
// Not enough real estate to place entry in a row - reset x position and leave the y pos which is next line
|
||||
xpos = startX;
|
||||
pnode.set('position', { x: xpos + (cellWidth - dimension.width) / 2, y: ypos + (cellHeight - dimension.height) / 2});
|
||||
} else {
|
||||
// Enough real estate to place entry in a row - adjust y position
|
||||
if (prevNode.attr('metadata/name')) {
|
||||
ypos -= cellHeight;
|
||||
}
|
||||
pnode.set('position', { x: xpos + (cellWidth - dimension.width) / 2, y: ypos + (cellHeight - dimension.height) / 2});
|
||||
}
|
||||
// increment x position and y position (can be reorganized)
|
||||
xpos += cellWidth;
|
||||
ypos += cellHeight;
|
||||
}
|
||||
prevNode = pnode;
|
||||
});
|
||||
palette.setDimensions(parentWidth, ypos);
|
||||
$log.info('buildPalette took '+(new Date().getTime()-startTime)+'ms');
|
||||
}
|
||||
|
||||
var _metamodelListener = {
|
||||
metadataError: function(data/*, status, headers, config*/) {
|
||||
$log.error(JSON.stringify(data));
|
||||
},
|
||||
metadataRefresh: function() {
|
||||
|
||||
},
|
||||
metadataChanged: function(data) {
|
||||
buildPalette(data.newData);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO handle case of clicking header whilst already rotating
|
||||
|
||||
/*
|
||||
* Modify the rotation of the arrow in the header from horizontal(closed) to vertical(open)
|
||||
*/
|
||||
function rotateOpen(element) {
|
||||
var angle = 90;
|
||||
var rotateIt = function() {
|
||||
angle = angle - 10;
|
||||
element.attr({'path':{'transform':'rotate(-'+angle+',15,13)'}});
|
||||
if (angle <= 0) {
|
||||
element.set('isOpen',true);
|
||||
closedGroups = _.without(closedGroups,element.get('header'));
|
||||
metamodelService.load().then(buildPalette);
|
||||
return;
|
||||
} else {
|
||||
$timeout(rotateIt,10);
|
||||
}
|
||||
};
|
||||
$timeout(rotateIt);
|
||||
}
|
||||
|
||||
// TODO better name for this function as this does the animation *and* updates the palette
|
||||
|
||||
/*
|
||||
* Modify the rotation of the arrow in the header from vertical(open) to horizontal(closed)
|
||||
*/
|
||||
function rotateClosed(element) {
|
||||
var angle = 0;
|
||||
var rotateIt = function() {
|
||||
angle = angle + 10;
|
||||
element.attr({'path':{'transform':'rotate(-'+angle+',15,13)'}});
|
||||
if (angle >= 90) {
|
||||
element.set('isOpen',false);
|
||||
closedGroups.push(element.get('header'));
|
||||
metamodelService.load().then(buildPalette);
|
||||
return;
|
||||
} else {
|
||||
$timeout(rotateIt,10);
|
||||
}
|
||||
};
|
||||
$timeout(rotateIt);
|
||||
}
|
||||
|
||||
function handleMouseUp(/*event*/) {
|
||||
$(document).off('mousemove', handleDrag);
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
if (metamodelService && angular.isFunction(metamodelService.unsubscribe)) {
|
||||
metamodelService.unsubscribe(_metamodelListener);
|
||||
}
|
||||
$(document).off('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
function filterTextUpdated() {
|
||||
metamodelService.load().then(buildPalette);
|
||||
}
|
||||
|
||||
function init(element/*, attrs*/) {
|
||||
|
||||
domContext = element;
|
||||
|
||||
metamodelService = $injector.get($scope.metamodelServiceName);
|
||||
|
||||
if ($scope.renderServiceName) {
|
||||
renderService = $injector.get($scope.renderServiceName);
|
||||
}
|
||||
|
||||
// Create the paper for the palette using the specified element view
|
||||
palette = new joint.dia.Paper({
|
||||
el: $('#palette-paper', domContext),
|
||||
gridSize:1,
|
||||
model:paletteGraph,
|
||||
height: $(domContext.parentNode).height(),
|
||||
width: $(domContext.parentNode).width(),
|
||||
elementView: ConstraintElementView
|
||||
});
|
||||
|
||||
palette.on('cell:pointerup',
|
||||
function(cellview, evt) {
|
||||
$log.debug('pointerup');
|
||||
if (viewBeingDragged) {
|
||||
trigger('drop',{'dragged':viewBeingDragged,'evt':evt});
|
||||
viewBeingDragged = null;
|
||||
}
|
||||
clickedElement = null;
|
||||
$('#palette-floater').remove();
|
||||
});
|
||||
|
||||
// Toggle the header open/closed on a click
|
||||
palette.on('cell:pointerclick',
|
||||
function(cellview/*,evt*/) {
|
||||
// TODO [design][palette] should the user need to click on the arrow rather than anywhere on the header?
|
||||
// Click position within the element would be: evt.offsetX, evt.offsetY
|
||||
var element = cellview.model;
|
||||
if (cellview.model.attributes.header) {
|
||||
// Toggle the header open/closed
|
||||
if (element.get('isOpen')) {
|
||||
rotateClosed(element);
|
||||
} else {
|
||||
rotateOpen(element);
|
||||
}
|
||||
}
|
||||
// TODO [palette] ensure other mouse handling events do nothing for headers
|
||||
// TODO [palette] move 'metadata' field to the right place (not inside attrs I think)
|
||||
});
|
||||
|
||||
$(document).on('mouseup', handleMouseUp);
|
||||
|
||||
if (metamodelService) {
|
||||
metamodelService.load().then(function(data) {
|
||||
buildPalette(data);
|
||||
if (metamodelService && angular.isFunction(metamodelService.subscribe)) {
|
||||
metamodelService.subscribe(_metamodelListener);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$log.error('No Metamodel service specified for palette!');
|
||||
}
|
||||
|
||||
if ($scope.flo) {
|
||||
$scope.flo.paletteSize = $scope.flo.paletteSize || $(domContext.parentNode).width();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', dispose);
|
||||
|
||||
$scope.init = init;
|
||||
|
||||
$scope.filterTextUpdated = filterTextUpdated;
|
||||
|
||||
if ($scope.flo) {
|
||||
$scope.flo._paletteGraph = function() {
|
||||
return paletteGraph;
|
||||
};
|
||||
$scope.flo.paletteEntryPadding = $scope.flo.paletteEntryPadding || {x:12, y:12};
|
||||
}
|
||||
|
||||
$scope.$watch(function() {
|
||||
return $scope.flo.paletteSize;
|
||||
}, function(newValue, oldValue) {
|
||||
if (oldValue !== newValue) {
|
||||
metamodelService.load().then(buildPalette);
|
||||
}
|
||||
});
|
||||
|
||||
}];
|
||||
|
||||
});
|
||||
798
src/properties-manager.js
Executable file
798
src/properties-manager.js
Executable file
@@ -0,0 +1,798 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This module manages the properties viewer for Flo, showing detailed information on supported
|
||||
* properties for modules and enabling them to be edited to new values.
|
||||
*
|
||||
* Events coming out of here:
|
||||
* 'change' - something changed!
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
var CodeMirror = require('codemirror');
|
||||
|
||||
require('codemirror/addon/mode/loadmode');
|
||||
require('codemirror/addon/edit/matchbrackets');
|
||||
require('codemirror/addon/edit/closebrackets');
|
||||
|
||||
// languages
|
||||
require('codemirror/mode/groovy/groovy');
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/python/python');
|
||||
require('codemirror/mode/clike/clike'); // for Java
|
||||
|
||||
CodeMirror.modeURL = 'codemirror/mode/%N/%N';
|
||||
|
||||
return function(domContext, metamodelService) {
|
||||
|
||||
function cellTitle(cell, newValue) {
|
||||
var titleAttr = cell.attr('metadata/metadata/titleProperty') ? cell.attr('metadata/metadata/titleProperty') : '.label/text';
|
||||
if (newValue === undefined) {
|
||||
return cell.attr(titleAttr);
|
||||
} else {
|
||||
cell.attr(titleAttr, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO more home grown eventing, remove!
|
||||
|
||||
var triggerFns = {};
|
||||
|
||||
function on(triggerEvent, fn) {
|
||||
var triggers = triggerFns[triggerEvent];
|
||||
if (!triggers) {
|
||||
triggers = [];
|
||||
triggerFns[triggerEvent] = triggers;
|
||||
}
|
||||
triggers.push(fn);
|
||||
}
|
||||
|
||||
function trigger(triggerEvent) {
|
||||
var triggers = triggerFns[triggerEvent];
|
||||
if (triggers) {
|
||||
for (var i=0;i<triggers.length;i++) {
|
||||
triggers[i]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isTitleReadOnly(cell) {
|
||||
var titleAttr = cell.attr('metadata/metadata/titleProperty') ? cell.attr('metadata/metadata/titleProperty') : '.label/text';
|
||||
return titleAttr.indexOf('metadata') === 0;
|
||||
}
|
||||
|
||||
function isApplicableFor(cellview) {
|
||||
return cellview && cellview.model && cellview.model.attr('metadata') &&
|
||||
!cellview.model.attr('metadata/metadata/noEditableProps') && !cellview.model.attr('metadata/unresolved');
|
||||
}
|
||||
|
||||
function setVisible(visible) {
|
||||
if (visible) {
|
||||
$('#properties', domContext).height('calc(30% - 1px)');
|
||||
$('#paper', domContext).height('70%');
|
||||
} else {
|
||||
$('#properties', domContext).height('0%');
|
||||
$('#paper', domContext).height('100%');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the properties view is currently being displayed. Optionally an element Id can
|
||||
* be passed and if so this function will only return true if the properties view is
|
||||
* open *and* it is displaying the properties for the specified element.
|
||||
*/
|
||||
function isVisible(optionalElementId) {
|
||||
if (optionalElementId) {
|
||||
if ($('#properties', domContext).height()===0) {
|
||||
return false;
|
||||
}
|
||||
var currentlyDisplayedElementId = $('#properties').attr('elementId');
|
||||
return currentlyDisplayedElementId === optionalElementId;
|
||||
} else {
|
||||
return $('#properties', domContext).height();
|
||||
}
|
||||
}
|
||||
|
||||
function togglePropertiesView(cellview) {
|
||||
if (isApplicableFor(cellview)) {
|
||||
// change state of properties view
|
||||
// TODO crude because not detecting 'double click' just detecting a second press sometime later
|
||||
var displayState = isVisible();
|
||||
setVisible(!displayState);
|
||||
}
|
||||
}
|
||||
|
||||
function resize() {
|
||||
// $('#properties', domContext).offset({left:$('#paper', domContext).position().left+$('#paper', domContext).width()+25-400});
|
||||
// $('#properties', domContext).offset({top:$('#paper', domContext).position().top+$('#paper', domContext).height()-$('#properties', domContext).height()-5});
|
||||
}
|
||||
|
||||
function moveTooltip(x, y, offsetChoice) {
|
||||
// var ppcPos = $('#properties', domContext).size();
|
||||
var mx = x;
|
||||
var my = y;
|
||||
if (offsetChoice === 'topleft') {
|
||||
mx = mx + 10; /*- $('.node-tooltip', domContext).width();*/
|
||||
my = my - 25 - $('.node-tooltip').height();
|
||||
}
|
||||
$('.node-tooltip').css({ top: my, left: mx });
|
||||
}
|
||||
|
||||
function closeTooltip() {
|
||||
$('.node-tooltip').remove();
|
||||
}
|
||||
|
||||
function createTooltip(tooltipText, x, y, offsetChoice) {
|
||||
var nodeTooltip = document.createElement('div');
|
||||
$(nodeTooltip).addClass('node-tooltip');
|
||||
// TODO as general purpose, append it somewhere else
|
||||
$(nodeTooltip).appendTo('body').fadeIn('fast');
|
||||
$(nodeTooltip).addClass('tooltip-description');
|
||||
var nodeDescription = document.createElement('div');
|
||||
$(nodeDescription).text(tooltipText);
|
||||
$(nodeTooltip).append(nodeDescription);
|
||||
moveTooltip(x,y,offsetChoice);
|
||||
}
|
||||
|
||||
function attachTooltip(element, text) {
|
||||
if (text) {
|
||||
$(element).mouseenter(function(evt) {
|
||||
createTooltip(text,evt.pageX,evt.pageY,'topleft');
|
||||
});
|
||||
$(element).mousemove(function(evt) {
|
||||
moveTooltip(evt.pageX,evt.pageY,'topleft');
|
||||
});
|
||||
$(element).mouseleave(function() {
|
||||
closeTooltip();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addNewPropertyKeyValue(cellview, key, newvalue) {
|
||||
cellview.model.attr('props/' + key, newvalue);
|
||||
// $('#properties', domContext).trigger('property-change');
|
||||
// paper.trigger('resync-required');
|
||||
trigger('change');
|
||||
}
|
||||
|
||||
function addNewPropertyRow(table, cellview, rowNum) {
|
||||
// Final row is where new properties can be added
|
||||
var row = document.createElement('tr');
|
||||
rowNum++;
|
||||
if ((rowNum % 2)===1) {
|
||||
$(row).addClass('properties-row-even');
|
||||
} else {
|
||||
$(row).addClass('properties-row-odd');
|
||||
}
|
||||
$(table).append(row);
|
||||
var keytext = document.createElement('input');
|
||||
$(keytext).addClass('properties-input');
|
||||
$(keytext).val('...');
|
||||
$(keytext).addClass('properties-new-property');
|
||||
if ((rowNum%2)===1) {
|
||||
$(keytext).addClass('properties-row-text-even');
|
||||
} else {
|
||||
$(keytext).addClass('properties-row-text-odd');
|
||||
}
|
||||
|
||||
var key = document.createElement('td');
|
||||
$(key).addClass('properties-key');
|
||||
key.appendChild(keytext);
|
||||
$(row).append(key);
|
||||
// TODO do something special when typing in the 'new key' row
|
||||
|
||||
var value = document.createElement('td');
|
||||
var valuetext = document.createElement('input');
|
||||
$(valuetext).addClass('properties-input');
|
||||
$(valuetext).val('...');
|
||||
$(valuetext).addClass('properties-new-property');
|
||||
if ((rowNum % 2)===1) {
|
||||
$(valuetext).addClass('properties-row-text-even');
|
||||
} else {
|
||||
$(valuetext).addClass('properties-row-text-odd');
|
||||
}
|
||||
|
||||
$(value).addClass('properties-value');
|
||||
// $(value).on('keyup',function(rowNumber,valueNode,evt) {
|
||||
// // cellview gives us the ID of the node that changed
|
||||
// // row gives us which entry in the properties list
|
||||
// var newKeyValue = $(valueNode).val();
|
||||
// }.bind(this,r,valuetext));
|
||||
|
||||
value.appendChild(valuetext);
|
||||
$(row).append(value);
|
||||
|
||||
$(keytext).on('focus',function(keyTextInputField, evt) { // jshint ignore:line
|
||||
console.log('properties: focus event on key in \'new value\' row');
|
||||
// Set it to blank so the user can start typing
|
||||
$(keyTextInputField).val('');
|
||||
$(this).val('placeholder');
|
||||
// Make them black and not gray for now
|
||||
$(keyTextInputField).removeClass('properties-new-property');
|
||||
$(this).removeClass('properties-new-property');
|
||||
}.bind(valuetext, keytext));
|
||||
|
||||
$(key).on('keydown', function(rowNumber, keyTextInputField, cellview, evt) {
|
||||
if (evt.keyCode === 13) {
|
||||
var newKeyValue = $(keyTextInputField).val();
|
||||
if (newKeyValue === '') {
|
||||
// set it back to how it was...
|
||||
$(keyTextInputField).addClass('properties-new-property');
|
||||
$(this).addClass('properties-new-property');
|
||||
$(keyTextInputField).val('...');
|
||||
$(this).val('...');
|
||||
}
|
||||
else {
|
||||
addNewPropertyKeyValue(cellview, newKeyValue, $(this).val());
|
||||
}
|
||||
}
|
||||
}.bind(valuetext, rowNum, keytext, cellview));
|
||||
|
||||
|
||||
$(key).on('focusout',function(rowNumber, keyTextInputField, cellview, evt) { // jshint ignore:line
|
||||
var newKeyValue = $(keyTextInputField).val();
|
||||
if (newKeyValue === '') {
|
||||
// set it back to how it was...
|
||||
$(keyTextInputField).addClass('properties-new-property');
|
||||
$(this).addClass('properties-new-property');
|
||||
$(keyTextInputField).val('...');
|
||||
$(this).val('...');
|
||||
}
|
||||
else {
|
||||
addNewPropertyKeyValue(cellview, newKeyValue, $(this).val());
|
||||
}
|
||||
}.bind(valuetext, rowNum, keytext, cellview));
|
||||
|
||||
// $(valuetext).on('keydown', function(rowNumber, keyTextInputField, cellview, evt) {
|
||||
// if (evt.keyCode == 13) {
|
||||
// var newKeyValue = $(keyTextInputField).val();
|
||||
// if (newKeyValue == '' || newKeyValue == '...') {
|
||||
// // set it back to how it was...
|
||||
// $(keyTextInputField).addClass('properties-new-property');
|
||||
// $(this).addClass('properties-new-property');
|
||||
// $(keyTextInputField).val('...');
|
||||
// $(this).val('...');
|
||||
// }
|
||||
// else {
|
||||
// addNewPropertyKeyValue(cellview, newKeyValue, $(this).val());
|
||||
// }
|
||||
// }
|
||||
// }.bind(valuetext, r, keytext, cellview));
|
||||
//
|
||||
// $(valuetext).on('focusout',function(rowNumber, keyTextInputField, cellview, evt) {
|
||||
// var newKeyValue = $(keyTextInputField).val();
|
||||
// if (newKeyValue == '' || newKeyValue == '...') {
|
||||
// // set it back to how it was...
|
||||
// $(keyTextInputField).addClass('properties-new-property');
|
||||
// $(this).addClass('properties-new-property');
|
||||
// $(keyTextInputField).val('...');
|
||||
// $(this).val('...');
|
||||
// }
|
||||
// else {
|
||||
//// updateModelPropertyValue(cellview, key, null, newValue);
|
||||
// //OR?
|
||||
// addNewPropertyKeyValue(cellview, newKeyValue, $(this).val());
|
||||
// }
|
||||
// }.bind(valuetext, r, keytext, cellview));
|
||||
}
|
||||
|
||||
function getDisplayDefaultValue(property) {
|
||||
if (property && property.defaultValue !== undefined && property.defaultValue !== null) {
|
||||
return property.defaultValue;
|
||||
}
|
||||
return '...';
|
||||
}
|
||||
|
||||
|
||||
function updateModelPropertyKey(cellview, oldkeyvalue, newkeyvalue) {
|
||||
var props = cellview.model.attr('props');
|
||||
if (newkeyvalue) {
|
||||
if (!props[newkeyvalue]) {
|
||||
props[newkeyvalue] = props[oldkeyvalue];
|
||||
}
|
||||
}
|
||||
delete props[oldkeyvalue];
|
||||
// $('#properties', domContext).trigger('property-change');
|
||||
}
|
||||
|
||||
function updateModelPropertyValue(cellview, key, oldvalue, newvalue) {
|
||||
// var props = cellview.model.attr('props');
|
||||
var hasDefaultValue = false;
|
||||
var defaultValue;
|
||||
var propertyMetadata = cellview.model.attr('metadata/properties/'+key);
|
||||
if (propertyMetadata && propertyMetadata.defaultValue !== undefined && propertyMetadata.defaultValue !== null) {
|
||||
hasDefaultValue = true;
|
||||
defaultValue = propertyMetadata.defaultValue;
|
||||
}
|
||||
// Setting it back to original value (or blank, undefined or null - if no default value)
|
||||
if ((hasDefaultValue && newvalue === defaultValue) || newvalue === '' || newvalue === undefined || newvalue === null) {
|
||||
// trigger events listening in for a change
|
||||
cellview.model.removeAttr('props/'+key,{'propertyPath':'attrs/props/'+key});
|
||||
} else {
|
||||
cellview.model.attr('props/'+key,newvalue);
|
||||
}
|
||||
// $('#properties', domContext).trigger('property-change');
|
||||
trigger('change');//paper.trigger('resync-required');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a selection is made or changed so that the properties view will correctly
|
||||
* represent the properties set on the new selection.
|
||||
*/
|
||||
function updatePropertiesView(cellview) {
|
||||
/*jshint validthis:true */
|
||||
var propertiesNode = $('#properties', domContext);
|
||||
if (!isApplicableFor(cellview)) {
|
||||
propertiesNode.empty();
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
setVisible(true);
|
||||
var properties = cellview.model.attr('props');
|
||||
// propertiesNode.off('property-change');
|
||||
// propertiesNode.on('property-change',function(e) {
|
||||
// console.log("Event detected: property-change");
|
||||
// updatePropertiesView(cellview);
|
||||
// });
|
||||
var moduleName = cellTitle(cellview.model);
|
||||
|
||||
var moduleSchema = cellview.model.attr('metadata');
|
||||
|
||||
moduleSchema.get('properties').then(function(moduleSchemaProperties) {
|
||||
|
||||
$(propertiesNode).empty();
|
||||
$(propertiesNode).attr('elementId',cellview.model.id);
|
||||
|
||||
var nodename = document.createElement('input');
|
||||
if (isTitleReadOnly(cellview.model)) {
|
||||
$(nodename).prop('readonly', true);
|
||||
}
|
||||
$(nodename).addClass('properties-node-name');
|
||||
$(nodename).val(moduleName);
|
||||
|
||||
moduleSchema.get('description').then(function(description) {
|
||||
attachTooltip(nodename, description);
|
||||
}, function(error) {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
function processNewModuleName() {
|
||||
var currentValidModuleName = cellTitle(cellview.model);
|
||||
var newModuleName = $(this).val();
|
||||
if (newModuleName !== currentValidModuleName) {
|
||||
if (newModuleName === '') {
|
||||
// ignore trying to set it to blank, just reset it
|
||||
$(this).val(currentValidModuleName);
|
||||
} else {
|
||||
cellTitle(cellview.model, newModuleName);
|
||||
// $('#properties', domContext).trigger('property-change');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Support editing of the module name - when the user presses ENTER
|
||||
$(nodename).on('keydown',function(cellview, originalModuleName, evt) {
|
||||
// TODO can't edit title of tap/namedchannel
|
||||
if (evt.keyCode === 13/*ENTER*/) {
|
||||
processNewModuleName.bind(this)();
|
||||
}
|
||||
}.bind(nodename, cellview, moduleName));
|
||||
// Support editing of the module name - when the user tabs out of the element
|
||||
$(nodename).on('focusout',function(cellview, originalModuleName, evt) { // jshint ignore:line
|
||||
processNewModuleName.bind(this)();
|
||||
}.bind(nodename, cellview, moduleName));
|
||||
|
||||
// Build the table of key/value options
|
||||
var table = document.createElement('table');
|
||||
$(propertiesNode).append(table);
|
||||
|
||||
$(table).width('100%');
|
||||
$(table).css('border', '0px');
|
||||
$(table).css('border-spacing', '0px');
|
||||
|
||||
var row = document.createElement('tr');
|
||||
$(row).addClass('properties-node-name-row');
|
||||
var headerCell = document.createElement('th');
|
||||
headerCell.colSpan = 2;
|
||||
headerCell.appendChild(nodename);
|
||||
$(row).append(headerCell);
|
||||
$(table).append(row);
|
||||
|
||||
var rowNum = 0;
|
||||
|
||||
// Special handling when a named channel has the channel property set, update the
|
||||
// label on the node in the graph (like taps, channel nodes include the channel
|
||||
// embedded in the icon)
|
||||
// function setNamedChannelLabel(namedChannel,value) {
|
||||
// // console.log("Named channel has channel updated to '"+value+"'");
|
||||
// var labelText = value;
|
||||
// if (value) {
|
||||
// var colonPos = value.indexOf(':');
|
||||
// if (colonPos !==-1) {
|
||||
// colonPos = value.indexOf(':',colonPos+1);
|
||||
// if (colonPos !== -1) {
|
||||
// labelText = value.substring(0,colonPos)+':\n'+value.substring(colonPos+1);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// namedChannel.model.attr('.label2/text',labelText);
|
||||
// }
|
||||
|
||||
|
||||
function processPropertyKeyChange(checkKey, rowNumber, originalKeyValue, keyTextInputField, cellview, evt) {
|
||||
if (!checkKey || evt.keyCode === 13) {
|
||||
var newKeyValue = $(keyTextInputField).val();
|
||||
if (newKeyValue !== originalKeyValue) {
|
||||
console.log('properties: key changed from '+originalKeyValue+' to '+newKeyValue);
|
||||
updateModelPropertyKey(cellview, originalKeyValue, newKeyValue);
|
||||
}
|
||||
else {
|
||||
console.log('Key unchanged');
|
||||
}
|
||||
}
|
||||
}
|
||||
function processNewPropertyValue(checkKey, rowNumber, key, valueTextInputField, cellview, evt) {
|
||||
if (!checkKey || evt.keyCode === 13) {
|
||||
// TODO need to search props for the key, it isn't a map like this - underscore helper?
|
||||
var originalValue = cellview.model.attr('props/' + key);
|
||||
var newValue = $(valueTextInputField).val();
|
||||
if (newValue !== originalValue) {
|
||||
console.log('properties: for key \''+key+'\' value changed from '+originalValue+' to '+newValue);
|
||||
if (_.isFunction(metamodelService.isValidPropertyValue)) {
|
||||
if (metamodelService.isValidPropertyValue(cellview.model, key, newValue)) {
|
||||
updateModelPropertyValue(cellview, key, originalValue, newValue);
|
||||
} else {
|
||||
updateModelPropertyValue(cellview, key, originalValue, originalValue);
|
||||
}
|
||||
} else {
|
||||
updateModelPropertyValue(cellview, key, originalValue, newValue);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('Value unchanged');
|
||||
}
|
||||
}
|
||||
}
|
||||
function processNewPropertyValueForCodeMirrorDoc(doc, key, cellview) {
|
||||
// TODO need to search props for the key, it isn't a map like this - underscore helper?
|
||||
var originalValue = cellview.model.attr('props/' + key);
|
||||
var newValue = doc.getValue();
|
||||
if (metamodelService && _.isFunction(metamodelService.encodeTextToDSL)) {
|
||||
newValue = metamodelService.encodeTextToDSL(newValue);
|
||||
}
|
||||
if (newValue !== originalValue) {
|
||||
console.log('properties: for key \''+key+'\' value changed from '+originalValue+' to '+newValue);
|
||||
if (_.isFunction(metamodelService.isValidPropertyValue)) {
|
||||
if (metamodelService.isValidPropertyValue(cellview.model, key, newValue)) {
|
||||
updateModelPropertyValue(cellview, key, originalValue, newValue);
|
||||
} else {
|
||||
updateModelPropertyValue(cellview, key, originalValue, originalValue);
|
||||
}
|
||||
} else {
|
||||
updateModelPropertyValue(cellview, key, originalValue, newValue);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('Value unchanged');
|
||||
}
|
||||
}
|
||||
|
||||
rowNum = 0;
|
||||
if (properties) {
|
||||
Object.keys(properties).sort().forEach(function(propertyName) {
|
||||
if (propertyName === cellview.model.attr('metadata/metadata/titleProperty')) {
|
||||
// Ignore the title property. It'll be displayed at the top anyway
|
||||
return;
|
||||
}
|
||||
if (!properties[propertyName] || properties[propertyName] === moduleSchemaProperties[propertyName].defaultValue) {
|
||||
// Ignore properties equal to default value
|
||||
return;
|
||||
}
|
||||
var row = document.createElement('tr');
|
||||
if ((rowNum%2) === 0) {
|
||||
$(row).addClass('properties-row-even');
|
||||
} else {
|
||||
$(row).addClass('properties-row-odd');
|
||||
}
|
||||
$(table).append(row);
|
||||
var keytext = document.createElement('input');
|
||||
$(keytext).addClass('properties-input');
|
||||
|
||||
if ((rowNum % 2) === 0) {
|
||||
$(keytext).addClass('properties-row-text-even');
|
||||
} else {
|
||||
$(keytext).addClass('properties-row-text-odd');
|
||||
}
|
||||
|
||||
var schemaProperty;
|
||||
if (moduleSchemaProperties && moduleSchemaProperties[propertyName]) {
|
||||
schemaProperty = moduleSchemaProperties[propertyName];
|
||||
// $(keytext).attr('title',schemaProperty.description);
|
||||
attachTooltip(row, schemaProperty.description);
|
||||
}
|
||||
|
||||
$(keytext).val(schemaProperty && schemaProperty.name ? schemaProperty.name : propertyName);
|
||||
|
||||
var key = document.createElement('td');
|
||||
$(key).addClass('properties-key');
|
||||
key.appendChild(keytext);
|
||||
$(row).append(key);
|
||||
// attachTooltip(row,rowdata.description);
|
||||
|
||||
|
||||
// Process editing the property key - ENTER key pressed
|
||||
$(key).on('keydown', processPropertyKeyChange.bind(this, true, rowNum, propertyName, keytext, cellview));
|
||||
// Process editing the property key - when cell loses focus (e.g. tab pressed)
|
||||
$(key).on('focusout',processPropertyKeyChange.bind(this, false, rowNum, propertyName, keytext, cellview));
|
||||
|
||||
var value = document.createElement('td');
|
||||
$(row).append(value);
|
||||
var valuetext;
|
||||
if (schemaProperty && schemaProperty.source) {
|
||||
valuetext = document.createElement('textarea');
|
||||
value.appendChild(valuetext);
|
||||
$(valuetext).addClass('properties-input');
|
||||
var doc = CodeMirror.fromTextArea(valuetext, {
|
||||
// gutters: ["CodeMirror-lint-markers"],
|
||||
// lint: {
|
||||
// async: true,
|
||||
// getAnnotations: function (text, updateFun) {
|
||||
// if (!updateLinting) {
|
||||
// updateLinting = updateFun;
|
||||
// $scope.$watch("definition.parseError", refreshMarkers);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
extraKeys: {'Ctrl-Space': 'autocomplete'},
|
||||
// hintOptions: {
|
||||
// async: 'true',
|
||||
// hint: contentAssist
|
||||
// },
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
matchBrackets: true,
|
||||
autoCloseBrackets: true,
|
||||
mode: schemaProperty.source.mime
|
||||
});
|
||||
CodeMirror.autoLoadMode(doc, schemaProperty.source.type);
|
||||
var valueToSet = properties[propertyName] === undefined ? getDisplayDefaultValue(schemaProperty) : properties[propertyName];
|
||||
if (metamodelService && _.isFunction(metamodelService.decodeTextFromDSL)) {
|
||||
valueToSet = metamodelService.decodeTextFromDSL(valueToSet);
|
||||
}
|
||||
doc.setValue(valueToSet);
|
||||
doc.on('blur', function () {
|
||||
processNewPropertyValueForCodeMirrorDoc(doc, propertyName, cellview);
|
||||
});
|
||||
} else {
|
||||
valuetext = document.createElement('input');
|
||||
value.appendChild(valuetext);
|
||||
$(valuetext).addClass('properties-input');
|
||||
$(valuetext).val(properties[propertyName] === undefined ? getDisplayDefaultValue(schemaProperty) : properties[propertyName]);
|
||||
|
||||
// Handle new property value - pressing ENTER
|
||||
$(value).on('keydown', processNewPropertyValue.bind(this, true, rowNum, propertyName, valuetext, cellview));
|
||||
// Handle new property value - exiting property value (e.g. TAB pressed)
|
||||
$(value).on('focusout',processNewPropertyValue.bind(this, false, rowNum, propertyName, valuetext, cellview));
|
||||
}
|
||||
$(value).addClass('properties-value');
|
||||
|
||||
if ((rowNum % 2)===0) {
|
||||
$(valuetext).addClass('properties-row-text-even');
|
||||
} else {
|
||||
$(valuetext).addClass('properties-row-text-odd');
|
||||
}
|
||||
|
||||
rowNum++;
|
||||
});
|
||||
}
|
||||
|
||||
function processNewPropertyValueUpdate(checkKey, keyText, keyTextInputField, cellview, evt) {
|
||||
var property = moduleSchemaProperties[keyText];
|
||||
var newValue = $(this).val();
|
||||
if (!checkKey || evt.keyCode === 13) {
|
||||
// If nothing typed, reset to '...'
|
||||
if (newValue === '' || (property && property.defaultValue === newValue)) {
|
||||
$(this).val(getDisplayDefaultValue(property));
|
||||
$(keyTextInputField).addClass('properties-new-property');
|
||||
$(this).addClass('properties-new-property');
|
||||
} else {
|
||||
if (_.isFunction(metamodelService.isValidPropertyValue)) {
|
||||
if (metamodelService.isValidPropertyValue(cellview.model, keyText, newValue)) {
|
||||
addNewPropertyKeyValue(cellview, keyText, $(this).val());
|
||||
} else {
|
||||
$(this).val(getDisplayDefaultValue(property));
|
||||
}
|
||||
} else {
|
||||
addNewPropertyKeyValue(cellview, keyText, $(this).val());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processNewPropertyValueUpdateForCodeCodeMirrorDoc(doc, textarea, key, keyText, cellview) {
|
||||
var property = moduleSchemaProperties[key];
|
||||
var rawNewValue = doc.getValue();
|
||||
var newValue = doc.getValue();
|
||||
if (metamodelService && _.isFunction(metamodelService.encodeTextToDSL)) {
|
||||
newValue = metamodelService.encodeTextToDSL(rawNewValue);
|
||||
}
|
||||
// If nothing typed, reset to '...'
|
||||
if (rawNewValue === '' || (property && property.defaultValue === newValue)) {
|
||||
doc.setValue(getDisplayDefaultValue(property));
|
||||
$(keyText).addClass('properties-new-property');
|
||||
$(textarea).addClass('properties-new-property');
|
||||
} else {
|
||||
if (_.isFunction(metamodelService.isValidPropertyValue)) {
|
||||
if (metamodelService.isValidPropertyValue(cellview.model, keyText, newValue)) {
|
||||
addNewPropertyKeyValue(cellview, key, newValue);
|
||||
} else {
|
||||
var defValue = getDisplayDefaultValue(property);
|
||||
if (metamodelService && _.isFunction(metamodelService.decodeTextFromDSL)) {
|
||||
defValue = metamodelService.decodeTextFromDSL(defValue);
|
||||
}
|
||||
doc.setValue(defValue);
|
||||
}
|
||||
} else {
|
||||
addNewPropertyKeyValue(cellview, key, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add possible keys that don't have values yet, in gray
|
||||
if (moduleSchemaProperties) {
|
||||
Object.keys(moduleSchemaProperties).sort().forEach(function(propertyName) {
|
||||
if (propertyName === cellview.model.attr('metadata/metadata/titleProperty')) {
|
||||
// Ignore the title property. It'll be displayed at the top anyway
|
||||
return;
|
||||
}
|
||||
if (properties[propertyName]) {
|
||||
// Ensure not set properties are processed here
|
||||
return;
|
||||
}
|
||||
var property = moduleSchemaProperties[propertyName];
|
||||
var row = document.createElement('tr');
|
||||
if ((rowNum % 2)===0) {
|
||||
$(row).addClass('properties-row-even');
|
||||
} else {
|
||||
$(row).addClass('properties-row-odd');
|
||||
}
|
||||
$(table).append(row);
|
||||
var keytext = document.createElement('input');
|
||||
$(keytext).addClass('properties-input');
|
||||
$(keytext).val(property.name);
|
||||
$(keytext).addClass('properties-new-property');
|
||||
|
||||
if ((rowNum % 2)===0) {
|
||||
$(keytext).addClass('properties-row-text-even');
|
||||
} else {
|
||||
$(keytext).addClass('properties-row-text-odd');
|
||||
}
|
||||
$(keytext).attr('readonly', true);
|
||||
|
||||
var key = document.createElement('td');
|
||||
$(key).addClass('properties-key');
|
||||
key.appendChild(keytext);
|
||||
$(row).append(key);
|
||||
|
||||
attachTooltip(row, property.description);
|
||||
|
||||
var value = document.createElement('td');
|
||||
$(row).append(value);
|
||||
var valuetext;
|
||||
if (property && property.source) {
|
||||
valuetext = document.createElement('textarea');
|
||||
value.appendChild(valuetext);
|
||||
$(valuetext).addClass('properties-input');
|
||||
$(valuetext).addClass('properties-new-property');
|
||||
$(valuetext).addClass('properties-input');
|
||||
var doc = CodeMirror.fromTextArea(valuetext, {
|
||||
// gutters: ["CodeMirror-lint-markers"],
|
||||
// lint: {
|
||||
// async: true,
|
||||
// getAnnotations: function (text, updateFun) {
|
||||
// if (!updateLinting) {
|
||||
// updateLinting = updateFun;
|
||||
// $scope.$watch("definition.parseError", refreshMarkers);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
extraKeys: {'Ctrl-Space': 'autocomplete'},
|
||||
// hintOptions: {
|
||||
// async: 'true',
|
||||
// hint: contentAssist
|
||||
// },
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
matchBrackets: true,
|
||||
autoCloseBrackets: true,
|
||||
mode: property.source.mime
|
||||
});
|
||||
CodeMirror.autoLoadMode(doc, property.source.type);
|
||||
var valueToSet = getDisplayDefaultValue(property);
|
||||
if (metamodelService && _.isFunction(metamodelService.decodeTextFromDSL)) {
|
||||
valueToSet = metamodelService.decodeTextFromDSL(valueToSet);
|
||||
}
|
||||
doc.setValue(valueToSet);
|
||||
doc.on('focus', function () {
|
||||
doc.setValue('');
|
||||
$(valuetext).removeClass('properties-new-property');
|
||||
});
|
||||
doc.on('blur', function () {
|
||||
processNewPropertyValueUpdateForCodeCodeMirrorDoc(doc, valuetext, propertyName, keytext, cellview);
|
||||
});
|
||||
} else {
|
||||
valuetext = document.createElement('input');
|
||||
value.appendChild(valuetext);
|
||||
$(valuetext).addClass('properties-input');
|
||||
$(valuetext).val(getDisplayDefaultValue(property));
|
||||
$(valuetext).addClass('properties-new-property');
|
||||
|
||||
// Focus event on property value - set it to blank so the user can start typing
|
||||
$(valuetext).on('focus',function(keyText, keyTextInputField, nodeNumber, evt) { // jshint ignore:line
|
||||
$(this).val('');
|
||||
// Make them black and not gray for now
|
||||
$(keyTextInputField).removeClass('properties-new-property');
|
||||
$(this).removeClass('properties-new-property');
|
||||
}.bind(valuetext, propertyName, keytext));
|
||||
|
||||
// Handle new property value - ENTER pressed
|
||||
$(valuetext).on('keydown', processNewPropertyValueUpdate.bind(valuetext, true, propertyName, keytext, cellview));
|
||||
// Handle new property value - focus leaving cell (e.g. TAB pressed)
|
||||
$(valuetext).on('focusout',processNewPropertyValueUpdate.bind(valuetext, false, propertyName, keytext, cellview));
|
||||
}
|
||||
|
||||
if ((rowNum % 2)===0) {
|
||||
$(valuetext).addClass('properties-row-text-even');
|
||||
} else {
|
||||
$(valuetext).addClass('properties-row-text-odd');
|
||||
}
|
||||
$(value).addClass('properties-value');
|
||||
|
||||
|
||||
rowNum++;
|
||||
});
|
||||
}
|
||||
|
||||
// If the module supports new (undefined) properties, add a row to support entering them
|
||||
if (cellview.model.attr('metadata/metadata/allow-additional-properties')) {
|
||||
addNewPropertyRow(table, cellview, rowNum);
|
||||
}
|
||||
|
||||
resize();
|
||||
}, function(error) {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
resize: resize,
|
||||
on: on,
|
||||
updatePropertiesView: updatePropertiesView,
|
||||
togglePropertiesView: togglePropertiesView,
|
||||
setVisible: setVisible,
|
||||
isVisible: isVisible
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
610
src/shapes-factory.js
Executable file
610
src/shapes-factory.js
Executable file
@@ -0,0 +1,610 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Define the custom nodes and links that will be used in our graphs and
|
||||
* functions to create them.
|
||||
*/
|
||||
define(['joint', 'underscore'],function(joint, _) {
|
||||
'use strict';
|
||||
|
||||
var isChrome = !!window.chrome;
|
||||
var isFF = typeof InstallTrigger !== 'undefined';
|
||||
|
||||
var IMAGE_W = 120,
|
||||
IMAGE_H = 35;
|
||||
|
||||
var ERROR_MARKER_SIZE = {width: 16, height: 16};
|
||||
|
||||
var HANDLE_SIZE = {width: 10, height: 10};
|
||||
|
||||
joint.shapes.flo = {};
|
||||
|
||||
joint.shapes.flo.NODE_TYPE = 'sinspctr.IntNode';
|
||||
joint.shapes.flo.LINK_TYPE = 'sinspctr.Link';
|
||||
joint.shapes.flo.DECORATION_TYPE = 'decoration';
|
||||
joint.shapes.flo.HANDLE_TYPE = 'handle';
|
||||
|
||||
joint.shapes.flo.CANVAS_TYPE = 'canvas';
|
||||
joint.shapes.flo.PALETTE_TYPE = 'palette';
|
||||
joint.shapes.flo.FEEDBACK_TYPE = 'feedback';
|
||||
|
||||
var HANDLE_ICON_MAP = {
|
||||
'remove': 'icons/delete.svg'
|
||||
};
|
||||
|
||||
var DECORATION_ICON_MAP = {
|
||||
'error': 'icons/error.svg'
|
||||
};
|
||||
|
||||
joint.util.filter.redscale = function(args) {
|
||||
|
||||
var amount = _.isFinite(args.amount) ? args.amount : 1;
|
||||
|
||||
return _.template('<filter><feColorMatrix type="matrix" values="${a} ${b} ${c} 0 ${d} ${e} ${f} ${g} 0 0 ${h} ${i} ${k} 0 0 0 0 0 1 0"/></filter>', {
|
||||
a: 1 - 0.96 * amount,
|
||||
b: 0.95 * amount,
|
||||
c: 0.01 * amount,
|
||||
d: 0.3 * amount,
|
||||
e: 0.2 * amount,
|
||||
f: 1 - 0.9 * amount,
|
||||
g: 0.7 * amount,
|
||||
h: 0.05 * amount,
|
||||
i: 0.05 * amount,
|
||||
k: 1 - 0.1 * amount
|
||||
});
|
||||
};
|
||||
|
||||
joint.util.filter.orangescale = function(args) {
|
||||
|
||||
var amount = _.isFinite(args.amount) ? args.amount : 1;
|
||||
|
||||
return _.template('<filter><feColorMatrix type="matrix" values="${a} ${b} ${c} 0 ${d} ${e} ${f} ${g} 0 ${h} ${i} ${k} ${l} 0 0 0 0 0 1 0"/></filter>', {
|
||||
a: 1.0 + 0.5 * amount,
|
||||
b: 1.4 * amount,
|
||||
c: 0.2 * amount,
|
||||
d: 0.3 * amount,
|
||||
e: 0.3 * amount,
|
||||
f: 1 + 0.05 * amount,
|
||||
g: 0.2 * amount,
|
||||
h: 0.15 * amount,
|
||||
i: 0.3 * amount,
|
||||
k: 0.3 * amount,
|
||||
l: 1 - 0.6 * amount
|
||||
});
|
||||
};
|
||||
|
||||
joint.shapes.flo.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="output-port"/>'+
|
||||
'<rect class="output-port-cover"/>',
|
||||
|
||||
defaults: joint.util.deepSupplement({
|
||||
|
||||
type: 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: 8, width: 8,
|
||||
magnet: true,
|
||||
fill: '#eeeeee',
|
||||
transform: 'translate(' + -4 + ',' + ((IMAGE_H/2)-4) + ')',
|
||||
stroke: '#34302d',
|
||||
'stroke-width': 1
|
||||
},
|
||||
'.output-port': {
|
||||
type: 'output',
|
||||
height: 8, width: 8,
|
||||
magnet: true,
|
||||
fill: '#eeeeee',
|
||||
transform: 'translate(' + (IMAGE_W-4) + ',' + ((IMAGE_H/2)-4) + ')',
|
||||
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.flo.Link = joint.dia.Link.extend({
|
||||
defaults: joint.util.deepSupplement({
|
||||
type: joint.shapes.flo.LINK_TYPE,
|
||||
attrs: {
|
||||
'.connection': { stroke: '#34302d', 'stroke-width': 2 },
|
||||
// Lots of alternatives that have been played with:
|
||||
// '.smoooth': true
|
||||
// '.marker-source': { stroke: '#9B59B6', fill: '#9B59B6', d: 'M24.316,5.318,9.833,13.682,9.833,5.5,5.5,5.5,5.5,25.5,9.833,25.5,9.833,17.318,24.316,25.682z' },
|
||||
// '.marker-target': { stroke: '#F39C12', fill: '#F39C12', d: 'M14.615,4.928c0.487-0.986,1.284-0.986,1.771,0l2.249,4.554c0.486,0.986,1.775,1.923,2.864,2.081l5.024,0.73c1.089,0.158,1.335,0.916,0.547,1.684l-3.636,3.544c-0.788,0.769-1.28,2.283-1.095,3.368l0.859,5.004c0.186,1.085-0.459,1.553-1.433,1.041l-4.495-2.363c-0.974-0.512-2.567-0.512-3.541,0l-4.495,2.363c-0.974,0.512-1.618,0.044-1.432-1.041l0.858-5.004c0.186-1.085-0.307-2.6-1.094-3.368L3.93,13.977c-0.788-0.768-0.542-1.525,0.547-1.684l5.026-0.73c1.088-0.158,2.377-1.095,2.864-2.081L14.615,4.928z' },
|
||||
// '.connection': { 'stroke':'black'},
|
||||
// '.': { filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } } },
|
||||
// '.connection': { 'stroke-width': 10, 'stroke-linecap': 'round' },
|
||||
// This means: moveto 10 0, lineto 0 5, lineto, 10 10 closepath(z)
|
||||
// '.marker-target': { d: 'M 5 0 L 0 7 L 5 14 z', stroke: '#34302d','stroke-width' : 1},
|
||||
// '.marker-target': { d: 'M 14 2 L 9,2 L9,0 L 0,7 L 9,14 L 9,12 L 14,12 z', 'stroke-width' : 1, fill: '#34302d', stroke: '#34302d'},
|
||||
// '.marker-source': {d: 'M 5 0 L 5,10 L 0,10 L 0,0 z', 'stroke-width' : 0, fill: '#34302d', stroke: '#34302d'},
|
||||
// '.marker-target': { stroke: '#E74C3C', fill: '#E74C3C', d: 'M 10 0 L 0 5 L 10 10 z' },
|
||||
'.marker-arrowheads': { display: 'none' },
|
||||
'.tool-options': { display: 'none' }
|
||||
},
|
||||
// connector: { name: 'normalDimFix' }
|
||||
}, joint.dia.Link.prototype.defaults)
|
||||
});
|
||||
|
||||
joint.shapes.flo.ErrorDecoration = joint.shapes.basic.Generic.extend({
|
||||
|
||||
markup: '<g class="rotatable"><g class="scalable"><image/></g></g>',
|
||||
|
||||
defaults: joint.util.deepSupplement({
|
||||
|
||||
type: joint.shapes.flo.DECORATION_TYPE,
|
||||
size: ERROR_MARKER_SIZE,
|
||||
attrs: {
|
||||
'image': ERROR_MARKER_SIZE
|
||||
}
|
||||
|
||||
}, joint.shapes.basic.Generic.prototype.defaults)
|
||||
});
|
||||
|
||||
function createCustomElement(renderFn, paper, metadata, props, position) {
|
||||
var node;
|
||||
var graph = paper.model;
|
||||
if (!position) {
|
||||
position = {x: 0, y: 0};
|
||||
}
|
||||
node = new joint.shapes.flo.GenericElement({
|
||||
position: position,
|
||||
renderFunction: renderFn,
|
||||
attrs: {
|
||||
'props': props,
|
||||
'metadata': metadata
|
||||
}
|
||||
});
|
||||
if (graph) {
|
||||
graph.addCell(node);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JointJS node that embeds extra metadata (properties).
|
||||
*/
|
||||
function createNode(params) {
|
||||
params = params || {};
|
||||
var renderService = params.renderService;
|
||||
var paper = params.paper;
|
||||
var metadata = params.metadata;
|
||||
var position = params.position;
|
||||
var props = params.props;
|
||||
var graph = params.graph || (params.paper ? params.paper.model : undefined);
|
||||
|
||||
var node;
|
||||
if (!props) {
|
||||
props = {};
|
||||
}
|
||||
if (!position) {
|
||||
position = {x: 0, y: 0};
|
||||
}
|
||||
if (renderService && _.isFunction(renderService.createNode)) {
|
||||
node = renderService.createNode(metadata, props);
|
||||
} else {
|
||||
node = new joint.shapes.flo.Node();
|
||||
node.attr('.label/text', metadata.name);
|
||||
}
|
||||
node.set('type', joint.shapes.flo.NODE_TYPE);
|
||||
node.set('position', position);
|
||||
node.attr('props', props);
|
||||
node.attr('metadata', metadata);
|
||||
if (graph) {
|
||||
graph.addCell(node);
|
||||
}
|
||||
if (renderService && _.isFunction(renderService.initializeNewNode)) {
|
||||
renderService.initializeNewNode(node, {
|
||||
'paper': paper,
|
||||
'graph': graph
|
||||
});
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function createLink(params) {
|
||||
params = params || {};
|
||||
var renderService = params.renderService;
|
||||
var paper = params.paper;
|
||||
var metadata = params.metadata;
|
||||
var source = params.source;
|
||||
var target = params.target;
|
||||
var props = params.props;
|
||||
var graph = params.graph || (params.paper ? params.paper.model : undefined);
|
||||
|
||||
var link;
|
||||
if (!props) {
|
||||
props = {};
|
||||
}
|
||||
if (renderService && _.isFunction(renderService.createLink)) {
|
||||
link = renderService.createLink(source, target, metadata, props);
|
||||
} else {
|
||||
link = new joint.shapes.flo.Link();
|
||||
}
|
||||
link.set('source', source);
|
||||
link.set('target', target);
|
||||
link.set('type', joint.shapes.flo.LINK_TYPE);
|
||||
if (metadata) {
|
||||
link.attr('metadata', metadata);
|
||||
}
|
||||
link.attr('props', props);
|
||||
if (graph) {
|
||||
graph.addCell(link);
|
||||
}
|
||||
if (renderService && _.isFunction(renderService.initializeNewLink)) {
|
||||
renderService.initializeNewLink(link, {
|
||||
'paper': paper,
|
||||
'graph': graph
|
||||
});
|
||||
}
|
||||
// prevent creation of link breaks
|
||||
link.attr('.marker-vertices/display', 'none');
|
||||
return link;
|
||||
}
|
||||
|
||||
function createDecoration(params) {
|
||||
params = params || {};
|
||||
var renderService = params.renderService;
|
||||
var paper = params.paper;
|
||||
var parent = params.parent;
|
||||
var kind = params.kind;
|
||||
var messages = params.messages;
|
||||
var location = params.position;
|
||||
var graph = params.graph || (params.paper ? params.paper.model : undefined);
|
||||
|
||||
if (!location) {
|
||||
location = {x: 0, y: 0};
|
||||
}
|
||||
var decoration;
|
||||
if (renderService && _.isFunction(renderService.createDecoration)) {
|
||||
decoration = renderService.createDecoration(kind, parent);
|
||||
} else {
|
||||
decoration = new joint.shapes.flo.ErrorDecoration({
|
||||
attrs: {
|
||||
image: { 'xlink:href': DECORATION_ICON_MAP[kind] },
|
||||
}
|
||||
});
|
||||
}
|
||||
decoration.set('type', joint.shapes.flo.DECORATION_TYPE);
|
||||
decoration.set('position', location);
|
||||
if ((isChrome || isFF) && parent && typeof parent.get('z') === 'number') {
|
||||
decoration.set('z', parent.get('z') + 1);
|
||||
}
|
||||
decoration.attr('./kind', kind);
|
||||
decoration.attr('messages', messages);
|
||||
if (graph) {
|
||||
graph.addCell(decoration);
|
||||
}
|
||||
parent.embed(decoration);
|
||||
if (renderService && _.isFunction(renderService.initializeNewDecoration)) {
|
||||
renderService.initializeNewDecoration(decoration, {
|
||||
'paper': paper,
|
||||
'graph': graph
|
||||
});
|
||||
}
|
||||
return decoration;
|
||||
}
|
||||
|
||||
function createHandle(params) {
|
||||
params = params || {};
|
||||
var renderService = params.renderService;
|
||||
var paper = params.paper;
|
||||
var parent = params.parent;
|
||||
var kind = params.kind;
|
||||
var location = params.position;
|
||||
var graph = params.graph || (params.paper ? params.paper.model : undefined);
|
||||
|
||||
var handle;
|
||||
if (!location) {
|
||||
location = {x: 0, y: 0};
|
||||
}
|
||||
if (renderService && _.isFunction(renderService.createHandle)) {
|
||||
handle = renderService.createHandle(kind, parent);
|
||||
} else {
|
||||
handle = new joint.shapes.flo.ErrorDecoration({
|
||||
size: HANDLE_SIZE,
|
||||
attrs: {
|
||||
'image': {
|
||||
'xlink:href': HANDLE_ICON_MAP[kind]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
handle.set('type', joint.shapes.flo.HANDLE_TYPE);
|
||||
handle.set('position', location);
|
||||
if ((isChrome || isFF) && parent && typeof parent.get('z') === 'number') {
|
||||
handle.set('z', parent.get('z') + 1);
|
||||
}
|
||||
handle.attr('./kind', kind);
|
||||
if (graph) {
|
||||
graph.addCell(handle);
|
||||
}
|
||||
parent.embed(handle);
|
||||
if (renderService && _.isFunction(renderService.initializeNewHandle)) {
|
||||
renderService.initializeNewHandle(handle, {
|
||||
'paper': paper,
|
||||
'graph': graph
|
||||
});
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
joint.shapes.flo.GenericElement = joint.shapes.basic.Generic.extend({
|
||||
defaults: joint.util.deepSupplement({
|
||||
type: 'flo.GenericElement',
|
||||
size: { width: IMAGE_W, height: IMAGE_H },
|
||||
renderFunction: joint.dia.ElementView.prototype.renderMarkup,
|
||||
}, joint.shapes.basic.Generic.prototype.defaults)
|
||||
});
|
||||
|
||||
joint.shapes.flo.GenericElementView = joint.dia.ElementView.extend({
|
||||
renderMarkup: function() {
|
||||
var renderFn = this.model.get('renderFunction');
|
||||
if (_.isFunction(renderFn)) {
|
||||
renderFn.call(this.model, this.el);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Gradient can only be applied to elements with non-0 size. Horizontal and vertical lines don't have any size.
|
||||
* Therefore, add a tiny value to target point x and y to ensure line is not completely vertical/horizontal
|
||||
*/
|
||||
joint.connectors.normalDimFix = function(sourcePoint, targetPoint, vertices) {
|
||||
var dimensionFix = 1e-3;
|
||||
var d = ['M', sourcePoint.x, sourcePoint.y];
|
||||
_.each(vertices, function(vertex) { d.push(vertex.x, vertex.y); });
|
||||
d.push(targetPoint.x + dimensionFix, targetPoint.y + dimensionFix);
|
||||
return d.join(' ');
|
||||
};
|
||||
|
||||
joint.shapes.flo.PatternLinkView = joint.dia.LinkView.extend({
|
||||
patternMarkup: '<pattern id="pattern-<%= id %>" patternUnits="userSpaceOnUse"></pattern>',
|
||||
initialize: function() {
|
||||
joint.dia.LinkView.prototype.initialize.apply(this, arguments);
|
||||
_.bindAll(this, 'fillWithPattern');
|
||||
},
|
||||
render: function() {
|
||||
joint.dia.LinkView.prototype.render.apply(this, arguments);
|
||||
|
||||
// make sure that pattern doesn't already exist
|
||||
if (!this.pattern) {
|
||||
this.pattern = joint.V(_.template(this.patternMarkup, { id: this.id }));
|
||||
joint.V(this.paper.svg).defs().append(this.pattern);
|
||||
}
|
||||
|
||||
// tell the '.connection' path to use the pattern
|
||||
var connection = joint.V(this.el).findOne('.connection').attr({
|
||||
stroke: 'url(#pattern-' + this.id + ')'
|
||||
});
|
||||
|
||||
// cache the stroke width
|
||||
this.strokeWidth = connection.attr('stroke-width') || 1;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
// make sure we stop an ongoing pattern update
|
||||
joint.util.cancelFrame(this.frameId);
|
||||
joint.dia.LinkView.prototype.remove.apply(this, arguments);
|
||||
this.pattern.remove();
|
||||
},
|
||||
|
||||
update: function() {
|
||||
joint.dia.LinkView.prototype.update.apply(this, arguments);
|
||||
joint.util.cancelFrame(this.frameId);
|
||||
this.frameId = joint.util.nextFrame(this.fillWithPattern);
|
||||
return this;
|
||||
},
|
||||
|
||||
fillWithPattern: function() {
|
||||
var strokeWidth = this.strokeWidth;
|
||||
// we get the bounding box of the linkView without the transformations
|
||||
// and expand it to all 4 sides by the stroke width
|
||||
// (making sure there is always enough room for drawing,
|
||||
// even if the bounding box was tiny.
|
||||
// Note that the bounding box doesn't include the stroke.)
|
||||
var bbox = joint.g.rect(joint.V(this.el).bbox(true)).moveAndExpand({
|
||||
x: - strokeWidth,
|
||||
y: - strokeWidth,
|
||||
width: 2 * strokeWidth,
|
||||
height: 2 * strokeWidth
|
||||
});
|
||||
|
||||
// create an array of all points the link goes through
|
||||
// (route doesn't contain the connection points)
|
||||
var points = [].concat(this.sourcePoint, this.route, this.targetPoint);
|
||||
|
||||
// transform all points to the links coordinate system
|
||||
points = _.map(points, function(point) {
|
||||
return joint.g.point(point.x - bbox.x, point.y - bbox.y);
|
||||
});
|
||||
|
||||
// iterate over the points and execute the drawing function
|
||||
// for each segment
|
||||
var elements = [];
|
||||
for (var i=0, pointsCount = points.length - 1; i < pointsCount; i++) {
|
||||
this.drawPattern.call(this, points[i], points[i+1], strokeWidth, i, elements);
|
||||
}
|
||||
|
||||
// Remove previous pattern if there is one
|
||||
while (this.pattern.node.firstChild) {
|
||||
this.pattern.node.removeChild(this.pattern.node.firstChild);
|
||||
}
|
||||
|
||||
// update the pattern dimensions and SVG contents
|
||||
this.pattern.attr(bbox);
|
||||
this.pattern.append(elements);
|
||||
},
|
||||
|
||||
// finds a gradient with perpendicular direction to a link segment
|
||||
gradientPoints: function(from, to, width) {
|
||||
|
||||
var angle = joint.g.toRad(from.theta(to) - 90);
|
||||
var center = joint.g.line(from, to).midpoint();
|
||||
var start = joint.g.point.fromPolar(width / 2, angle, center);
|
||||
var end = joint.g.point.fromPolar(width / 2, Math.PI + angle, center);
|
||||
return [start.x, start.y, end.x, end.y];
|
||||
},
|
||||
|
||||
// A drawing function executed for all links segments.
|
||||
drawPattern: function(from, to, width, i, elements) {
|
||||
var outlineWidth = 2;
|
||||
var innerWidth = width - outlineWidth;
|
||||
var outerWidth = width;
|
||||
var buttFrom = joint.g.point(from).move(to, -0.001);
|
||||
var buttTo = joint.g.point(to).move(from, -0.001);
|
||||
var nsSVG = 'http://www.w3.org/2000/svg';
|
||||
var gradientPoints = this.gradientPoints(from, to, width);
|
||||
|
||||
// Black background to paint the black outer border
|
||||
var background = document.createElementNS(nsSVG, 'path');
|
||||
background.setAttribute('stroke-width', outerWidth);
|
||||
background.setAttribute('stroke', 'rgba(52, 48, 45, 1.0)');
|
||||
background.setAttribute('d', ['M', from.x, from.y, to.x, to.y].join(' '));
|
||||
elements.push(joint.V(background));
|
||||
|
||||
// Construct gradient element for the pipe's inner contents
|
||||
var gradient = document.createElementNS(nsSVG, 'linearGradient');
|
||||
var gradientId = 'pattern-' + this.id + '-' + i;
|
||||
gradient.setAttribute('id', gradientId);
|
||||
gradient.setAttribute('gradientUnits', 'userSpaceOnUse');
|
||||
gradient.setAttribute('x1', gradientPoints[0]);
|
||||
gradient.setAttribute('y1', gradientPoints[1]);
|
||||
gradient.setAttribute('x2', gradientPoints[2]);
|
||||
gradient.setAttribute('y2', gradientPoints[3]);
|
||||
var stop1 = document.createElementNS(nsSVG, 'stop');
|
||||
stop1.setAttribute('offset', '0%');
|
||||
stop1.setAttribute('style', 'stop-color:rgba(52, 48, 45, 1.0)');
|
||||
gradient.appendChild(stop1);
|
||||
var stop2 = document.createElementNS(nsSVG, 'stop');
|
||||
stop2.setAttribute('offset', '50%');
|
||||
stop2.setAttribute('style', 'stop-color:white');
|
||||
gradient.appendChild(stop2);
|
||||
var stop3 = document.createElementNS(nsSVG, 'stop');
|
||||
stop3.setAttribute('offset', '100%');
|
||||
stop3.setAttribute('style', 'stop-color:rgba(52, 48, 45, 1.0)');
|
||||
gradient.appendChild(stop3);
|
||||
elements.push(joint.V(gradient));
|
||||
|
||||
// Inner contents of the pipe: path filled with gradient
|
||||
var interior = document.createElementNS(nsSVG, 'path');
|
||||
interior.setAttribute('stroke-width', innerWidth);
|
||||
interior.setAttribute('stroke', 'url(#' + gradientId +')');
|
||||
interior.setAttribute('d', joint.connectors.normalDimFix(from, to));
|
||||
elements.push(joint.V(interior));
|
||||
|
||||
// Rectangle at the start of the pipe
|
||||
var sourceEnd = document.createElementNS(nsSVG, 'path');
|
||||
sourceEnd.setAttribute('stroke-linecap', 'round');
|
||||
sourceEnd.setAttribute('stroke-width', outerWidth);
|
||||
sourceEnd.setAttribute('stroke', 'rgba(52, 48, 45, 1.0)');
|
||||
sourceEnd.setAttribute('d', ['M', from.x, from.y, buttFrom.x, buttFrom.y].join(' '));
|
||||
elements.push(joint.V(sourceEnd));
|
||||
|
||||
// Rectangle at the end of the pipe
|
||||
var targetEnd = document.createElementNS(nsSVG, 'path');
|
||||
targetEnd.setAttribute('stroke-linecap', 'round');
|
||||
targetEnd.setAttribute('stroke-width', outerWidth);
|
||||
targetEnd.setAttribute('stroke', 'rgba(52, 48, 45, 1.0)');
|
||||
targetEnd.setAttribute('d', ['M', to.x, to.y, buttTo.x, buttTo.y].join(' '));
|
||||
elements.push(joint.V(targetEnd));
|
||||
|
||||
// Arrow in the middle of the pipe
|
||||
var mid = joint.g.line(from,to).midpoint();
|
||||
var arrowStart = joint.g.point(mid).move(from, -15);
|
||||
var arrowEnd = joint.g.point(mid).move(to, -15);
|
||||
var arrow = document.createElementNS(nsSVG, 'path');
|
||||
var edgePoints = this.gradientPoints(from, to, innerWidth - 2);
|
||||
arrow.setAttribute('stroke-width', 1);
|
||||
arrow.setAttribute('stroke-linejoin', 'miter');
|
||||
arrow.setAttribute('stroke', 'black');
|
||||
arrow.setAttribute('fill', 'black');
|
||||
arrow.setAttribute('d', ['M', arrowStart.x, arrowStart.y, mid.x, mid.y, edgePoints[0], edgePoints[1], arrowEnd.x, arrowEnd.y, edgePoints[2], edgePoints[3], mid.x, mid.y].join(' '));
|
||||
elements.push(joint.V(arrow));
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
'createNode': createNode,
|
||||
'createLink': createLink,
|
||||
'createDecoration': createDecoration,
|
||||
'createHandle': createHandle,
|
||||
'createCustomElement' : createCustomElement
|
||||
};
|
||||
});
|
||||
36
tests/.jshintrc
Normal file
36
tests/.jshintrc
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"esnext": true,
|
||||
"bitwise": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"indent": 2,
|
||||
"latedef": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
"regexp": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"strict": true,
|
||||
"trailing": true,
|
||||
"smarttabs": true,
|
||||
"globals": {
|
||||
"after": false,
|
||||
"afterEach": false,
|
||||
"angular": false,
|
||||
"before": false,
|
||||
"beforeEach": false,
|
||||
"browser": false,
|
||||
"describe": false,
|
||||
"expect": false,
|
||||
"inject": false,
|
||||
"it": false,
|
||||
"jasmine": false,
|
||||
"spyOn": false,
|
||||
"define": true
|
||||
}
|
||||
}
|
||||
89
tests/karma-test-main.js
Normal file
89
tests/karma-test-main.js
Normal file
@@ -0,0 +1,89 @@
|
||||
var tests = Object.keys(window.__karma__.files).filter(function (file) {
|
||||
return /spec\.js$/.test(file);
|
||||
});
|
||||
|
||||
requirejs.config({
|
||||
// Karma serves files from '/base'
|
||||
|
||||
paths: {
|
||||
joint: '/base/lib/joint/joint',
|
||||
backbone: '/base/lib/backbone/backbone',
|
||||
|
||||
text : '/base/lib/requirejs-text/text',
|
||||
angular: '/base/lib/angular/angular',
|
||||
jquery: '/base/lib/jquery/jquery',
|
||||
bootstrap: '/base/lib/bootstrap/bootstrap',
|
||||
lodash: '/base/lib/lodash/lodash.compat',
|
||||
dagre: '/base/lib/dagre/dagre.core.min',
|
||||
graphlib: '/base/lib/graphlib/graphlib.core',
|
||||
angularMocks: '/base/lib/angular-mocks/angular-mocks',
|
||||
jshint: '/base/lib/jshint/dist/jshint',
|
||||
|
||||
flo: '/base/dist/spring-flo',
|
||||
metamodelService: '/base/tests/resources/metamodel-service',
|
||||
'editor-service': '/base/tests/resources/editor-service',
|
||||
'graph-to-text': '/base/tests/resources/graph-to-text',
|
||||
'render-service': '/base/tests/resources/render-service'
|
||||
|
||||
},
|
||||
map: {
|
||||
'*': {
|
||||
// Backbone requires underscore. This forces requireJS to load lodash instead:
|
||||
'underscore': 'lodash'
|
||||
}
|
||||
},
|
||||
shim: {
|
||||
angular: {
|
||||
deps: ['bootstrap'],
|
||||
exports: 'angular'
|
||||
},
|
||||
'angularMocks': {
|
||||
deps:['angular'],
|
||||
'exports':'angular.mock'
|
||||
},
|
||||
bootstrap: {
|
||||
deps: ['jquery']
|
||||
},
|
||||
graphlib: {
|
||||
deps: ['underscore']
|
||||
},
|
||||
dagre: {
|
||||
deps: ['graphlib', 'underscore']
|
||||
},
|
||||
|
||||
backbone: {
|
||||
//These script dependencies should be loaded before loading backbone.js.
|
||||
deps: ['underscore', 'jquery'],
|
||||
},
|
||||
joint: {
|
||||
deps: ['jquery', 'underscore', 'backbone'],
|
||||
},
|
||||
underscore: {
|
||||
exports: '_'
|
||||
},
|
||||
jshint: {
|
||||
deps: ['lodash']
|
||||
},
|
||||
flo: {
|
||||
deps: ['angular', 'jquery', 'joint', 'underscore']
|
||||
},
|
||||
metamodelService: {
|
||||
deps: [ 'flo', 'graph-to-text' ]
|
||||
},
|
||||
'editor-service': {
|
||||
deps: [ 'flo' ]
|
||||
},
|
||||
'render-service': {
|
||||
deps: [ 'flo' ]
|
||||
},
|
||||
'graph2text': {
|
||||
deps: [ 'joint' ]
|
||||
}
|
||||
},
|
||||
|
||||
// ask Require.js to load these files (all our tests)
|
||||
deps: tests,
|
||||
|
||||
// start test run, once Require.js is done
|
||||
callback: window.__karma__.start
|
||||
});
|
||||
539
tests/resources/editor-service.js
Normal file
539
tests/resources/editor-service.js
Normal file
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var joint = require('joint');
|
||||
var app = require('flo');
|
||||
|
||||
app.factory('sample-editor-service', [function() {
|
||||
|
||||
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) { // jshint ignore:line
|
||||
// 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;
|
||||
}
|
||||
// var message = validateLinkEdit(flo.getGraph(), (cellViewS ? cellViewS.model : null), (cellViewT ? cellViewT.model : null), linkView.model, end);
|
||||
// return !message;
|
||||
return cellViewS.model && cellViewT.model && !(cellViewS.model instanceof joint.shapes.flo.ErrorDecoration) && !(cellViewT.model instanceof joint.shapes.flo.ErrorDecoration);
|
||||
}
|
||||
|
||||
// function validateLinkEdit(graph, source, target, link, end) {
|
||||
// var newOutgoing = 0, newIncoming = 0;
|
||||
// if (end === 'source') {
|
||||
// newOutgoing = 1;
|
||||
// } else if (end === 'target') {
|
||||
// newIncoming = 1;
|
||||
// } else if (end === 'addition') {
|
||||
// newOutgoing = 1;
|
||||
// newIncoming = 1;
|
||||
// } else if (end === 'deletion') {
|
||||
// newOutgoing = -1;
|
||||
// newIncoming = -1;
|
||||
// }
|
||||
// var outboundLinks = graph.getConnectedLinks(source, { outbound: true });
|
||||
// var inboundLinks = graph.getConnectedLinks(source, { inbound: true });
|
||||
// var sourceMaxOutgoingLinksNumber = source.attr('metadata/constraints/maxOutgoingLinksNumber');
|
||||
// var sourceMinOutgoingLinksNumber = source.attr('metadata/constraints/minOutgoingLinksNumber');
|
||||
// var targetMaxIncomingLinksNumber = target.attr('metadata/constraints/maxIncomingLinksNumber');
|
||||
// var targetMinIncomingLinksNumber = target.attr('metadata/constraints/minIncomingLinksNumber');
|
||||
// if (typeof sourceMaxOutgoingLinksNumber === 'number' && sourceMaxOutgoingLinksNumber < outboundLinks.length + newOutgoing) {
|
||||
// return 'Source cannot have more than ' + sourceMaxOutgoingLinksNumber + ' outgoing links';
|
||||
// }
|
||||
// if (typeof sourceMinOutgoingLinksNumber === 'number' && sourceMinOutgoingLinksNumber < outboundLinks.length + newOutgoing) {
|
||||
// return 'Source cannot have less than ' + sourceMinOutgoingLinksNumber + ' outgoing links';
|
||||
// }
|
||||
// if (typeof targetMaxIncomingLinksNumber === 'number' && targetMaxIncomingLinksNumber < inboundLinks.length + newIncoming) {
|
||||
// return 'Target cannot have more than ' + targetMaxIncomingLinksNumber + ' incoming links';
|
||||
// }
|
||||
// if (typeof targetMinIncomingLinksNumber === 'number' && targetMinIncomingLinksNumber < outboundLinks.length + newIncoming) {
|
||||
// return 'Source cannot have less than ' + targetMinIncomingLinksNumber + ' incoming links';
|
||||
// }
|
||||
// }
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function preDelete(flo, cell) {
|
||||
repairDamage(flo, cell);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
console.log('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;
|
||||
}
|
||||
|
||||
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 handleNodeDropping(flo, dragDescriptor) {
|
||||
var relinking = dragDescriptor.context && dragDescriptor.context.palette;
|
||||
var graph = flo.getGraph();
|
||||
var source = dragDescriptor.source ? dragDescriptor.source.cell : undefined;
|
||||
var target = dragDescriptor.target ? dragDescriptor.target.cell : undefined;
|
||||
if (target instanceof joint.dia.Element && target.attr('metadata/name')) { // jshint ignore:line
|
||||
var type = source.attr('metadata/name');
|
||||
if (type === 'tap') {
|
||||
// Fill in the channel on the new node
|
||||
// work out tap name, something like: tap:stream:mystream.filter (filter optional if on head of stream)
|
||||
// 1. work your way back from node that was dropped on to the first node (the one with no further links)
|
||||
var oncell = target;
|
||||
var headerCell = oncell;
|
||||
var incomingLinks = graph.getConnectedLinks(oncell, { inbound: true });
|
||||
while (incomingLinks.length !== 0) {
|
||||
headerCell = graph.getCell(incomingLinks[0].get('source').id);
|
||||
incomingLinks = graph.getConnectedLinks(headerCell, { inbound: true });
|
||||
}
|
||||
var streamname = 'STREAM';
|
||||
if (headerCell.attr('stream-name')) {
|
||||
streamname = headerCell.attr('stream-name');
|
||||
}
|
||||
else {
|
||||
var streamId = headerCell.attr('stream-id');
|
||||
if (streamId) {
|
||||
streamname = 'STREAM'+streamId;
|
||||
} else {
|
||||
streamname = 'STREAM';
|
||||
}
|
||||
headerCell.attr('stream-name',streamname);
|
||||
}
|
||||
var channel = 'tap:stream:'+streamname;
|
||||
var label2 = 'tap:stream:\n' + streamname;
|
||||
if (oncell.id !== headerCell.id) {
|
||||
channel = channel + '.' + oncell.attr('.label/text');
|
||||
label2 = label2 + '.' + oncell.attr('.label/text');
|
||||
}
|
||||
source.attr('.label2/text', label2);
|
||||
source.attr('props/channel', channel);
|
||||
relinking = true;
|
||||
} else {
|
||||
if (dragDescriptor.target.selector === '.output-port') {
|
||||
moveNodeOnNode(flo, source, target, 'right', true);
|
||||
relinking = true;
|
||||
} else if (dragDescriptor.target.selector === '.input-port') {
|
||||
moveNodeOnNode(flo, source, target, 'left', true);
|
||||
relinking = true;
|
||||
}
|
||||
}
|
||||
} else if (target instanceof joint.dia.Link) { // jshint ignore:line
|
||||
moveNodeOnLink(flo, source, target);
|
||||
relinking = true;
|
||||
}
|
||||
// Turn off auto layout
|
||||
// if (relinking) {
|
||||
// flo.performLayout();
|
||||
// }
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return {
|
||||
'createHandles': createHandles,
|
||||
'validatePort': validatePort,
|
||||
'validateLink': validateLink,
|
||||
'calculateDragDescriptor': calculateDragDescriptor,
|
||||
'handleNodeDropping': handleNodeDropping,
|
||||
'validateNode': validateNode,
|
||||
'preDelete': preDelete,
|
||||
'interactive': {
|
||||
'vertexAdd': false
|
||||
},
|
||||
'allowLinkVertexEdit': false
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
return app;
|
||||
|
||||
});
|
||||
241
tests/resources/graph-to-text.js
Normal file
241
tests/resources/graph-to-text.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Graph representation of stream(s)/module(s).
|
||||
* @type {joint.dia.Graph}
|
||||
*/
|
||||
var g;
|
||||
|
||||
/**
|
||||
* Number of Links left to visit
|
||||
* @type {number}
|
||||
*/
|
||||
var numberOfLinksToVisit;
|
||||
|
||||
/**
|
||||
* Number of nodes left to visit
|
||||
* @type {number}
|
||||
*/
|
||||
var numberOfNodesToVisit;
|
||||
|
||||
/**
|
||||
* Links left to visit indexed by id
|
||||
* @type {Object.<string,{joint.dia.Link}>}
|
||||
*/
|
||||
var linksToVisit;
|
||||
|
||||
/**
|
||||
* Nodes left to visit indexed by id
|
||||
* @type {Object.<string,{joint.dia.Element}>}
|
||||
*/
|
||||
var nodesToVisit;
|
||||
|
||||
/**
|
||||
* Nodes incoming non-visited links degrees index by node id
|
||||
* @type {Object.<string,number>}
|
||||
*/
|
||||
var nodesInDegrees;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Priority:
|
||||
// 1. find links whose source has no other links pointing at it
|
||||
// 2. find links whose source has already been processed
|
||||
// 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 init(graph) {
|
||||
numberOfLinksToVisit = 0;
|
||||
numberOfNodesToVisit = 0;
|
||||
linksToVisit = {};
|
||||
nodesToVisit = {};
|
||||
nodesInDegrees = {};
|
||||
g = graph;
|
||||
g.getElements().forEach(function(element) {
|
||||
if (element.attr('metadata/name')) {
|
||||
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++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isChannel(e) {
|
||||
return e && (e.attr('metadata/name') === 'tap' || e.attr('metadata/name') === 'named-channel');
|
||||
}
|
||||
|
||||
function isJobDefinition(e) {
|
||||
return e && e.attr('metadata/group') === 'job definition';
|
||||
}
|
||||
|
||||
function node2text(element, first) {
|
||||
var text = '';
|
||||
var props = element.attr('props');
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
if (element.attr('stream-name')) {
|
||||
text += element.attr('stream-name') + '=';
|
||||
}
|
||||
}
|
||||
if ('job definition' === element.attr('metadata/group')) {
|
||||
// expressed as a queue
|
||||
if (first) {
|
||||
text += 'tap:job:' + element.attr('metadata/name');
|
||||
if (props) {
|
||||
Object.keys(props).forEach(function(propertyName) {
|
||||
var prop = props[propertyName];
|
||||
if (prop && prop.length !== 0) {
|
||||
text += '.' + props[propertyName];
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
text += 'queue:job:' + element.attr('metadata/name');
|
||||
}
|
||||
} else if ('tap' === element.attr('metadata/name') || 'named-channel' === element.attr('metadata/name')) {
|
||||
// if ('tap' === element.attr('metadata/name')) {
|
||||
// text += 'tap ';
|
||||
// }
|
||||
if (props.channel) {
|
||||
text += props.channel;
|
||||
}
|
||||
} else {
|
||||
if (element.attr('.label/text') && element.attr('.label/text') !== element.attr('metadata/name')) {
|
||||
text += element.attr('.label/text') + ': ';
|
||||
}
|
||||
text += element.attr('metadata/name');
|
||||
if (props) {
|
||||
Object.keys(props).forEach(function(propertyName) {
|
||||
text += ' --' + propertyName + '=' + props[propertyName];
|
||||
});
|
||||
}
|
||||
}
|
||||
visit(element);
|
||||
return text;
|
||||
}
|
||||
|
||||
function stream2text(link) {
|
||||
var text = '';
|
||||
var source = g.getCell(link.get('source').id);
|
||||
text += node2text(source, true);
|
||||
while (link) {
|
||||
var target = g.getCell(link.get('target').id);
|
||||
text += (isChannel(source) || isChannel(target) ||
|
||||
isJobDefinition(source) || isJobDefinition(target)) ? ' > '
|
||||
: ' | ';
|
||||
text += node2text(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;
|
||||
}
|
||||
|
||||
function appendStreamText(text, stream) {
|
||||
if (stream) {
|
||||
if (text) {
|
||||
text += '\n';
|
||||
}
|
||||
text += stream;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the stream(s)/module(s) definition(s) from graph form into text form
|
||||
* @param {joint.dia.Graph} g Graph form of stream(s) and or module(s)
|
||||
* @return {string} Textual form
|
||||
*/
|
||||
return function(g) {
|
||||
var text = '';
|
||||
var streamText;
|
||||
var node;
|
||||
var id;
|
||||
|
||||
init(g);
|
||||
/*
|
||||
* Visit all connected nodes via links
|
||||
*/
|
||||
while (numberOfLinksToVisit) {
|
||||
streamText = stream2text(nextLink());
|
||||
text = appendStreamText(text, streamText);
|
||||
}
|
||||
/*
|
||||
* Visit all disconnected nodes
|
||||
*/
|
||||
for (id in nodesToVisit) {
|
||||
node = nodesToVisit[id];
|
||||
streamText = node2text(nodesToVisit[id], true);
|
||||
if (streamText && isChannel(node)) {
|
||||
streamText += ' > ';
|
||||
}
|
||||
text = appendStreamText(text, streamText);
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
});
|
||||
293
tests/resources/metamodel-service.js
Normal file
293
tests/resources/metamodel-service.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var angular = require('angular');
|
||||
var app = require('flo');
|
||||
|
||||
var convertGraphToText = require('graph-to-text');
|
||||
|
||||
app.factory('StandaloneMetamodel', ['$http', '$q', '$timeout', '$log', 'MetamodelUtils', function($http, $q, $timeout, $log, metamodelUtils) {
|
||||
|
||||
/**
|
||||
* List of listeners to metamodel changes
|
||||
* Each listener may have the following functions defined: metadataChanged(oldData, newData), metadataRefresh(), metadataError()
|
||||
* @type {Object}
|
||||
*/
|
||||
var listeners = [];
|
||||
|
||||
var metamodel;
|
||||
|
||||
/**
|
||||
* Internally stored metamodel load promise
|
||||
*/
|
||||
var request;
|
||||
|
||||
function subscribe(listener) {
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
function unsubscribe(listener) {
|
||||
var index = listeners.indexOf(listener);
|
||||
if (index >= 0) {
|
||||
listeners.splice(index);
|
||||
}
|
||||
}
|
||||
|
||||
function createMetadata(entry) {
|
||||
var props = {};
|
||||
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 refresh() {
|
||||
listeners.forEach(function(listener) {
|
||||
if (angular.isFunction(listener.metadataRefresh)) {
|
||||
listener.metadataRefresh();
|
||||
}
|
||||
});
|
||||
var deferred = $q.defer();
|
||||
$http.get('/metamodel').success(function(json) {
|
||||
var newData = {};
|
||||
if (Array.isArray(json)) {
|
||||
json.forEach(function(data) {
|
||||
var metadata = createMetadata(data);
|
||||
if (!newData[metadata.group]) {
|
||||
newData[metadata.group] = {};
|
||||
}
|
||||
newData[metadata.group][metadata.name] = metadata;
|
||||
});
|
||||
}
|
||||
var change = {
|
||||
newData: newData,
|
||||
oldData: metamodel
|
||||
};
|
||||
metamodel = newData;
|
||||
listeners.forEach(function(listener) {
|
||||
if (angular.isFunction(listener.metadataChanged)) {
|
||||
listener.metadataChanged(change);
|
||||
}
|
||||
});
|
||||
deferred.resolve(metamodel);
|
||||
}).error(function(data, status, headers, config) {
|
||||
listeners.forEach(function(listener) {
|
||||
if (angular.isFunction(listener.metadataError)) {
|
||||
listener.metadataError(data, status, headers, config);
|
||||
}
|
||||
});
|
||||
deferred.reject(data);
|
||||
});
|
||||
request = deferred.promise;
|
||||
return request;
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (!request) {
|
||||
refresh();
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
function parseAndRefreshGraph(definitionText,updateGraphFn,setErrorFn) {
|
||||
return $http.get('/parse', { params: {'text': definitionText}}).success(function(data) {
|
||||
if (angular.isFunction(setErrorFn)) {
|
||||
setErrorFn(null);
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
data = angular.fromJson(data);
|
||||
}
|
||||
// TODO handle error case, clear the graph
|
||||
$log.info('parse responded with data:\''+JSON.stringify(data)+'\'');
|
||||
if (angular.isFunction(updateGraphFn)) {
|
||||
updateGraphFn(data);
|
||||
}
|
||||
}).error(function(data, status, headers, config, statusText) { // jshint ignore:line
|
||||
if (typeof data === 'string') {
|
||||
data = angular.fromJson(data);
|
||||
}
|
||||
if (angular.isFunction(setErrorFn)) {
|
||||
setErrorFn(data);
|
||||
}
|
||||
|
||||
$log.info(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the JSON description of the flow as provided by the server and map it into a series
|
||||
* of nodes that can be processed by dagre/joint.
|
||||
*/
|
||||
function buildGraphFromJson(flo, jsonFormatData, metamodel) {
|
||||
var inputnodes = jsonFormatData.nodes;
|
||||
var inputlinks = jsonFormatData.links;
|
||||
|
||||
var incoming = {};
|
||||
var outgoing = {};
|
||||
var link;
|
||||
for (var i = 0; i < inputlinks.length; i++) {
|
||||
link = inputlinks[i];
|
||||
if (typeof link.from === 'number') {
|
||||
if (typeof outgoing[link.from] !== 'number') {
|
||||
outgoing[link.from] = 0;
|
||||
}
|
||||
outgoing[link.from]++;
|
||||
}
|
||||
if (typeof link.to === 'number') {
|
||||
if (typeof incoming[link.to] !== 'number') {
|
||||
incoming[link.to] = 0;
|
||||
}
|
||||
incoming[link.to]++;
|
||||
}
|
||||
}
|
||||
|
||||
var inputnodesCount = inputnodes ? inputnodes.length : 0;
|
||||
var nodesIndex = [];
|
||||
for (var n = 0; n < inputnodesCount; n++) {
|
||||
var name = inputnodes[n].name;
|
||||
var label = inputnodes[n].label || inputnodes[n].name;
|
||||
var group = inputnodes[n].group;
|
||||
if (!group) {
|
||||
group = metamodelUtils.matchGroup(metamodel, name, incoming[n], outgoing[n]);
|
||||
}
|
||||
var newNode = flo.createNode(metamodelUtils.getMetadata(metamodel, name, group), inputnodes[n].properties);
|
||||
newNode.attr('.label/text', label);
|
||||
var streamname = inputnodes[n]['stream-name'];
|
||||
if (streamname) {
|
||||
newNode.attr('stream-name',streamname);
|
||||
}
|
||||
if (inputnodes[n].range) {
|
||||
newNode.attr('range',inputnodes[n].range);
|
||||
}
|
||||
if (inputnodes[n].propertiesranges) {
|
||||
newNode.attr('propertiesranges',inputnodes[n].propertiesranges);
|
||||
}
|
||||
if (inputnodes[n]['stream-id']) {
|
||||
newNode.attr('stream-id',inputnodes[n]['stream-id']);
|
||||
}
|
||||
nodesIndex.push(newNode.id);
|
||||
}
|
||||
// var nextId = inputnodesCount; // For dropped nodes, they will start getting this ID
|
||||
|
||||
var inputlinksCount = inputlinks?inputlinks.length:0;
|
||||
// $log.info('Links ' + inputlinksCount);
|
||||
for (var l = 0; l < inputlinksCount; l++) {
|
||||
link = inputlinks[l];
|
||||
flo.createLink({
|
||||
'id': nodesIndex[link.from],
|
||||
'selector': '.output-port'
|
||||
}, {
|
||||
'id': nodesIndex[link.to],
|
||||
'selector': '.input-port'
|
||||
});
|
||||
}
|
||||
|
||||
flo.performLayout();
|
||||
|
||||
flo.fitToPage();
|
||||
}
|
||||
|
||||
function textToGraph(flo, definition) {
|
||||
return parseAndRefreshGraph(definition.text, function(json) {
|
||||
flo.getGraph().clear();
|
||||
load().then(function(metamodel) {
|
||||
buildGraphFromJson(flo, json, metamodel);
|
||||
});
|
||||
},function(errors) {
|
||||
definition.parseError = errors;
|
||||
});
|
||||
}
|
||||
|
||||
function graphToText(flo, definition) {
|
||||
definition.text = convertGraphToText(flo.getGraph());
|
||||
return $timeout(function() {
|
||||
return parseAndRefreshGraph(definition.text, null, function(errors) {
|
||||
definition.parseError = errors;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isValidPropertyValue(model, property, value) {
|
||||
var type = model.attr('metadata/name');
|
||||
if (type === 'tap' && property === 'channel') {
|
||||
return value.indexOf('tap:stream:') === 0 || value.indexOf('tap:job:') === 0;
|
||||
}
|
||||
if (type === 'named-channel' && property === 'channel') {
|
||||
return value.indexOf('queue:') === 0 || value.indexOf('topic:') === 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function encodeTextToDSL(value) {
|
||||
return value.replace(/(?:\r\n|\r|\n)/g, '\\n');
|
||||
}
|
||||
|
||||
function decodeTextFromDSL(value) {
|
||||
return value.replace(/\\n/g, '\n');
|
||||
}
|
||||
|
||||
return {
|
||||
'subscribe': subscribe,
|
||||
'unsubscribe': unsubscribe,
|
||||
'refresh': refresh,
|
||||
'load': load,
|
||||
'textToGraph': textToGraph,
|
||||
'graphToText': graphToText,
|
||||
'isValidPropertyValue': isValidPropertyValue,
|
||||
'encodeTextToDSL': encodeTextToDSL,
|
||||
'decodeTextFromDSL': decodeTextFromDSL
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
return app;
|
||||
|
||||
});
|
||||
942
tests/resources/metamodel.json
Executable file
942
tests/resources/metamodel.json
Executable file
@@ -0,0 +1,942 @@
|
||||
[
|
||||
{
|
||||
"name":"http",
|
||||
"group":"source",
|
||||
"description":"HTTP input source",
|
||||
"properties":[
|
||||
{
|
||||
"id":"server-port",
|
||||
"name":"port",
|
||||
"defaultValue":"9000",
|
||||
"description":"The http port where data will be posted"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"mail",
|
||||
"group":"source",
|
||||
"description":"Input source for receiving emails",
|
||||
"properties":[
|
||||
{
|
||||
"name":"protocol",
|
||||
"defaultValue":"imaps",
|
||||
"description":"the protocol to use amongst pop3, pop3s, imap, imaps (only imap variants for the imap module)"
|
||||
},
|
||||
{
|
||||
"name":"username",
|
||||
"description":"the username to use to connect to the mail server"
|
||||
},
|
||||
{
|
||||
"name":"password",
|
||||
"description":"the password to use to connect to the mail server"
|
||||
},
|
||||
{
|
||||
"name":"host",
|
||||
"defaultValue":"localhost",
|
||||
"description":"the hostname of the mail server (default: localhost)"
|
||||
},
|
||||
{
|
||||
"name":"port",
|
||||
"description":"the port of the mail server"
|
||||
},
|
||||
{
|
||||
"name":"folder",
|
||||
"defaultValue":"INBOX",
|
||||
"description":"the folder to take emails from"
|
||||
},
|
||||
{
|
||||
"name":"markAsRead",
|
||||
"defaultValue":false,
|
||||
"description":"whether to mark emails as read once they?ve been fetched by the module"
|
||||
},
|
||||
{
|
||||
"name":"delete",
|
||||
"defaultValue":true,
|
||||
"description":"whether to delete the emails once they?ve been fetched by the module"
|
||||
},
|
||||
{
|
||||
"name":"fixedDelay",
|
||||
"defaultValue":60,
|
||||
"description":"Does not apply to the imap source, the polling interval used for looking up messages, expressed in seconds."
|
||||
},
|
||||
{
|
||||
"name":"charset",
|
||||
"defaultValue":"UTF-8",
|
||||
"description":"the charset used to transform the body of the incoming emails to Strings."
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"file",
|
||||
"group":"source",
|
||||
"description":"Stream the data from a file on the host OS",
|
||||
"properties":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"file name"
|
||||
},
|
||||
{
|
||||
"name":"dir",
|
||||
"description":"directory name"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"twittersearch",
|
||||
"group":"source",
|
||||
"description":"The twittersearch source runs a continuous query against Twitter",
|
||||
"properties":[
|
||||
{
|
||||
"name":"connectTimeout",
|
||||
"description":"the connection timeout for making a connection to Twitter (ms)",
|
||||
"defaultValue":5000
|
||||
},
|
||||
{
|
||||
"name":"query",
|
||||
"description":"the query that will run against twitter, e.g. #springeone2gx",
|
||||
"defaultValue":""
|
||||
},
|
||||
{
|
||||
"name":"consumerKey",
|
||||
"description":"an application consumer key issued by twitter"
|
||||
},
|
||||
{
|
||||
"name":"consumerSecret",
|
||||
"description":"the secret corresponding to the consumerKey"
|
||||
},
|
||||
{
|
||||
"name":"geocode",
|
||||
"description":"geo-location given as latitude,longitude,radius. e.g., \"37.781157,-122.398720,1mi\"",
|
||||
"defaultValue":""
|
||||
},
|
||||
{
|
||||
"name":"icludeEntities",
|
||||
"description":"whether to include entities such as urls, media and hashtags",
|
||||
"defaultValue":true
|
||||
},
|
||||
{
|
||||
"name":"language",
|
||||
"description":"language code e.g. \"en\"",
|
||||
"defaultValue":""
|
||||
},
|
||||
{
|
||||
"name":"readTimeout",
|
||||
"description":"the read timeout for the underlying URLConnection to the twitter stream (ms)",
|
||||
"defaultValue":9000
|
||||
},
|
||||
{
|
||||
"name":"resultType",
|
||||
"description":"result type: recent, popular, or mixed",
|
||||
"defaultValue":"mixed"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"time",
|
||||
"group":"source",
|
||||
"description":"Emits time ticks",
|
||||
"properties":[
|
||||
{
|
||||
"name":"fixedDelay",
|
||||
"description":"how often to emit a message, expressed in seconds",
|
||||
"defaultValue":1
|
||||
},
|
||||
{
|
||||
"name":"format",
|
||||
"description":"how to render the current time, using SimpleDateFormat",
|
||||
"defaultValue":"yyyy-MM-dd HH:mm:ss"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"tcp",
|
||||
"group":"source",
|
||||
"description":"The tcp source acts as a server and allows a remote party to connect to XD and submit data over a raw tcp socket.",
|
||||
"properties":[
|
||||
{
|
||||
"name":"bufferSize",
|
||||
"description":"the size of the buffer (bytes) to use when encoding/decoding",
|
||||
"defaultValue":2048
|
||||
},
|
||||
{
|
||||
"name":"charset",
|
||||
"description":"the charset used when converting from bytes to String",
|
||||
"defaultValue":"UTF-8"
|
||||
},
|
||||
{
|
||||
"name":"decoder",
|
||||
"description":"the decoder to use when receiving messages",
|
||||
"defaultValue":"CRLF"
|
||||
},
|
||||
{
|
||||
"name":"nio",
|
||||
"description":"whether or not to use NIO",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"port",
|
||||
"description":"the port on which to listen",
|
||||
"defaultValue":1234
|
||||
},
|
||||
{
|
||||
"name":"reverseLookup",
|
||||
"description":"perform a reverse DNS lookup on the remote IP Address",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"socketTimeout",
|
||||
"description":"the timeout (ms) before closing the socket when no data is received",
|
||||
"defaultValue":120000
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"tail",
|
||||
"group":"source",
|
||||
"description":"Tail file input source",
|
||||
"properties":[
|
||||
{
|
||||
"name":"delay",
|
||||
"description":"how often (ms) to poll for new lines (forces use of the Apache Tailer, requires nativeOptions=\"\")"
|
||||
},
|
||||
{
|
||||
"name":"fileDelay",
|
||||
"description":"on platforms that do not wait for a missing file to appear, how often (ms) to look for the file",
|
||||
"defaultValue":5000
|
||||
},
|
||||
{
|
||||
"name":"fromEnd",
|
||||
"description":"whether to tail from the end (true) or from the start (false) of the file (forces use of the Apache Tailer, requires nativeOptions=\"\")"
|
||||
},
|
||||
{
|
||||
"name":"lines",
|
||||
"description":"the number of lines prior to the end of an existing file to tail; does not apply if \"nativeOptions\" is provided",
|
||||
"defaultValue":0
|
||||
},
|
||||
{
|
||||
"name":"name",
|
||||
"description":"the absolute path of the file to tail",
|
||||
"defaultValue":"/tmp/xd/input/<stream name>"
|
||||
},
|
||||
{
|
||||
"name":"nativeOptions",
|
||||
"description":"options for a native tail command; do not set and use \"end\", \"delay\", and/or \"reOpen\" to use the Apache Tailer"
|
||||
},
|
||||
{
|
||||
"name":"reOpen",
|
||||
"description":"whether to reopen the file each time it is polled (forces use of the Apache Tailer, requires nativeOptions=\"\""
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"mqtt",
|
||||
"group":"source",
|
||||
"description":"The mqtt source connects to an mqtt server and receives telemetry messages",
|
||||
"properties":[
|
||||
{
|
||||
"name":"clientId",
|
||||
"description":"identifies the client",
|
||||
"defaultValue":"xd.mqtt.client.id.src"
|
||||
},
|
||||
{
|
||||
"name":"password",
|
||||
"description":"the password to use when connecting to the broker",
|
||||
"defaultValue":"guest"
|
||||
},
|
||||
{
|
||||
"name":"topics",
|
||||
"description":"the topic(s) to which the source will subscribe",
|
||||
"defaultValue":"xd.mqtt.test"
|
||||
},
|
||||
{
|
||||
"name":"url",
|
||||
"description":"location of the mqtt broker",
|
||||
"defaultValue":"tcp://localhost:1883"
|
||||
},
|
||||
{
|
||||
"name":"username",
|
||||
"description":"the username to use when connecting to the broker",
|
||||
"defaultValue":"guest"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"tap",
|
||||
"group":"other",
|
||||
"description":"Input channel for tapping into an existing stream",
|
||||
"metadata":{
|
||||
"fixed-name": true,
|
||||
"hide-tooltip-options": true
|
||||
},
|
||||
"properties":[
|
||||
{
|
||||
"name":"channel"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":0,
|
||||
"maxOutgoingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"named-channel",
|
||||
"metadata":{
|
||||
"fixed-name": true
|
||||
},
|
||||
"description":"A named channel that can be used as a source or a sync",
|
||||
"group":"other",
|
||||
"properties":[
|
||||
{
|
||||
"name":"channel",
|
||||
"description":"the input/output channel name: e.g. queue:topic:foo"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxOutgoingLinksNumber":1,
|
||||
"maxIncomingLinksNumber":1,
|
||||
"xorSourceSink": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"filter",
|
||||
"group":"processor",
|
||||
"description":"Filters messages from the stream where the supplied expression evaluates to false",
|
||||
"properties":[
|
||||
{
|
||||
"name":"expression",
|
||||
"description":"SpEL expression returning true or false"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxOutgoingLinksNumber":1,
|
||||
"maxIncomingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"transform",
|
||||
"group":"processor",
|
||||
"description":"Modify a messages content or structure in the stream",
|
||||
"properties":[
|
||||
{
|
||||
"name":"expression",
|
||||
"description":"A SpEL expression to transform the message, default: payload.toString()"
|
||||
},
|
||||
{
|
||||
"name":"script",
|
||||
"description":"Reference to a script used to transform the message"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxOutgoingLinksNumber":1,
|
||||
"maxIncomingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"groovy-transform",
|
||||
"group":"processor",
|
||||
"description":"Modify a messages content or structure in the stream",
|
||||
"properties":[
|
||||
{
|
||||
"name":"groovy",
|
||||
"source": {
|
||||
"mime":"text/x-groovy",
|
||||
"type":"groovy"
|
||||
},
|
||||
"description":"Groovy script to transform the message",
|
||||
"defaultValue":"/* Groovy script goes here */"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxOutgoingLinksNumber":1,
|
||||
"maxIncomingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"aggregator",
|
||||
"group":"processor",
|
||||
"description":"Aggregates messages from the stream where the supplied expression evaluates to false",
|
||||
"properties":[
|
||||
{
|
||||
"name":"expression",
|
||||
"description":"SpEL expression returning true or false"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxOutgoingLinksNumber":1,
|
||||
"maxIncomingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"splitter",
|
||||
"group":"processor",
|
||||
"description":"Splits messages from the stream where the supplied expression evaluates to false",
|
||||
"properties":[
|
||||
{
|
||||
"name":"expression",
|
||||
"description":"SpEL expression returning true or false"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxOutgoingLinksNumber":1,
|
||||
"maxIncomingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"json-to-tuple",
|
||||
"group":"processor",
|
||||
"description":"Transforms JSON message into an object of tuples",
|
||||
"properties":[
|
||||
|
||||
],
|
||||
"constraints":{
|
||||
"maxOutgoingLinksNumber":1,
|
||||
"maxIncomingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"object-to-json",
|
||||
"group":"processor",
|
||||
"description":"Takes in object message and sends out the JSON representation of the object further",
|
||||
"properties":[
|
||||
|
||||
],
|
||||
"constraints":{
|
||||
"maxOutgoingLinksNumber":1,
|
||||
"maxIncomingLinksNumber":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"log",
|
||||
"metadata":{
|
||||
"allow-additional-properties": true
|
||||
},
|
||||
"description":"A simple sink that logs the data using the application logger",
|
||||
"group":"sink",
|
||||
"properties":[
|
||||
{
|
||||
"name":"expression",
|
||||
"defaultValue":"payload",
|
||||
"description":"the expression to be evaluated for the log content; use #root to log the full message, default: payload"
|
||||
},
|
||||
{
|
||||
"name":"name",
|
||||
"defaultValue":"<streamName>",
|
||||
"description":"sink name (prefixed with logger during output)"
|
||||
},
|
||||
{
|
||||
"name":"level",
|
||||
"defaultValue":"INFO",
|
||||
"description":"the log level"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"file",
|
||||
"group":"sink",
|
||||
"description":"Stream the data to a file on the host OS",
|
||||
"properties":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"file name"
|
||||
},
|
||||
{
|
||||
"name":"dir",
|
||||
"description":"directory name"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"mail",
|
||||
"group":"sink",
|
||||
"description":"Output source for sending out emails",
|
||||
"properties":[
|
||||
{
|
||||
"name":"protocol",
|
||||
"defaultValue":"imaps",
|
||||
"description":"the protocol to use amongst pop3, pop3s, imap, imaps (only imap variants for the imap module)"
|
||||
},
|
||||
{
|
||||
"name":"username",
|
||||
"description":"the username to use to connect to the mail server"
|
||||
},
|
||||
{
|
||||
"name":"password",
|
||||
"description":"the password to use to connect to the mail server"
|
||||
},
|
||||
{
|
||||
"name":"host",
|
||||
"defaultValue":"localhost",
|
||||
"description":"the hostname of the mail server (default: localhost)"
|
||||
},
|
||||
{
|
||||
"name":"port",
|
||||
"description":"the port of the mail server"
|
||||
},
|
||||
{
|
||||
"name":"folder",
|
||||
"defaultValue":"INBOX",
|
||||
"description":"the folder to take emails from"
|
||||
},
|
||||
{
|
||||
"name":"markAsRead",
|
||||
"defaultValue":false,
|
||||
"description":"whether to mark emails as read once they?ve been fetched by the module"
|
||||
},
|
||||
{
|
||||
"name":"delete",
|
||||
"defaultValue":true,
|
||||
"description":"whether to delete the emails once they?ve been fetched by the module"
|
||||
},
|
||||
{
|
||||
"name":"fixedDelay",
|
||||
"defaultValue":60,
|
||||
"description":"Does not apply to the imap source, the polling interval used for looking up messages, expressed in seconds."
|
||||
},
|
||||
{
|
||||
"name":"charset",
|
||||
"defaultValue":"UTF-8",
|
||||
"description":"the charset used to transform the body of the incoming emails to Strings."
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"mqtt",
|
||||
"group":"sink",
|
||||
"description":"MQTT messages output",
|
||||
"properties":[
|
||||
{
|
||||
"name":"clientId",
|
||||
"description":"identifies the client",
|
||||
"defaultValue":"xd.mqtt.client.id.src"
|
||||
},
|
||||
{
|
||||
"name":"password",
|
||||
"description":"the password to use when connecting to the broker",
|
||||
"defaultValue":"guest"
|
||||
},
|
||||
{
|
||||
"name":"topics",
|
||||
"description":"the topic(s) to which the source will subscribe",
|
||||
"defaultValue":"xd.mqtt.test"
|
||||
},
|
||||
{
|
||||
"name":"url",
|
||||
"description":"location of the mqtt broker",
|
||||
"defaultValue":"tcp://localhost:1883"
|
||||
},
|
||||
{
|
||||
"name":"username",
|
||||
"description":"the username to use when connecting to the broker",
|
||||
"defaultValue":"guest"
|
||||
},
|
||||
{
|
||||
"name":"qos",
|
||||
"description":"the quality of service to use",
|
||||
"defaultValue":1
|
||||
},
|
||||
{
|
||||
"name":"retained",
|
||||
"description":"whether to set the \"retained\" flag",
|
||||
"defaultValue":false
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"hdfs",
|
||||
"group":"sink",
|
||||
"description":"HDFS output",
|
||||
"properties":[
|
||||
{
|
||||
"name":"codec",
|
||||
"description":"compression codec alias name (gzip, snappy, bzip2, lzo, or slzo)",
|
||||
"defaultValue":""
|
||||
},
|
||||
{
|
||||
"name":"fileExtension",
|
||||
"description":"the base filename extension to use for the created files",
|
||||
"defaultValue":"txt"
|
||||
},
|
||||
{
|
||||
"name":"fileOpenAttempts",
|
||||
"description":"maximum number of file open attempts to find a path",
|
||||
"defaultValue":10
|
||||
},
|
||||
{
|
||||
"name":"fileName",
|
||||
"description":"the base filename to use for the created files",
|
||||
"defaultValue":"<stream name>"
|
||||
},
|
||||
{
|
||||
"name":"directory",
|
||||
"description":"where to output the files in the Hadoop FileSystem",
|
||||
"defaultValue":"/xd/<stream name>"
|
||||
},
|
||||
{
|
||||
"name":"fileUuid",
|
||||
"description":"whether file name should contain uuid",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"fsUri",
|
||||
"description":"the URI to use to access the Hadoop FileSystem",
|
||||
"defaultValue":"${spring.hadoop.fsUri}"
|
||||
},
|
||||
{
|
||||
"name":"idleTimeout",
|
||||
"description":"inactivity timeout after file will be automatically closed",
|
||||
"defaultValue":0
|
||||
},
|
||||
{
|
||||
"name":"inUsePrefix",
|
||||
"description":"prefix for files currently being written",
|
||||
"defaultValue":""
|
||||
},
|
||||
{
|
||||
"name":"inUseSuffix",
|
||||
"description":"suffix for files currently being written",
|
||||
"defaultValue":".tmp"
|
||||
},
|
||||
{
|
||||
"name":"overwrite",
|
||||
"description":"whether writer is allowed to overwrite files in Hadoop FileSystem",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"partitionPath",
|
||||
"description":"a SpEL expression defining the partition path",
|
||||
"defaultValue":""
|
||||
},
|
||||
{
|
||||
"name":"rollover",
|
||||
"description":"threshold in bytes when file will be automatically rolled over",
|
||||
"defaultValue":"1G"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"jdbc",
|
||||
"group":"sink",
|
||||
"description":"Inserts message payload data into a relational database table",
|
||||
"properties":[
|
||||
{
|
||||
"name":"abandonWhenPercentageFull",
|
||||
"description":"connections that have timed out wont get closed and reported up unless the number of connections in use are above the percentage ",
|
||||
"defaultValue":0
|
||||
},
|
||||
{
|
||||
"name":"alternateUsernameAllowed",
|
||||
"description":"uses an alternate user name if connection fails",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"columns",
|
||||
"description":"the database columns to map the data to",
|
||||
"defaultValue":"payload"
|
||||
},
|
||||
{
|
||||
"name":"driverClassName",
|
||||
"description":"the JDBC driver to use"
|
||||
},
|
||||
{
|
||||
"name":"fairQueue",
|
||||
"description":"set to true if you wish that calls to getConnection should be treated fairly in a true FIFO fashion",
|
||||
"defaultValue":"false"
|
||||
},
|
||||
{
|
||||
"name":"initSQL",
|
||||
"description":"custom query to be run when a connection is first created"
|
||||
},
|
||||
{
|
||||
"name":"initialSize",
|
||||
"description":"initial number of connections that are created when the pool is started",
|
||||
"defaultValue":0
|
||||
},
|
||||
{
|
||||
"name":"initializeDatabase",
|
||||
"description":"whether the database initialization script should be run",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"jdbcInterceptors",
|
||||
"description":"semicolon separated list of classnames extending org.apache.tomcat.jdbc.pool.JdbcInterceptor"
|
||||
},
|
||||
{
|
||||
"name":"jmxEnabled",
|
||||
"description":"register the pool with JMX or not",
|
||||
"defaultValue":true
|
||||
},
|
||||
{
|
||||
"name":"logAbandoned",
|
||||
"description":"flag to log stack traces for application code which abandoned a Connection",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"maxActive",
|
||||
"description":"maximum number of active connections that can be allocated from this pool at the same time",
|
||||
"defaultValue":100
|
||||
},
|
||||
{
|
||||
"name":"maxAge",
|
||||
"description":"time in milliseconds to keep this connection",
|
||||
"defaultValue":0
|
||||
},
|
||||
{
|
||||
"name":"maxIdle",
|
||||
"description":"maximum number of connections that should be kept in the pool at all times",
|
||||
"defaultValue":100
|
||||
},
|
||||
{
|
||||
"name":"maxWait",
|
||||
"description":"maximum number of milliseconds that the pool will wait for a connection",
|
||||
"defaultValue":30000
|
||||
},
|
||||
{
|
||||
"name":"minEvictableIdleTimeMillis",
|
||||
"description":"minimum amount of time an object may sit idle in the pool before it is eligible for eviction",
|
||||
"defaultValue":60000
|
||||
},
|
||||
{
|
||||
"name":"minIdle",
|
||||
"description":"minimum number of established connections that should be kept in the pool at all times",
|
||||
"defaultValue":10
|
||||
},
|
||||
{
|
||||
"name":"password",
|
||||
"description":"the JDBC password"
|
||||
},
|
||||
{
|
||||
"name":"removeAbandoned",
|
||||
"description":"flag to remove abandoned connections if they exceed the removeAbandonedTimout",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"removeAbandonedTimeout",
|
||||
"description":"timeout in seconds before an abandoned connection can be removed",
|
||||
"defaultValue":60
|
||||
},
|
||||
{
|
||||
"name":"suspectTimeout",
|
||||
"description":"this simply logs the warning after timeout, connection remains",
|
||||
"defaultValue":0
|
||||
},
|
||||
{
|
||||
"name":"tableName",
|
||||
"description":"the database table to which the data will be written",
|
||||
"defaultValue":"<stream name>"
|
||||
},
|
||||
{
|
||||
"name":"testOnBorrow",
|
||||
"description":"indication of whether objects will be validated before being borrowed from the pool",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"testOnReturn",
|
||||
"description":"indication of whether objects will be validated before being returned to the pool",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"testWhileIdle",
|
||||
"description":"indication of whether objects will be validated by the idle object evictor",
|
||||
"defaultValue":false
|
||||
},
|
||||
{
|
||||
"name":"timeBetweenEvictionRunsMillis",
|
||||
"description":"number of milliseconds to sleep between runs of the idle connection validation/cleaner thread",
|
||||
"defaultValue":5000
|
||||
},
|
||||
{
|
||||
"name":"url",
|
||||
"description":"the JDBC URL for the database"
|
||||
},
|
||||
{
|
||||
"name":"useEquals",
|
||||
"description":"true if you wish the ProxyConnection class to use String.equals",
|
||||
"defaultValue":true
|
||||
},
|
||||
{
|
||||
"name":"username",
|
||||
"description":"the JDBC username"
|
||||
},
|
||||
{
|
||||
"name":"validationInterval",
|
||||
"description":"avoid excess validation, only run validation at most at this frequency - time in milliseconds",
|
||||
"defaultValue":30000
|
||||
},
|
||||
{
|
||||
"name":"validationQuery",
|
||||
"description":"sql query that will be used to validate connections from this pool"
|
||||
},
|
||||
{
|
||||
"name":"validatorClassName",
|
||||
"description":"name of a class which implements the org.apache.tomcat.jdbc.pool.Validator"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"null",
|
||||
"group":"sink",
|
||||
"description":"Used when the main stream is not focused on stream destination but the tap streams are used for analytics etc., It is also useful to iteratively add in steps to a stream without worrying about having to land data anywhere.",
|
||||
"properties":[
|
||||
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"counter",
|
||||
"group":"sink",
|
||||
"description":"A counter is a Metric that associates a unique name with a long value. It is primarily used for counting events triggered by incoming messages on a target stream",
|
||||
"properties":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"the name of the metric to contribute to (will be created if necessary)",
|
||||
"defaultValue":"<stream name>"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"field-value-counter",
|
||||
"group":"sink",
|
||||
"description":"A field value counter is a Metric used for counting occurrences of unique values for a named field in a message payload",
|
||||
"properties":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"the name of the metric to contribute to (will be created if necessary)",
|
||||
"defaultValue":"<stream name>"
|
||||
},
|
||||
{
|
||||
"name":"fieldName",
|
||||
"description":"the name of the field for which values are counted"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"aggregate-counter",
|
||||
"group":"sink",
|
||||
"description":"The aggregate counter differs from a simple counter in that it not only keeps a total value for the count, but also retains the total count values for each minute, hour day and month of the period for which it is run. The data can then be queried by supplying a start and end date and the resolution at which the data should be returned.",
|
||||
"properties":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"the name of the metric to contribute to (will be created if necessary)",
|
||||
"defaultValue":"<stream name>"
|
||||
},
|
||||
{
|
||||
"name":"dateFormat",
|
||||
"description":"a pattern (as in SimpleDateFormat) for parsing/formatting dates and timestamps",
|
||||
"defaultValue":"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
},
|
||||
{
|
||||
"name":"timeField",
|
||||
"description":"name of a field in the message that contains the timestamp to contribute to",
|
||||
"defaultValue":null
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"gauge",
|
||||
"group":"sink",
|
||||
"description":"A gauge is a Metric, similar to a counter in that it holds a single long value associated with a unique name. In this case the value can represent any numeric value defined by the application",
|
||||
"properties":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"the name of the metric to contribute to (will be created if necessary)",
|
||||
"defaultValue":"<stream name>"
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name":"rich-gauge",
|
||||
"group":"sink",
|
||||
"description":"A rich gauge is a Metric that holds a double value associated with a unique name. In addition to the value, the rich gauge keeps a running average, along with the minimum and maximum values and the sample count",
|
||||
"properties":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"the name of the metric to contribute to (will be created if necessary)",
|
||||
"defaultValue":"<stream name>"
|
||||
},
|
||||
{
|
||||
"name":"alpha",
|
||||
"description":"smoothing constant, or -1 to use arithmetic mean",
|
||||
"defaultValue":-1.0
|
||||
}
|
||||
],
|
||||
"constraints":{
|
||||
"maxIncomingLinksNumber":1,
|
||||
"maxOutgoingLinksNumber":0
|
||||
}
|
||||
}
|
||||
]
|
||||
202
tests/resources/render-service.js
Normal file
202
tests/resources/render-service.js
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var app = require('flo');
|
||||
|
||||
var joint = require('joint');
|
||||
|
||||
var dagre = require('dagre');
|
||||
|
||||
var HANDLE_ICON_MAP = {
|
||||
'remove': 'icons/delete.svg',
|
||||
};
|
||||
|
||||
var DECORATION_ICON_MAP = {
|
||||
'error': 'icons/error.svg'
|
||||
};
|
||||
|
||||
app.factory('SampleRenderService', function() {
|
||||
|
||||
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) { // jshint ignore:line
|
||||
return new joint.shapes.flo.Node();
|
||||
}
|
||||
|
||||
function refreshVisuals(element, changedPropertyPath, paper) { // jshint ignore:line
|
||||
var type = element.attr('metadata/name');
|
||||
|
||||
if (type === 'tap' && changedPropertyPath === 'props/channel') {
|
||||
var name = element.attr('props/channel');
|
||||
if (name) {
|
||||
var colon = name.indexOf(':');
|
||||
if (colon !== -1) {
|
||||
colon = name.indexOf(':',colon+1);
|
||||
if (colon !== -1) {
|
||||
var displayValue = name.substring(0,colon) + '\n' + name.substring(colon);
|
||||
element.attr('.label2/text', displayValue);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
element.removeAttr('.label2/text');
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'named-channel' && changedPropertyPath === 'props/channel') {
|
||||
element.attr('.label2/text', element.attr('props/channel'));
|
||||
}
|
||||
}
|
||||
|
||||
function initializeNewNode(node, context) {
|
||||
var metadata = node.attr('metadata');
|
||||
if (metadata) {
|
||||
node.attr('.label/text', node.attr('metadata/name'));
|
||||
//node.attr('.image/xlink:href', metadata && metadata.icon ? metadata.icon : 'icons/xd/unknown.png');
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function createLink() {
|
||||
var link = new joint.shapes.flo.Link();
|
||||
link.set('smooth', true);
|
||||
return link;
|
||||
}
|
||||
|
||||
function getLinkView() {
|
||||
return joint.dia.LinkView;
|
||||
}
|
||||
|
||||
function isSemanticProperty(propertyPath) {
|
||||
return propertyPath === '.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 type = magnet.getAttribute('type');
|
||||
var bbox = joint.V(magnet).bbox(false, linkView.paper.viewport);
|
||||
var rect = joint.g.rect(bbox);
|
||||
if (type === 'input') {
|
||||
return joint.g.point(rect.x, rect.y + rect.height / 2);
|
||||
} else {
|
||||
return joint.g.point(rect.x + rect.width, rect.y + rect.height / 2);
|
||||
}
|
||||
} else {
|
||||
return reference;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'createHandle': createHandle,
|
||||
'createDecoration': createDecoration,
|
||||
'createNode': createNode,
|
||||
'getLinkView': getLinkView,
|
||||
'createLink': createLink,
|
||||
'initializeNewNode': initializeNewNode,
|
||||
'isSemanticProperty': isSemanticProperty,
|
||||
'refreshVisuals': refreshVisuals,
|
||||
'layout': layout,
|
||||
'getLinkAnchorPoint': getLinkAnchorPoint
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
return app;
|
||||
|
||||
});
|
||||
232
tests/spec/flo-spec.js
Normal file
232
tests/spec/flo-spec.js
Normal 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.
|
||||
*/
|
||||
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var angular = require('angular');
|
||||
require('angularMocks');
|
||||
require('metamodelService');
|
||||
require('editor-service');
|
||||
require('render-service');
|
||||
require('flo');
|
||||
require('text');
|
||||
|
||||
var data = JSON.parse(require('text!../resources/metamodel.json'));
|
||||
var domElement;
|
||||
|
||||
var groupsData = {};
|
||||
data.forEach(function(e) {
|
||||
if (!groupsData[e.group]) {
|
||||
groupsData[e.group] = {};
|
||||
}
|
||||
groupsData[e.group][e.name] = e;
|
||||
});
|
||||
|
||||
describe('Unit testing Flo editor', function() {
|
||||
var $compile, $httpBackend, $scope, $http, $timeout, StandaloneMetamodel, MetamodelUtils;
|
||||
|
||||
// Load the Flo module, which contains the directive
|
||||
beforeEach(angular.mock.module('spring.flo'));
|
||||
|
||||
// Store references to $rootScope and $compile
|
||||
// so they are available to all tests in this describe block
|
||||
beforeEach(function(done) {
|
||||
inject(function($rootScope, $controller, _$http_, _$compile_, _$httpBackend_, _$timeout_, _StandaloneMetamodel_, _MetamodelUtils_) {
|
||||
$compile = _$compile_;
|
||||
$scope = $rootScope.$new();
|
||||
$http = _$http_;
|
||||
$httpBackend = _$httpBackend_;
|
||||
$timeout = _$timeout_;
|
||||
StandaloneMetamodel = _StandaloneMetamodel_;
|
||||
MetamodelUtils = _MetamodelUtils_;
|
||||
|
||||
// Set up the mock http service responses
|
||||
$httpBackend.when('GET', '/parse?text=').respond(200, {format: 'xd', nodes: [], links: []});
|
||||
$httpBackend.when('GET', '/metamodel').respond(200, data);
|
||||
|
||||
// Setup initial scope.
|
||||
$scope.flo = {};
|
||||
$scope.definition = {};
|
||||
$scope.editorServiceName = 'sample-editor-service';
|
||||
$scope.renderServiceName = 'SampleRenderService';
|
||||
$scope.metamodelServiceName = 'StandaloneMetamodel';
|
||||
|
||||
// Compile a piece of HTML containing the directive
|
||||
domElement = $compile('<div><flo-editor></flo-editor></div>')($scope);
|
||||
angular.element(document.body).append(domElement);
|
||||
$scope.$digest();
|
||||
$scope.$apply();
|
||||
$httpBackend.flush();
|
||||
setTimeout(function() {
|
||||
done();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
$scope.flo.clearGraph();
|
||||
domElement.remove();
|
||||
});
|
||||
|
||||
it('Replaces the element with the appropriate content', function() {
|
||||
// Check that the compiled element contains the templated content
|
||||
expect(domElement.find('#paper').length).toBe(1);
|
||||
expect(domElement.find('#palette-paper').length).toBe(1);
|
||||
expect(domElement.find('#properties').length).toBe(1);
|
||||
});
|
||||
|
||||
it('Verify palette contents', function() {
|
||||
var paletteGraph = $scope.flo._paletteGraph();
|
||||
|
||||
var groups = {};
|
||||
var elementCounter = {};
|
||||
paletteGraph.getElements().forEach(function(e) {
|
||||
if (e.attr('metadata')) {
|
||||
var group = e.attr('metadata/group');
|
||||
var name = e.attr('metadata/name');
|
||||
if (group) {
|
||||
if (!elementCounter[group]) {
|
||||
elementCounter[group] = {};
|
||||
}
|
||||
elementCounter[group][name] = e;
|
||||
}
|
||||
} else if (e.attr('text/text')) {
|
||||
groups[e.attr('text/text')] = e;
|
||||
}
|
||||
});
|
||||
|
||||
expect(Object.keys(groups).sort()).toEqual(Object.keys(groupsData).sort());
|
||||
expect(Object.keys(elementCounter).sort()).toEqual(Object.keys(groupsData).sort());
|
||||
Object.keys(elementCounter).forEach(function(group) {
|
||||
var current = Object.keys(elementCounter[group]).sort();
|
||||
var expected = Object.keys(groupsData[group]).sort();
|
||||
expect(current).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('Create module', function(done) {
|
||||
StandaloneMetamodel.load().then(function(metamodel) {
|
||||
var node = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'http', 'source'));
|
||||
var view = $scope.flo.getPaper().findViewByModel(node);
|
||||
expect(view).toBeDefined();
|
||||
expect(node.attr('.label/text')).toBe('http');
|
||||
expect($scope.flo.zoomPercent()).toBe(100);
|
||||
var bbox = view.getBBox();
|
||||
expect(node.get('position')).toEqual({x: 0, y: 0});
|
||||
expect(node.get('size')).toEqual({width: 120, height: 35});
|
||||
expect(bbox.x).toBe(0);
|
||||
expect(bbox.y).toBeDefined();
|
||||
expect(bbox.width).toBe(124);
|
||||
// Account for the label above.
|
||||
expect(bbox.height).toBe(35);
|
||||
done();
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
it('Test link creation', function(done) {
|
||||
StandaloneMetamodel.load().then(function(metamodel) {
|
||||
var http = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'http', 'source'));
|
||||
var log = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'log', 'sink'));
|
||||
$scope.flo.getPaper().findViewByModel(http); // httpView
|
||||
$scope.flo.getPaper().findViewByModel(log); // logView
|
||||
$scope.flo.createLink({
|
||||
'id': http.get('id'),
|
||||
'selector': '.output'
|
||||
}, {
|
||||
'id': log.get('id'),
|
||||
'selector': '.input'
|
||||
});
|
||||
expect($scope.flo.getGraph().getLinks().length).toBe(1);
|
||||
var link = $scope.flo.getGraph().getLinks()[0];
|
||||
expect($scope.flo.getPaper().findViewByModel(link)).toBeDefined();
|
||||
expect(link.get('source').id).toEqual(http.get('id'));
|
||||
expect(link.get('target').id).toEqual(log.get('id'));
|
||||
|
||||
// Test layout
|
||||
$scope.flo.performLayout();
|
||||
expect(http.get('position').y).toEqual(log.get('position').y);
|
||||
expect(http.get('position').x + http.get('size').width + 20).toBeLessThan(log.get('position').x);
|
||||
done();
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
it('Test basic layout of disoconnected elements', function(done) {
|
||||
StandaloneMetamodel.load().then(function(metamodel) {
|
||||
var http = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'http', 'source'));
|
||||
var log = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'log', 'sink'));
|
||||
|
||||
// Test layout
|
||||
$scope.flo.performLayout();
|
||||
expect(http.get('position').x).toEqual(log.get('position').x);
|
||||
expect(http.get('position').y + http.get('size').height + 20).toBeLessThan(log.get('position').y);
|
||||
done();
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
it('Test sync text stream', function(done) {
|
||||
StandaloneMetamodel.load().then(function(metamodel) {
|
||||
var mail = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'mail', 'source'));
|
||||
var log = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'log', 'sink'));
|
||||
$scope.flo.createLink({
|
||||
'id': mail.get('id'),
|
||||
'selector': '.output'
|
||||
}, {
|
||||
'id': log.get('id'),
|
||||
'selector': '.input'
|
||||
});
|
||||
/*return*/ $scope.flo.updateTextRepresentation();
|
||||
// }).then(function() {
|
||||
expect($scope.definition.text).toEqual('mail | log');
|
||||
done();
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
it('Test deferred automated text stream sync', function(done) {
|
||||
StandaloneMetamodel.load().then(function(metamodel) {
|
||||
var mail = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'mail', 'source'));
|
||||
var log = $scope.flo.createNode(MetamodelUtils.getMetadata(metamodel, 'log', 'sink'));
|
||||
$scope.flo.createLink({
|
||||
'id': mail.get('id'),
|
||||
'selector': '.output'
|
||||
}, {
|
||||
'id': log.get('id'),
|
||||
'selector': '.input'
|
||||
});
|
||||
expect($scope.definition.text).toEqual('');
|
||||
setTimeout(function() {
|
||||
var flush = true;
|
||||
while (flush) {
|
||||
try {
|
||||
$timeout.flush();
|
||||
} catch (error) {
|
||||
flush = false;
|
||||
}
|
||||
}
|
||||
expect($scope.definition.text).toEqual('mail | log');
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
56
tests/spec/test1-spec.js
Normal file
56
tests/spec/test1-spec.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
define(['joint', 'angular'], function (joint) {
|
||||
'use strict';
|
||||
|
||||
console.log('RUNNING SPEC TEST!');
|
||||
|
||||
describe('Joint JS Graph', function () {
|
||||
var graph;
|
||||
|
||||
beforeEach(function() {
|
||||
graph = new joint.dia.Graph();
|
||||
var rect = new joint.shapes.basic.Rect({
|
||||
position: { x: 100, y: 30 },
|
||||
size: { width: 100, height: 30 },
|
||||
attrs: { rect: { fill: 'blue' }, text: { text: 'my box', fill: 'white' } }
|
||||
});
|
||||
var rect2 = rect.clone();
|
||||
rect2.translate(300);
|
||||
var link = new joint.dia.Link({
|
||||
source: { id: rect.id },
|
||||
target: { id: rect2.id }
|
||||
});
|
||||
graph.addCells([rect, rect2, link]);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
if (graph) {
|
||||
graph.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it('create element on a graph', function () {
|
||||
expect(graph.getElements().length).toEqual(2);
|
||||
});
|
||||
|
||||
it('create link on a graph', function () {
|
||||
expect(graph.getLinks().length).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user