Browse Source

[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 years ago
parent
commit
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;
 var kill_view_model = null;
 function workflow_load_success(data) {
 function workflow_load_success(data) {
   if (data.status == 0) {
   if (data.status == 0) {
-    workflow.reload(data.data);
+    var workflow_model = new WorkflowModel(data.data);
+    workflow.reload(workflow_model);
 
 
     //// Kill node
     //// Kill node
     kill_view_model = ManageKillModule($, workflow, nodeModelChooser, Node, NodeModel);
     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()">
 <%def name="slaForm()">
-  <div data-bind="foreach: sla">
+  <div data-bind="foreach: { 'data': sla, 'afterRender': addSLATextAndPlaceholder }">
     <div class="controls">
     <div class="controls">
       <div class="span3">
       <div class="span3">
-        <span data-bind="text: getSLAText(key)['niceName']"></span>
+        <span></span>
       </div>
       </div>
       <div class="span9">
       <div class="span9">
-        <!-- ko if:  key == 'enabled' -->
+        <!-- ko if:  key() == 'enabled' -->
         <input type="checkbox" data-bind="checked: value"/>
         <input type="checkbox" data-bind="checked: value"/>
         <!-- /ko -->
         <!-- /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 -->
         <!-- /ko -->
       </div>
       </div>
     </div>
     </div>
@@ -280,21 +280,27 @@
 
 
 ## Would be nice include it in slaForm() somehow
 ## Would be nice include it in slaForm() somehow
 <%def name="slaGlobal()">
 <%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>
 </%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) {
 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 module = function(options) {
     var self = this;
     var self = this;
 
 
@@ -348,16 +394,29 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
 
 
           return map_params(options, subscribe);
           return map_params(options, subscribe);
         }
         }
+      },
+      data: {
+        create: function(options) {
+          return map_data(options);
+        },
+        update: function(options) {
+          return map_data(options);
+        }
       }
       }
     });
     });
 
 
     $.extend(self, mapping);
     $.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.read_only = ko.observable( options.read_only || false );
     self.new_node = ko.observable();
     self.new_node = ko.observable();
 
 
+    self.sla = ko.computed(function() {
+      return self.data.sla();
+    });
+
     self.url = ko.computed(function() {
     self.url = ko.computed(function() {
-      return '/oozie/workflows/' + self.id()
+      return '/oozie/workflows/' + self.id();
     });
     });
 
 
     // Events
     // Events
@@ -396,9 +459,6 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
 
 
     module.prototype.initialize.apply(self, arguments);
     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;
     return self;
   };
   };
 
 
@@ -473,6 +533,16 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
               case 'nodes':
               case 'nodes':
               break;
               break;
 
 
+              case 'data':
+                var data = {};
+                try {
+                  data = $.parseJSON(value);
+                } catch (error){
+                  data = value;
+                }
+                updateData(self[key], data);
+              break;
+
               default:
               default:
                 self[key](value);
                 self[key](value);
               break;
               break;
@@ -606,10 +676,10 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
       var prop = { name: ko.observable(""), value: ko.observable("") };
       var prop = { name: ko.observable(""), value: ko.observable("") };
       // force bubble up to containing observable array.
       // force bubble up to containing observable array.
       prop.name.subscribe(function(){
       prop.name.subscribe(function(){
-        self.parameters.valueHasMutated();
+        self.job_properties.valueHasMutated();
       });
       });
       prop.value.subscribe(function(){
       prop.value.subscribe(function(){
-        self.parameters.valueHasMutated();
+        self.job_properties.valueHasMutated();
       });
       });
       self.job_properties.push(prop);
       self.job_properties.push(prop);
     },
     },

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

@@ -32,7 +32,7 @@ var DEFAULT_SLA = [
 ];
 ];
 
 
 function getDefaultData() {
 function getDefaultData() {
- return {'sla': jQuery.extend(true, [], DEFAULT_SLA)};
+ return {'sla': DEFAULT_SLA.slice(0)};
 }
 }
 
 
 function normalize_model_fields(node_model) {
 function normalize_model_fields(node_model) {
@@ -55,15 +55,54 @@ var map_params = function(options, subscribe) {
     });
     });
     return mapping;
     return mapping;
   } else {
   } else {
-    var mapping =  ko.mapping.fromJS(options.data, {});
+    var mapping = ko.mapping.fromJS(options.data, {});
     subscribe(mapping);
     subscribe(mapping);
     return 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.
 // Maps JSON strings to fields in the view model.
 var MAPPING_OPTIONS = {
 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: {
   job_properties: {
     create: function(options) {
     create: function(options) {
       var parent = options.parent;
       var parent = options.parent;
@@ -235,7 +274,6 @@ var MAPPING_OPTIONS = {
           parent.moves.valueHasMutated();
           parent.moves.valueHasMutated();
         });
         });
       };
       };
-
       return map_params(options, subscribe);
       return map_params(options, subscribe);
     },
     },
     update: function(options) {
     update: function(options) {
@@ -248,64 +286,69 @@ var MAPPING_OPTIONS = {
           parent.moves.valueHasMutated();
           parent.moves.valueHasMutated();
         });
         });
       };
       };
-
       return map_params(options, subscribe);
       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($) {
 var ModelModule = function($) {
@@ -342,6 +385,16 @@ var ModelModule = function($) {
   return module;
   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($);
 var WorkflowModel = ModelModule($);
 $.extend(WorkflowModel.prototype, {
 $.extend(WorkflowModel.prototype, {
   id: 0,
   id: 0,
@@ -354,7 +407,8 @@ $.extend(WorkflowModel.prototype, {
   is_shared: true,
   is_shared: true,
   parameters: '[]',
   parameters: '[]',
   job_xml: '',
   job_xml: '',
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var NodeModel = ModelModule($);
 var NodeModel = ModelModule($);
@@ -399,7 +453,8 @@ $.extend(DistCPModel.prototype, {
   job_xml: '',
   job_xml: '',
   params: '[]',
   params: '[]',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var MapReduceModel = ModelModule($);
 var MapReduceModel = ModelModule($);
@@ -416,7 +471,8 @@ $.extend(MapReduceModel.prototype, {
   prepares: '[]',
   prepares: '[]',
   job_xml: '',
   job_xml: '',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var StreamingModel = ModelModule($);
 var StreamingModel = ModelModule($);
@@ -432,7 +488,8 @@ $.extend(StreamingModel.prototype, {
   mapper: '',
   mapper: '',
   reducer: '',
   reducer: '',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var JavaModel = ModelModule($);
 var JavaModel = ModelModule($);
@@ -453,7 +510,8 @@ $.extend(JavaModel.prototype, {
   java_opts: '',
   java_opts: '',
   capture_output: false,
   capture_output: false,
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var PigModel = ModelModule($);
 var PigModel = ModelModule($);
@@ -471,7 +529,8 @@ $.extend(PigModel.prototype, {
   params: '[]',
   params: '[]',
   script_path: '',
   script_path: '',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var HiveModel = ModelModule($);
 var HiveModel = ModelModule($);
@@ -489,7 +548,8 @@ $.extend(HiveModel.prototype, {
   params: '[]',
   params: '[]',
   script_path: '',
   script_path: '',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var SqoopModel = ModelModule($);
 var SqoopModel = ModelModule($);
@@ -507,7 +567,8 @@ $.extend(SqoopModel.prototype, {
   params: '[]',
   params: '[]',
   script_path: '',
   script_path: '',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var ShellModel = ModelModule($);
 var ShellModel = ModelModule($);
@@ -526,7 +587,8 @@ $.extend(ShellModel.prototype, {
   command: '',
   command: '',
   capture_output: false,
   capture_output: false,
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var SshModel = ModelModule($);
 var SshModel = ModelModule($);
@@ -542,7 +604,8 @@ $.extend(SshModel.prototype, {
   command: '',
   command: '',
   capture_output: false,
   capture_output: false,
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var FsModel = ModelModule($);
 var FsModel = ModelModule($);
@@ -558,7 +621,8 @@ $.extend(FsModel.prototype, {
   chmods: '[]',
   chmods: '[]',
   touchzs: '[]',
   touchzs: '[]',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var EmailModel = ModelModule($);
 var EmailModel = ModelModule($);
@@ -573,7 +637,8 @@ $.extend(EmailModel.prototype, {
   subject: '',
   subject: '',
   body: '',
   body: '',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var SubWorkflowModel = ModelModule($);
 var SubWorkflowModel = ModelModule($);
@@ -587,7 +652,8 @@ $.extend(SubWorkflowModel.prototype, {
   propagate_configuration: true,
   propagate_configuration: true,
   job_properties: '[]',
   job_properties: '[]',
   child_links: [],
   child_links: [],
-  data: getDefaultData()
+  data: getDefaultData(),
+  initialize: initializeData
 });
 });
 
 
 var GenericModel = ModelModule($);
 var GenericModel = ModelModule($);

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

@@ -126,6 +126,12 @@ var NodeModule = function($, IdGeneratorTable, NodeFields) {
     }
     }
 
 
     // Data manipulation
     // Data manipulation
+    if (self.data && self.data.sla) {
+      self.sla = ko.computed(function() {
+        return self.data.sla();
+      });
+    }
+
     if ('files' in model) {
     if ('files' in model) {
       //// WARNING: The following order should be preserved!
       //// 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);
     self.initialize.apply(self, arguments);
 
 
     return self;
     return self;
   };
   };
 
 
   $.extend(true, module.prototype, NodeFields, {
   $.extend(true, module.prototype, NodeFields, {
-    // Data.
     children: null,
     children: null,
     model: null,
     model: null,