jobsub.ko.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  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. * Design representation
  18. */
  19. var Design = (function($, ko, NodeFields) {
  20. var module = function(options) {
  21. var self = this;
  22. self.options = {};
  23. self.model = {};
  24. self.initialize(options);
  25. };
  26. // NodeFields is defined in oozie/static/js/workflow.node-fields.js.
  27. // It provides the more advanced field manipulation for the fields
  28. // that hava JSON representation.
  29. $.extend(module.prototype, NodeFields, {
  30. initialize: function(options) {
  31. var self = this;
  32. self.options = $.extend(self.options, options);
  33. self.model = options.model;
  34. self.model.errors = self.model.errors || {};
  35. for(var key in self.model) {
  36. switch(key) {
  37. case 'initialize':
  38. case 'toString':
  39. case 'errors':
  40. break;
  41. default:
  42. if (!(key in self.model.errors)) {
  43. self.model.errors[key] = [];
  44. }
  45. break;
  46. }
  47. }
  48. // @see http://knockoutjs.com/documentation/plugins-mapping.html
  49. // MAPPING_OPTIONS comes from /oozie/static/js/workflow.models.js
  50. // We don't update the observed object using ko.mapping because
  51. // the plugin does not work with mixed objects.
  52. ko.mapping.fromJS(self.model, $.extend({
  53. is_shared: {
  54. create: function(options) {
  55. return ko.observable(($.type(options.data) == "string") ? value.toLowerCase() == "true" : new Boolean(options.data).valueOf());
  56. },
  57. update: function(options) {
  58. return ($.type(options.data) == "string") ? value.toLowerCase() == "true" : new Boolean(options.data);
  59. }
  60. },
  61. parameters: {
  62. // Will receive individual objects to subscribe.
  63. // Containing array is mapped automagically
  64. create: function(options) {
  65. var parent = options.parent;
  66. var subscribe = function(mapping) {
  67. mapping.name.subscribe(function(value) {
  68. parent.parameters.valueHasMutated();
  69. });
  70. mapping.value.subscribe(function(value) {
  71. parent.parameters.valueHasMutated();
  72. });
  73. };
  74. return map_params(options, subscribe);
  75. },
  76. update: function(options) {
  77. var parent = options.parent;
  78. var subscribe = function(mapping) {
  79. mapping.name.subscribe(function(value) {
  80. parent.parameters.valueHasMutated();
  81. });
  82. mapping.value.subscribe(function(value) {
  83. parent.parameters.valueHasMutated();
  84. });
  85. };
  86. return map_params(options, subscribe);
  87. }
  88. }
  89. }, MAPPING_OPTIONS), self);
  90. // hack on '<key>ErrorClass' and '<key>Condition'.
  91. $.each(self.__ko_mapping__, function(key, enabled) {
  92. if (ko.isObservable(self[key])) {
  93. self[key+'_condition'] = ko.computed(function() {
  94. return self[key]().length > 0;
  95. });
  96. self[key+'_error_class'] = ko.computed(function() {
  97. return ( self.errors[key]().length > 0 ) ? 'control-group error' : 'control-group';
  98. });
  99. }
  100. });
  101. if (!('is_dirty' in self)) {
  102. self.is_dirty = ko.observable(true);
  103. }
  104. if (!('new' in self)) {
  105. self.new = ko.computed(function() {
  106. return !self.id();
  107. });
  108. }
  109. if (!('editable' in self)) {
  110. self.editable = ko.observable(false);
  111. }
  112. if ('files' in self) {
  113. var files = self.files();
  114. self.files = ko.observableArray([]);
  115. // ['file', ...] => [{'name': 'file', 'dummy': ''}, ...].
  116. $.each(files, function(index, filename) {
  117. var prop = { name: ko.observable(filename), dummy: ko.observable("") };
  118. prop.name.subscribe(function(value) {
  119. self.files.valueHasMutated();
  120. });
  121. prop.dummy.subscribe(function(value) {
  122. self.files.valueHasMutated();
  123. });
  124. self.files.push(prop);
  125. });
  126. }
  127. $(document).trigger('initialize.design', [options, self]);
  128. },
  129. request: function(url, options) {
  130. var self = this;
  131. var request = $.extend({
  132. url: url,
  133. dataType: 'json',
  134. type: 'GET',
  135. success: $.noop,
  136. error: $.noop
  137. }, options || {});
  138. $.ajax(request);
  139. },
  140. load: function(options) {
  141. var self = this;
  142. var options = $.extend({
  143. success: function(data) {
  144. self.is_dirty(false);
  145. $(document).trigger('load.design', [options, data, self]);
  146. }
  147. }, options);
  148. this.request('/jobsub/designs/' + self.id(), options);
  149. },
  150. save: function(options) {
  151. // First try to save, then update error list if fail.
  152. // Response should be json object. IE: {data: {errors: {files: ['example', ...], ... }}}
  153. var self = this;
  154. var model_dict = {};
  155. $.each(ko.mapping.toJS(self), function(key, value) {
  156. if (key != 'errors') {
  157. if (key == 'files') {
  158. // [{'name': 'file', 'dummy': ''}, ...] => ['file', ...].
  159. var files = ko.utils.unwrapObservable(value);
  160. model_dict[key] = [];
  161. $.each(files, function(i, value) {
  162. model_dict[key].push(value.name);
  163. });
  164. } else {
  165. model_dict[key] = ko.utils.unwrapObservable(value);
  166. }
  167. }
  168. });
  169. var data = normalize_model_fields($.parseJSON(JSON.stringify(model_dict)));
  170. var options = $.extend({
  171. type: 'POST',
  172. data: data,
  173. error: function(xhr) {
  174. var response = $.parseJSON(xhr.responseText);
  175. if (response) {
  176. var model = ko.mapping.toJS(self);
  177. $.extend(model.errors, response.data.errors);
  178. ko.mapping.fromJS(model, self);
  179. $(document).trigger('error.design', [options, data]);
  180. }
  181. },
  182. success: function(data) {
  183. $(document).trigger('save.design', [options, data]);
  184. }
  185. }, options);
  186. self.request((self.new()) ? '/jobsub/designs/'+self.node_type()+'/new' : '/jobsub/designs/'+self.id()+'/save', options);
  187. },
  188. clone: function(options) {
  189. var self = this;
  190. var options = $.extend({
  191. type: 'POST',
  192. success: function(data) {
  193. $(document).trigger('clone.design', [options, data]);
  194. }
  195. }, options);
  196. this.request('/jobsub/designs/' + self.id() + '/clone', options);
  197. },
  198. delete: function(options) {
  199. var self = this;
  200. var options = $.extend({
  201. type: 'POST',
  202. success: function(data) {
  203. $(document).trigger('delete.design', [options, data]);
  204. }
  205. }, options);
  206. this.request('/jobsub/designs/' + self.id() + '/delete', options);
  207. },
  208. // More node field methods
  209. addParameter: function(data) {
  210. var self = this;
  211. var prop = { name: ko.observable(""), value: ko.observable("") };
  212. prop.name.subscribe(function(value) {
  213. self.parameters.valueHasMutated();
  214. });
  215. prop.value.subscribe(function(value) {
  216. self.parameters.valueHasMutated();
  217. });
  218. self.parameters.push(prop);
  219. $(document).trigger('add.parameter.workflow', [data]);
  220. },
  221. removeParameter: function(data) {
  222. var self = this;
  223. self.parameters.remove(data);
  224. $(document).trigger('remove.parameter.workflow', [data]);
  225. }
  226. });
  227. return module;
  228. })($, ko, NodeFields);
  229. /**
  230. * List of designs
  231. */
  232. var Designs = (function($, ko, NodeModelChooser) {
  233. var module = function(options) {
  234. var self = this;
  235. self.options = options || {
  236. models: []
  237. };
  238. self.temporary = ko.observable();
  239. self.designs = ko.observableArray([]);
  240. self.selectedDesignObjects = ko.computed(function() {
  241. var selected = [];
  242. $.each(self.designs(), function(index, designObject) {
  243. if (designObject.selected()) {
  244. selected.push(designObject);
  245. }
  246. });
  247. return selected;
  248. });
  249. self.selectedDesignObject = ko.computed(function () {
  250. return self.selectedDesignObjects()[0];
  251. });
  252. self.selectedDesign = ko.computed(function() {
  253. if (self.selectedDesignObject()) {
  254. return self.selectedDesignObject().design();
  255. } else {
  256. return null;
  257. }
  258. });
  259. self.selectedIndex = ko.computed(function() {
  260. var selected = -1;
  261. $.each(self.designs(), function(index, designObject) {
  262. if (selected == -1 && designObject.selected()) {
  263. selected = index;
  264. }
  265. });
  266. return selected;
  267. });
  268. self.allSelected = ko.computed(function() {
  269. return self.selectedDesignObjects().length == self.designs().length && self.selectedDesignObjects().length > 0;
  270. });
  271. self.initialize(options);
  272. };
  273. $.extend(module.prototype, {
  274. initialize: function(options) {
  275. var self = this;
  276. self.options = $.extend(self.options, options);
  277. self.designs.removeAll();
  278. self.createDesigns(self.options.models);
  279. self.temporary({
  280. design: ko.observable(null),
  281. selected: ko.observable(false),
  282. template: ko.observable(null)
  283. })
  284. self.deselectAll();
  285. $(document).trigger('initialize.designs', [options, self]);
  286. },
  287. load: function(options) {
  288. // Fetch designs from backend.
  289. var self = this;
  290. var request = $.extend({
  291. url: '/jobsub/designs',
  292. dataType: 'json',
  293. type: 'GET',
  294. success: function(data) {
  295. $(document).trigger('load.designs', [options, data]);
  296. self.initialize({models: data});
  297. },
  298. error: $.noop
  299. }, options || {});
  300. $.ajax(request);
  301. },
  302. ensureListFields: function(model) {
  303. if (!('name' in model)) {
  304. model.name = '';
  305. }
  306. if (!('description' in model)) {
  307. model.description = '';
  308. }
  309. if (!('owner' in model)) {
  310. model.owner = '';
  311. }
  312. if (!('last_modified' in model)) {
  313. model.last_modified = 0;
  314. }
  315. return model;
  316. },
  317. createDesign: function(model) {
  318. var self = this;
  319. var NodeModel = NodeModelChooser(model.node_type);
  320. var node_model = new NodeModel(self.ensureListFields(model));
  321. var design = new Design({model: node_model});
  322. return design;
  323. },
  324. createDesigns: function(models) {
  325. var self = this;
  326. $.each(models, function(index, model) {
  327. self.designs.push({
  328. design: ko.observable(self.createDesign(model)),
  329. selected: ko.observable(false),
  330. template: ko.observable(model.node_type)
  331. });
  332. });
  333. },
  334. toggleSelect: function(index) {
  335. var self = this;
  336. self.designs()[index].selected(!self.designs()[index].selected());
  337. },
  338. select: function(index) {
  339. var self = this;
  340. self.designs()[index].selected(true);
  341. },
  342. toggleSelectAll: function() {
  343. var self = this;
  344. if (self.allSelected()) {
  345. self.deselectAll();
  346. } else {
  347. self.selectAll();
  348. }
  349. },
  350. selectAll: function() {
  351. var self = this;
  352. $.each(self.designs(), function(index, value) {
  353. value.selected(true);
  354. });
  355. },
  356. deselectAll: function() {
  357. var self = this;
  358. $.each(self.designs(), function(index, value) {
  359. value.selected(false);
  360. });
  361. },
  362. //// Design delegation
  363. newDesign: function(node_type) {
  364. var self = this;
  365. var design = self.createDesign({
  366. id: null,
  367. node_type: node_type,
  368. is_shared: true,
  369. parameters: '[{"name":"oozie.use.system.libpath","value":"true"}]'
  370. });
  371. // Reversing the order of the next two statements may cause KO to break.
  372. self.temporary().template(node_type);
  373. self.temporary().design(design);
  374. // Do not do any thing with any other design.
  375. self.deselectAll();
  376. $(document).trigger('new.design', [design]);
  377. },
  378. saveDesign: function(data, event) {
  379. var self = this;
  380. self.temporary().design().save();
  381. },
  382. cloneDesigns: function() {
  383. var self = this;
  384. $.each(self.selectedDesignObjects(), function(index, designObject) {
  385. designObject.design().clone();
  386. });
  387. },
  388. deleteDesigns: function() {
  389. var self = this;
  390. $.each(self.selectedDesignObjects(), function(index, designObject) {
  391. designObject.design().delete();
  392. });
  393. },
  394. editDesign: function(index) {
  395. var self = this;
  396. if (self.selectedDesignObject()) {
  397. var design = self.selectedDesignObject().design();
  398. if (design.is_dirty()) {
  399. $(document).one('load.design', function(e, options, data, design) {
  400. design.initialize({model: data});
  401. self.temporary().design(design);
  402. self.temporary().template(self.selectedDesignObject().template());
  403. $(document).trigger('edit.design', [design]);
  404. });
  405. design.load();
  406. } else {
  407. self.temporary().design(design);
  408. self.temporary().template(self.selectedDesignObject().template());
  409. $(document).trigger('edit.design', [design]);
  410. }
  411. }
  412. },
  413. closeDesign: function() {
  414. var self = this;
  415. self.temporary().design(null);
  416. self.temporary().template(null);
  417. }
  418. });
  419. return module;
  420. })($, ko, nodeModelChooser);
  421. var designs = new Designs({models: []});