Bläddra i källkod

[oozie] Allow kill actions to pass through an email action

Abraham Elmahrek 12 år sedan
förälder
incheckning
e59de3d

+ 7 - 2
apps/oozie/src/oozie/models.py

@@ -419,8 +419,13 @@ class Workflow(Job):
 
   def get_hierarchy(self):
     node = Start.objects.get(workflow=self) # Uncached version of start.
-    return self.get_hierarchy_rec(node=node) + [[Kill.objects.get(workflow=node.workflow)],
-                                           [End.objects.get(workflow=node.workflow)]]
+    kill = Kill.objects.get(workflow=node.workflow)
+    # Special case: manage error email actions separately
+    try:
+      kill_nodes = [Link.objects.filter(child=kill).get(name='ok').parent, kill]
+    except Link.DoesNotExist:
+      kill_nodes = [kill]
+    return self.get_hierarchy_rec(node=node) + [kill_nodes, [End.objects.get(workflow=node.workflow)]]
 
   def get_hierarchy_rec(self, node=None):
     if node is None:

+ 252 - 242
apps/oozie/src/oozie/templates/editor/action_utils.mako

@@ -22,7 +22,7 @@
 <%namespace name="utils" file="../utils.inc.mako" />
 
 
-<%def name="action_form(action_form, node_type, template=True)">
+<%def name="action_form_modal_template(action_form, node_type, template=True)">
 % if template:
   <script type="text/html" id="${node_type}EditTemplate">
 % endif
@@ -35,251 +35,12 @@
 
       <div class="modal-content">
         <fieldset class="span12">
-          % for field in action_form:
-            % if field.html_name in ('name', 'description'):
-              ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'disable: $root.context().read_only, value: %s' % field.name}) }
-            % endif
-          % endfor
-
-          ${ utils.render_constant(_('Action type'), node_type) }
-
-          <div class="control-group ">
-            <label class="control-label">
-              <a href="javascript:void(0);" id="advanced-btn" onclick="$('#node-advanced-container').toggle('hide')">
-                <i class="icon-share-alt"></i> ${ _('Advanced') }</a>
-            </label>
-            <div class="controls"></div>
-          </div>
-
-          <div id="node-advanced-container" class="hide">
-            <div class="control-group">
-              <label class="control-label">${_('Error link to')}</label>
-              <div class="controls">
-                <div style="padding-top:4px">
-                  <select data-bind="options: $root.context().nodes,
-                                     optionsText: function(item) {
-                                       return (item.name()) ? item.name() : item.node_type() + '-' + item.id();
-                                     },
-                                     optionsValue: function(item) {
-                                       return item.id();
-                                     },
-                                     value: $root.context().error_node"></select>
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <hr/>
-
-          <div class="control-group">
-            <label class="control-label"></label>
-            <div class="controls">
-            <p class="alert alert-info span7">
-              % if node_type != 'fs':
-                ${ _('All the paths are relative to the deployment directory. They can be absolute but this is not recommended.') }
-              % else:
-                ${ _('All the paths need to be absolute.') }
-              % endif
-              <br/>
-              ${ _('You can parameterize values using case sensitive') } <code>${"${"}parameter}</code>.
-            </p>
-            % if node_type == 'ssh':
-              <p class="alert alert-warn span5">
-                ${ _('The SSH server requires passwordless login') }.
-              </p>
-            % endif
-            % if node_type == 'java':
-              % if get_oozie().security_enabled:
-                <p class="alert alert-warn span5">
-                  ${ _('The delegation token needs to be propagated from the launcher job to the MR job') }.
-                  <a href="https://issues.apache.org/jira/browse/OOZIE-1172">OOZIE-1172</a>
-                </p>
-              % endif
-            % endif
-            % if node_type == 'email':
-              <p class="alert alert-warn span5">
-                ${ _('Requires some SMTP server configuration to be present (in oozie-site.xml)') }.
-              </p>
-            % endif
-            </div>
-          </div>
-
-          % for field in action_form:
-            % if field.html_name not in ('name', 'description', 'node_type', 'job_xml'):
-              % if field.html_name in ('capture_output', 'is_single', 'sub_workflow', 'propagate_configuration'):
-                ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'disable: $root.context().read_only, checked: %s' % field.name}) }
-              % else:
-                ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'disable: $root.context().read_only, value: %s' % field.name}) }
-              % endif
-            % endif
-          % endfor
-
-          % if 'prepares' in action_form.fields:
-            <%
-              prepares_field(action_form['prepares'], {
-                'name': 'prepares',
-                'add': [
-                  {'label': _('Add delete'), 'method': 'addPrepareDelete'},
-                  {'label': _('Add mkdir'), 'method': 'addPrepareMkdir'},
-                ],
-                'remove': '$parent.removePrepare.bind($parent)'
-              })
-            %>
-          % endif
-
-          % if 'params' in action_form.fields:
-            % if node_type == 'pig':
-              <%
-              params_field(action_form['params'], {
-                'name': 'params',
-                'add': [
-                  {'label': _('Add param'), 'method': 'addParam'},
-                  {'label': _('Add argument'), 'method': 'addArgument'},
-                ],
-                'remove': '$parent.removeParam.bind($parent)'
-              })
-              %>
-            % endif
-
-            % if node_type == 'shell':
-              <%
-              params_field(action_form['params'], {
-                'name': 'params',
-                'add': [
-                  {'label': _('Add argument'), 'method': 'addArgument'},
-                  {'label': _('Add env-var'), 'method': 'addEnvVar'},
-                ],
-                'remove': '$parent.removeParam.bind($parent)'
-              })
-              %>
-            % endif
-
-            % if node_type == 'hive':
-              <%
-              params_field(action_form['params'], {
-                'name': 'params',
-                'add': [
-                  {'label': _('Add param'), 'method': 'addParam'},
-                ],
-                'remove': '$parent.removeParam.bind($parent)'
-              })
-              %>
-            % endif
-
-            % if node_type == 'distcp':
-              <%
-              params_field(action_form['params'], {
-                'name': 'params',
-                'add': [
-                  {'label': _('Add argument'), 'method': 'addArgument'},
-                ],
-                'remove': '$parent.removeParam.bind($parent)'
-              })
-              %>
-            % endif
-
-            % if node_type in ('sqoop', 'ssh'):
-              <%
-              params_field(action_form['params'], {
-                'name': 'params',
-                'add': [
-                  {'label': _('Add arg'), 'method': 'addArg'},
-                ],
-                'remove': '$parent.removeParam.bind($parent)'
-              })
-              %>
-            % endif
-          % endif
-
-          % if 'job_properties' in action_form.fields:
-            <%
-            job_properties_field(action_form['job_properties'], {
-              'name': 'job_properties',
-              'add': 'addProperty',
-              'remove': '$parent.removeProperty.bind($parent)'
-            })
-            %>
-          % endif
-
-          % if 'files' in action_form.fields:
-            <%
-            file_field(action_form['files'], {
-              'name': 'files',
-              'add': 'addFile',
-              'remove': '$parent.removeFile.bind($parent)'
-            })
-            %>
-          % endif
-
-          % if 'archives' in action_form.fields:
-            <%
-            archives_field(action_form['archives'], {
-              'name': 'archives',
-              'add': 'addArchive',
-              'remove': '$parent.removeArchive.bind($parent)'
-            })
-            %>
-          % endif
-
-          % if 'job_xml' in action_form.fields:
-            ${ utils.render_field_with_error_js(action_form['job_xml'], action_form['job_xml'].name, extra_attrs={'data-bind': 'disable: $root.context().read_only, fileChooser: $data, value: %s' % action_form['job_xml'].name}) }
-          % endif
-
-          % if 'deletes' in action_form.fields:
-            <%
-            file_field(action_form['deletes'], {
-              'name': 'deletes',
-              'add': 'addDelete',
-              'remove': '$parent.removeDelete.bind($parent)'
-            })
-            %>
-          % endif
-
-          % if 'mkdirs' in action_form.fields:
-            <%
-            file_field(action_form['mkdirs'], {
-              'name': 'mkdirs',
-              'add': 'addMkdir',
-              'remove': '$parent.removeMkdir.bind($parent)'
-            })
-            %>
-          % endif
-
-          % if 'moves' in action_form.fields:
-            <%
-            move_field(action_form['moves'], {
-              'name': 'moves',
-              'add': 'addMove',
-              'remove': '$parent.removeMove.bind($parent)'
-            })
-            %>
-          % endif
-
-          % if 'chmods' in action_form.fields:
-            <%
-            chmod_field(action_form['chmods'], {
-              'name': 'chmods',
-              'add': 'addChmod',
-              'remove': '$parent.removeChmod.bind($parent)'
-            })
-            %>
-          % endif
-
-          % if 'touchzs' in action_form.fields:
-            <%
-            file_field(action_form['touchzs'], {
-              'name': 'touchzs',
-              'add': 'addTouchz',
-              'remove': '$parent.removeTouchz.bind($parent)'
-            })
-            %>
-          % endif
-
+          ${ action_form_fields(action_form, node_type) }
         </fieldset>
       </div>
 
       <div class="modal-footer">
-        <a class="btn cancelButton" href="javascript:void(0);">Cancel</a>
+        <a class="btn cancelButton" href="javascript:void(0);">${_('Cancel')}</a>
         <button class="btn btn-primary doneButton disable-feedback" type="button" data-bind="visible: !$root.context().read_only">${ _('Done')}</button>
       </div>
 
@@ -290,6 +51,255 @@
 % endif
 </%def>
 
+<%def name="action_form_fields(action_form, node_type, show_primary=True)">
+% if show_primary:
+  % for field in action_form:
+    % if field.html_name in ('name', 'description'):
+      ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'disable: $root.context().read_only, value: %s' % field.name}) }
+    % endif
+  % endfor
+
+  ${ utils.render_constant(_('Action type'), node_type) }
+
+  <!-- ko if: $root.context().nodes && $root.context().error_node -->
+  <div class="control-group ">
+    <label class="control-label">
+      <a href="javascript:void(0);" id="advanced-btn" onclick="$('#node-advanced-container').toggle('hide')">
+        <i class="icon-share-alt"></i> ${ _('Advanced') }</a>
+    </label>
+    <div class="controls"></div>
+  </div>
+
+  <div id="node-advanced-container" class="hide">
+    <div class="control-group">
+      <label class="control-label">${_('Error link to')}</label>
+      <div class="controls">
+        <div style="padding-top:4px">
+          <select data-bind="options: $root.context().nodes,
+                             optionsText: function(item) {
+                               return (item.name()) ? item.name() : item.node_type() + '-' + item.id();
+                             },
+                             optionsValue: function(item) {
+                               return item.id();
+                             },
+                             value: $root.context().error_node"></select>
+        </div>
+      </div>
+    </div>
+  </div>
+  <!-- /ko -->
+% endif
+
+<hr/>
+
+<div class="control-group">
+  <label class="control-label"></label>
+  <div class="controls">
+  <p class="alert alert-info span7">
+    % if node_type != 'fs':
+      ${ _('All the paths are relative to the deployment directory. They can be absolute but this is not recommended.') }
+    % else:
+      ${ _('All the paths need to be absolute.') }
+    % endif
+    <br/>
+    ${ _('You can parameterize values using case sensitive') } <code>${"${"}parameter}</code>.
+  </p>
+  % if node_type == 'ssh':
+    <br style="clear: both" />
+    <p class="alert alert-warn span5">
+      ${ _('The SSH server requires passwordless login') }.
+    </p>
+  % endif
+  % if node_type == 'java':
+    % if get_oozie().security_enabled:
+    <br style="clear: both" />
+      <p class="alert alert-warn span5">
+        ${ _('The delegation token needs to be propagated from the launcher job to the MR job') }.
+        <a href="https://issues.apache.org/jira/browse/OOZIE-1172">OOZIE-1172</a>
+      </p>
+    % endif
+  % endif
+  % if node_type == 'email':
+    <br style="clear: both" />
+    <p class="alert alert-warn span5">
+      ${ _('Requires some SMTP server configuration to be present (in oozie-site.xml)') }.
+    </p>
+  % endif
+  </div>
+</div>
+
+% for field in action_form:
+  % if field.html_name not in ('name', 'description', 'node_type', 'job_xml'):
+    % if field.html_name in ('capture_output', 'is_single', 'sub_workflow', 'propagate_configuration'):
+      ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'disable: $root.context().read_only, checked: %s' % field.name}) }
+    % else:
+      ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'disable: $root.context().read_only, value: %s' % field.name}) }
+    % endif
+  % endif
+% endfor
+
+% if 'prepares' in action_form.fields:
+  <%
+    prepares_field(action_form['prepares'], {
+      'name': 'prepares',
+      'add': [
+        {'label': _('Add delete'), 'method': 'addPrepareDelete'},
+        {'label': _('Add mkdir'), 'method': 'addPrepareMkdir'},
+      ],
+      'remove': '$parent.removePrepare.bind($parent)'
+    })
+  %>
+% endif
+
+% if 'params' in action_form.fields:
+  % if node_type == 'pig':
+    <%
+    params_field(action_form['params'], {
+      'name': 'params',
+      'add': [
+        {'label': _('Add param'), 'method': 'addParam'},
+        {'label': _('Add argument'), 'method': 'addArgument'},
+      ],
+      'remove': '$parent.removeParam.bind($parent)'
+    })
+    %>
+  % endif
+
+  % if node_type == 'shell':
+    <%
+    params_field(action_form['params'], {
+      'name': 'params',
+      'add': [
+        {'label': _('Add argument'), 'method': 'addArgument'},
+        {'label': _('Add env-var'), 'method': 'addEnvVar'},
+      ],
+      'remove': '$parent.removeParam.bind($parent)'
+    })
+    %>
+  % endif
+
+  % if node_type == 'hive':
+    <%
+    params_field(action_form['params'], {
+      'name': 'params',
+      'add': [
+        {'label': _('Add param'), 'method': 'addParam'},
+      ],
+      'remove': '$parent.removeParam.bind($parent)'
+    })
+    %>
+  % endif
+
+  % if node_type == 'distcp':
+    <%
+    params_field(action_form['params'], {
+      'name': 'params',
+      'add': [
+        {'label': _('Add argument'), 'method': 'addArgument'},
+      ],
+      'remove': '$parent.removeParam.bind($parent)'
+    })
+    %>
+  % endif
+
+  % if node_type in ('sqoop', 'ssh'):
+    <%
+    params_field(action_form['params'], {
+      'name': 'params',
+      'add': [
+        {'label': _('Add arg'), 'method': 'addArg'},
+      ],
+      'remove': '$parent.removeParam.bind($parent)'
+    })
+    %>
+  % endif
+% endif
+
+% if 'job_properties' in action_form.fields:
+  <%
+  job_properties_field(action_form['job_properties'], {
+    'name': 'job_properties',
+    'add': 'addProperty',
+    'remove': '$parent.removeProperty.bind($parent)'
+  })
+  %>
+% endif
+
+% if 'files' in action_form.fields:
+  <%
+  file_field(action_form['files'], {
+    'name': 'files',
+    'add': 'addFile',
+    'remove': '$parent.removeFile.bind($parent)'
+  })
+  %>
+% endif
+
+% if 'archives' in action_form.fields:
+  <%
+  archives_field(action_form['archives'], {
+    'name': 'archives',
+    'add': 'addArchive',
+    'remove': '$parent.removeArchive.bind($parent)'
+  })
+  %>
+% endif
+
+% if 'job_xml' in action_form.fields:
+  ${ utils.render_field_with_error_js(action_form['job_xml'], action_form['job_xml'].name, extra_attrs={'data-bind': 'disable: $root.context().read_only, fileChooser: $data, value: %s' % action_form['job_xml'].name}) }
+% endif
+
+% if 'deletes' in action_form.fields:
+  <%
+  file_field(action_form['deletes'], {
+    'name': 'deletes',
+    'add': 'addDelete',
+    'remove': '$parent.removeDelete.bind($parent)'
+  })
+  %>
+% endif
+
+% if 'mkdirs' in action_form.fields:
+  <%
+  file_field(action_form['mkdirs'], {
+    'name': 'mkdirs',
+    'add': 'addMkdir',
+    'remove': '$parent.removeMkdir.bind($parent)'
+  })
+  %>
+% endif
+
+% if 'moves' in action_form.fields:
+  <%
+  move_field(action_form['moves'], {
+    'name': 'moves',
+    'add': 'addMove',
+    'remove': '$parent.removeMove.bind($parent)'
+  })
+  %>
+% endif
+
+% if 'chmods' in action_form.fields:
+  <%
+  chmod_field(action_form['chmods'], {
+    'name': 'chmods',
+    'add': 'addChmod',
+    'remove': '$parent.removeChmod.bind($parent)'
+  })
+  %>
+% endif
+
+% if 'touchzs' in action_form.fields:
+  <%
+  file_field(action_form['touchzs'], {
+    'name': 'touchzs',
+    'add': 'addTouchz',
+    'remove': '$parent.removeTouchz.bind($parent)'
+  })
+  %>
+% endif
+</%def>
+
 <%def name="file_field(field, javascript_attrs={})">
 <div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }">
   <label class="control-label">${ field.label }</label>

+ 39 - 5
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -51,6 +51,9 @@ ${ layout.menubar(section='workflows') }
           <li>
             <a data-bind="attr: {href: '/filebrowser/view' + deployment_dir() }" target="_blank" title="${ _('Go upload additional files and libraries to the deployment directory on HDFS') }" rel="tooltip" data-placement="right"><i class="icon-folder-open"></i> ${ _('Workspace') }</a>
           </li>
+          <li data-bind="visible: !loading()">
+            <a title="${ _('Edit kill node') }" rel="tooltip" data-placement="right" href="#kill"><i class="icon-off"></i> ${ _('Kill node') }</a>
+          </li>
         % endif
 
         % if user_can_edit_job:
@@ -116,6 +119,17 @@ ${ layout.menubar(section='workflows') }
       </fieldset>
     </div>
 
+    <div id="editKill" class="section hide span12">
+      <div class="alert alert-info"><h3>${ _('Kill node') }</h3></div>
+      <fieldset data-bind="with: context().node">
+        % for form_info in action_forms:
+          % if form_info[0] == 'email':
+            ${ actions.action_form_fields(action_form=form_info[1], node_type=form_info[0], show_primary=False) }
+          % endif
+        % endfor
+      </fieldset>
+    </div>
+
     <div id="importAction" class="section hide">
       <ul class="nav nav-tabs">
         <li class="active"><a href="#importJobsub" data-toggle="tab">${ _('Job Designer') }</a></li>
@@ -333,7 +347,7 @@ ${ layout.menubar(section='workflows') }
 
   <div id="formActions" class="form-actions center">
   % if user_can_edit_job:
-    <button data-bind="disable: workflow.read_only, visible: !workflow.read_only(), click: function() { workflow.loading(true); workflow.save({ success: workflow_save_success, error: workflow_save_error }) }" class="btn btn-primary" id="btn-save-wf">${ _('Save') }</button>
+    <button data-bind="disable: workflow.read_only, visible: !workflow.read_only(), click: save_workflow" class="btn btn-primary" id="btn-save-wf">${ _('Save') }</button>
   % endif
     <a href="${ url('oozie:list_workflows') }" class="btn">${ _('Back') }</a>
   </div>
@@ -385,7 +399,7 @@ ${ layout.menubar(section='workflows') }
 
 
 % for form_info in action_forms:
-  ${ actions.action_form(action_form=form_info[1], node_type=form_info[0], template=True) }
+  ${ actions.action_form_modal_template(action_form=form_info[1], node_type=form_info[0], template=True) }
 % endfor
 
 ${ controls.fork_convert_form(node_type='fork', template=True, javascript_attrs={'convert': 'function(data, event) { $data.convertToDecision(); $data._workflow.rebuild(); }'}) }
@@ -553,18 +567,35 @@ function workflow_save_error(data) {
   $("#btn-save-wf").button('reset');
 }
 
+function workflow_read_only_handler() {
+  $.jHueNotify.error("${ _('Workflow is in read only mode.') }");
+  workflow.loading(false);
+}
+
+var kill_view_model = null;
 function workflow_load_success(data) {
   if (data.status == 0) {
     workflow.reload(data.data);
+
+    //// Kill node
+    kill_view_model = ManageKillModule($, workflow, nodeModelChooser, Node, NodeModel);
+    ko.applyBindings(kill_view_model, $('#editKill')[0]);
+
   } else {
     $.jHueNotify.error("${ _('Received invalid response from server: ') }" + JSON.stringify(data));
   }
   workflow.loading(false);
 }
 
-function workflow_read_only_handler() {
-  $.jHueNotify.error("${ _('Workflow is in read only mode.') }");
-  workflow.loading(false);
+function save_workflow() {
+  workflow.loading(true);
+  if (kill_view_model.enabled()) {
+    if (kill_view_model.isValid()) {
+      workflow.save({ success: workflow_save_success, error: workflow_save_error });
+    }
+  } else {
+    workflow.save({ success: workflow_save_success, error: workflow_save_error });
+  }
 }
 
 // Fetch all nodes from server.
@@ -906,6 +937,9 @@ $(document).ready(function () {
     'properties':function () {
       showSection('properties');
     },
+    'kill':function() {
+      showSection('editKill');
+    },
     'importAction':function() {
       $("#importAction *[rel=tooltip]").tooltip();
       showSection('importAction');

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

@@ -284,6 +284,7 @@ $.extend(DecisionNode.prototype, ForkNode.prototype, {
  * Workflow module
  */
 var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable) {
+
   var module = function(options) {
     var self = this;
 
@@ -980,4 +981,84 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
 
   return module;
 };
-var Workflow = WorkflowModule($, nodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable);
+var Workflow = WorkflowModule($, nodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable);
+
+// Manage Kill Module
+function ManageKillModule($, workflow, NodeModelChooser, Node, NodeModel) {
+  var email_action = null;
+  var parents = workflow.kill.findParents();
+  var email_enabled = ko.observable();
+  if (parents.length > 0) {
+    email_action = parents[0];
+    email_enabled(true);
+  } else {
+    var email_json = {
+      "description": "",
+      "workflow": workflow.id(),
+      "child_links": [],
+      "node_type": "email",
+      "message": "Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]",
+      "name": 'killemail',
+      "id": IdGeneratorTable['email'].nextId()
+    };
+    var NodeModel = NodeModelChooser(email_json.node_type);
+    var model = new NodeModel(email_json);
+    email_action = new Node(workflow, model, workflow.registry);
+    email_enabled(false);
+  }
+
+  var replace_email = function(email_action) {
+    email_action.removeAllChildren();
+    email_action.removeErrorChildren();
+
+    $.each(workflow.registry.nodes, function(index, node) {
+      if (node.getErrorChild() && node.id() != email_action.id()) {
+        node.putErrorChild(workflow.kill);
+      }
+    });
+  };
+
+  var replace_kill = function(email_action) {
+    if (email_action.findChildren().length == 0) {
+      email_action.addChild(workflow.kill, 'ok');
+    }
+
+    if (!email_action.getErrorChild()) {
+      email_action.putErrorChild(workflow.kill);
+    }
+
+    $.each(workflow.registry.nodes, function(index, node) {
+      if (node.getErrorChild() && node.id() != email_action.id()) {
+        node.putErrorChild(email_action);
+      }
+    });
+  };
+
+  // Add/Remove kill email action node from registry so that it is not sent to server.
+  email_action.to.subscribe(function(value) {
+    if (value && !email_enabled()) {
+      workflow.registry.add(email_action.id(), email_action);
+      replace_kill(email_action);
+      email_enabled(true);
+    } else if (email_enabled()) {
+      replace_email(email_action);
+      email_enabled(false);
+      workflow.registry.remove(email_action.id());
+      email_action.id(IdGeneratorTable['email'].nextId());
+    }
+
+    return value;
+  });
+
+  // View model
+  return {
+    'enabled': email_enabled,
+    'isValid': function() {
+      return email_action.validate();
+    },
+    'context': ko.observable({
+      'node': ko.observable(email_action),
+      'read_only': ko.observable(workflow.read_only())
+    })
+  };
+};

+ 3 - 2
apps/oozie/static/js/workflow.node.js

@@ -350,12 +350,13 @@ var NodeModule = function($, IdGeneratorTable, NodeFields) {
      * Add child
      * Update child links for this node.
      */
-    addChild: function(node) {
+    addChild: function(node, link_type) {
       var self = this;
+      var link_type = link_type || linkTypeChooser(self, node);
       var link = {
         parent: ko.observable(self.id()),
         child: ko.observable(node.id()),
-        name: ko.observable(linkTypeChooser(self, node)),
+        name: ko.observable(link_type),
         comment: ko.observable('')
       };
       self.child_links.unshift(link);

+ 1 - 1
apps/oozie/static/js/workflow.utils.js

@@ -92,4 +92,4 @@ function edit_node_modal(modal, workflow, node, save, cancel, template) {
 
     edit_node_modal(node);
   });
-}
+}