Explorar o código

[oozie] Front end for SLA

proper ko.mapping support for SLA and unstructured data.
models are automatically updated when view models update.
use better rendering semantics.
Abraham Elmahrek %!s(int64=12) %!d(string=hai) anos
pai
achega
b3d1abc33c

+ 2 - 1
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -624,7 +624,8 @@ function workflow_read_only_handler() {
 var kill_view_model = null;
 function workflow_load_success(data) {
   if (data.status == 0) {
-    workflow.reload(data.data);
+    var workflow_model = new WorkflowModel(data.data);
+    workflow.reload(workflow_model);
 
     //// Kill node
     kill_view_model = ManageKillModule($, workflow, nodeModelChooser, Node, NodeModel);

+ 24 - 18
apps/oozie/src/oozie/templates/utils.inc.mako

@@ -260,17 +260,17 @@
 
 
 <%def name="slaForm()">
-  <div data-bind="foreach: sla">
+  <div data-bind="foreach: { 'data': sla, 'afterRender': addSLATextAndPlaceholder }">
     <div class="controls">
       <div class="span3">
-        <span data-bind="text: getSLAText(key)['niceName']"></span>
+        <span></span>
       </div>
       <div class="span9">
-        <!-- ko if:  key == 'enabled' -->
+        <!-- ko if:  key() == 'enabled' -->
         <input type="checkbox" data-bind="checked: value"/>
         <!-- /ko -->
-        <!-- ko if:  key != 'enabled' -->
-        <input type="text" data-bind="value: value, attr: {placeholder: getSLAText(key)['placeHolder']}" class="span7">
+        <!-- ko if:  key() != 'enabled' -->
+        <input type="text" data-bind="value: value" class="span7">
         <!-- /ko -->
       </div>
     </div>
@@ -280,21 +280,27 @@
 
 ## Would be nice include it in slaForm() somehow
 <%def name="slaGlobal()">
-
-  function getSLAText(key) {
-    return {
-       'enabled': {'niceName': '${ _("Enabled") }', 'placeHolder': ''},
-       'nominal-time': {'niceName': '${ _("Nominal time") } *', 'placeHolder': '${"$"}{nominal_time}'},
-       'should-start': {'niceName': '${ _("Should start") }', 'placeHolder': '${"$"}{10 * MINUTES}'},
-       'should-end': {'niceName': '${ _("Should end") } *', 'placeHolder': '${"$"}{30 * MINUTES}'},
-       'max-duration': {'niceName': '${ _("Max duration") }', 'placeHolder': '${"$"}{30 * MINUTES}'},
-       'alert-events': {'niceName': '${ _("Alert events") }', 'placeHolder': 'start_miss,end_miss,duration_miss'},
-       'alert-contact': {'niceName': '${ _("Alert contact") }', 'placeHolder': 'joe@example.com,bob@example.com'},
-       'notification-msg': {'niceName': '${ _("Notification message") }', 'placeHolder': '${ _("My Job has encountered an SLA event!") }'},
-       'upstream-apps': {'niceName': '${ _("Upstream apps") }', 'placeHolder': 'dependent-app-1, dependent-app-2'}
-    }[key];
+  var SLA_TEXT = {
+    'enabled': {'niceName': '${ _("Enabled") }', 'placeHolder': ''},
+    'nominal-time': {'niceName': '${ _("Nominal time") } *', 'placeHolder': '${"$"}{nominal_time}'},
+    'should-start': {'niceName': '${ _("Should start") }', 'placeHolder': '${"$"}{10 * MINUTES}'},
+    'should-end': {'niceName': '${ _("Should end") } *', 'placeHolder': '${"$"}{30 * MINUTES}'},
+    'max-duration': {'niceName': '${ _("Max duration") }', 'placeHolder': '${"$"}{30 * MINUTES}'},
+    'alert-events': {'niceName': '${ _("Alert events") }', 'placeHolder': 'start_miss,end_miss,duration_miss'},
+    'alert-contact': {'niceName': '${ _("Alert contact") }', 'placeHolder': 'joe@example.com,bob@example.com'},
+    'notification-msg': {'niceName': '${ _("Notification message") }', 'placeHolder': '${ _("My Job has encountered an SLA event!") }'},
+    'upstream-apps': {'niceName': '${ _("Upstream apps") }', 'placeHolder': 'dependent-app-1, dependent-app-2'}
   };
 
+  function addSLATextAndPlaceholder(elements, $data) {
+    if (SLA_TEXT[$data.key()]) {
+      $(elements).find('input').attr('placeholder', SLA_TEXT[$data.key()].placeHolder);
+      $(elements).find('span').text(SLA_TEXT[$data.key()].niceName);
+    } else {
+      $(elements).find('input').attr('placeholder', '');
+      $(elements).find('span').text('');
+    }
+  }
 </%def>
 
 

+ 82 - 12
apps/oozie/static/js/workflow.js

@@ -285,6 +285,52 @@ $.extend(DecisionNode.prototype, ForkNode.prototype, {
  */
 var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable) {
 
+  function addModelModificationHandlers(workflow, mapping, model, key) {
+    mapping.subscribe(function(value) {
+      workflow.is_dirty(true);
+      model[key] = ko.mapping.toJS(value);
+    });
+  }
+
+  function updateData(mapping, model) {
+    // Unstructured data section.
+    // Assumes ko.mapping was used to create children.
+    // Assumes one of three structures:
+    //   - members are literals
+    //   - members are arrays of literals
+    //   - members are arrays of objects with literal members
+    // The point is to keep any parent containers and replace child observables.
+    // Templates will likely be tied to parent containers... so if we bubble changes
+    // to the parent container... the UI will reflect that.
+    $.each(mapping, function(member, value) {
+      if (member in model) {
+        if ($.isArray(value()) && $.isArray(model[member])) {
+          mapping[member].removeAll();
+          $.each(model[member], function(key, object_or_literal) {
+            if ($.isPlainObject(object_or_literal)) {
+              var obj = {};
+              $.each(object_or_literal, function(key, literal) {
+                obj[key] = ko.mapping.fromJS(literal);
+                obj[key].subscribe(function() {
+                  mapping[member].valueHasMutated();
+                });
+              });
+              mapping[member].push(obj);
+            } else {
+              var literal = ko.mapping.fromJS(object_or_literal);
+              mapping[member].push(literal);
+              literal.subscribe(function() {
+                mapping[member].valueHasMutated();
+              });
+            }
+          });
+        } else {
+          mapping[member](model[member]);
+        }
+      }
+    });
+  }
+
   var module = function(options) {
     var self = this;
 
@@ -348,16 +394,29 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
 
           return map_params(options, subscribe);
         }
+      },
+      data: {
+        create: function(options) {
+          return map_data(options);
+        },
+        update: function(options) {
+          return map_data(options);
+        }
       }
     });
 
     $.extend(self, mapping);
-    $.each(mapping['__ko_mapping__'].mappedProperties, function(key, value) {
-      var key = key;
-      if (self[key]) { //ugly? // allow custom sub bindings in foreach
-        self[key].subscribe(function(value) {
-          self.is_dirty(true);
-          self.model[key] = ko.mapping.toJS(value);
+
+    $.each(self['__ko_mapping__'].mappedProperties, function(key, value) {
+      if (ko.isObservable(self[key])) {
+        addModelModificationHandlers(self, self[key], options.model, key);
+      } else {
+        // Unstructured data object.
+        $.each(self[key], function(_key, _value) {
+          // @TODO: Don't assume all children are observable
+          if (ko.isObservable(self[key][_key])) {
+            addModelModificationHandlers(self, self[key][_key], options.model[key], _key);
+          }
         });
       }
     });
@@ -373,8 +432,12 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
     self.read_only = ko.observable( options.read_only || false );
     self.new_node = ko.observable();
 
+    self.sla = ko.computed(function() {
+      return self.data.sla();
+    });
+
     self.url = ko.computed(function() {
-      return '/oozie/workflows/' + self.id()
+      return '/oozie/workflows/' + self.id();
     });
 
     // Events
@@ -396,9 +459,6 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
 
     module.prototype.initialize.apply(self, arguments);
 
-    //self.sla = self.model['sla']; // Need to persists here for some reason
-    self.data = self.model['data']; // Need to persists here for some reason
-
     return self;
   };
 
@@ -473,6 +533,16 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
               case 'nodes':
               break;
 
+              case 'data':
+                var data = {};
+                try {
+                  data = $.parseJSON(value);
+                } catch (error){
+                  data = value;
+                }
+                updateData(self[key], data);
+              break;
+
               default:
                 self[key](value);
               break;
@@ -606,10 +676,10 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
       var prop = { name: ko.observable(""), value: ko.observable("") };
       // force bubble up to containing observable array.
       prop.name.subscribe(function(){
-        self.parameters.valueHasMutated();
+        self.job_properties.valueHasMutated();
       });
       prop.value.subscribe(function(){
-        self.parameters.valueHasMutated();
+        self.job_properties.valueHasMutated();
       });
       self.job_properties.push(prop);
     },

+ 139 - 73
apps/oozie/static/js/workflow.models.js

@@ -32,7 +32,7 @@ var DEFAULT_SLA = [
 ];
 
 function getDefaultData() {
- return {'sla': jQuery.extend(true, [], DEFAULT_SLA)};
+ return {'sla': DEFAULT_SLA.slice(0)};
 }
 
 function normalize_model_fields(node_model) {
@@ -55,15 +55,54 @@ var map_params = function(options, subscribe) {
     });
     return mapping;
   } else {
-    var mapping =  ko.mapping.fromJS(options.data, {});
+    var mapping = ko.mapping.fromJS(options.data, {});
     subscribe(mapping);
     return mapping;
   }
 };
 
+// Find all members of the data and apply appropriate mapping.
+// Arrays might contain literals or plain objects.
+// Plain objects should have their members mapped and literals should
+// be replaced with observables.
+// Plain object members will notify their containing arrays when they update.
+// Literals will notify their containing arrays when they've changed.
+var map_data = function(options) {
+  var data = {};
+  options.data = ($.type(options.data) == "string") ? $.parseJSON(options.data) : options.data;
+  $.each(options.data, function(member, value) {
+    // @TODO: Should we support unstructureed data as children?
+    if ($.isArray(value)) {
+      // @TODO: Support more than {'member': 'value',...} and 'value'.
+      data[member] = ko.observableArray();
+      $.each(value, function(index, object_or_literal) {
+        if ($.isPlainObject(object_or_literal)) {
+          var obj = {};
+          $.each(object_or_literal, function(key, literal) {
+            obj[key] = ko.mapping.fromJS(literal);
+            obj[key].subscribe(function() {
+              data[member].valueHasMutated();
+            });
+          });
+          data[member].push(obj);
+        } else {
+          var literal = ko.mapping.fromJS(object_or_literal);
+          data[member].push(literal);
+          literal.subscribe(function() {
+            data[member].valueHasMutated();
+          });
+        }
+      });
+    } else {
+      data[member] = ko.mapping.fromJS(value);
+    }
+  });
+  return data;
+};
+
 // Maps JSON strings to fields in the view model.
 var MAPPING_OPTIONS = {
-  ignore: ['initialize', 'toString', 'copy', 'data'], // Do not support cancel edit on data
+  ignore: ['initialize', 'toString', 'copy'], // Do not support cancel edit on data
   job_properties: {
     create: function(options) {
       var parent = options.parent;
@@ -235,7 +274,6 @@ var MAPPING_OPTIONS = {
           parent.moves.valueHasMutated();
         });
       };
-
       return map_params(options, subscribe);
     },
     update: function(options) {
@@ -248,64 +286,69 @@ var MAPPING_OPTIONS = {
           parent.moves.valueHasMutated();
         });
       };
-
       return map_params(options, subscribe);
     }
-   },
-   chmods: {
-     create: function(options) {
-       var parent = options.parent;
-       var subscribe = function(mapping) {
-         mapping.path.subscribe(function(value) {
-           parent.chmods.valueHasMutated();
-         });
-         mapping.permissions.subscribe(function(value) {
-           parent.chmods.valueHasMutated();
-         });
-         mapping.recursive.subscribe(function(value) {
-           parent.chmods.valueHasMutated();
-         });
-       };
-
-       return map_params(options, subscribe);
-     },
-     update: function(options) {
-       var parent = options.parent;
-       var subscribe = function(mapping) {
-         mapping.path.subscribe(function(value) {
-           parent.chmods.valueHasMutated();
-         });
-         mapping.permissions.subscribe(function(value) {
-           parent.chmods.valueHasMutated();
-         });
-         mapping.recursive.subscribe(function(value) {
-           parent.chmods.valueHasMutated();
-         });
-       };
-
-       return map_params(options, subscribe);
-     },
-   },
-   touchzs: {
-     create: function(options) {
-       var parent = options.parent;
-       var subscribe = function(mapping) {
-         mapping.name.subscribe(function(value) {
-           parent.touchzs.valueHasMutated();
-         });
-       };
-       return map_params(options, subscribe);
-     },
-     update: function(options) {
-       var parent = options.parent;
-       var subscribe = function(mapping) {
-         mapping.name.subscribe(function(value) {
-           parent.touchzs.valueHasMutated();
-         });
-       };
-       return map_params(options, subscribe);
-     }
-   }
+  },
+  chmods: {
+    create: function(options) {
+      var parent = options.parent;
+      var subscribe = function(mapping) {
+        mapping.path.subscribe(function(value) {
+          parent.chmods.valueHasMutated();
+        });
+        mapping.permissions.subscribe(function(value) {
+          parent.chmods.valueHasMutated();
+        });
+        mapping.recursive.subscribe(function(value) {
+          parent.chmods.valueHasMutated();
+        });
+      };
+      return map_params(options, subscribe);
+    },
+    update: function(options) {
+      var parent = options.parent;
+      var subscribe = function(mapping) {
+        mapping.path.subscribe(function(value) {
+          parent.chmods.valueHasMutated();
+        });
+        mapping.permissions.subscribe(function(value) {
+          parent.chmods.valueHasMutated();
+        });
+        mapping.recursive.subscribe(function(value) {
+          parent.chmods.valueHasMutated();
+        });
+      };
+      return map_params(options, subscribe);
+    },
+  },
+  touchzs: {
+    create: function(options) {
+      var parent = options.parent;
+      var subscribe = function(mapping) {
+        mapping.name.subscribe(function(value) {
+          parent.touchzs.valueHasMutated();
+        });
+      };
+      return map_params(options, subscribe);
+    },
+    update: function(options) {
+      var parent = options.parent;
+      var subscribe = function(mapping) {
+        mapping.name.subscribe(function(value) {
+          parent.touchzs.valueHasMutated();
+        });
+      };
+      return map_params(options, subscribe);
+    }
+  },
+  data: {
+    create: function(options) {
+      return map_data(options);
+    },
+    update: function(options) {
+      return map_data(options);
+    }
+  }
 };
 
 var ModelModule = function($) {
@@ -342,6 +385,16 @@ var ModelModule = function($) {
   return module;
 };
 
+function initializeData() {
+  var self = this;
+
+  self.data = ($.type(self.data) == "string") ? $.parseJSON(self.data) : self.data;
+
+  if (!('sla' in self.data)) {
+    self.data['sla'] = DEFAULT_SLA.slice(0);
+  }
+}
+
 var WorkflowModel = ModelModule($);
 $.extend(WorkflowModel.prototype, {
   id: 0,
@@ -354,7 +407,8 @@ $.extend(WorkflowModel.prototype, {
   is_shared: true,
   parameters: '[]',
   job_xml: '',
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var NodeModel = ModelModule($);
@@ -399,7 +453,8 @@ $.extend(DistCPModel.prototype, {
   job_xml: '',
   params: '[]',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var MapReduceModel = ModelModule($);
@@ -416,7 +471,8 @@ $.extend(MapReduceModel.prototype, {
   prepares: '[]',
   job_xml: '',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var StreamingModel = ModelModule($);
@@ -432,7 +488,8 @@ $.extend(StreamingModel.prototype, {
   mapper: '',
   reducer: '',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var JavaModel = ModelModule($);
@@ -453,7 +510,8 @@ $.extend(JavaModel.prototype, {
   java_opts: '',
   capture_output: false,
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var PigModel = ModelModule($);
@@ -471,7 +529,8 @@ $.extend(PigModel.prototype, {
   params: '[]',
   script_path: '',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var HiveModel = ModelModule($);
@@ -489,7 +548,8 @@ $.extend(HiveModel.prototype, {
   params: '[]',
   script_path: '',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var SqoopModel = ModelModule($);
@@ -507,7 +567,8 @@ $.extend(SqoopModel.prototype, {
   params: '[]',
   script_path: '',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var ShellModel = ModelModule($);
@@ -526,7 +587,8 @@ $.extend(ShellModel.prototype, {
   command: '',
   capture_output: false,
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var SshModel = ModelModule($);
@@ -542,7 +604,8 @@ $.extend(SshModel.prototype, {
   command: '',
   capture_output: false,
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var FsModel = ModelModule($);
@@ -558,7 +621,8 @@ $.extend(FsModel.prototype, {
   chmods: '[]',
   touchzs: '[]',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var EmailModel = ModelModule($);
@@ -573,7 +637,8 @@ $.extend(EmailModel.prototype, {
   subject: '',
   body: '',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var SubWorkflowModel = ModelModule($);
@@ -587,7 +652,8 @@ $.extend(SubWorkflowModel.prototype, {
   propagate_configuration: true,
   job_properties: '[]',
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 
 var GenericModel = ModelModule($);

+ 6 - 7
apps/oozie/static/js/workflow.node.js

@@ -126,6 +126,12 @@ var NodeModule = function($, IdGeneratorTable, NodeFields) {
     }
 
     // Data manipulation
+    if (self.data && self.data.sla) {
+      self.sla = ko.computed(function() {
+        return self.data.sla();
+      });
+    }
+
     if ('files' in model) {
       //// WARNING: The following order should be preserved!
 
@@ -170,19 +176,12 @@ var NodeModule = function($, IdGeneratorTable, NodeFields) {
       };
     }
 
-    // Manage custom 'data' here
-    if ('sla' in self.model) {
-      var data = self.model['sla'];
-      self.sla = ko.observableArray(self.model['sla']);
-    }
-
     self.initialize.apply(self, arguments);
 
     return self;
   };
 
   $.extend(true, module.prototype, NodeFields, {
-    // Data.
     children: null,
     model: null,