Bläddra i källkod

[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 12 år sedan
förälder
incheckning
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,