workflow.js 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943
  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. // adding missing .filter for IE8
  17. if (!('filter' in Array.prototype)) {
  18. Array.prototype.filter= function(filter, that /*opt*/) {
  19. var other= [], v;
  20. for (var i=0, n= this.length; i<n; i++)
  21. if (i in this && filter.call(that, v= this[i], i, this))
  22. other.push(v);
  23. return other;
  24. };
  25. }
  26. /**
  27. * Registry of models
  28. * - Each model should have an ID attribute.
  29. */
  30. var RegistryModule = function($) {
  31. var module = function() {
  32. var self = this;
  33. self.nodes = {};
  34. module.prototype.initialize.apply(self, arguments);
  35. return self;
  36. };
  37. $.extend(module.prototype, {
  38. // Normal stuff
  39. initialize: function() {},
  40. toString: function() {
  41. var self = this;
  42. var s = $.map(self.nodes, function(node) {
  43. return node.id();
  44. }).join();
  45. return s;
  46. },
  47. add: function(id, node) {
  48. var self = this;
  49. $(self).trigger('registry:add');
  50. self.nodes[String(id)] = node;
  51. },
  52. remove: function(id) {
  53. var self = this;
  54. $(self).trigger('registry:remove');
  55. delete self.nodes[String(id)];
  56. },
  57. get: function(id) {
  58. var self = this;
  59. return self.nodes[id];
  60. },
  61. clear: function() {
  62. var self = this;
  63. delete self.nodes;
  64. self.nodes = {};
  65. }
  66. });
  67. return module;
  68. };
  69. var Registry = RegistryModule($);
  70. /**
  71. * Modal Control
  72. * Displays a single node in a modal window of your choosing.
  73. * Make sure to update the 'context' member and 'template' members.
  74. * IMPORTANT: Use 'setTemplate', then 'show'.
  75. */
  76. var ModalModule = function($, ko) {
  77. var module = function(modal, template) {
  78. var self = this;
  79. self.modal = $(modal);
  80. self.context = ko.observable();
  81. self.template = ko.observable(template || '');
  82. self.bound = false;
  83. };
  84. module.prototype.show = function(context) {
  85. var self = this;
  86. if (context) {
  87. self.context(context);
  88. }
  89. ko.applyBindings(self, self.modal[0]);
  90. self.modal.modal('show');
  91. };
  92. module.prototype.setTemplate = function(template) {
  93. var self = this;
  94. ko.cleanNode(self.modal[0]);
  95. self.template( template );
  96. };
  97. module.prototype.recenter = function(offset_x, offset_y) {
  98. var self = this;
  99. var top = ( ($(window).height() - self.modal.outerHeight(false)) / 2 );
  100. var left = ( ($(window).width() - self.modal.outerWidth(false)) / 2 );
  101. if (top < 0) {
  102. top = 0;
  103. }
  104. if (left < 0) {
  105. left = 0;
  106. }
  107. top += offset_y || 0;
  108. left += offset_x || 0;
  109. self.modal.css({top: top +'px', left: left+'px'});
  110. };
  111. module.prototype.addDecorations = function () {
  112. $(".popover").remove();
  113. $("input[name='job_xml']:not(.pathChooser)").addClass("pathChooser").after(getFileBrowseButton($("input[name='job_xml']:not(.pathChooser)")));
  114. $("input[name='jar_path']").addClass("pathChooser").after(getFileBrowseButton($("input[name='jar_path']")));
  115. $("input[name='script_path']").addClass("pathChooser").after(getFileBrowseButton($("input[name='script_path']")));
  116. $("input[name='command']").addClass("pathChooser").after(getFileBrowseButton($("input[name='command']")));
  117. $("*[rel=popover]").popover({
  118. placement:'left',
  119. trigger:'hover'
  120. });
  121. }
  122. return module;
  123. };
  124. var Modal = ModalModule($, ko);
  125. /**
  126. * ID Generator
  127. * Generate a new ID starting from 1.
  128. * - Accepts a prefix that will be prepended like so: <prefix>:<id number>
  129. */
  130. var IdGeneratorModule = function($) {
  131. return function(options) {
  132. var self = this;
  133. $.extend(self, options);
  134. self.counter = 1;
  135. self.nextId = function() {
  136. return ((self.prefix) ? self.prefix + ':' : '') + self.counter++;
  137. };
  138. };
  139. };
  140. var IdGenerator = IdGeneratorModule($);
  141. var ModelModule = function($) {
  142. var module = function(attrs) {
  143. var self = this;
  144. $.extend(self, attrs);
  145. module.prototype.initialize.apply(self, arguments);
  146. return self;
  147. };
  148. $.extend(module.prototype, {
  149. // Normal stuff
  150. initialize: function(){},
  151. toString: function() {
  152. var self = this;
  153. return JSON.stringify(self, null, '\t');
  154. }
  155. });
  156. return module;
  157. };
  158. var WorkflowModel = ModelModule($);
  159. $.extend(WorkflowModel.prototype, {
  160. id: 0,
  161. name: '',
  162. description: '',
  163. start: 0,
  164. end: 0,
  165. schema_version: 0.4,
  166. deployment_dir: '',
  167. is_shared: true,
  168. parameters: '[]',
  169. job_xml: ''
  170. });
  171. var NodeModel = ModelModule($);
  172. $.extend(NodeModel.prototype, {
  173. id: 0,
  174. name: '',
  175. description: '',
  176. node_type: '',
  177. workflow: 0,
  178. child_links: []
  179. });
  180. var ForkModel = ModelModule($);
  181. $.extend(ForkModel.prototype, {
  182. id: 0,
  183. name: '',
  184. description: '',
  185. node_type: 'fork',
  186. workflow: 0,
  187. child_links: []
  188. });
  189. var DecisionModel = ModelModule($);
  190. $.extend(DecisionModel.prototype, {
  191. id: 0,
  192. name: '',
  193. description: '',
  194. node_type: 'decision',
  195. workflow: 0,
  196. child_links: []
  197. });
  198. var DistCPModel = ModelModule($);
  199. $.extend(DistCPModel.prototype, {
  200. id: 0,
  201. name: '',
  202. description: '',
  203. node_type: 'distcp',
  204. workflow: 0,
  205. job_properties: [],
  206. prepares: [],
  207. job_xml: '',
  208. params: [],
  209. child_links: []
  210. });
  211. var MapReduceModel = ModelModule($);
  212. $.extend(MapReduceModel.prototype, {
  213. id: 0,
  214. name: '',
  215. description: '',
  216. node_type: 'mapreduce',
  217. workflow: 0,
  218. files: [],
  219. archives: [],
  220. job_properties: [],
  221. jar_path: '',
  222. prepares: [],
  223. job_xml: '',
  224. child_links: []
  225. });
  226. var StreamingModel = ModelModule($);
  227. $.extend(StreamingModel.prototype, {
  228. id: 0,
  229. name: '',
  230. description: '',
  231. node_type: 'streaming',
  232. workflow: 0,
  233. files: [],
  234. archives: [],
  235. job_properties: [],
  236. mapper: '',
  237. reducer: '',
  238. child_links: []
  239. });
  240. var JavaModel = ModelModule($);
  241. $.extend(JavaModel.prototype, {
  242. id: 0,
  243. name: '',
  244. description: '',
  245. node_type: 'java',
  246. workflow: 0,
  247. files: [],
  248. archives: [],
  249. job_properties: [],
  250. jar_path: '',
  251. prepares: [],
  252. job_xml: '',
  253. main_class: '',
  254. args: '',
  255. java_opts: '',
  256. child_links: []
  257. });
  258. var PigModel = ModelModule($);
  259. $.extend(PigModel.prototype, {
  260. id: 0,
  261. name: '',
  262. description: '',
  263. node_type: 'pig',
  264. workflow: 0,
  265. files: [],
  266. archives: [],
  267. job_properties: [],
  268. prepares: [],
  269. job_xml: '',
  270. params: [],
  271. script_path: '',
  272. child_links: []
  273. });
  274. var HiveModel = ModelModule($);
  275. $.extend(HiveModel.prototype, {
  276. id: 0,
  277. name: '',
  278. description: '',
  279. node_type: 'hive',
  280. workflow: 0,
  281. files: [],
  282. archives: [],
  283. job_properties: [],
  284. prepares: [],
  285. job_xml: '',
  286. params: [],
  287. script_path: '',
  288. child_links: []
  289. });
  290. var SqoopModel = ModelModule($);
  291. $.extend(SqoopModel.prototype, {
  292. id: 0,
  293. name: '',
  294. description: '',
  295. node_type: 'sqoop',
  296. workflow: 0,
  297. files: [],
  298. archives: [],
  299. job_properties: [],
  300. prepares: [],
  301. job_xml: '',
  302. params: [],
  303. script_path: '',
  304. child_links: []
  305. });
  306. var ShellModel = ModelModule($);
  307. $.extend(ShellModel.prototype, {
  308. id: 0,
  309. name: '',
  310. description: '',
  311. node_type: 'shell',
  312. workflow: 0,
  313. files: [],
  314. archives: [],
  315. job_properties: [],
  316. prepares: [],
  317. job_xml: '',
  318. params: [],
  319. command: '',
  320. capture_output: false,
  321. child_links: []
  322. });
  323. var SshModel = ModelModule($);
  324. $.extend(SshModel.prototype, {
  325. id: 0,
  326. name: '',
  327. description: '',
  328. node_type: 'ssh',
  329. workflow: 0,
  330. user: '',
  331. host: '',
  332. params: [],
  333. command: '',
  334. capture_output: false,
  335. child_links: []
  336. });
  337. function nodeModelChooser(node_type) {
  338. switch(node_type) {
  339. case 'mapreduce':
  340. return MapReduceModel;
  341. case 'streaming':
  342. return StreamingModel;
  343. case 'java':
  344. return JavaModel;
  345. case 'pig':
  346. return PigModel;
  347. case 'hive':
  348. return HiveModel;
  349. case 'sqoop':
  350. return SqoopModel;
  351. case 'shell':
  352. return ShellModel;
  353. case 'ssh':
  354. return SshModel;
  355. case 'distcp':
  356. return DistCPModel;
  357. case 'fork':
  358. return ForkModel;
  359. case 'decision':
  360. return DecisionModel;
  361. default:
  362. return NodeModel;
  363. }
  364. }
  365. var IdGeneratorTable = {
  366. mapreduce: new IdGenerator({prefix: 'mapreduce'}),
  367. streaming: new IdGenerator({prefix: 'streaming'}),
  368. java: new IdGenerator({prefix: 'java'}),
  369. pig: new IdGenerator({prefix: 'pig'}),
  370. hive: new IdGenerator({prefix: 'hive'}),
  371. sqoop: new IdGenerator({prefix: 'sqoop'}),
  372. shell: new IdGenerator({prefix: 'shell'}),
  373. ssh: new IdGenerator({prefix: 'ssh'}),
  374. distcp: new IdGenerator({prefix: 'distcp'}),
  375. fork: new IdGenerator({prefix: 'fork'}),
  376. decision: new IdGenerator({prefix: 'decision'}),
  377. join: new IdGenerator({prefix: 'join'})
  378. };
  379. var NodeModule = function($, IdGeneratorTable) {
  380. var META_LINKS = ['related', 'default', 'error'];
  381. var linkTypeChooser = function(parent, child) {
  382. if (child.node_type() == 'kill') {
  383. return 'error';
  384. }
  385. switch(parent.node_type()) {
  386. case 'start':
  387. return (child.node_type() == 'end') ? 'related' : 'to';
  388. case 'fork':
  389. return (child.node_type() == 'join') ? 'related' : 'start';
  390. case 'decision':
  391. return 'start';
  392. case 'join':
  393. return 'to';
  394. default:
  395. return 'ok';
  396. };
  397. };
  398. var module = function(workflow, model, registry) {
  399. var self = this;
  400. self.map(model);
  401. self.links = ko.computed(function() {
  402. var links = self.child_links().filter(function(element, index, arr) {
  403. return $.inArray(element.name(), META_LINKS) == -1;
  404. });
  405. return links;
  406. });
  407. self.meta_links = ko.computed(function() {
  408. var links = self.child_links().filter(function(element, index, arr) {
  409. return $.inArray(element.name(), META_LINKS) != -1;
  410. });
  411. return links;
  412. });
  413. self._workflow = workflow;
  414. self.registry = registry;
  415. self.children = ko.observableArray([]);
  416. self.model = model;
  417. self.edit_template = model.node_type + 'EditTemplate';
  418. switch(model.node_type) {
  419. case 'start':
  420. self.view_template = ko.observable('startTemplate');
  421. break;
  422. case 'kill':
  423. case 'end':
  424. self.view_template = ko.observable('emptyTemplate');
  425. break;
  426. case 'fork':
  427. self.view_template = ko.observable('forkTemplate');
  428. break;
  429. case 'decision':
  430. self.view_template = ko.observable('decisionTemplate');
  431. break;
  432. case 'join':
  433. self.view_template = ko.observable('joinTemplate');
  434. break;
  435. default:
  436. self.view_template = ko.observable('nodeTemplate');
  437. break;
  438. }
  439. // Data manipulation
  440. if ('job_properties' in model) {
  441. self.addProp = function() {
  442. var prop = { name: ko.observable(""), value: ko.observable("") };
  443. prop.name.subscribe(function(value) {
  444. self.job_properties.valueHasMutated();
  445. });
  446. prop.value.subscribe(function(value) {
  447. self.job_properties.valueHasMutated();
  448. });
  449. self.job_properties.push(prop);
  450. // $(".propKey:last").each(addAutoComplete);
  451. };
  452. self.removeProp = function(val) {
  453. self.job_properties.remove(val);
  454. };
  455. }
  456. if ('files' in model) {
  457. self.addFile = function() {
  458. var prop = { name: ko.observable(""), dummy: ko.observable("") };
  459. prop.name.subscribe(function(value) {
  460. self.files.valueHasMutated();
  461. });
  462. prop.dummy.subscribe(function(value) {
  463. self.files.valueHasMutated();
  464. });
  465. self.files.push(prop);
  466. };
  467. self.removeFile = function(val) {
  468. self.files.remove(val);
  469. };
  470. }
  471. if ('archives' in model) {
  472. self.addArchive = function() {
  473. var prop = { name: ko.observable(""), dummy: ko.observable("") };
  474. prop.name.subscribe(function(value) {
  475. self.archives.valueHasMutated();
  476. });
  477. prop.dummy.subscribe(function(value) {
  478. self.archives.valueHasMutated();
  479. });
  480. self.archives.push(prop);
  481. };
  482. self.removeArchive = function(val) {
  483. self.archives.remove(val);
  484. };
  485. }
  486. if ('params' in model) {
  487. self.addParam = function() {
  488. var prop = { value: ko.observable(""), type: ko.observable("param") };
  489. prop.value.subscribe(function(value) {
  490. self.params.valueHasMutated();
  491. });
  492. prop.type.subscribe(function(value) {
  493. self.params.valueHasMutated();
  494. });
  495. self.params.push(prop);
  496. };
  497. self.addArgument = function() {
  498. var prop = { value: ko.observable(""), type: ko.observable("argument") };
  499. prop.value.subscribe(function(value) {
  500. self.params.valueHasMutated();
  501. });
  502. prop.type.subscribe(function(value) {
  503. self.params.valueHasMutated();
  504. });
  505. self.params.push(prop);
  506. };
  507. self.addArg = function() {
  508. var prop = { value: ko.observable(""), type: ko.observable("arg") };
  509. prop.value.subscribe(function(value) {
  510. self.params.valueHasMutated();
  511. });
  512. prop.type.subscribe(function(value) {
  513. self.params.valueHasMutated();
  514. });
  515. self.params.push(prop);
  516. };
  517. self.addEnvVar = function() {
  518. var prop = { value: ko.observable(""), type: ko.observable("env-var") };
  519. prop.value.subscribe(function(value) {
  520. self.params.valueHasMutated();
  521. });
  522. prop.type.subscribe(function(value) {
  523. self.params.valueHasMutated();
  524. });
  525. self.params.push(prop);
  526. };
  527. self.removeParam = function(val) {
  528. self.params.remove(val);
  529. };
  530. }
  531. if ('prepares' in model) {
  532. self.addPrepareDelete = function() {
  533. var prop = { value: ko.observable(""), type: ko.observable("delete") };
  534. prop.value.subscribe(function(value) {
  535. self.prepares.valueHasMutated();
  536. });
  537. prop.type.subscribe(function(value) {
  538. self.prepares.valueHasMutated();
  539. });
  540. self.prepares.push(prop);
  541. };
  542. self.addPrepareMkdir = function() {
  543. var prop = { value: ko.observable(""), type: ko.observable("mkdir") };
  544. prop.value.subscribe(function(value) {
  545. self.prepares.valueHasMutated();
  546. });
  547. prop.type.subscribe(function(value) {
  548. self.prepares.valueHasMutated();
  549. });
  550. self.prepares.push(prop);
  551. };
  552. self.removePrepare = function(val) {
  553. self.prepares.remove(val);
  554. };
  555. }
  556. self.initialize.apply(self, arguments);
  557. return self;
  558. };
  559. $.extend(module.prototype, {
  560. // Data.
  561. children: null,
  562. model: null,
  563. // Normal stuff
  564. /**
  565. * Called when creating a new node
  566. */
  567. initialize: function(workflow, model, registry) {},
  568. toString: function() {
  569. return '';
  570. },
  571. /**
  572. * Fetches registry
  573. */
  574. getRegistry: function() {
  575. return registry;
  576. },
  577. /**
  578. * Maps a model to self
  579. * Called when creating a new node before any thing else
  580. */
  581. map: function(model) {
  582. var self = this;
  583. var map_params = function(options, subscribe) {
  584. options.data = ($.type(options.data) == "string") ? $.parseJSON(options.data) : options.data;
  585. if ($.isArray(options.data)) {
  586. var mapping = ko.mapping.fromJS(options.data);
  587. $.each(mapping(), function(index, value) {
  588. subscribe(value);
  589. });
  590. return mapping;
  591. } else {
  592. var mapping = ko.mapping.fromJS(options.data, {});
  593. subscribe(mapping);
  594. return mapping;
  595. }
  596. };
  597. // @see http://knockoutjs.com/documentation/plugins-mapping.html
  598. var mapping = ko.mapping.fromJS(model, {
  599. ignore: ['initialize','toString'],
  600. job_properties: {
  601. create: function(options) {
  602. var parent = options.parent;
  603. var subscribe = function(mapping) {
  604. mapping.name.subscribe(function(value) {
  605. parent.job_properties.valueHasMutated();
  606. });
  607. mapping.value.subscribe(function(value) {
  608. parent.job_properties.valueHasMutated();
  609. });
  610. };
  611. return map_params(options, subscribe);
  612. }
  613. },
  614. files: {
  615. create: function(options) {
  616. return map_params(options, function() {});
  617. }
  618. },
  619. archives: {
  620. create: function(options) {
  621. var parent = options.parent;
  622. var subscribe = function(mapping) {
  623. mapping.name.subscribe(function(value) {
  624. parent.archives.valueHasMutated();
  625. });
  626. mapping.dummy.subscribe(function(value) {
  627. parent.archives.valueHasMutated();
  628. });
  629. };
  630. return map_params(options, subscribe);
  631. }
  632. },
  633. params: {
  634. create: function(options) {
  635. var parent = options.parent;
  636. var subscribe = function(mapping) {
  637. mapping.value.subscribe(function(value) {
  638. parent.params.valueHasMutated();
  639. });
  640. mapping.type.subscribe(function(value) {
  641. parent.params.valueHasMutated();
  642. });
  643. };
  644. return map_params(options, subscribe);
  645. }
  646. },
  647. prepares: {
  648. create: function(options) {
  649. var parent = options.parent;
  650. var subscribe = function(mapping) {
  651. mapping.value.subscribe(function(value) {
  652. parent.prepares.valueHasMutated();
  653. });
  654. mapping.type.subscribe(function(value) {
  655. parent.prepares.valueHasMutated();
  656. });
  657. };
  658. return map_params(options, subscribe);
  659. }
  660. }
  661. });
  662. $.extend(self, mapping);
  663. $.each(mapping, function(key, value) {
  664. var key = key;
  665. if (ko.isObservable(self[key])) {
  666. self[key].subscribe(function(value) {
  667. model[key] = ko.mapping.toJS(value);
  668. });
  669. }
  670. });
  671. $.each(self.child_links(), function(index, link) {
  672. var $index = index;
  673. link.comment.subscribe(function(value) {
  674. self.model.child_links[$index].comment = value;
  675. });
  676. link.child.subscribe(function(value) {
  677. self.model.child_links[$index].child = value;
  678. });
  679. });
  680. },
  681. // Hierarchy manipulation.
  682. /**
  683. * Append node to self
  684. * Does not support multiple children.
  685. * Ensures single child.
  686. * Ensures no cycles.
  687. * 1. Finds all children and attaches them to node (cleans node first).
  688. * 2. Remove all children from self.
  689. * 3. Attach node to self.
  690. */
  691. append: function(node) {
  692. var self = this;
  693. // Not fork nor decision nor self
  694. if ($.inArray(self.node_type(), ['fork', 'decision']) == -1 && node.id() != self.id()) {
  695. node.removeAllChildren();
  696. $.each(self.links(), function(index, link) {
  697. node.addChild(registry.get(link.child()));
  698. });
  699. self.removeAllChildren();
  700. self.addChild(node);
  701. }
  702. },
  703. /**
  704. * Find all parents of current node
  705. */
  706. findParents: function() {
  707. var self = this;
  708. var parents = [];
  709. $.each(registry.nodes, function(id, node) {
  710. $.each(node.links(), function(index, link) {
  711. if (link.child() == self.id()) {
  712. parents.push(node);
  713. }
  714. });
  715. });
  716. return parents;
  717. },
  718. /**
  719. * Find all children of current node
  720. */
  721. findChildren: function() {
  722. var self = this;
  723. var children = [];
  724. $.each(self.links(), function(index, link) {
  725. children.push(registry.get(link.child()));
  726. });
  727. return children;
  728. },
  729. /**
  730. * Detach current node from the graph
  731. * 1. Takes children of self node, removes them from self node, and adds them to each parent of self node.
  732. * 2. The self node is then removed from every parent.
  733. * 3. Does not support multiple children since we do not automatically fork.
  734. */
  735. detach: function() {
  736. var self = this;
  737. $.each(self.findParents(), function(index, parent) {
  738. $.each(self.links(), function(index, link) {
  739. var node = registry.get(link.child());
  740. parent.replaceChild(self, node);
  741. });
  742. });
  743. $(self).trigger('detached');
  744. self.removeAllChildren();
  745. },
  746. /**
  747. * Add child
  748. * Update child links for this node.
  749. */
  750. addChild: function(node) {
  751. var self = this;
  752. var link = {
  753. parent: ko.observable(self.id()),
  754. child: ko.observable(node.id()),
  755. name: ko.observable(linkTypeChooser(self, node)),
  756. comment: ko.observable('')
  757. };
  758. self.child_links.unshift(link);
  759. },
  760. /**
  761. * Remove child node
  762. * 1. Find child node link
  763. * 2. Remove child node link
  764. */
  765. removeChild: function(node) {
  766. var self = this;
  767. var spliceIndex = -1;
  768. $.each(self.child_links(), function(index, link) {
  769. if (link.child() == node.id()) {
  770. spliceIndex = index;
  771. }
  772. });
  773. if (spliceIndex > -1) {
  774. self.child_links.splice(spliceIndex, 1);
  775. }
  776. return spliceIndex != -1;
  777. },
  778. /**
  779. * Remove all children
  780. * Removes all children except for related, default, and error links
  781. * Note: we hold on to related, default, and error links because
  782. * we have to.
  783. */
  784. removeAllChildren: function() {
  785. var self = this;
  786. var keep_links = [];
  787. $.each(self.child_links(), function(index, link) {
  788. if ($.inArray(link.name(), META_LINKS) > -1) {
  789. keep_links.push(link);
  790. }
  791. });
  792. self.child_links.removeAll();
  793. $.each(keep_links, function(index, link) {
  794. self.child_links.push(link);
  795. });
  796. },
  797. /**
  798. * Replace child node with another node in the following way:
  799. * 1. Find child index
  800. * 2. Remove child index
  801. * 3. Remove and remember every element after child
  802. * 4. Add replacement node
  803. * 5. Add every child that was remembered
  804. */
  805. replaceChild: function(child, replacement) {
  806. var self = this;
  807. var index = -1;
  808. $.each(self.child_links(), function(i, link) {
  809. if (link.child() == child.id()) {
  810. index = i;
  811. }
  812. });
  813. if (index > -1) {
  814. self.child_links.splice(index, 1);
  815. var links = self.child_links.splice(index);
  816. var link = {
  817. parent: ko.observable(self.id()),
  818. child: ko.observable(replacement.id()),
  819. name: ko.observable(linkTypeChooser(self, replacement)),
  820. comment: ko.observable(''),
  821. };
  822. self.child_links.push(link);
  823. $.each(links, function(index, link) {
  824. self.child_links.push(link);
  825. });
  826. }
  827. return index != -1;
  828. },
  829. isChild: function(node) {
  830. var self = this;
  831. var res = false;
  832. $.each(self.links(), function(index, link) {
  833. if (link.child() == node.id()) {
  834. res = true;
  835. }
  836. });
  837. return res;
  838. },
  839. erase: function() {
  840. var self = this;
  841. self.registry.remove(self.id());
  842. }
  843. });
  844. return module;
  845. };
  846. var Node = NodeModule($, IdGeneratorTable);
  847. var ForkNode = NodeModule($, IdGeneratorTable);
  848. $.extend(ForkNode.prototype, Node.prototype, {
  849. // Join nodes are connected through 'related' links
  850. join: function() {
  851. var self = this;
  852. var join = null;
  853. $.each(self.child_links(), function(index, link) {
  854. if (link.name() == 'related') {
  855. join = self.registry.get(link.child());
  856. }
  857. });
  858. return join;
  859. },
  860. /**
  861. * Append a node to the current fork
  862. * Also adds join node to node.
  863. * When adding the join node, append will remove all the children from the join!
  864. * We need to make sure the join remembers its children.
  865. * NOTE: Cannot append a fork! Use addChild or replaceChild instead!
  866. */
  867. append: function(node) {
  868. var self = this;
  869. if (node.node_type() != 'fork') {
  870. var join = self.join();
  871. if (join.id() != node.id()) {
  872. var children = join.findChildren();
  873. self.addChild(node);
  874. node.append(join);
  875. // remember children
  876. $.each(children, function(index, child) {
  877. join.addChild(child);
  878. });
  879. }
  880. }
  881. },
  882. /**
  883. * Replace child node with another node
  884. * 1. Remove child if the replacement is related join
  885. * 2. Apply Node.replaceChild for all other causes
  886. * NOTE: can assume the only join this fork will ever see is the related join!
  887. * This is because the related will always be the closest join for children.
  888. * Also, the operations allowed that would make this all possible: detach, append;
  889. * are inherently going to remove any other joins if they exist.
  890. * This is also easier given nodes are contained within Forks!
  891. */
  892. replaceChild: function(child, replacement) {
  893. var self = this;
  894. var ret = true;
  895. if (self.join().id() == replacement.id()) {
  896. ret = self.removeChild(child);
  897. } else {
  898. ret = Node.prototype.replaceChild.apply(self, arguments);
  899. }
  900. var links = self.links().filter(function(element, index, arr) {
  901. return self.registry.get(element.child()).node_type() != 'join';
  902. });
  903. if (links.length < 2) {
  904. self.detach();
  905. self.join().detach();
  906. self.erase();
  907. self.join().erase();
  908. }
  909. return ret;
  910. },
  911. /**
  912. * Converts fork node into decision node in the following way:
  913. * 1. Copies contents of current fork node into a new decision node
  914. * 2. Detach fork node
  915. * 3. Erase fork node
  916. * 4. Append decision node to parent
  917. */
  918. convertToDecision: function() {
  919. var self = this;
  920. var join = self.join();
  921. var end = null;
  922. var child = join.findChildren()[0];
  923. if (child.findParents().length == 1) {
  924. end = child;
  925. }
  926. // Attaches child of join to parents of join
  927. join.detach();
  928. var decision_model = new DecisionModel({
  929. id: IdGeneratorTable['decision'].nextId(),
  930. name: self.name(),
  931. description: self.description(),
  932. node_type: 'decision',
  933. workflow: self.workflow(),
  934. child_links: self.model.child_links
  935. });
  936. var default_link = {
  937. parent: decision_model.id,
  938. child: self._workflow.end(),
  939. name: 'default',
  940. comment: ''
  941. };
  942. decision_model.child_links.push(default_link);
  943. $.each(decision_model.child_links, function(index, link) {
  944. link.parent = decision_model.id;
  945. });
  946. var decision_node = new DecisionNode(self._workflow, decision_model, self.registry);
  947. if (end) {
  948. decision_node.addEnd(end);
  949. }
  950. var parents = self.findParents();
  951. decision_node.removeChild(join);
  952. join.erase();
  953. self.erase();
  954. self.registry.add(decision_node.id(), decision_node);
  955. $.each(parents, function(index, parent) {
  956. parent.replaceChild(self, decision_node);
  957. });
  958. }
  959. });
  960. var DecisionNode = NodeModule($, IdGeneratorTable);
  961. $.extend(DecisionNode.prototype, ForkNode.prototype, {
  962. initialize: function(workflow, model, registry) {
  963. var self = this;
  964. var registry = registry;
  965. var end = null;
  966. $.each(self.child_links(), function(index, link) {
  967. if (link.name() == 'related') {
  968. $(registry).bind('registry:add', function(e) {
  969. var end = self.end();
  970. if (end) {
  971. self.removeEnd();
  972. self.addEnd(end);
  973. $(registry).unbind(e);
  974. }
  975. });
  976. }
  977. });
  978. },
  979. // Only a single end allowed for now!
  980. // Finds end for branch
  981. findEnd: function( ) {
  982. var self = this;
  983. var end = self.end();
  984. if (!end) {
  985. return self._findEnd( self, 0 );
  986. }
  987. return end;
  988. },
  989. _findEnd: function( node, count ) {
  990. var self = this;
  991. var end = null;
  992. if (node.findParents().length > 1 && --count == 0) {
  993. return node;
  994. }
  995. if (node.node_type() == 'decision' || node.node_type() == 'fork') {
  996. count++;
  997. }
  998. $.each(node.findChildren(), function(index, node) {
  999. if (end == null) {
  1000. end = self._findEnd(node, count);
  1001. }
  1002. });
  1003. return end;
  1004. },
  1005. end: function() {
  1006. var self = this;
  1007. var end = null;
  1008. $.each(self.child_links(), function(index, link) {
  1009. if (link.name() == 'related') {
  1010. end = self.registry.get(link.child());
  1011. }
  1012. });
  1013. return end;
  1014. },
  1015. addEnd: function(node) {
  1016. var self = this;
  1017. self.child_links.push({
  1018. parent: ko.observable(self.id()),
  1019. child: ko.observable(node.id()),
  1020. name: ko.observable('related'),
  1021. comment: ko.observable(''),
  1022. });
  1023. $(node).one('detached', function(e) {
  1024. var end = self.end();
  1025. if (end.links().length == 1) {
  1026. self.removeEnd();
  1027. var new_end = self.findEnd();
  1028. self.addEnd(new_end);
  1029. } else {
  1030. // Should never hit this case.
  1031. self.removeEnd();
  1032. }
  1033. });
  1034. },
  1035. removeEnd: function() {
  1036. var self = this;
  1037. var spliceIndex = -1;
  1038. $.each(self.child_links(), function(index, link) {
  1039. if (link.name() == 'related') {
  1040. spliceIndex = index;
  1041. }
  1042. });
  1043. if (spliceIndex > -1) {
  1044. var end = registry.get(self.child_links()[spliceIndex].child());
  1045. self.child_links.splice(spliceIndex, 1);
  1046. return true;
  1047. }
  1048. return false;
  1049. },
  1050. isChild: function(node) {
  1051. var self = this;
  1052. return self._isChild(node, self, 0);
  1053. },
  1054. _isChild: function( test_child, node, count ) {
  1055. var self = this;
  1056. var result = node.id() == test_child.id();
  1057. if (node.findParents().length > 1 && --count == 0) {
  1058. return result;
  1059. }
  1060. if (node.node_type() == 'decision' || node.node_type() == 'fork') {
  1061. count++;
  1062. }
  1063. $.each(node.findChildren(), function(index, node) {
  1064. if (!result) {
  1065. result = self._isChild(test_child, node, count);
  1066. }
  1067. });
  1068. return result;
  1069. },
  1070. /**
  1071. * Append a node to the current decision
  1072. * Also appends end node to node.
  1073. * NOTE: Cannot append a decision! Use addChild or replaceChild instead!
  1074. */
  1075. append: function(node) {
  1076. var self = this;
  1077. var end = self.findEnd();
  1078. if (end.id() == node.id()) {
  1079. return false;
  1080. }
  1081. self.addChild(node);
  1082. node.append(end);
  1083. return true;
  1084. },
  1085. /**
  1086. * Replace child node with another node
  1087. * 1. Remove child if the replacement is end
  1088. * 2. Apply Node.replaceChild for all other causes
  1089. */
  1090. replaceChild: function(child, replacement) {
  1091. var self = this;
  1092. var ret = true;
  1093. var end = self.findEnd();
  1094. if (end && end.id() == replacement.id()) {
  1095. ret = self.removeChild(child);
  1096. } else {
  1097. ret = Node.prototype.replaceChild.apply(self, arguments);
  1098. }
  1099. if (self.links().length < 2) {
  1100. self.detach();
  1101. self.erase();
  1102. }
  1103. return ret;
  1104. }
  1105. });
  1106. /**
  1107. * Workflow module
  1108. */
  1109. var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable) {
  1110. var module = function(options) {
  1111. var self = this;
  1112. // @see http://knockoutjs.com/documentation/plugins-mapping.html
  1113. var mapping = ko.mapping.fromJS(options.model, {
  1114. ignore: ['initialize','toString'],
  1115. job_properties: {
  1116. create: function(options) {
  1117. var mapping = ko.mapping.fromJS($.parseJSON(options.data) || options.data);
  1118. var parent = options.parent;
  1119. mapping.name.subscribe(function(value) {
  1120. parent.job_properties.valueHasMutated();
  1121. });
  1122. mapping.value.subscribe(function(value) {
  1123. parent.job_properties.valueHasMutated();
  1124. });
  1125. return mapping;
  1126. }
  1127. },
  1128. parameters: {
  1129. // Will receive individual objects to subscribe.
  1130. // Containing array is mapped automagically
  1131. create: function(options) {
  1132. var mapping = ko.mapping.fromJS($.parseJSON(options.data) || options.data);
  1133. var parent = options.parent;
  1134. mapping.name.subscribe(function(value) {
  1135. parent.parameters.valueHasMutated();
  1136. });
  1137. mapping.value.subscribe(function(value) {
  1138. parent.parameters.valueHasMutated();
  1139. });
  1140. return mapping;
  1141. }
  1142. }
  1143. });
  1144. $.extend(self, mapping);
  1145. $.each(mapping['__ko_mapping__'].mappedProperties, function(key, value) {
  1146. var key = key;
  1147. self[key].subscribe(function(value) {
  1148. workflow.model.is_dirty = true;
  1149. self.model[key] = ko.mapping.toJS(value);
  1150. });
  1151. });
  1152. self.model = options.model;
  1153. self.registry = options.registry;
  1154. self.options = options;
  1155. self.el = $('#workflow');
  1156. self.nodes = ko.observableArray([]);
  1157. self.kill = null;
  1158. self.url = ko.computed(function() {
  1159. return '/oozie/workflows/' + self.id()
  1160. });
  1161. // Events
  1162. self.el.on('workflow:rebuild', function() {
  1163. self.rebuild();
  1164. });
  1165. self.el.on('workflow:events:load', function() {
  1166. self.dragAndDropEvents();
  1167. });
  1168. self.el.on('workflow:droppables:load', function() {
  1169. self.droppables();
  1170. });
  1171. self.el.on('workflow:draggables:load', function() {
  1172. self.draggables();
  1173. });
  1174. self.dragAndDropEvents();
  1175. self.el.trigger('workflow:events:loaded');
  1176. module.prototype.initialize.apply(self, arguments);
  1177. return self;
  1178. };
  1179. $.extend(module.prototype, {
  1180. // Normal stuff
  1181. initialize: function(options) {
  1182. var self = this;
  1183. if ('data' in options) {
  1184. // Initialize nodes
  1185. if (options.data.nodes) {
  1186. self.registry.clear();
  1187. $.each(options.data.nodes, function(index, node) {
  1188. var NodeModel = NodeModelChooser(node.node_type);
  1189. var model = new NodeModel(node);
  1190. var temp = null;
  1191. switch(node.node_type) {
  1192. case 'fork':
  1193. temp = new ForkNode(self, model, self.registry);
  1194. break;
  1195. case 'decision':
  1196. temp = new DecisionNode(self, model, self.registry);
  1197. break;
  1198. case 'kill':
  1199. temp = self.kill = new Node(self, model, self.registry);
  1200. break;
  1201. default:
  1202. temp = new Node(self, model, self.registry);
  1203. break;
  1204. }
  1205. if ('main_class' in temp) {
  1206. console.log(temp.main_class());
  1207. }
  1208. self.registry.add(temp.id(), temp);
  1209. });
  1210. }
  1211. // Update data
  1212. $.each(options.data, function (key, value) {
  1213. if (key in self) {
  1214. switch(key) {
  1215. case 'job_properties':
  1216. case 'parameters':
  1217. // These may be serialized JSON data since that is how they are stored
  1218. self[key].removeAll();
  1219. var arr = $.parseJSON(value) || value;
  1220. $.each(arr, function(index, obj) {
  1221. var mapping = ko.mapping.fromJS(obj);
  1222. mapping.name.subscribe(function(value) {
  1223. self[key].valueHasMutated();
  1224. });
  1225. mapping.value.subscribe(function(value) {
  1226. self[key].valueHasMutated();
  1227. });
  1228. self[key].push(mapping);
  1229. });
  1230. break;
  1231. case 'nodes':
  1232. break;
  1233. default:
  1234. self[key](value);
  1235. break;
  1236. }
  1237. }
  1238. });
  1239. workflow.model.is_dirty = false;
  1240. }
  1241. },
  1242. toString: function() {
  1243. return '';
  1244. var s = '[';
  1245. $.each(self.registry.nodes, function(key, node) {
  1246. s += node.model.toString() + ",\n";
  1247. });
  1248. return s + ']';
  1249. },
  1250. // Data manipulation
  1251. cloneNode: function(node, event) {
  1252. var self = this;
  1253. var model_copy = $.extend(true, {}, node.model);
  1254. var template = model_copy.node_type + 'EditTemplate';
  1255. var NodeModel = NodeModelChooser(node.node_type());
  1256. model_copy.id = IdGeneratorTable[model_copy.node_type].nextId();
  1257. model_copy.name += '-copy';
  1258. model_copy.child_links = [];
  1259. var model = new NodeModel(model_copy);
  1260. var new_node = new Node(self, model, self.registry);
  1261. self.registry.add(new_node.id(), new_node);
  1262. new_node.addChild(self.kill);
  1263. node.append(new_node);
  1264. self.rebuild();
  1265. modal.setTemplate(template);
  1266. modal.show(new_node);
  1267. modal.recenter(280, 250);
  1268. modal.addDecorations();
  1269. // $(".propKey").each(addAutoComplete);
  1270. workflow.model.is_dirty = true;
  1271. },
  1272. removeNode: function(node, event) {
  1273. var self = this;
  1274. node.detach();
  1275. node.erase();
  1276. self.rebuild();
  1277. workflow.model.is_dirty = true;
  1278. },
  1279. save: function( options ) {},
  1280. load: function() {
  1281. var self = this;
  1282. $.getJSON(self.url(), function(data) {
  1283. if (data.status == 0) {
  1284. self.reload(data.data);
  1285. } else {
  1286. $.jHueNotify.error("${ _('Received invalid response from server: ') }" + JSON.stringify(data));
  1287. }
  1288. });
  1289. },
  1290. reload: function(data) {
  1291. var self = this;
  1292. // Clear all children
  1293. $.each(self.registry.nodes, function(index, node) {
  1294. node.children.removeAll();
  1295. });
  1296. self.nodes.removeAll();
  1297. self.initialize({data: data});
  1298. self.rebuild();
  1299. self.el.trigger('workflow:loaded');
  1300. },
  1301. addParameter: function(data, event) {
  1302. var self = this;
  1303. var prop = { name: ko.observable(""), value: ko.observable("") };
  1304. // force bubble up to containing observable array.
  1305. prop.name.subscribe(function(){
  1306. self.parameters.valueHasMutated();
  1307. });
  1308. prop.value.subscribe(function(){
  1309. self.parameters.valueHasMutated();
  1310. });
  1311. self.parameters.push(prop);
  1312. },
  1313. removeParameter: function(data, event) {
  1314. var self = this;
  1315. self.parameters.remove(data);
  1316. },
  1317. addJobProperty: function(data, event) {
  1318. var self = this;
  1319. var prop = { name: ko.observable(""), value: ko.observable("") };
  1320. // force bubble up to containing observable array.
  1321. prop.name.subscribe(function(){
  1322. self.parameters.valueHasMutated();
  1323. });
  1324. prop.value.subscribe(function(){
  1325. self.parameters.valueHasMutated();
  1326. });
  1327. self.job_properties.push(prop);
  1328. },
  1329. removeJobProperty: function(data, event) {
  1330. var self = this;
  1331. self.job_properties.remove(data);
  1332. },
  1333. // Workflow UI
  1334. // Function to build nodes... recursively.
  1335. build: function() {
  1336. var self = this;
  1337. var maximum = 50;
  1338. var count = 0;
  1339. var methodChooser = function(node, collection, skip_parents_check) {
  1340. if (count++ >= maximum) {
  1341. if (window.error) {
  1342. console.error('Hit maximum number of node recursion: ' + maximum);
  1343. }
  1344. return null;
  1345. }
  1346. if (!node) {
  1347. return node;
  1348. }
  1349. var parents = node.findParents();
  1350. // Found end of decision node or found join!
  1351. if (parents.length > 1 && !skip_parents_check) {
  1352. return node;
  1353. }
  1354. switch(node.node_type()) {
  1355. case 'start':
  1356. case 'end':
  1357. case 'kill':
  1358. case 'fork':
  1359. case 'decision':
  1360. case 'join':
  1361. return control(node, collection);
  1362. default:
  1363. return normal(node, collection);
  1364. }
  1365. };
  1366. var normal = function(node, collection) {
  1367. collection.push(node);
  1368. var retNode = null;
  1369. $.each(node.links(), function(index, link) {
  1370. var next_node = self.registry.get(link.child());
  1371. retNode = methodChooser(next_node, collection, false, true);
  1372. });
  1373. return retNode;
  1374. };
  1375. var control = function(node, collection, skip_parents_check) {
  1376. switch(node.node_type()) {
  1377. case 'start':
  1378. case 'end':
  1379. case 'kill':
  1380. case 'join':
  1381. return normal(node, collection, false, true);
  1382. case 'fork':
  1383. collection.push(node);
  1384. // Wait for join.
  1385. // Iterate through all children and add them to child collection.
  1386. var join = null;
  1387. $.each(node.links(), function(index, link) {
  1388. var next_node = self.registry.get(link.child());
  1389. var collection = ko.observableArray([]);
  1390. node.children.push(collection);
  1391. join = methodChooser(next_node, collection, false, true);
  1392. });
  1393. // Add join to collection, then find its single child.
  1394. return methodChooser(join, collection, true, true);
  1395. case 'decision':
  1396. collection.push(node);
  1397. // Waits for end, then runs through children of end node
  1398. var end = null;
  1399. $.each(node.links(), function(index, link) {
  1400. var next_node = self.registry.get(link.child());
  1401. var collection = ko.observableArray([]);
  1402. node.children.push(collection);
  1403. end = methodChooser(next_node, collection, true, true);
  1404. });
  1405. // Add end
  1406. if (node.end() && end.id() == node.end().id()) {
  1407. return methodChooser(end, collection, true, true);
  1408. } else {
  1409. return end;
  1410. }
  1411. default:
  1412. // Should never get here.
  1413. return node;
  1414. }
  1415. };
  1416. methodChooser(self.registry.get(self.start()), self.nodes, false, true);
  1417. },
  1418. rebuild: function() {
  1419. var self = this;
  1420. // Clear all children
  1421. $.each(self.registry.nodes, function(index, node) {
  1422. node.children.removeAll();
  1423. });
  1424. self.nodes.removeAll();
  1425. // Rebuild
  1426. self.build();
  1427. self.draggables();
  1428. self.droppables();
  1429. self.el.trigger('workflow:rebuilt');
  1430. },
  1431. draggables: function() {
  1432. var self = this;
  1433. self.el.find('.node-action').each(function(index, el) {
  1434. if (!$(el).hasClass('ui-draggable')) {
  1435. $(el).draggable({
  1436. containment: [ self.el.offset().left - 10, self.el.offset().top - 10,
  1437. self.el.offset().left + self.el.outerWidth(), self.el.offset().top + self.el.outerHeight() ],
  1438. handle: '.node',
  1439. refreshPositions: true,
  1440. revert: true
  1441. });
  1442. }
  1443. });
  1444. },
  1445. droppables: function() {
  1446. var self = this;
  1447. self.el.find('.node-link').each(function(index, el) {
  1448. $(el).droppable({
  1449. 'hoverClass': 'node-link-hover',
  1450. 'greedy': true,
  1451. 'accept': '.node-action',
  1452. 'tolerance': 'pointer'
  1453. });
  1454. });
  1455. self.el.find('.node-decision-end').each(function(index, el) {
  1456. $(el).droppable({
  1457. 'hoverClass': 'node-link-hover',
  1458. 'greedy': true,
  1459. 'accept': '.node-action',
  1460. 'tolerance': 'pointer'
  1461. });
  1462. });
  1463. self.el.find('.node-fork .action').each(function(index, el) {
  1464. $(el).droppable({
  1465. 'hoverClass': 'node-fork-hover',
  1466. 'greedy': true,
  1467. 'accept': '.node-action',
  1468. 'tolerance': 'pointer'
  1469. });
  1470. });
  1471. self.el.find('.node-decision .action').each(function(index, el) {
  1472. $(el).droppable({
  1473. 'hoverClass': 'node-fork-hover',
  1474. 'greedy': true,
  1475. 'accept': '.node-action',
  1476. 'tolerance': 'pointer'
  1477. });
  1478. });
  1479. self.el.find('.node-action .action').each(function(index, el) {
  1480. $(el).droppable({
  1481. 'hoverClass': 'node-action-hover',
  1482. 'greedy': true,
  1483. 'accept': '.node-action',
  1484. 'tolerance': 'pointer'
  1485. });
  1486. });
  1487. },
  1488. dragAndDropEvents: function() {
  1489. var self = this;
  1490. // Build event delegations.
  1491. // Drop on node link
  1492. self.el.on('drop', '.node-link', function(e, ui) {
  1493. // draggable should be a node.
  1494. // droppable should be a link.
  1495. var draggable = ko.contextFor(ui.draggable[0]).$data;
  1496. var droppable = ko.contextFor(this).$data;
  1497. // If newParent is fork, prepend to child instead.
  1498. // This will make it so that we can drop and drop to the top of a node list within a fork.
  1499. var newParent = self.registry.get(droppable.parent());
  1500. if (newParent.id() != draggable.id()) {
  1501. if (newParent.isChild(draggable)) {
  1502. if (draggable.findParents().length > 1) {
  1503. // End of decision tree is being dragged to the bottom of a branch
  1504. draggable.detach();
  1505. newParent.append(draggable);
  1506. workflow.model.is_dirty = true;
  1507. self.rebuild();
  1508. }
  1509. } else {
  1510. switch(newParent.node_type()) {
  1511. case 'fork':
  1512. case 'decision':
  1513. draggable.detach();
  1514. var child = self.registry.get(droppable.child());
  1515. newParent.replaceChild(child, draggable);
  1516. draggable.addChild(child);
  1517. break;
  1518. case 'join':
  1519. // Join may disappear when we detach...
  1520. // Remember its children and append to child.
  1521. var parents = newParent.findParents();
  1522. draggable.detach();
  1523. if (newParent.findParents().length < 2) {
  1524. $.each(parents, function(index, parent) {
  1525. parent.append(draggable);
  1526. });
  1527. } else {
  1528. newParent.append(draggable);
  1529. }
  1530. break;
  1531. default:
  1532. draggable.detach();
  1533. newParent.append(draggable);
  1534. break;
  1535. }
  1536. workflow.model.is_dirty = true;
  1537. self.rebuild();
  1538. }
  1539. }
  1540. // Prevent bubbling events
  1541. return false;
  1542. });
  1543. // Drop on fork
  1544. self.el.on('drop', '.node-fork', function(e, ui) {
  1545. // draggable should be a node.
  1546. // droppable should be a fork.
  1547. var draggable = ko.contextFor(ui.draggable[0]).$data;
  1548. var droppable = ko.contextFor(this).$data;
  1549. if (!droppable.isChild(draggable) && droppable.id() != draggable.id()) {
  1550. draggable.detach();
  1551. droppable.append(draggable);
  1552. self.rebuild();
  1553. }
  1554. // Prevent bubbling events
  1555. return false;
  1556. });
  1557. // Drop on decision
  1558. self.el.on('drop', '.node-decision', function(e, ui) {
  1559. // draggable should be a node.
  1560. // droppable should be a fork.
  1561. var draggable = ko.contextFor(ui.draggable[0]).$data;
  1562. var droppable = ko.contextFor(this).$data;
  1563. if (!droppable.isChild(draggable) && droppable.id() != draggable.id()) {
  1564. draggable.detach();
  1565. droppable.append(draggable);
  1566. self.rebuild();
  1567. }
  1568. // Prevent bubbling events
  1569. return false;
  1570. });
  1571. // Drop on decision end link
  1572. self.el.on('drop', '.node-decision-end', function(e, ui) {
  1573. // draggable should be a node.
  1574. // droppable should be a decision end link... which should give us the decision node.
  1575. var draggable = ko.contextFor(ui.draggable[0]).$data;
  1576. var droppable = ko.contextFor(this).$data;
  1577. var end = droppable.findEnd(droppable);
  1578. if (end.id() != draggable.id()) {
  1579. draggable.detach();
  1580. $.each(end.findParents(), function(index, parent) {
  1581. if (droppable.isChild(parent)) {
  1582. parent.append(draggable);
  1583. }
  1584. });
  1585. droppable.removeEnd();
  1586. droppable.addEnd(draggable);
  1587. self.rebuild();
  1588. }
  1589. // Prevent bubbling events
  1590. return false;
  1591. });
  1592. // Drop on action
  1593. self.el.on('drop', '.node-action', function(e, ui) {
  1594. // draggable should be a node.
  1595. // droppable should be a node.
  1596. var draggable = ko.contextFor(ui.draggable[0]).$data;
  1597. var droppable = ko.contextFor(this).$data;
  1598. // Create a fork and join programatically.
  1599. var newParents = droppable.findParents();
  1600. // skip forking beneathe a decision node
  1601. if (droppable.id() != draggable.id() && newParents.length == 1 && draggable.findParents().length == 1) {
  1602. var ForkModel = NodeModelChooser('fork');
  1603. var JoinModel = NodeModelChooser('join');
  1604. var fork = new ForkModel({
  1605. id: IdGeneratorTable['fork'].nextId(),
  1606. description: "",
  1607. workflow: self.id,
  1608. node_type: "fork",
  1609. child_links: []
  1610. });
  1611. var forkNode = new ForkNode(self, fork, self.registry);
  1612. var join = new JoinModel({
  1613. id: IdGeneratorTable['join'].nextId(),
  1614. description: "",
  1615. workflow: self.id,
  1616. node_type: "join",
  1617. child_links: []
  1618. });
  1619. var joinNode = new Node(self, join, self.registry);
  1620. self.registry.add(forkNode.id(), forkNode);
  1621. self.registry.add(joinNode.id(), joinNode);
  1622. forkNode.addChild(joinNode);
  1623. // Handles fork creation.
  1624. $.each(newParents, function(index, parent) {
  1625. parent.replaceChild(droppable, forkNode);
  1626. });
  1627. draggable.detach();
  1628. forkNode.append(draggable);
  1629. forkNode.append(droppable);
  1630. self.rebuild();
  1631. }
  1632. // Prevent bubbling events.
  1633. return false;
  1634. });
  1635. }
  1636. });
  1637. return module;
  1638. };
  1639. var Workflow = WorkflowModule($, nodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable);