浏览代码

HUE-1267 [oozie] Import action from josub or a workflow

- Created new control that manages import actions.
- Can import from jobsub or oozie
- Jobsub import is just like oozie import, except oozie requires action selection
Abraham Elmahrek 12 年之前
父节点
当前提交
ca2386abc7

+ 10 - 0
apps/oozie/src/oozie/models.py

@@ -77,6 +77,10 @@ A Workflow/Coordinator can be modified only by its owner or a superuser.
 Permissions checking happens by adding the decorators.
 """
 class JobManager(models.Manager):
+  def is_accessible(self, user, job_id):
+    job = Job.objects.select_related().get(pk=job_id).get_full_node()
+    return job.is_accessible(user)
+
   def is_accessible_or_exception(self, request, job_id, exception_class=PopupException):
     if job_id is None:
       return
@@ -261,6 +265,12 @@ class WorkflowManager(TrashManager):
     workflow.save()
     workflow.delete(skip_trash=True)
 
+  def managed(self):
+    return self.filter(managed=True)
+
+  def unmanaged(self):
+    return self.filter(managed=False)
+
 
 class Workflow(Job):
   """

+ 0 - 69
apps/oozie/src/oozie/templates/editor/action_utils.mako

@@ -22,75 +22,6 @@
 <%namespace name="utils" file="../utils.inc.mako" />
 
 
-<%def name="import_jobsub_form(template=True)">
-% if template:
-  <script type="text/html" id="ImportNodeTemplate">
-% endif
-
-  <div data-bind="with: context().node">
-    <form class="form-horizontal" id="import-node-form" method="POST">
-      <div class="modal-header">
-        <a href="#" class="close">&times;</a>
-        <h3 class="message">${_('Import Action')}</h3>
-      </div>
-
-      <div class="modal-content">
-        <fieldset class="span12">
-          <table id="jobdesignerActionsTable" class="table datatables">
-            <thead>
-              <tr>
-                <th></th>
-                <th>${ _('Name') }</th>
-                <th>${ _('Description') }</th>
-              </tr>
-            </thead>
-            <tbody data-bind="visible: available_nodes().length > 0, foreach: available_nodes">
-              <tr class="action-row">
-                <td class=".btn-large action-column" data-row-selector-exclude="true" style="background-color: white;">
-                  <input type="radio" name="jobsub_id" data-bind="attr: { 'value': id }, click: $parent.setJobDesignerId, disable: $root.context().read_only" />
-                </td>
-                <td data-bind="text: $data.name"></td>
-                <td data-bind="text: $data.description"></td>
-              </tr>
-            </tbody>
-            <tbody data-bind="visible: available_nodes().length == 0">
-              <tr class="action-row">
-                <td>${ _('N/A') }</td><td></td><td></td>
-              </tr>
-            </tbody>
-          </table>
-        </fieldset>
-      </div>
-
-      <div class="modal-footer">
-        <a class="btn cancelButton" href="javascript:void(0);">${ _('Cancel') }</a>
-        <button class="btn btn-primary doneButton" type="button" data-bind="visible: !$root.context().read_only">${ _('Import') }</button>
-      </div>
-
-    </form>
-  </div>
-
-  <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
-
-  <script type="text/javascript" charset="utf-8">
-    $(document).ready(function() {
-      $(".action-row").click(function(e){
-        var select_btn = $(this).find('input');
-        select_btn.prop("checked", true);
-
-        $(".action-row").css("background-color", "");
-        $(this).css("background-color", "#ECF4F8");
-      });
-
-      $("a[data-row-selector='true']").jHueRowSelector();
-    });
-  </script>
-
-% if template:
-  </script>
-% endif
-</%def>
-
 <%def name="action_form(action_form, node_type, template=True)">
 % if template:
   <script type="text/html" id="${node_type}EditTemplate">

+ 211 - 72
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -39,14 +39,14 @@ ${ layout.menubar(section='workflows') }
 
   <div class="row-fluid">
   <div class="span2">
-    <div class="well sidebar-nav">
+    <div id="workflowControls" class="well sidebar-nav">
       <ul class="nav nav-list">
         <li class="nav-header">${ _('Properties') }</li>
         <li><a href="#properties">${ _('Edit properties') }</a></li>
 
         <li class="nav-header">${ _('Editor') }</li>
         <li><a href="#editWorkflow">${ _('Edit workflow') }</a></li>
-        <li><a href="javascript:void(0)" class="import-jobsub-node-link" title="${ _('Click to import a Job Designer action and add it to the end of the flow') }" rel="tooltip" data-placement="right">${ _('Import action') }</a></li>
+        <li><a href="#importAction" title="${ _('Click to import an Oozie workflow action or Job Designer action') }" rel="tooltip" data-placement="right">${ _('Import action') }</a></li>
         % if user_can_edit_job:
           <li>
             <a data-bind="attr: {href: '/filebrowser/view' + deployment_dir() }" target="_blank" title="${ _('Upload additional files and libraries to the deployment directory') }" rel="tooltip" data-placement="right"><i class="icon-share-alt"></i> ${ _('Upload') }</a>
@@ -116,6 +116,96 @@ ${ layout.menubar(section='workflows') }
       </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>
+        <li><a href="#importOozie" data-toggle="tab">${ _('Oozie') }</a></li>
+      </ul>
+
+      <div class="tab-content">
+        <div class="tab-pane active" id="importJobsub" data-bind="with: jobsub">
+          <div class="alert alert-info">
+            <h3>${ _('Import Action from Job Designer') }</h3>
+            <p>${_('Click on a row to import the design as an action in the workflow. The action will be added to the beginning of the flow.')}</p>
+          </div>
+          <table id="jobsubActionsTable" class="table">
+            <thead>
+              <tr>
+                <th>${ _('Name') }</th>
+                <th>${ _('Description') }</th>
+              </tr>
+            </thead>
+            <tbody data-bind="visible: workflows().length > 0, foreach: workflows">
+              <tr class="action-row" rel="tooltip" title="${ _('Click to import action to workflow') }">
+                <td>
+                  <a href="javascript:void(0);" data-bind="text: $data.name"></a>
+                </td>
+                <td data-bind="text: $data.description"></td>
+              </tr>
+            </tbody>
+            <tbody data-bind="visible: workflows().length == 0">
+              <tr class="action-row">
+                <td>${ _('N/A') }</td><td></td><td></td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+
+        <div class="tab-pane" id="importOozie" data-bind="with: oozie">
+          <div class="alert alert-info"><h3>${ _('Import Action from Oozie') }</h3></div>
+          <table id="oozieWorkflowsTable" class="table">
+            <thead>
+              <tr>
+                <th>${ _('Name') }</th>
+                <th>${ _('Description') }</th>
+              </tr>
+            </thead>
+            <tbody data-bind="visible: workflows().length > 0, foreach: workflows">
+              <tr class="action-row">
+                <td>
+                  <a href="javascript:void(0);" data-bind="text: $data.name"></a>
+                </td>
+                <td data-bind="text: $data.description"></td>
+              </tr>
+            </tbody>
+            <tbody data-bind="visible: workflows().length == 0">
+              <tr class="action-row">
+                <td>${ _('N/A') }</td><td></td><td></td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+
+    <div id="importOozieAction" class="section hide" data-bind="if: selected_workflow()">
+      <div class="alert alert-info">
+        <h3>${ _('Import Action from Workflow: ') } <span data-bind="text: selected_workflow().name"></span></h3>
+        <p>${_('Click on a row to import the action.')}</p>
+      </div>
+      <table id="oozieActionsTable" class="table">
+        <thead>
+          <tr>
+            <th>${ _('Name') }</th>
+            <th>${ _('Description') }</th>
+          </tr>
+        </thead>
+        <tbody data-bind="visible: nodes().length > 0, foreach: nodes">
+          <tr class="action-row" rel="tooltip" title="${ _('Click to import action to workflow') }">
+            <td>
+              <a href="javascript:void(0);" data-bind="text: $data.name"></a>
+            </td>
+            <td data-bind="text: $data.description"></td>
+          </tr>
+        </tbody>
+        <tbody data-bind="visible: nodes().length == 0">
+          <tr class="action-row">
+            <td>${ _('N/A') }</td><td></td><td></td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
     <div id="editWorkflow" class="section hide">
 
       <div class="alert alert-info"><h3>${ _('Editor') }</h3></div>
@@ -241,7 +331,7 @@ ${ layout.menubar(section='workflows') }
   </div>
   </div>
 
-  <div class="form-actions center">
+  <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>
   % endif
@@ -280,6 +370,7 @@ ${ layout.menubar(section='workflows') }
 <script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/jquery/plugins/jquery-ui-draggable-droppable-sortable-1.8.23.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/routie-0.3.0.min.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
 
 <link rel="stylesheet" href="/oozie/static/css/workflow.css">
 <script type="text/javascript" src="/oozie/static/js/workflow.utils.js"></script>
@@ -290,14 +381,13 @@ ${ layout.menubar(section='workflows') }
 <script type="text/javascript" src="/oozie/static/js/workflow.node-fields.js"></script>
 <script type="text/javascript" src="/oozie/static/js/workflow.node.js"></script>
 <script type="text/javascript" src="/oozie/static/js/workflow.js"></script>
+<script type="text/javascript" src="/oozie/static/js/workflow.import-node.js"></script>
 
 
 % for form_info in action_forms:
   ${ actions.action_form(action_form=form_info[1], node_type=form_info[0], template=True) }
 % endfor
 
-${ actions.import_jobsub_form(template=True) }
-
 ${ controls.fork_convert_form(node_type='fork', template=True, javascript_attrs={'convert': 'function(data, event) { $data.convertToDecision(); $data._workflow.rebuild(); }'}) }
 ${ controls.fork_edit_form(form=node_form, node_type='fork', template=True) }
 
@@ -433,11 +523,19 @@ ${ controls.decision_form(node_form, link_form, default_link_form, 'decision', T
  * Initialize the workflow, registry, modal, and import objects.
  */
  // Custom handlers for saving, loading, error checking, etc.
-function import_load_available_nodes_success(data) {
+function import_jobsub_load_success(data) {
   if (data.status == 0) {
-    import_node.initialize(data.data);
+    import_jobsub_action.initialize(data.data);
   } else {
-    $.jHueNotify.error("${ _('Received invalid response from server: ') }" + JSON.stringify(data));
+    $.jHueNotify.error("${ _('Received invalid response from server: ') } " + JSON.stringify(data));
+  }
+}
+
+function import_workflow_load_success(data) {
+  if (data.status == 0) {
+    import_workflow_action.initialize(data.data);
+  } else {
+    $.jHueNotify.error("${ _('Received invalid response from server: ') } " + JSON.stringify(data));
   }
 }
 
@@ -487,12 +585,16 @@ var workflow = new Workflow({
   model: workflow_model,
   registry: registry,
   read_only: ${ str(not user_can_edit_job).lower() },
-  read_only_error_handler: workflow_read_only_handler
+  read_only_error_handler: workflow_read_only_handler,
+  el: '#editWorkflow'
 });
-var import_node = new ImportNode({workflow: workflow});
+var import_jobsub_action = new ImportJobsubAction({workflow: workflow});
+var import_workflow_action = new ImportWorkflowAction({workflow: workflow});
 var modal = new Modal($('#node-modal'));
 
 // Load data.
+import_jobsub_action.fetchWorkflows({ success: import_jobsub_load_success });
+import_workflow_action.fetchWorkflows({ success: import_workflow_load_success });
 {
   var spinner = $('<img src="/static/art/spinner.gif" />');
   workflow.loading.subscribe(function(value) {
@@ -505,7 +607,6 @@ var modal = new Modal($('#node-modal'));
   workflow.loading(true);
   workflow.load({ success: workflow_load_success });
 }
-import_node.loadAvailableNodes({ success: import_load_available_nodes_success });
 
 /**
  * Modals
@@ -553,7 +654,7 @@ function edit_node_modal(node, save, cancel, template) {
 }
 
 // Drag a new node onto the canvas
-$('#workflow').on('mousedown', '.new-node-link', function(e) {
+workflow.el.on('mousedown', '.new-node-link', function(e) {
   e.preventDefault();
 
   // Node starts off graph, then is validated/dropped onto graph, after being dragged onto graph.
@@ -585,7 +686,7 @@ $('#workflow').on('mousedown', '.new-node-link', function(e) {
     node.erase();
     modal.hide();
     workflow.is_dirty( is_dirty );
-    $('#workflow').trigger('workflow:rebuild');
+    workflow.el.trigger('workflow:rebuild');
   };
 
   var try_save = function(e) {
@@ -593,7 +694,7 @@ $('#workflow').on('mousedown', '.new-node-link', function(e) {
       workflow.is_dirty( true );
       modal.hide();
       node.addChild(workflow.kill);
-      $('#workflow').trigger('workflow:rebuild');
+      workflow.el.trigger('workflow:rebuild');
     }
   };
 
@@ -610,19 +711,19 @@ $('#workflow').on('mousedown', '.new-node-link', function(e) {
 });
 
 // Modal for editing a node
-$('#workflow').on('click', '.edit-node-link', function(e) {
+workflow.el.on('click', '.edit-node-link', function(e) {
   var node = ko.contextFor(this).$data;
   edit_node_modal(node);
 });
 
 // Modal for converting to a decision node
-$('#workflow').on('click', '.convert-node-link', function(e) {
+workflow.el.on('click', '.convert-node-link', function(e) {
   var node = ko.contextFor(this).$data;
   edit_node_modal(node, null, null, 'forkConvertTemplate');
 });
 
 // Modal for cloning a node
-$('#workflow').on('click', '.clone-node-btn', function(e) {
+workflow.el.on('click', '.clone-node-btn', function(e) {
   var node = ko.contextFor(this).$data;
   var model = node.model.copy();
   model.id = IdGeneratorTable[model.node_type].nextId();
@@ -644,7 +745,7 @@ $('#workflow').on('click', '.clone-node-btn', function(e) {
       // save, add kill, add node to workflow.
       new_node.addChild(workflow.kill);
       node.append(new_node);
-      $('#workflow').trigger('workflow:rebuild');
+      workflow.el.trigger('workflow:rebuild');
     }
   };
 
@@ -652,7 +753,7 @@ $('#workflow').on('click', '.clone-node-btn', function(e) {
 });
 
 // Modal for deleting a node
-$('#workflow').on('click', '.delete-node-btn', function(e) {
+workflow.el.on('click', '.delete-node-btn', function(e) {
   var node = ko.contextFor(this).$data;
   $('#confirmation').find('h3').text('${ _('Confirm Delete') }');
   $('#confirmation').find('.modal-body').html('${ _('Are you sure you want to delete ') }<strong>' + node.name() + '</strong>?');
@@ -670,66 +771,88 @@ $('#workflow').on('click', '.delete-node-btn', function(e) {
   $('#confirmation').modal('show');
 });
 
-// Modal for importing a node
-$('#workflow').on('click', '.import-jobsub-node-link', function(e) {
-  var tempModelView = {
-    selected_node: ko.observable(),
-    available_nodes: ko.observableArray(import_node.getAvailableNodes()),
-    setJobDesignerId: function(data, event) {
-      tempModelView.selected_node(data);
-      return true;
-    }
-  };
-
-  modal.hide();
-  modal.setTemplate('ImportNodeTemplate');
-  modal.show({node: tempModelView, read_only: workflow.read_only()});
-  modal.recenter(280, 250);
-  modal.addDecorations();
+//// Import actions
+var import_view_model = {
+  jobsub: ko.observable(import_jobsub_action),
+  oozie: ko.observable(import_workflow_action),
+};
 
-  var cancel_edit = function() {
-    modal.hide();
+function importAction(workflow, model) {
+  model.id = IdGeneratorTable[model.node_type].nextId();
+  var node = new Node(workflow, model, workflow.registry);
+  workflow.registry.add(model.id, node);
 
-    // Prevent event propagation
-    return false;
-  };
+  // Add kill, add node to workflow as child of start.
+  workflow.is_dirty( true );
+  node.addChild(workflow.kill);
+  workflow.registry.get(workflow.start()).append(node);
+}
 
-  var try_save = function() {
-    import_node.convertNode({
+// Step 1 - Jobsub
+// Select and import action
+$('#importJobsub').on('click', '.action-row', function(e) {
+  // Check ID to make sure we are not hitting N/A
+  if ('id' in ko.contextFor($(this)[0]).$data) {
+    // Select workflow, then fetch jobsub workflow.
+    // Current KO context for clicked element should be the workflow.
+    import_view_model.jobsub().selected_workflow(ko.contextFor($(this)[0]).$data);
+    // Should only have 1 node since jobsub workflows are single node workflows.
+    import_view_model.jobsub().fetchNodes({
       success: function(data) {
-        var node = data.data.node;
-        var NodeModel = nodeModelChooser(node.node_type);
-        node.id = IdGeneratorTable[node.node_type].nextId();
-        node.child_links = [];
-        var model = new NodeModel(node);
-        var new_node = new Node(workflow, model, workflow.registry);
-        workflow.registry.add(new_node.id(), new_node);
-
-        // save, add kill, add node to workflow.
-        new_node.addChild(workflow.kill);
-        workflow.nodes()[workflow.nodes().length - 2].append(new_node);
-        workflow.is_dirty( true );
-        $('#workflow').trigger('workflow:rebuild');
-
-        $.jHueNotify.info("${ _('Imported Job Designer workflow as node.') }");
-        routie('editWorkflow');
-        $('html, body').animate({ scrollTop: $(document).height() });
-      },
-      error: function() {
-        $.jHueNotify.error("${ _('Could not import Job Designer workflow as node.') }");
+        if (data.status == 0) {
+          import_view_model.jobsub().initialize({nodes: data.data.actions});
+
+          // Remember, jobsub guarantees exactly one node.
+          importAction(workflow, import_view_model.jobsub().nodes()[0]);
+
+          workflow.el.trigger('workflow:rebuild');
+          routie('editWorkflow');
+          $.jHueNotify.success("${ _('Action imported at the top of the workflow.') } ");
+        } else {
+          $.jHueNotify.error("${ _('Received invalid response from server: ') } " + JSON.stringify(data));
+        }
       }
-    }, tempModelView.selected_node().id);
-    modal.hide();
-  };
-
-  $('.modal-backdrop').on('click', cancel_edit);
-  modal.el.on('click', '.close', cancel_edit);
-  modal.el.on('click', '.cancelButton', cancel_edit);
-  modal.el.on('click', '.doneButton', try_save);
+    });
+  }
 });
 
 
+// Step 1 - Oozie
+// Select workflow
+$('#importOozie').on('click', '.action-row', function(e) {
+  // Check ID to make sure we are not hitting N/A
+  if ('id' in ko.contextFor($(this)[0]).$data) {
+    // Select workflow, then fetch oozie workflow.
+    // Current KO context for clicked element should be the workflow.
+    import_view_model.oozie().selected_workflow(ko.contextFor($(this)[0]).$data);
+    import_view_model.oozie().fetchNodes({
+      success: function(data) {
+        if (data.status == 0) {
+          import_view_model.oozie().initialize({nodes: data.data.actions});
+          routie('importAction/oozie');
+        } else {
+          $.jHueNotify.error("${ _('Received invalid response from server: ') } " + JSON.stringify(data));
+        }
+      }
+    });
+  }
+});
+
+// Step 2 - Oozie
+// Select and import action
+$('#importOozieAction').on('click', '.action-row', function(e) {
+  // Check ID to make sure we are not hitting N/A
+  if ('id'  in ko.contextFor($(this)[0]).$data) {
+    // Current KO context for clicked element should be the action
+    importAction(workflow, ko.contextFor($(this)[0]).$data);
+
+    workflow.el.trigger('workflow:rebuild');
+    routie('editWorkflow');
+    $.jHueNotify.success("${ _('Action imported at the top of the workflow.') } ");
+  }
+});
 
+// Bindings
 ko.bindingHandlers.fileChooser = {
   init: function(element, valueAccessor, allBindings, model) {
     var self = $(element);
@@ -737,7 +860,12 @@ ko.bindingHandlers.fileChooser = {
   }
 };
 
-ko.applyBindings(workflow, $('#workflow')[0]);
+ko.applyBindings(workflow, workflow.el[0]);
+ko.applyBindings(workflow, $('#formActions')[0]);
+ko.applyBindings(workflow, $('#properties')[0]);
+ko.applyBindings(workflow, $('#workflowControls')[0]);
+ko.applyBindings(import_view_model, $('#importAction')[0]);
+ko.applyBindings(import_view_model.oozie(), $('#importOozieAction')[0]);
 
 window.onbeforeunload = function (e) {
   if (workflow.is_dirty()) {
@@ -816,7 +944,15 @@ $(document).ready(function () {
     'properties':function () {
       showSection('properties');
     },
-    'editWorkflow':function () {
+    'importAction':function() {
+      $("#importAction *[rel=tooltip]").tooltip();
+      showSection('importAction');
+    },
+    'importAction/oozie':function() {
+      $("#importOozieAction *[rel=tooltip]").tooltip();
+      showSection('importOozieAction');
+    },
+    'editWorkflow':function() {
       showSection('editWorkflow');
     },
     'listHistory':function () {
@@ -826,6 +962,9 @@ $(document).ready(function () {
 
   function highlightMenu(section) {
     $('.nav-list li').removeClass('active');
+    if (section == 'importOozieAction') {
+      section = 'importAction';
+    }
     $('a[href="#' + section + '"]:first').parent().addClass('active');
   }
 

+ 41 - 30
apps/oozie/src/oozie/tests.py

@@ -43,6 +43,7 @@ from oozie.models import Workflow, Node, Kill, Streaming, Link, Job, Coordinator
 from oozie.conf import SHARE_JOBS
 from oozie.utils import workflow_to_dict, model_to_dict, smart_path
 from oozie.import_workflow import import_workflow
+from oozie.import_jobsub import convert_jobsub_design
 
 
 LOG = logging.getLogger(__name__)
@@ -600,40 +601,33 @@ class TestAPI(OozieMockBase):
     assert_true('archives' in test_response_json_object['data'], test_response_json_object['data'])
     assert_equal(0, len(test_response_json_object['data']['archives']), test_response_json_object['data'])
 
-  def test_autocomplete(self):
-    response = self.c.get(reverse('oozie:autocomplete_properties'))
-    test_response_json = response.content
-    assert_true('mapred.input.dir' in test_response_json)
+  def test_workflows(self):
+    response = self.c.get(reverse('oozie:workflows') + "?managed=true", HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+    response_json_dict = json.loads(response.content)
+    assert_equal(0, response_json_dict['status'])
+    assert_equal(1, len(response_json_dict['data']['workflows']))
 
+    response = self.c.get(reverse('oozie:workflows') + "?managed=false", HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+    response_json_dict = json.loads(response.content)
+    assert_equal(0, response_json_dict['status'])
+    assert_equal(0, len(response_json_dict['data']['workflows']))
 
-class TestAPIWithOozie(OozieBase):
-  def setUp(self):
-    OozieBase.setUp(self)
-
-    # When updating wf, update wf_json as well!
-    self.wf = Workflow.objects.get(name='MapReduce', managed=True).clone(self.cluster.fs, self.user)
+  def test_workflow_actions(self):
+    response = self.c.get(reverse('oozie:workflow_actions', kwargs={'workflow': self.wf.pk}), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+    response_json_dict = json.loads(response.content)
+    assert_equal(0, response_json_dict['status'])
+    assert_equal(0, len(response_json_dict['data']['actions']))
 
-  def test_import_jobsub_actions(self):
-    # Setup jobsub examples
-    if not jobsub_setup.Command().has_been_setup():
-      jobsub_setup.Command().handle()
-
-    # There should be 3 from examples
-    jobsub_design = OozieDesign.objects.filter(root_action__action_type='streaming')[0]
-    response = self.c.post(reverse('oozie:workflow_jobsub_actions', kwargs={'workflow': self.wf.id}), data={'jobsub_id': jobsub_design.id}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
-    assert_equal(200, response.status_code)
-    response_dict = json.loads(response.content)
-    assert_equal(0, response_dict['status'], response)
-    assert_equal(jobsub_design.name, response_dict['data']['node']['name'], response)
-    assert_equal(jobsub_design.description, response_dict['data']['node']['description'], response)
-    assert_equal('streaming', response_dict['data']['node']['node_type'], response)
+    self.setup_simple_workflow()
+    response = self.c.get(reverse('oozie:workflow_actions', kwargs={'workflow': self.wf.pk}), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+    response_json_dict = json.loads(response.content)
+    assert_equal(0, response_json_dict['status'])
+    assert_equal(3, len(response_json_dict['data']['actions']))
 
-    # There should now be an imported action at the end of Node list
-    # Need to test properties to make sure we got it right
-    # Must also make sure that jobsub field values are translated
-    translation_regex = re.compile('(?<!\$)\$(\w+)')
-    for field in Streaming.PARAM_FIELDS:
-      assert_equal(translation_regex.sub(r'${\1}', getattr(jobsub_design.get_root_action(), field)), response_dict['data']['node'][field], response)
+  def test_autocomplete(self):
+    response = self.c.get(reverse('oozie:autocomplete_properties'))
+    test_response_json = response.content
+    assert_true('mapred.input.dir' in test_response_json)
 
 
 class TestApiPermissionsWithOozie(OozieBase):
@@ -2795,6 +2789,23 @@ class TestDashboard(OozieMockBase):
     assert_equal(Workflow.objects.available().count(), workflow_count)
 
 
+class GeneralTestsWithOozie(OozieBase):
+  def setUp(self):
+    OozieBase.setUp(self)
+
+  def test_import_jobsub_actions(self):
+    # Setup jobsub examples
+    if not jobsub_setup.Command().has_been_setup():
+      jobsub_setup.Command().handle()
+
+    # There should be 3 from examples
+    jobsub_design = OozieDesign.objects.filter(root_action__action_type='streaming')[0]
+    action = convert_jobsub_design(jobsub_design)
+    assert_equal(jobsub_design.name, action.name)
+    assert_equal(jobsub_design.description, action.description)
+    assert_equal('streaming', action.node_type)
+
+
 class TestUtils(OozieMockBase):
 
   def setUp(self):

+ 2 - 1
apps/oozie/src/oozie/urls.py

@@ -68,10 +68,11 @@ urlpatterns = patterns(
 urlpatterns += patterns(
   'oozie.views.api',
 
+  url(r'^workflows$', 'workflows', name='workflows'),
   url(r'^workflows/(?P<workflow>\d+)$', 'workflow', name='workflow'),
   url(r'^workflows/(?P<workflow>\d+)/save$', 'workflow_save', name='workflow_save'),
+  url(r'^workflows/(?P<workflow>\d+)/actions$', 'workflow_actions', name='workflow_actions'),
   url(r'^workflows/(?P<workflow>\d+)/nodes/(?P<node_type>\w+)/validate$', 'workflow_validate_node', name='workflow_validate_node'),
-  url(r'^workflows/(?P<workflow>\d+)/jobsub/actions$', 'workflow_jobsub_actions', name='workflow_jobsub_actions'),
   url(r'^workflows/autocomplete_properties/$', 'autocomplete_properties', name='autocomplete_properties'),
 )
 

+ 29 - 44
apps/oozie/src/oozie/views/api.py

@@ -26,11 +26,8 @@ from django.utils.translation import ugettext as _
 
 from desktop.lib.exceptions import StructuredException
 
-from jobsub.models import OozieDesign
-
-from oozie.forms import WorkflowForm, ImportJobsubDesignForm, NodeForm, design_form_by_type
-from oozie.import_jobsub import convert_jobsub_design
-from oozie.models import Workflow, Node, Start, End, Kill, Mapreduce, Java, Streaming,\
+from oozie.forms import WorkflowForm, NodeForm, design_form_by_type
+from oozie.models import Job, Workflow, Node, Start, End, Kill,\
                          Link, Decision, Fork, DecisionEnd, Join,\
                          NODE_TYPES, ACTION_TYPES, _STD_PROPERTIES_JSON
 from oozie.decorators import check_job_access_permission, check_job_edition_permission
@@ -244,6 +241,7 @@ def _workflow(request, workflow):
   response = {'status': -1, 'data': 'None'}
 
   workflow_dict = model_to_dict(workflow)
+
   node_list = [node.get_full_node() for node in workflow.node_list]
   nodes = [model_to_dict(node) for node in node_list]
 
@@ -335,47 +333,34 @@ def workflow(request, workflow):
 
 
 @check_job_access_permission(exception_class=(lambda x: StructuredException(code="UNAUTHORIZED_REQUEST_ERROR", message=x, data=None, error_code=401)))
-@check_job_edition_permission(exception_class=(lambda x: StructuredException(code="UNAUTHORIZED_REQUEST_ERROR", message=x, data=None, error_code=401)))
-def workflow_jobsub_actions(request, workflow):
-  if request.method not in ['GET', 'POST']:
-    raise StructuredException(code="METHOD_NOT_ALLOWED_ERROR", message=_('Must be GET or POST request.'), error_code=405)
-
-  available_actions = OozieDesign.objects.all()
-  if request.method == 'POST':
-    form = ImportJobsubDesignForm(data=request.POST, choices=[(action.id, action.name) for action in available_actions])
-    if form.is_valid():
-      try:
-        design = OozieDesign.objects.get(id=form.cleaned_data['jobsub_id'])
-        action = convert_jobsub_design(design)
-        action.workflow = workflow
-
-        response = {
-          'status': 0,
-          'data': {
-            'node': model_to_dict(action)
-          }
-        }
-        response['data']['node']['child_links'] = []
-        return HttpResponse(json.dumps(response), mimetype="application/json")
-      except OozieDesign.DoesNotExist, e:
-        raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Job Designer design does not exist.'), data={'exception': str(e)}, error_code=400)
-      except (Mapreduce.DoesNotExist, Streaming.DoesNotExist, Java.DoesNotExist), e:
-        raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Could not convert Job Designer design.'), data={'exception': str(e)}, error_code=400)
-      except Exception, e:
-        raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Error importing node from Job Designer'), data={'exception': str(e)}, error_code=400)
-    else:
-      raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Error importing node from Job Designer'), data={'errors': form.errors}, error_code=400)
+def workflow_actions(request, workflow):
+  if request.method != 'GET':
+    raise StructuredException(code="METHOD_NOT_ALLOWED_ERROR", message=_('Must be GET request.'), error_code=405)
 
-  else:
-    available_actions = OozieDesign.objects.all()
-    response = {
-      'status': 0,
-      'data': {
-        'nodes': [model_to_dict(action) for action in available_actions]
-      }
+  action_list = [action.get_full_node() for action in workflow.actions]
+  response = {
+    'status': 0,
+    'data': {
+      'actions': [model_to_dict(action) for action in action_list]
     }
-    return HttpResponse(json.dumps(response), mimetype="application/json")
+  }
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def workflows(request):
+  if request.method not in ['GET']:
+    raise StructuredException(code="METHOD_NOT_ALLOWED_ERROR", message=_('Must be GET request.'), error_code=405)
+
+  workflows = request.GET.get('managed', 'false').lower() == 'true' and Workflow.objects.managed() or Workflow.objects.unmanaged()
+  workflows_accessible = filter(lambda x: Job.objects.is_accessible(request.user, x.id), workflows)
+  response = {
+    'status': 0,
+    'data': {
+      'workflows': [model_to_dict(workflow) for workflow in workflows_accessible]
+    }
+  }
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
 
 def autocomplete_properties(request):
   return HttpResponse(_STD_PROPERTIES_JSON, mimetype="application/json")
-

+ 4 - 0
apps/oozie/static/css/workflow.css

@@ -148,4 +148,8 @@ ul {
 
 .typeahead {
   z-index: 2000;
+}
+
+#importAction .action-row, #importOozieAction .action-row {
+  cursor: pointer;
 }

+ 105 - 0
apps/oozie/static/js/workflow.import-node.js

@@ -0,0 +1,105 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * Import workflow action module
+ * Enables selection of workflows.
+ * Enables selection of actions within workflows.
+ */
+var ImportNodeModule = function($, managed) {
+  var module = function(options) {
+    var self = this;
+
+    self.nodes = ko.observableArray();
+    self.workflows = ko.observableArray();
+    self.selected_workflow = ko.observable();
+    self.nodes_url = ko.computed(function() {
+      return '/oozie/workflows/' + ((self.selected_workflow()) ? self.selected_workflow().id : 0) + '/actions';
+    });
+    self.workflows_url = ko.computed(function() {
+      if (managed) {
+        return '/oozie/workflows?managed=true';
+      } else {
+        return '/oozie/workflows?managed=false';
+      }
+    });
+
+    module.prototype.initialize.apply(self, arguments);
+
+    return self;
+  };
+
+  $.extend(module.prototype, {
+    initialize: function(options) {
+      var self = this;
+
+      var options = options || {};
+
+      if (options.workflows) {
+        self.workflows.removeAll();
+        $.each(options.workflows, function(index, workflow) {
+          self.workflows.push(new WorkflowModel(workflow));
+        });
+      }
+
+      if (options.nodes) {
+        self.nodes.removeAll();
+        $.each(options.nodes, function(index, node) {
+          self.nodes.push(new NodeModel(node));
+        });
+      }
+    },
+
+    getAvailableNodes: function() {
+      var self = this;
+
+      return self.available_nodes;
+    },
+
+    fetchWorkflows: function(options) {
+      var self = this;
+
+      var request = $.extend({
+        url: self.workflows_url(),
+        dataType: 'json',
+        type: 'GET',
+        success: $.noop,
+        error: $.noop
+      }, options || {});
+
+      $.ajax(request);
+    },
+
+    fetchNodes: function(options) {
+      var self = this;
+
+      var request = $.extend({
+        url: self.nodes_url(),
+        dataType: 'json',
+        type: 'GET',
+        success: $.noop,
+        error: $.noop
+      }, options || {});
+
+      $.ajax(request);
+    }
+  });
+
+  return module;
+};
+
+var ImportWorkflowAction = ImportNodeModule($, true);
+var ImportJobsubAction = ImportNodeModule($, false);

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

@@ -18,78 +18,6 @@ var Registry = RegistryModule($);
 
 var Modal = ModalModule($, ko);
 
-/**
- * Import node module
- * Assists in the conversion of a jobsub node.
- * Assists in the population of available nodes.
- */
-var ImportNodeModule = function($) {
-  var module = function(options) {
-    var self = this;
-
-    self.workflow = options.workflow;
-    self.available_nodes = [];
-    self.url = ko.computed(function() {
-      return '/oozie/workflows/' + options.workflow.id() + '/jobsub/actions'
-    });
-
-    module.prototype.initialize.apply(self, arguments);
-
-    return self;
-  };
-
-  $.extend(module.prototype, {
-    initialize: function(options) {
-      var self = this;
-
-      if (options.nodes) {
-        $.each(options.nodes, function(index, node) {
-          self.available_nodes.push(node);
-        });
-      }
-    },
-
-    getAvailableNodes: function() {
-      var self = this;
-
-      return self.available_nodes;
-    },
-
-    loadAvailableNodes: function(options) {
-      var self = this;
-
-      var request = $.extend({
-        url: self.url(),
-        dataType: 'json',
-        type: 'GET',
-        success: $.noop,
-        error: $.noop
-      }, options || {});
-
-      $.ajax(request);
-    },
-
-    convertNode: function(options, jobsub_id) {
-      var self = this;
-
-      var options = options || {};
-
-      var request = $.extend({
-        url: self.url(),
-        type: 'POST',
-        data: { 'jobsub_id': jobsub_id },
-        success: $.noop,
-        error: $.noop
-      }, options);
-
-      $.ajax(request);
-    }
-  });
-
-  return module;
-};
-var ImportNode = ImportNodeModule($);
-
 var Node = NodeModule($, IdGeneratorTable, NodeFields);
 
 var StartNode = NodeModule($, IdGeneratorTable, NodeFields);
@@ -434,7 +362,7 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
     self.model = options.model;
     self.registry = options.registry;
     self.options = options;
-    self.el = $('#workflow');
+    self.el = (options.el) ? $(options.el) : $('#workflow');
     self.nodes = ko.observableArray([]);
     self.kill = null;
     self.is_dirty = ko.observable( false );

+ 2 - 2
apps/oozie/static/js/workflow.registry.js

@@ -44,13 +44,13 @@ var RegistryModule = function($) {
 
     add: function(id, node) {
       var self = this;
-      $(self).trigger('registry:add');
+      $(self).trigger('registry.add');
       self.nodes[String(id)] = node;
     },
 
     remove: function(id) {
       var self = this;
-      $(self).trigger('registry:remove');
+      $(self).trigger('registry.remove');
       delete self.nodes[String(id)];
     },