workflow.node.js 15 KB

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