Explorar o código

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 %!s(int64=13) %!d(string=hai) anos
pai
achega
e43c3c4

+ 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'}),