jobsub.ko.js 16 KB

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