first commit

This commit is contained in:
Andy Clement
2016-05-10 14:27:26 -07:00
commit 633ed0ceb2
35 changed files with 37964 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
node/
bower_components/
node_modules/
lib/
target/
.idea/
.project
.DS_Store
spring-flo.iml

30
.jshintrc Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

36
README.adoc Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

150
gruntfile.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

60
src/event-manager.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
});

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

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

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

View 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
View File

@@ -0,0 +1,232 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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
View 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);
});
});
});