浏览代码

HUE-940 [oozie] Revamp import JobDesigner action

With the improvements made to the workflow builder,
importing an action from the JobDesigner was removed in an effort
to reduce complexity.
abec 13 年之前
父节点
当前提交
e43c3c4ad6

+ 1 - 1
apps/oozie/src/oozie/forms.py

@@ -83,7 +83,7 @@ class ImportJobsubDesignForm(forms.Form):
   """Used for specifying what oozie actions to import"""
   def __init__(self, choices=[], *args, **kwargs):
     super(ImportJobsubDesignForm, self).__init__(*args, **kwargs)
-    self.fields['action_id'] = forms.ChoiceField(choices=choices, widget=forms.RadioSelect(attrs={'class':'radio'}))
+    self.fields['jobsub_id'] = forms.ChoiceField(choices=choices, widget=forms.RadioSelect(attrs={'class':'radio'}))
 
 
 class NodeForm(forms.ModelForm):

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

@@ -21,6 +21,75 @@
 <%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">
+    <form class="form-horizontal" id="import-node-form" method="POST">
+      <div class="modal-header">
+        <a href="#" class="close">&times;</a>
+        <h3 class="message">${_('Import Node')}</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" />
+                </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">${ _('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">

+ 74 - 0
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -71,6 +71,9 @@ ${ layout.menubar(section='workflows') }
               <li class="active">
                 <a href="#add" data-toggle="tab">${ _('Add') }</a>
               </li>
+              <li>
+                <a href="#import" data-toggle="tab">${ _('Import') }</a>
+              </li>
              </ul>
 
             <div class="tab-content">
@@ -154,6 +157,13 @@ ${ layout.menubar(section='workflows') }
                 </a>
                 <p/>
               </div>
+              <div class="tab-pane" id="import">
+                <p>
+                <a title="${ _('Click to add to the end of the workflow') }" class="btn import-jobsub-node-link">
+                  <i class="icon-plus"></i> ${ _('Import Job Designer Workflow') }
+                </a>
+                <p/>
+              </div>
             </div>
              % endif
           </div>
@@ -383,6 +393,8 @@ ${ layout.menubar(section='workflows') }
   ${ actions.action_form(action_form=form_info[1], node_type=form_info[0], template=True) }
 % endfor
 
+${ actions.import_jobsub_form(template=True) }
+
 ${ controls.fork_form('fork', True, javascript_attrs={'convert': 'function(data, event) { $data.convertToDecision(); $data._workflow.rebuild(); }'}) }
 
 ${ controls.decision_form(link_form, default_link_form, 'decision', True) }
@@ -710,6 +722,68 @@ $('#workflow').on('click', '.delete-node-btn', function(e) {
   workflow.is_dirty( true );
 });
 
+/**
+ * Import node
+ */
+var import_node = new ImportNode({workflow: workflow});
+import_node.loadAvailableNodes();
+
+$('#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(tempModelView);
+  modal.recenter(280, 250);
+  modal.addDecorations();
+
+  var cancel_edit = function() {
+    modal.hide();
+
+    // Prevent event propagation
+    return false;
+  };
+
+  var try_save = function() {
+    import_node.convertNode({
+      success: function(data) {
+        var node = data.data.node;
+        var NodeModel = nodeModelChooser(node.node_type);
+        node.id = IdGeneratorTable[node.node_type].nextId();
+        node.name = node.id;
+        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.') }");
+      },
+      error: function() {
+        $.jHueNotify.error("${ _('Could not import Job Designer workflow as node.') }");
+      }
+    }, 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);
+});
+
 
 
 ko.bindingHandlers.fileChooser = {

+ 31 - 25
apps/oozie/src/oozie/tests.py

@@ -38,7 +38,7 @@ from liboozie.oozie_api_test import OozieServerProvider
 from liboozie.types import WorkflowList, Workflow as OozieWorkflow, Coordinator as OozieCoordinator,\
   CoordinatorList, WorkflowAction
 
-from oozie.models import Workflow, Node, Kill, Link, Job, Coordinator, History,\
+from oozie.models import Workflow, Node, Kill, Streaming, Link, Job, Coordinator, History,\
   find_parameters, NODE_TYPES
 from oozie.conf import SHARE_JOBS
 from oozie.utils import workflow_to_dict, model_to_dict
@@ -419,6 +419,36 @@ class TestAPI(OozieMockBase):
     assert_equal(0, len(test_response_json_object['data']['archives']), test_response_json_object['data'])
 
 
+class TestAPIWithOozie(OozieBase):
+  def setUp(self):
+    OozieBase.setUp(self)
+
+    # When updating wf, update wf_json as well!
+    self.wf = Workflow.objects.get(name='MapReduce').clone(self.cluster.fs, self.user)
+
+  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.all()[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)
+
+    # 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)
+
+
 class TestApiPermissionsWithOozie(OozieBase):
 
   def setUp(self):
@@ -1433,30 +1463,6 @@ class TestEditorWithOozie(OozieBase):
     assert_not_equal('', wf2.deployment_dir)
 
 
-  def test_import_action(self):
-    raise SkipTest
-
-    # 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.all()[0]
-    node_size = len(Node.objects.all())
-    kwargs = dict(workflow=self.wf.id, parent_action_id=self.wf.end.get_parents()[0].id)
-    response = self.c.post(reverse('oozie:import_action', kwargs=kwargs), {'action_id': jobsub_design.id})
-    assert_equal(302, response.status_code)
-    assert_equal(node_size + 1, len(Node.objects.all()))
-
-    # 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+)')
-    node = Node.objects.all()[len(Node.objects.all())-1].get_full_node()
-    for field in node.PARAM_FIELDS:
-      assert_equal(translation_regex.sub(r'${\1}', getattr(jobsub_design.get_root_action(), field)), getattr(node, field))
-
-
   def test_import_workflow(self):
     workflow_count = Workflow.objects.count()
 

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

@@ -31,8 +31,6 @@ urlpatterns = patterns(
   url(r'^clone_workflow/(?P<workflow>\d+)$', 'clone_workflow', name='clone_workflow'),
   url(r'^submit_workflow/(?P<workflow>\d+)$', 'submit_workflow', name='submit_workflow'),
   url(r'^schedule_workflow/(?P<workflow>\d+)$', 'schedule_workflow', name='schedule_workflow'),
-
-  url(r'^import_action/(?P<workflow>\d+)/(?P<parent_action_id>\d+)$', 'import_action', name='import_action'),
   url(r'^import_workflow/$', 'import_workflow', name='import_workflow'),
 
   url(r'^list_coordinators/(?P<workflow_id>[-\w]+)?$', 'list_coordinators', name='list_coordinators'),
@@ -59,6 +57,7 @@ urlpatterns += patterns(
   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/(?P<node_type>\w+)/validate$', 'workflow_validate_action', name='workflow_validate_action'),
+  url(r'^workflows/(?P<workflow>\d+)/jobsub/actions$', 'workflow_jobsub_actions', name='workflow_jobsub_actions'),
 )
 
 

+ 16 - 7
apps/oozie/src/oozie/utils.py

@@ -15,19 +15,28 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import logging
+
+from django.utils.translation import ugettext as _
+
+
+LOG = logging.getLogger(__name__)
 
 def model_to_dict(model):
   from django.db import models
   from datetime import datetime
   dictionary = {}
   for field in model._meta.fields:
-    attr = getattr(model, field.name, None)
-    if isinstance(attr, models.Model):
-      dictionary[field.name] = attr.id
-    elif isinstance(attr, datetime):
-      dictionary[field.name] = str(attr)
-    else:
-      dictionary[field.name] = attr
+    try:
+      attr = getattr(model, field.name, None)
+      if isinstance(attr, models.Model):
+        dictionary[field.name] = attr.id
+      elif isinstance(attr, datetime):
+        dictionary[field.name] = str(attr)
+      else:
+        dictionary[field.name] = attr
+    except Exception, e:
+      LOG.debug(_("Could not set field %(field)s: %(exception)s") % {'field': field.name, 'exception': str(e)})
   return dictionary
 
 

+ 55 - 2
apps/oozie/src/oozie/views/api.py

@@ -26,8 +26,11 @@ from django.utils.translation import ugettext as _
 
 from desktop.lib.exceptions import StructuredException
 
-from oozie.forms import WorkflowForm, design_form_by_type
-from oozie.models import Workflow, Node, Link, NODE_TYPES, ACTION_TYPES
+from jobsub.models import OozieDesign
+
+from oozie.forms import WorkflowForm, ImportJobsubDesignForm, design_form_by_type
+from oozie.import_jobsub import convert_jobsub_design
+from oozie.models import Workflow, Node, Mapreduce, Java, Streaming, Link, NODE_TYPES, ACTION_TYPES
 from oozie.decorators import check_job_access_permission, check_job_edition_permission
 from oozie.utils import model_to_dict
 
@@ -172,6 +175,9 @@ def workflow_validate_action(request, workflow, node_type):
 @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_save(request, workflow):
+  if request.method != 'POST':
+    raise StructuredException(code="METHOD_NOT_ALLOWED_ERROR", message=_('Must be POST request.'), error_code=405)
+
   json_workflow = format_dict_field_values(json.loads(str(request.POST.get('workflow'))))
   json_workflow.setdefault('schema_version', workflow.schema_version)
 
@@ -234,4 +240,51 @@ def _workflow(request, workflow):
 
 @check_job_access_permission(exception_class=(lambda x: StructuredException(code="UNAUTHORIZED_REQUEST_ERROR", message=x, data=None, error_code=401)))
 def workflow(request, workflow):
+  if request.method != 'GET':
+    raise StructuredException(code="METHOD_NOT_ALLOWED_ERROR", message=_('Must be GET request.'), error_code=405)
+
   return _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)
+
+  else:
+    available_actions = OozieDesign.objects.all()
+    response = {
+      'status': 0,
+      'data': {
+        'nodes': [model_to_dict(action) for action in available_actions]
+      }
+    }
+    return HttpResponse(json.dumps(response), mimetype="application/json")
+

+ 34 - 67
apps/oozie/src/oozie/views/editor.py

@@ -43,7 +43,7 @@ from oozie.decorators import check_job_access_permission, check_job_edition_perm
 from oozie.import_workflow import import_workflow as _import_workflow
 from oozie.import_jobsub import convert_jobsub_design
 from oozie.management.commands import oozie_setup
-from oozie.models import Job, Workflow, History, Coordinator,Mapreduce, Java, Streaming,\
+from oozie.models import Job, Workflow, History, Coordinator, Mapreduce, Java, Streaming,\
                          Dataset, DataInput, DataOutput, ACTION_TYPES
 from oozie.forms import WorkflowForm, CoordinatorForm, DatasetForm,\
   DataInputForm, DataInputSetForm, DataOutputForm, DataOutputSetForm, LinkForm,\
@@ -114,12 +114,43 @@ def create_workflow(request):
   })
 
 
+def import_workflow(request):
+  workflow = Workflow.objects.new_workflow(request.user)
+
+  if request.method == 'POST':
+    workflow_form = ImportWorkflowForm(request.POST, request.FILES, instance=workflow)
+
+    if workflow_form.is_valid():
+      workflow.save()
+
+      workflow_definition = workflow_form.cleaned_data['definition_file'].read()
+      schema_version = workflow_form.cleaned_data['schema_version']
+
+      try:
+        _import_workflow(workflow=workflow, workflow_definition=workflow_definition, schema_version=schema_version)
+        request.info(_('Workflow imported'))
+        return redirect(reverse('oozie:edit_workflow', kwargs={'workflow': workflow.id}))
+
+      except Exception, e:
+        request.error(_('Could not import workflow: %s' % e))
+        Workflow.objects.destroy(workflow, request.fs)
+        raise PopupException(_('Could not import workflow.'), detail=e)
+
+    else:
+      request.error(_('Errors on the form: %s') % workflow_form.errors)
+
+  else:
+    workflow_form = ImportWorkflowForm(instance=workflow)
+
+  return render('editor/import_workflow.mako', request, {
+    'workflow_form': workflow_form,
+    'workflow': workflow,
+  })
+
 @check_job_access_permission()
 def edit_workflow(request, workflow):
   history = History.objects.filter(submitter=request.user, job=workflow).order_by('-submission_date')
-
   workflow_form = WorkflowForm(instance=workflow)
-
   user_can_edit_job = workflow.is_editable(request.user)
 
   return render('editor/edit_workflow.mako', request, {
@@ -213,36 +244,6 @@ def schedule_workflow(request, workflow):
     return create_coordinator(request, workflow=workflow.id)
 
 
-@check_job_access_permission()
-def import_action(request, workflow, parent_action_id):
-  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['action_id'])
-        action = convert_jobsub_design(design)
-        action.workflow = workflow
-        action.save()
-
-        workflow.add_action(action, parent_action_id)
-      except OozieDesign.DoesNotExist:
-        request.error(_('Jobsub design doesn\'t exist.'))
-      except (Mapreduce.DoesNotExist, Streaming.DoesNotExist, Java.DoesNotExist):
-        request.error(_('Could not convert jobsub design'))
-      except:
-        request.error(_('Could not convert jobsub design or add action to workflow'))
-
-    return redirect(reverse('oozie:edit_workflow', kwargs={'workflow': action.workflow.id}))
-
-  return render('editor/import_workflow_action.mako', request, {
-    'workflow': workflow,
-    'available_actions': available_actions,
-    'form_url': reverse('oozie:import_action', kwargs={'workflow': workflow.id, 'parent_action_id': parent_action_id}),
-  })
-
-
 @check_job_access_permission()
 def create_coordinator(request, workflow=None):
   if workflow is not None:
@@ -545,39 +546,5 @@ def setup_app(request):
   return redirect(reverse('oozie:list_workflows'))
 
 
-def import_workflow(request):
-  workflow = Workflow.objects.new_workflow(request.user)
-
-  if request.method == 'POST':
-    workflow_form = ImportWorkflowForm(request.POST, request.FILES, instance=workflow)
-
-    if workflow_form.is_valid():
-      workflow.save()
-
-      workflow_definition = workflow_form.cleaned_data['definition_file'].read()
-      schema_version = workflow_form.cleaned_data['schema_version']
-
-      try:
-        _import_workflow(workflow=workflow, workflow_definition=workflow_definition, schema_version=schema_version)
-        request.info(_('Workflow imported'))
-        return redirect(reverse('oozie:edit_workflow', kwargs={'workflow': workflow.id}))
-
-      except Exception, e:
-        request.error(_('Could not import workflow: %s' % e))
-        Workflow.objects.destroy(workflow, request.fs)
-        raise PopupException(_('Could not import workflow.'), detail=e)
-
-    else:
-      request.error(_('Errors on the form: %s') % workflow_form.errors)
-
-  else:
-    workflow_form = ImportWorkflowForm(instance=workflow)
-
-  return render('editor/import_workflow.mako', request, {
-    'workflow_form': workflow_form,
-    'workflow': workflow,
-  })
-
-
 def jasmine(request):
   return render('editor/jasmine.mako', request, None)

+ 91 - 19
apps/oozie/static/js/workflow.js

@@ -187,25 +187,6 @@ var ModalModule = function($, ko) {
 };
 var Modal = ModalModule($, ko);
 
-/**
- * ID Generator
- * Generate a new ID starting from 1.
- * - Accepts a prefix that will be prepended like so: <prefix>:<id number>
- */
-var IdGeneratorModule = function($) {
-  return function(options) {
-    var self = this;
-    $.extend(self, options);
-
-    self.counter = 1;
-
-    self.nextId = function() {
-      return ((self.prefix) ? self.prefix + ':' : '') + self.counter++;
-    };
-  };
-};
-var IdGenerator = IdGeneratorModule($);
-
 var ModelModule = function($) {
   var module = function(attrs) {
     var self = this;
@@ -511,6 +492,97 @@ function nodeModelChooser(node_type) {
   }
 }
 
+
+/**
+ * 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() {
+      var self = this;
+
+      $.getJSON(self.url(), function(data) {
+        if (data.status == 0) {
+          self.initialize(data.data);
+        } else {
+          $.jHueNotify.error("${ _('Received invalid response from server: ') }" + JSON.stringify(data));
+        }
+      });
+    },
+
+    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($);
+
+
+/**
+ * ID Generator
+ * Generate a new ID starting from 1.
+ * - Accepts a prefix that will be prepended like so: <prefix>:<id number>
+ */
+var IdGeneratorModule = function($) {
+  return function(options) {
+    var self = this;
+    $.extend(self, options);
+
+    self.counter = 1;
+
+    self.nextId = function() {
+      return ((self.prefix) ? self.prefix + ':' : '') + self.counter++;
+    };
+  };
+};
+var IdGenerator = IdGeneratorModule($);
+
 var IdGeneratorTable = {
   mapreduce: new IdGenerator({prefix: 'mapreduce'}),
   streaming: new IdGenerator({prefix: 'streaming'}),