workflow.js 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  1. // Licensed to Cloudera, Inc. under one
  2. // or more contributor license agreements. See the NOTICE file
  3. // distributed with this work for additional information
  4. // regarding copyright ownership. Cloudera, Inc. licenses this file
  5. // to you under the Apache License, Version 2.0 (the
  6. // "License"); you may not use this file except in compliance
  7. // with the License. You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS,
  13. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. // See the License for the specific language governing permissions and
  15. // limitations under the License.
  16. var Registry = RegistryModule($);
  17. var Modal = ModalModule($, ko);
  18. /**
  19. * Import node module
  20. * Assists in the conversion of a jobsub node.
  21. * Assists in the population of available nodes.
  22. */
  23. var ImportNodeModule = function($) {
  24. var module = function(options) {
  25. var self = this;
  26. self.workflow = options.workflow;
  27. self.available_nodes = [];
  28. self.url = ko.computed(function() {
  29. return '/oozie/workflows/' + options.workflow.id() + '/jobsub/actions'
  30. });
  31. module.prototype.initialize.apply(self, arguments);
  32. return self;
  33. };
  34. $.extend(module.prototype, {
  35. initialize: function(options) {
  36. var self = this;
  37. if (options.nodes) {
  38. $.each(options.nodes, function(index, node) {
  39. self.available_nodes.push(node);
  40. });
  41. }
  42. },
  43. getAvailableNodes: function() {
  44. var self = this;
  45. return self.available_nodes;
  46. },
  47. loadAvailableNodes: function(options) {
  48. var self = this;
  49. var request = $.extend({
  50. url: self.url(),
  51. dataType: 'json',
  52. type: 'GET',
  53. success: $.noop,
  54. error: $.noop
  55. }, options || {});
  56. $.ajax(request);
  57. },
  58. convertNode: function(options, jobsub_id) {
  59. var self = this;
  60. var options = options || {};
  61. var request = $.extend({
  62. url: self.url(),
  63. type: 'POST',
  64. data: { 'jobsub_id': jobsub_id },
  65. success: $.noop,
  66. error: $.noop
  67. }, options);
  68. $.ajax(request);
  69. }
  70. });
  71. return module;
  72. };
  73. var ImportNode = ImportNodeModule($);
  74. var Node = NodeModule($, IdGeneratorTable, NodeFields);
  75. var StartNode = NodeModule($, IdGeneratorTable, NodeFields);
  76. $.extend(StartNode.prototype, Node.prototype, {
  77. /**
  78. * Same as addChild for nodes, except if we reach the end,
  79. * we add the end node to our child_links in the form of a
  80. * 'related' link and 'start' link.
  81. * We should always have a start link in an empty workflow!
  82. */
  83. replaceChild: function(child, replacement) {
  84. var self = this;
  85. var index = -1;
  86. $.each(self.child_links(), function(i, link) {
  87. if (link.child() == child.id()) {
  88. index = i;
  89. }
  90. });
  91. if (index > -1) {
  92. self.child_links.splice(index, 1);
  93. var links = self.child_links.splice(index);
  94. var link = {
  95. parent: ko.observable(self.id()),
  96. child: ko.observable(replacement.id()),
  97. name: ko.observable('to'),
  98. comment: ko.observable('')
  99. };
  100. self.child_links.push(link);
  101. $.each(links, function(index, link) {
  102. self.child_links.push(link);
  103. });
  104. }
  105. return index != -1;
  106. }
  107. });
  108. var ForkNode = NodeModule($, IdGeneratorTable, NodeFields);
  109. $.extend(ForkNode.prototype, Node.prototype, {
  110. // Join nodes are connected through 'related' links
  111. join: function() {
  112. var self = this;
  113. var join = null;
  114. $.each(self.child_links(), function(index, link) {
  115. if (link.name() == 'related') {
  116. join = self.registry.get(link.child());
  117. }
  118. });
  119. return join;
  120. },
  121. /**
  122. * Append a node to the current fork
  123. * Also adds join node to node.
  124. * When adding the join node, append will remove all the children from the join!
  125. * We need to make sure the join remembers its children since append will replace them.
  126. * NOTE: Cannot append a fork or decision! Use addChild or replaceChild instead!
  127. */
  128. append: function(node) {
  129. var self = this;
  130. if (node.node_type() != 'decision' && node.node_type() != 'fork') {
  131. var join = self.join();
  132. if (join.id() != node.id()) {
  133. var children = join.findChildren();
  134. self.addChild(node);
  135. node.append(join);
  136. // remember children
  137. $.each(children, function(index, child) {
  138. join.addChild(child);
  139. });
  140. }
  141. }
  142. },
  143. /**
  144. * Replace child node with another node
  145. * 1. Remove child if the replacement is related join
  146. * 2. Apply Node.replaceChild for all other causes
  147. * NOTE: can assume the only join this fork will ever see is the related join!
  148. * This is because the related will always be the closest join for children.
  149. * Also, the operations allowed that would make this all possible: detach, append;
  150. * are inherently going to remove any other joins if they exist.
  151. * This is also easier given nodes are contained within Forks!
  152. */
  153. replaceChild: function(child, replacement) {
  154. var self = this;
  155. var ret = true;
  156. if (self.join().id() == replacement.id()) {
  157. ret = self.removeChild(child);
  158. } else {
  159. ret = Node.prototype.replaceChild.apply(self, arguments);
  160. }
  161. var links = self.links().filter(function(element, index, arr) {
  162. return self.registry.get(element.child()).node_type() != 'join';
  163. });
  164. if (links.length < 2) {
  165. self.detach();
  166. self.join().detach();
  167. self.erase();
  168. self.join().erase();
  169. }
  170. return ret;
  171. },
  172. /**
  173. * Converts fork node into decision node in the following way:
  174. * 1. Copies contents of current fork node into a new decision node
  175. * 2. Detach fork node
  176. * 3. Erase fork node
  177. * 4. Append decision node to parent
  178. */
  179. convertToDecision: function() {
  180. var self = this;
  181. var join = self.join();
  182. var end = null;
  183. var child = join.findChildren()[0];
  184. // Replace join with decision end
  185. var decision_end_model = new NodeModel({
  186. id: IdGeneratorTable['decisionend'].nextId(),
  187. node_type: 'decisionend',
  188. workflow: self.workflow(),
  189. child_links: join.model.child_links
  190. });
  191. var decision_end_node = new Node(self._workflow, decision_end_model, self.registry);
  192. $.each(decision_end_model.child_links, function(index, link) {
  193. link.parent = decision_end_model.id;
  194. });
  195. var parents = join.findParents();
  196. $.each(parents, function(index, parent) {
  197. parent.replaceChild(join, decision_end_node);
  198. });
  199. // Replace fork with decision node
  200. var decision_model = new DecisionModel({
  201. id: IdGeneratorTable['decision'].nextId(),
  202. name: self.name(),
  203. description: self.description(),
  204. node_type: 'decision',
  205. workflow: self.workflow(),
  206. child_links: self.model.child_links
  207. });
  208. var default_link = {
  209. parent: decision_model.id,
  210. child: self._workflow.end(),
  211. name: 'default',
  212. comment: ''
  213. };
  214. decision_model.child_links.push(default_link);
  215. $.each(decision_model.child_links, function(index, link) {
  216. link.parent = decision_model.id;
  217. });
  218. var parents = self.findParents();
  219. var decision_node = new DecisionNode(self._workflow, decision_model, self.registry);
  220. decision_node.removeChild(join);
  221. decision_node.addChild(decision_end_node);
  222. $.each(parents, function(index, parent) {
  223. parent.replaceChild(self, decision_node);
  224. });
  225. // Get rid of fork and join in registry
  226. join.erase();
  227. self.erase();
  228. // Add decision and decision end to registry
  229. self.registry.add(decision_node.id(), decision_node);
  230. self.registry.add(decision_end_node.id(), decision_end_node);
  231. }
  232. });
  233. var DecisionNode = NodeModule($, IdGeneratorTable, NodeFields);
  234. $.extend(DecisionNode.prototype, ForkNode.prototype, {
  235. initialize: function(workflow, model, registry) {
  236. var self = this;
  237. var registry = registry;
  238. var end = null;
  239. },
  240. end: function() {
  241. var self = this;
  242. var end = null;
  243. $.each(self.child_links(), function(index, link) {
  244. if (link.name() == 'related') {
  245. end = self.registry.get(link.child());
  246. }
  247. });
  248. return end;
  249. },
  250. /**
  251. * Append a node to the current decision
  252. * Also appends end node to node.
  253. * NOTE: Cannot append a decision or fork! Use addChild or replaceChild instead!
  254. */
  255. append: function(node) {
  256. var self = this;
  257. if (node.node_type() != 'decision' && node.node_type() != 'fork') {
  258. var end = self.end();
  259. if (end.id() != node.id()) {
  260. var children = end.findChildren();
  261. self.addChild(node);
  262. node.append(end);
  263. // remember children
  264. $.each(children, function(index, child) {
  265. end.addChild(child);
  266. });
  267. }
  268. }
  269. },
  270. /**
  271. * Replace child node with another node
  272. * 1. Remove child if the replacement is end
  273. * 2. Apply Node.replaceChild for all other causes
  274. */
  275. replaceChild: function(child, replacement) {
  276. var self = this;
  277. var ret = true;
  278. var end = self.end();
  279. if (end && end.id() == replacement.id()) {
  280. ret = self.removeChild(child);
  281. } else {
  282. ret = Node.prototype.replaceChild.apply(self, arguments);
  283. }
  284. if (self.links().length < 2) {
  285. self.detach();
  286. end.detach();
  287. self.erase();
  288. end.erase();
  289. }
  290. return ret;
  291. }
  292. });
  293. /**
  294. * Workflow module
  295. */
  296. var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable) {
  297. var module = function(options) {
  298. var self = this;
  299. // @see http://knockoutjs.com/documentation/plugins-mapping.html
  300. var mapping = ko.mapping.fromJS(options.model, {
  301. ignore: ['initialize', 'toString', 'copy'],
  302. job_properties: {
  303. create: function(options) {
  304. var parent = options.parent;
  305. var subscribe = function(mapping) {
  306. mapping.name.subscribe(function(value) {
  307. parent.job_properties.valueHasMutated();
  308. });
  309. mapping.value.subscribe(function(value) {
  310. parent.job_properties.valueHasMutated();
  311. });
  312. };
  313. return map_params(options, subscribe);
  314. },
  315. update: function(options) {
  316. var parent = options.parent;
  317. var subscribe = function(mapping) {
  318. mapping.name.subscribe(function(value) {
  319. parent.job_properties.valueHasMutated();
  320. });
  321. mapping.value.subscribe(function(value) {
  322. parent.job_properties.valueHasMutated();
  323. });
  324. };
  325. return map_params(options, subscribe);
  326. }
  327. },
  328. parameters: {
  329. // Will receive individual objects to subscribe.
  330. // Containing array is mapped automagically
  331. create: function(options) {
  332. var parent = options.parent;
  333. var subscribe = function(mapping) {
  334. mapping.name.subscribe(function(value) {
  335. parent.parameters.valueHasMutated();
  336. });
  337. mapping.value.subscribe(function(value) {
  338. parent.parameters.valueHasMutated();
  339. });
  340. };
  341. return map_params(options, subscribe);
  342. },
  343. update: function(options) {
  344. var parent = options.parent;
  345. var subscribe = function(mapping) {
  346. mapping.name.subscribe(function(value) {
  347. parent.parameters.valueHasMutated();
  348. });
  349. mapping.value.subscribe(function(value) {
  350. parent.parameters.valueHasMutated();
  351. });
  352. };
  353. return map_params(options, subscribe);
  354. }
  355. }
  356. });
  357. $.extend(self, mapping);
  358. $.each(mapping['__ko_mapping__'].mappedProperties, function(key, value) {
  359. var key = key;
  360. self[key].subscribe(function(value) {
  361. self.is_dirty( true );
  362. self.model[key] = ko.mapping.toJS(value);
  363. });
  364. });
  365. self.model = options.model;
  366. self.registry = options.registry;
  367. self.options = options;
  368. self.el = $('#workflow');
  369. self.nodes = ko.observableArray([]);
  370. self.kill = null;
  371. self.is_dirty = ko.observable( false );
  372. self.loading = ko.observable( false );
  373. self.read_only = ko.observable( options.read_only || false );
  374. self.new_node = ko.observable();
  375. self.url = ko.computed(function() {
  376. return '/oozie/workflows/' + self.id()
  377. });
  378. // Events
  379. self.el.on('workflow:rebuild', function() {
  380. self.rebuild();
  381. });
  382. self.el.on('workflow:events:load', function() {
  383. self.dragAndDropEvents( options );
  384. });
  385. self.el.on('workflow:droppables:load', function() {
  386. self.droppables();
  387. });
  388. self.el.on('workflow:draggables:load', function() {
  389. self.draggables();
  390. });
  391. self.dragAndDropEvents( options );
  392. self.el.trigger('workflow:events:loaded');
  393. module.prototype.initialize.apply(self, arguments);
  394. return self;
  395. };
  396. $.extend(module.prototype, {
  397. // Normal stuff
  398. initialize: function(options) {
  399. var self = this;
  400. $.extend(self.options, options);
  401. if ('model' in options) {
  402. self.model = options.model;
  403. // Initialize nodes
  404. if (self.model.nodes) {
  405. self.registry.clear();
  406. $.each(self.model.nodes, function(index, node) {
  407. var NodeModel = NodeModelChooser(node.node_type);
  408. var model = new NodeModel(node);
  409. var temp = null;
  410. switch(node.node_type) {
  411. case 'start':
  412. temp = new StartNode(self, model, self.registry);
  413. break;
  414. case 'fork':
  415. temp = new ForkNode(self, model, self.registry);
  416. break;
  417. case 'decision':
  418. temp = new DecisionNode(self, model, self.registry);
  419. break;
  420. case 'kill':
  421. temp = self.kill = new Node(self, model, self.registry);
  422. break;
  423. default:
  424. temp = new Node(self, model, self.registry);
  425. break;
  426. }
  427. self.registry.add(temp.id(), temp);
  428. });
  429. }
  430. // Update data
  431. $.each(self.model, function (key, value) {
  432. if (key in self) {
  433. switch(key) {
  434. case 'job_properties':
  435. case 'parameters':
  436. // These may be serialized JSON data since that is how they are stored
  437. self[key].removeAll();
  438. var arr = $.parseJSON(value) || value;
  439. $.each(arr, function(index, obj) {
  440. var mapping = ko.mapping.fromJS(obj);
  441. mapping.name.subscribe(function(value) {
  442. self[key].valueHasMutated();
  443. });
  444. mapping.value.subscribe(function(value) {
  445. self[key].valueHasMutated();
  446. });
  447. self[key].push(mapping);
  448. });
  449. break;
  450. case 'nodes':
  451. break;
  452. default:
  453. self[key](value);
  454. break;
  455. }
  456. }
  457. });
  458. self.is_dirty( false );
  459. }
  460. if (!self.kill) {
  461. var kill_json = {
  462. "description": "",
  463. "workflow": self.id(),
  464. "child_links": [],
  465. "node_type": "kill",
  466. "message": "Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]",
  467. "name": "kill",
  468. "id": IdGeneratorTable['kill'].nextId()
  469. };
  470. var NodeModel = NodeModelChooser(kill_json.node_type);
  471. var model = new NodeModel(kill_json);
  472. self.kill = new Node(self, model, self.registry);
  473. self.registry.add(self.kill.id(), self.kill);
  474. }
  475. if ('read_only' in options) {
  476. self.read_only(options['read_only']);
  477. }
  478. },
  479. toString: function() {
  480. return '';
  481. var s = '[';
  482. $.each(self.registry.nodes, function(key, node) {
  483. s += node.model.toString() + ",\n";
  484. });
  485. return s + ']';
  486. },
  487. // Data manipulation
  488. toJSON: function() {
  489. var self = this;
  490. data = $.extend(true, {}, self.model);
  491. var nodes = [];
  492. $.each(self.registry.nodes, function(key, node) {
  493. // Create object with members from the actual model to address JSON.stringify bug
  494. // JSON.stringify does not pick up members specified in prototype prior to object creation.
  495. var model = {};
  496. for (var key in node.model) {
  497. model[key] = node.model[key];
  498. }
  499. nodes.push(model);
  500. });
  501. data['nodes'] = nodes;
  502. return JSON.stringify(data);
  503. },
  504. save: function( options ) {
  505. var self = this;
  506. var request = $.extend({
  507. url: self.url() + '/save',
  508. type: 'POST',
  509. data: { workflow: self.toJSON() },
  510. success: $.noop,
  511. error: $.noop
  512. }, options || {});
  513. $.ajax(request);
  514. },
  515. load: function( options ) {
  516. var self = this;
  517. var request = $.extend({
  518. url: self.url(),
  519. dataType: 'json',
  520. type: 'GET',
  521. success: $.noop,
  522. error: $.noop
  523. }, options || {});
  524. $.ajax(request);
  525. },
  526. reload: function(model) {
  527. var self = this;
  528. // Clear all children
  529. $.each(self.registry.nodes, function(index, node) {
  530. node.children.removeAll();
  531. });
  532. self.nodes.removeAll();
  533. self.initialize({model: model});
  534. self.rebuild();
  535. self.el.trigger('workflow:loaded');
  536. },
  537. addParameter: function(data, event) {
  538. var self = this;
  539. var prop = { name: ko.observable(""), value: ko.observable("") };
  540. // force bubble up to containing observable array.
  541. prop.name.subscribe(function(){
  542. self.parameters.valueHasMutated();
  543. });
  544. prop.value.subscribe(function(){
  545. self.parameters.valueHasMutated();
  546. });
  547. self.parameters.push(prop);
  548. },
  549. removeParameter: function(data, event) {
  550. var self = this;
  551. self.parameters.remove(data);
  552. },
  553. addJobProperty: function(data, event) {
  554. var self = this;
  555. var prop = { name: ko.observable(""), value: ko.observable("") };
  556. // force bubble up to containing observable array.
  557. prop.name.subscribe(function(){
  558. self.parameters.valueHasMutated();
  559. });
  560. prop.value.subscribe(function(){
  561. self.parameters.valueHasMutated();
  562. });
  563. self.job_properties.push(prop);
  564. },
  565. removeJobProperty: function(data, event) {
  566. var self = this;
  567. self.job_properties.remove(data);
  568. },
  569. // Workflow UI
  570. // Function to build nodes... recursively.
  571. build: function() {
  572. var self = this;
  573. var maximum = 50;
  574. var count = 0;
  575. var methodChooser = function(node, collection, skip_parents_check) {
  576. if (count++ >= maximum) {
  577. if (window.error) {
  578. console.error('Hit maximum number of node recursion: ' + maximum);
  579. }
  580. return null;
  581. }
  582. if (!node) {
  583. return node;
  584. }
  585. var parents = node.findParents();
  586. // Found end of decision node or found join!
  587. if (parents.length > 1 && !skip_parents_check) {
  588. return node;
  589. }
  590. switch(node.node_type()) {
  591. case 'start':
  592. case 'end':
  593. case 'kill':
  594. case 'fork':
  595. case 'join':
  596. case 'decision':
  597. case 'decisionend':
  598. return control(node, collection);
  599. default:
  600. return normal(node, collection);
  601. }
  602. };
  603. var normal = function(node, collection) {
  604. collection.push(node);
  605. var retNode = null;
  606. $.each(node.links(), function(index, link) {
  607. var next_node = self.registry.get(link.child());
  608. retNode = methodChooser(next_node, collection, false, true);
  609. });
  610. return retNode;
  611. };
  612. var control = function(node, collection, skip_parents_check) {
  613. switch(node.node_type()) {
  614. case 'start':
  615. case 'end':
  616. case 'kill':
  617. case 'join':
  618. case 'decisionend':
  619. return normal(node, collection, false, true);
  620. case 'fork':
  621. collection.push(node);
  622. // Wait for join.
  623. // Iterate through all children and add them to child collection.
  624. var join = null;
  625. $.each(node.links(), function(index, link) {
  626. var next_node = self.registry.get(link.child());
  627. var collection = ko.observableArray([]);
  628. node.children.push(collection);
  629. join = methodChooser(next_node, collection, false, true);
  630. });
  631. // Add join to collection, then find its single child.
  632. return methodChooser(join, collection, true, true);
  633. case 'decision':
  634. collection.push(node);
  635. // Waits for end, then runs through children of end node
  636. var end = null;
  637. $.each(node.links(), function(index, link) {
  638. var next_node = self.registry.get(link.child());
  639. var collection = ko.observableArray([]);
  640. node.children.push(collection);
  641. end = methodChooser(next_node, collection, true, true);
  642. });
  643. // Add end
  644. return methodChooser(node.end(), collection, true, true);
  645. default:
  646. // Should never get here.
  647. return node;
  648. }
  649. };
  650. methodChooser(self.registry.get(self.start()), self.nodes, false, true);
  651. $(".tooltip").remove();
  652. $("*[rel=tooltip]").tooltip();
  653. },
  654. rebuild: function() {
  655. var self = this;
  656. // Clear all children
  657. $.each(self.registry.nodes, function(index, node) {
  658. node.children.removeAll();
  659. });
  660. self.nodes.removeAll();
  661. // Rebuild
  662. self.build();
  663. self.draggables();
  664. self.droppables();
  665. self.el.trigger('workflow:rebuilt');
  666. },
  667. draggables: function() {
  668. var self = this;
  669. self.el.find('.node-action').each(function(index, el) {
  670. if (!$(el).hasClass('ui-draggable')) {
  671. $(el).find('.row-fluid').eq(0).css('cursor', 'move');
  672. $(el).draggable({
  673. containment: [ self.el.offset().left - 10, self.el.offset().top - 10,
  674. self.el.offset().left + self.el.outerWidth(), self.el.offset().top + self.el.outerHeight() ],
  675. refreshPositions: true,
  676. revert: true,
  677. zIndex: 1000,
  678. opacity: 0.45,
  679. revertDuration: 0,
  680. cancel: '.node-action-bar'
  681. });
  682. }
  683. });
  684. },
  685. droppables: function() {
  686. var self = this;
  687. self.el.find('.node-link').each(function(index, el) {
  688. $(el).droppable({
  689. 'hoverClass': 'node-link-hover',
  690. 'greedy': true,
  691. 'accept': '.node-action',
  692. 'tolerance': 'pointer'
  693. });
  694. });
  695. self.el.find('.node-decision-end').each(function(index, el) {
  696. $(el).droppable({
  697. 'hoverClass': 'node-link-hover',
  698. 'greedy': true,
  699. 'accept': '.node-action',
  700. 'tolerance': 'pointer'
  701. });
  702. });
  703. self.el.find('.node-fork .action').each(function(index, el) {
  704. $(el).droppable({
  705. 'hoverClass': 'node-fork-hover',
  706. 'greedy': true,
  707. 'accept': '.node-action',
  708. 'tolerance': 'pointer'
  709. });
  710. });
  711. self.el.find('.node-decision .action').each(function(index, el) {
  712. $(el).droppable({
  713. 'hoverClass': 'node-fork-hover',
  714. 'greedy': true,
  715. 'accept': '.node-action',
  716. 'tolerance': 'pointer'
  717. });
  718. });
  719. self.el.find('.node-action .action').each(function(index, el) {
  720. $(el).droppable({
  721. 'hoverClass': 'node-action-hover',
  722. 'greedy': true,
  723. 'accept': '.node-action',
  724. 'tolerance': 'pointer'
  725. });
  726. });
  727. },
  728. dragAndDropEvents: function( options ) {
  729. var self = this;
  730. var read_only_error_handler = options.read_only_error_handler;
  731. // Build event delegations.
  732. // Drop on node link
  733. self.el.on('drop', '.node-link', function(e, ui) {
  734. if (self.read_only()) {
  735. read_only_error_handler();
  736. return false;
  737. }
  738. // draggable should be a node.
  739. // droppable should be a link.
  740. var draggable = ko.contextFor(ui.draggable[0]).$data;
  741. var droppable = ko.contextFor(this).$data;
  742. // If newParent is fork, prepend to child instead.
  743. // This will make it so that we can drop and drop to the top of a node list within a fork.
  744. var newParent = self.registry.get(droppable.parent());
  745. if (newParent.id() != draggable.id() && !newParent.isChild(draggable)) {
  746. switch(newParent.node_type()) {
  747. case 'fork':
  748. case 'decision':
  749. // Children that are forks or decisions may be removed when we detach.
  750. // Remember children in this case to find correct node.
  751. var child = self.registry.get(droppable.child());
  752. var children_of_child = [];
  753. if (child.node_type() == 'fork' || child.node_type() == 'decision') {
  754. children_of_child = child.findChildren();
  755. }
  756. draggable.detach();
  757. // Make sure fork and decision still exist
  758. // Otherwise find child that replaced it
  759. var child_to_replace = child;
  760. if (child.node_type() == 'fork' || child.node_type() == 'decision') {
  761. if (!self.registry.get(child.id())) {
  762. // Guaranteed one because the fork is being removed right now.
  763. child_to_replace = $.grep(children_of_child, function(child_of_child, index) {
  764. return child_of_child.findParents().length > 0;
  765. })[0];
  766. }
  767. }
  768. newParent.replaceChild(child_to_replace, draggable);
  769. draggable.addChild(child_to_replace);
  770. break;
  771. case 'join':
  772. case 'decisionend':
  773. // Join and decisionend may disappear when we detach...
  774. // Remember its children and append to child.
  775. var parents = newParent.findParents();
  776. draggable.detach();
  777. if (newParent.findParents().length < 2) {
  778. $.each(parents, function(index, parent) {
  779. parent.append(draggable);
  780. });
  781. } else {
  782. newParent.append(draggable);
  783. }
  784. break;
  785. default:
  786. draggable.detach();
  787. newParent.append(draggable);
  788. break;
  789. }
  790. workflow.is_dirty( true );
  791. self.rebuild();
  792. }
  793. // Prevent bubbling events
  794. return false;
  795. });
  796. // Drop on fork
  797. self.el.on('drop', '.node-fork', function(e, ui) {
  798. if (self.read_only()) {
  799. read_only_error_handler();
  800. return false;
  801. }
  802. // draggable should be a node.
  803. // droppable should be a fork.
  804. var draggable = ko.contextFor(ui.draggable[0]).$data;
  805. var droppable = ko.contextFor(this).$data;
  806. if (!droppable.isChild(draggable) && droppable.id() != draggable.id()) {
  807. draggable.detach();
  808. droppable.append(draggable);
  809. self.rebuild();
  810. }
  811. // Prevent bubbling events
  812. return false;
  813. });
  814. // Drop on decision
  815. self.el.on('drop', '.node-decision', function(e, ui) {
  816. if (self.read_only()) {
  817. read_only_error_handler();
  818. return false;
  819. }
  820. // draggable should be a node.
  821. // droppable should be a fork.
  822. var draggable = ko.contextFor(ui.draggable[0]).$data;
  823. var droppable = ko.contextFor(this).$data;
  824. if (!droppable.isChild(draggable) && droppable.id() != draggable.id()) {
  825. draggable.detach();
  826. droppable.append(draggable);
  827. self.rebuild();
  828. }
  829. // Prevent bubbling events
  830. return false;
  831. });
  832. // Drop on action
  833. self.el.on('drop', '.node-action', function(e, ui) {
  834. if (self.read_only()) {
  835. read_only_error_handler();
  836. return false;
  837. }
  838. // draggable should be a node.
  839. // droppable should be a node.
  840. var draggable = ko.contextFor(ui.draggable[0]).$data;
  841. var droppable = ko.contextFor(this).$data;
  842. // Create a fork and join programatically.
  843. var newParents = droppable.findParents();
  844. // skip forking beneathe a decision node
  845. if (droppable.id() != draggable.id() && newParents.length == 1 && draggable.findParents().length <= 1) {
  846. var ForkModel = NodeModelChooser('fork');
  847. var JoinModel = NodeModelChooser('join');
  848. var fork = new ForkModel({
  849. id: IdGeneratorTable['fork'].nextId(),
  850. description: "",
  851. workflow: self.id,
  852. node_type: "fork",
  853. child_links: []
  854. });
  855. var forkNode = new ForkNode(self, fork, self.registry);
  856. var join = new JoinModel({
  857. id: IdGeneratorTable['join'].nextId(),
  858. description: "",
  859. workflow: self.id,
  860. node_type: "join",
  861. child_links: []
  862. });
  863. var joinNode = new Node(self, join, self.registry);
  864. self.registry.add(forkNode.id(), forkNode);
  865. self.registry.add(joinNode.id(), joinNode);
  866. forkNode.addChild(joinNode);
  867. // Handles fork creation.
  868. $.each(newParents, function(index, parent) {
  869. parent.replaceChild(droppable, forkNode);
  870. });
  871. draggable.detach();
  872. forkNode.append(draggable);
  873. forkNode.append(droppable);
  874. self.rebuild();
  875. }
  876. // Prevent bubbling events.
  877. return false;
  878. });
  879. }
  880. });
  881. return module;
  882. };
  883. var Workflow = WorkflowModule($, nodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable);