workflow.node.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  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. /**
  17. * Node
  18. * Displays node in a graph and handles graph manipulation.
  19. * The majority of nodes require similar logic.
  20. * This modules takes advantage of that fact.
  21. */
  22. var NodeModule = function($, IdGeneratorTable, NodeFields) {
  23. var META_LINKS = ['related', 'default', 'error'];
  24. var linkTypeChooser = function(parent, child) {
  25. if (child.node_type() == 'kill') {
  26. return 'error';
  27. }
  28. switch(parent.node_type()) {
  29. case 'start':
  30. return (child.node_type() == 'end') ? 'related' : 'to';
  31. case 'fork':
  32. return (child.node_type() == 'join') ? 'related' : 'start';
  33. case 'decision':
  34. return (child.node_type() == 'decisionend') ? 'related' : 'start';
  35. case 'join':
  36. case 'decisionend':
  37. return 'to';
  38. default:
  39. return 'ok';
  40. };
  41. };
  42. var module = function(workflow, model, registry) {
  43. var self = this;
  44. self.map(model);
  45. self.links = ko.computed(function() {
  46. var links = self.child_links().filter(function(element, index, arr) {
  47. return $.inArray(element.name(), META_LINKS) == -1;
  48. });
  49. return links;
  50. });
  51. self.meta_links = ko.computed(function() {
  52. var links = self.child_links().filter(function(element, index, arr) {
  53. return $.inArray(element.name(), META_LINKS) != -1;
  54. });
  55. return links;
  56. });
  57. self.non_error_links = ko.computed(function() {
  58. var links = self.child_links().filter(function(element, index, arr) {
  59. return element.name() != 'error';
  60. });
  61. return links;
  62. });
  63. self._workflow = workflow;
  64. self.registry = registry;
  65. self.children = ko.observableArray([]);
  66. self.model = model;
  67. var errors = {};
  68. for(var key in model) {
  69. switch(key) {
  70. case 'child_links':
  71. case 'node_ptr':
  72. case 'initialize':
  73. case 'toString':
  74. break;
  75. default:
  76. errors[key] = [];
  77. break;
  78. }
  79. }
  80. self.errors = ko.mapping.fromJS(errors);
  81. self.edit_template = model.node_type + 'EditTemplate';
  82. switch(model.node_type) {
  83. case 'start':
  84. case 'end':
  85. self.view_template = ko.observable('disabledNodeTemplate');
  86. break;
  87. case 'kill':
  88. self.view_template = ko.observable('emptyTemplate');
  89. break;
  90. case 'fork':
  91. self.view_template = ko.observable('forkTemplate');
  92. break;
  93. case 'join':
  94. self.view_template = ko.observable('joinTemplate');
  95. break;
  96. case 'decision':
  97. self.view_template = ko.observable('decisionTemplate');
  98. break;
  99. case 'decisionend':
  100. self.view_template = ko.observable('decisionEndTemplate');
  101. break;
  102. default:
  103. self.view_template = ko.observable('nodeTemplate');
  104. break;
  105. }
  106. // Data manipulation
  107. if ('files' in model) {
  108. //// WARNING: The following order should be preserved!
  109. // Need to represent files as some thing else for knockout mappings.
  110. // The KO idiom "value" requires a named parameter.
  111. self._files = self.files;
  112. self.files = ko.observableArray([]);
  113. // ['file', ...] => [{'name': 'file', 'dummy': ''}, ...].
  114. $.each(self._files(), function(index, filename) {
  115. var prop = { name: ko.observable(filename), dummy: ko.observable("") };
  116. prop.name.subscribe(function(value) {
  117. self.files.valueHasMutated();
  118. });
  119. prop.dummy.subscribe(function(value) {
  120. self.files.valueHasMutated();
  121. });
  122. self.files.push(prop);
  123. });
  124. // [{'name': 'file', 'dummy': ''}, ...] => ['file', ...].
  125. self.files.subscribe(function(value) {
  126. self._files.removeAll();
  127. $.each(self.files(), function(index, file) {
  128. self._files.push(file.name);
  129. });
  130. });
  131. self.addFile = function() {
  132. var prop = { name: ko.observable(""), dummy: ko.observable("") };
  133. prop.name.subscribe(function(value) {
  134. self.files.valueHasMutated();
  135. });
  136. prop.dummy.subscribe(function(value) {
  137. self.files.valueHasMutated();
  138. });
  139. self.files.push(prop);
  140. };
  141. self.removeFile = function(val) {
  142. self.files.remove(val);
  143. };
  144. }
  145. self.initialize.apply(self, arguments);
  146. return self;
  147. };
  148. $.extend(true, module.prototype, NodeFields, {
  149. // Data.
  150. children: null,
  151. model: null,
  152. // Normal stuff
  153. /**
  154. * Called when creating a new node
  155. */
  156. initialize: function(workflow, model, registry) {},
  157. toString: function() {
  158. return '';
  159. },
  160. /**
  161. * Fetches registry
  162. */
  163. getRegistry: function() {
  164. return registry;
  165. },
  166. /**
  167. * Maps a model to self
  168. * Called when creating a new node before any thing else
  169. */
  170. map: function(model) {
  171. var self = this;
  172. // @see http://knockoutjs.com/documentation/plugins-mapping.html
  173. // MAPPING_OPTIONS comes from /oozie/static/js/models.js
  174. var mapping = ko.mapping.fromJS(model, MAPPING_OPTIONS);
  175. $.extend(self, mapping);
  176. $.each(mapping, function(key, value) {
  177. var key = key;
  178. if (ko.isObservable(self[key])) {
  179. self[key].subscribe(function(value) {
  180. model[key] = ko.mapping.toJS(value);
  181. });
  182. }
  183. });
  184. $.each(self.child_links(), function(index, link) {
  185. var $index = index;
  186. link.comment.subscribe(function(value) {
  187. self.model.child_links[$index].comment = value;
  188. });
  189. link.child.subscribe(function(value) {
  190. self.model.child_links[$index].child = value;
  191. });
  192. });
  193. },
  194. validate: function( ) {
  195. var self = this;
  196. var options = {};
  197. data = $.extend(true, {}, self.model);
  198. var success = false;
  199. var request = $.extend({
  200. url: '/oozie/workflows/' + self._workflow.id() + '/nodes/' + self.node_type() + '/validate',
  201. type: 'POST',
  202. data: { node: JSON.stringify(data) },
  203. success: function(data) {
  204. ko.mapping.fromJS(data.data, self.errors);
  205. success = data.status == 0;
  206. },
  207. async: false
  208. }, options);
  209. $.ajax(request);
  210. return success;
  211. },
  212. // Hierarchy manipulation.
  213. /**
  214. * Append node to self
  215. * Does not support multiple children.
  216. * Ensures single child.
  217. * Ensures no cycles.
  218. * 1. Finds all children and attaches them to node (cleans node first).
  219. * 2. Remove all children from self.
  220. * 3. Attach node to self.
  221. */
  222. append: function(node) {
  223. var self = this;
  224. // Not fork nor decision nor self
  225. if ($.inArray(self.node_type(), ['fork', 'decision']) == -1 && node.id() != self.id() && !self.isChild(node)) {
  226. node.removeAllChildren();
  227. $.each(self.links(), function(index, link) {
  228. node.addChild(self.registry.get(link.child()));
  229. });
  230. self.removeAllChildren();
  231. self.addChild(node);
  232. }
  233. },
  234. /**
  235. * Find all parents of current node
  236. */
  237. findParents: function() {
  238. var self = this;
  239. var parents = [];
  240. $.each(self.registry.nodes, function(id, node) {
  241. $.each(node.links(), function(index, link) {
  242. if (link.child() == self.id()) {
  243. parents.push(node);
  244. }
  245. });
  246. });
  247. return parents;
  248. },
  249. findErrorParents: function() {
  250. var self = this;
  251. var parents = [];
  252. $.each(self.registry.nodes, function(id, node) {
  253. $.each(node.meta_links(), function(index, link) {
  254. if (link.child() == self.id()) {
  255. parents.push(node);
  256. }
  257. });
  258. });
  259. return parents;
  260. },
  261. /**
  262. * Find all children of current node
  263. */
  264. findChildren: function() {
  265. var self = this;
  266. var children = [];
  267. $.each(self.links(), function(index, link) {
  268. children.push(self.registry.get(link.child()));
  269. });
  270. return children;
  271. },
  272. /**
  273. * Detach current node from the graph
  274. * 1. Takes children of self node, removes them from self node, and adds them to each parent of self node.
  275. * 2. The self node is then removed from every parent.
  276. * 3. Does not support multiple children since we do not automatically fork.
  277. */
  278. detach: function() {
  279. var self = this;
  280. $.each(self.findParents(), function(index, parent) {
  281. $.each(self.links(), function(index, link) {
  282. var node = self.registry.get(link.child());
  283. parent.replaceChild(self, node);
  284. });
  285. });
  286. // Error links of parents reset to kill node.
  287. $.each(self.findErrorParents(), function(index, parent) {
  288. parent.putErrorChild(self._workflow.kill);
  289. });
  290. $(self).trigger('detached');
  291. self.removeAllChildren();
  292. },
  293. /**
  294. * Add child
  295. * Update child links for this node.
  296. */
  297. addChild: function(node, link_type) {
  298. var self = this;
  299. var link_type = link_type || linkTypeChooser(self, node);
  300. var link = {
  301. parent: ko.observable(self.id()),
  302. child: ko.observable(node.id()),
  303. name: ko.observable(link_type),
  304. comment: ko.observable('')
  305. };
  306. self.child_links.unshift(link);
  307. },
  308. /**
  309. * Remove child node
  310. * 1. Find child node link
  311. * 2. Remove child node link
  312. */
  313. removeChild: function(node) {
  314. var self = this;
  315. var spliceIndex = -1;
  316. $.each(self.child_links(), function(index, link) {
  317. if (link.child() == node.id()) {
  318. spliceIndex = index;
  319. }
  320. });
  321. if (spliceIndex > -1) {
  322. self.child_links.splice(spliceIndex, 1);
  323. }
  324. return spliceIndex != -1;
  325. },
  326. /**
  327. * Remove error child
  328. * 1. Find child node link
  329. * 2. Remove child node link
  330. */
  331. removeErrorChildren: function() {
  332. var self = this;
  333. var spliceIndexes = [];
  334. $.each(self.child_links(), function(index, link) {
  335. if (link.name() == 'error') {
  336. spliceIndexes.push(index);
  337. }
  338. });
  339. var spliceCount = 0;
  340. if (spliceIndexes.length > 0) {
  341. $.each(spliceIndexes, function(index, spliceIndex) {
  342. self.child_links.splice(spliceIndex - spliceCount++, 1);
  343. });
  344. }
  345. return spliceIndexes.length > 0;
  346. },
  347. /**
  348. * Remove all children
  349. * Removes all children except for related, default, and error links
  350. * Note: we hold on to related, default, and error links because
  351. * we have to.
  352. */
  353. removeAllChildren: function() {
  354. var self = this;
  355. var keep_links = [];
  356. $.each(self.child_links(), function(index, link) {
  357. if ($.inArray(link.name(), META_LINKS) > -1) {
  358. keep_links.push(link);
  359. }
  360. });
  361. self.child_links.removeAll();
  362. $.each(keep_links, function(index, link) {
  363. self.child_links.push(link);
  364. });
  365. },
  366. /**
  367. * Replace child node with another node in the following way:
  368. * 1. Find child index
  369. * 2. Remove child index
  370. * 3. Remove and remember every element after child
  371. * 4. Add replacement node
  372. * 5. Add every child that was remembered
  373. */
  374. replaceChild: function(child, replacement) {
  375. var self = this;
  376. var index = -1;
  377. $.each(self.non_error_links(), function(i, link) {
  378. if (link.child() == child.id()) {
  379. index = i;
  380. }
  381. });
  382. if (index > -1) {
  383. self.child_links.splice(index, 1);
  384. var links = self.child_links.splice(index);
  385. var link = {
  386. parent: ko.observable(self.id()),
  387. child: ko.observable(replacement.id()),
  388. name: ko.observable(linkTypeChooser(self, replacement)),
  389. comment: ko.observable('')
  390. };
  391. self.child_links.push(link);
  392. $.each(links, function(index, link) {
  393. self.child_links.push(link);
  394. });
  395. }
  396. return index != -1;
  397. },
  398. /**
  399. * Replace or add error node with another node in the following way:
  400. * 1. Find child index
  401. * 2. Remove child index
  402. * 3. Remove and remember every element after child
  403. * 4. Add replacement node
  404. * 5. Add every child that was remembered
  405. */
  406. putErrorChild: function(node) {
  407. var self = this;
  408. var index = -1;
  409. $.each(self.child_links(), function(i, link) {
  410. if (link.name() == 'error') {
  411. index = i;
  412. }
  413. });
  414. var link = {
  415. parent: ko.observable(self.id()),
  416. child: ko.observable(node.id()),
  417. name: ko.observable('error'),
  418. comment: ko.observable('')
  419. };
  420. if (index > -1) {
  421. var child_links = self.child_links();
  422. child_links.splice(index, 1);
  423. var links = child_links.splice(index);
  424. child_links.push(link);
  425. $.each(links, function(index, link) {
  426. child_links.push(link);
  427. });
  428. self.child_links(child_links);
  429. } else {
  430. self.child_links.push(link);
  431. }
  432. return index != -1;
  433. },
  434. /**
  435. * Get the error child
  436. */
  437. getErrorChild: function() {
  438. var self = this;
  439. var children = [];
  440. $.each(self.meta_links(), function(index, link) {
  441. if (link.name() == 'error') {
  442. children.push(self.registry.get(link.child()));
  443. }
  444. });
  445. return (children.length > 0) ? children[0] : null;
  446. },
  447. isChild: function(node) {
  448. var self = this;
  449. var res = false;
  450. $.each(self.links(), function(index, link) {
  451. if (link.child() == node.id()) {
  452. res = true;
  453. }
  454. });
  455. return res;
  456. },
  457. erase: function() {
  458. var self = this;
  459. self.registry.remove(self.id());
  460. }
  461. });
  462. return module;
  463. };