Эх сурвалжийг харах

[spark] Core of the notebook model

Works with HiveServer2
Romain Rigaux 11 жил өмнө
parent
commit
eedff0170c

+ 1 - 1
apps/beeswax/src/beeswax/models.py

@@ -409,7 +409,7 @@ class Session(models.Model):
 
 
 class QueryHandle(object):
-  def __init__(self, secret, guid=None, operation_type=None, has_result_set=None, modified_row_count=None, log_context=None):
+  def __init__(self, secret=None, guid=None, operation_type=None, has_result_set=None, modified_row_count=None, log_context=None):
     self.secret = secret
     self.guid = guid
     self.operation_type = operation_type

+ 199 - 53
apps/spark/src/spark/api.py

@@ -23,9 +23,14 @@ from django.utils.translation import ugettext as _
 
 from desktop.context_processors import get_app_name
 from desktop.lib.exceptions import StructuredException
+from desktop.lib.exceptions_renderable import PopupException
+from desktop.lib.i18n import force_unicode
 
 from beeswax import models as beeswax_models
+from beeswax.design import hql_query
+from beeswax.models import QUERY_TYPES, HiveServerQueryHandle, QueryHistory
 from beeswax.views import safe_get_design, save_design
+from beeswax.server import dbms
 
 from spark.job_server_api import get_api
 from spark.forms import SparkForm, QueryForm
@@ -35,8 +40,149 @@ from desktop.lib.rest.http_client import RestException
 
 from spark.decorators import json_error_handler
 
+
 LOG = logging.getLogger(__name__)
 
+
+class HS2Api():
+  
+  def __init__(self, user):
+    self.user = user
+  
+  def execute(self, notebook, snippet):
+    db = dbms.get(self.user)
+    query = hql_query(snippet['statement'], QUERY_TYPES[0])
+    handle = db.client.query(query)
+    
+#    if not handle.is_valid():
+#        msg = _("Server returning invalid handle for query id %(id)d [%(query)s]...") % {'id': query_history.id, 'query': query[:40]}
+#        raise QueryServerException(msg)
+#    except QueryServerException, ex:
+#      LOG.exception(ex)
+#      # Kind of expected (hql compile/syntax error, etc.)
+#      if hasattr(ex, 'handle') and ex.handle:
+#        query_history.server_id, query_history.server_guid = ex.handle.id, ex.handle.id
+#        query_history.log_context = ex.handle.log_context
+#      query_history.save_state(QueryHistory.STATE.failed)
+#      raise ex
+
+    # All good
+    server_id, server_guid  = handle.get()
+    return {
+        'secret': server_id,
+        'guid': server_guid,
+        'operation_type': handle.operation_type,
+        'has_result_set': handle.has_result_set,
+        'modified_row_count': handle.modified_row_count,
+        'log_context': handle.log_context
+    }    
+
+  def check_status(self, notebook, snippet):
+    db = dbms.get(self.user)
+      
+    snippet['result']['handle']['secret'], snippet['result']['handle']['guid'] = HiveServerQueryHandle.get_decoded(snippet['result']['handle']['secret'], snippet['result']['handle']['guid'])
+    handle = HiveServerQueryHandle(**snippet['result']['handle'])
+    status =  db.get_state(handle)
+    return {'query_status': 'running' if status in (QueryHistory.STATE.running.index, QueryHistory.STATE.submitted.index) else 'finished'}
+
+  def fetch_result(self, notebook, snippet):
+    db = dbms.get(self.user)
+      
+    snippet['result']['handle']['secret'], snippet['result']['handle']['guid'] = HiveServerQueryHandle.get_decoded(snippet['result']['handle']['secret'], snippet['result']['handle']['guid'])
+    handle = HiveServerQueryHandle(**snippet['result']['handle'])
+    results = db.fetch(handle, start_over=False, rows=10)
+    
+    # no escaping...
+    return {
+        'data': list(results.rows()),
+        'meta': [{
+          'name': column.name,
+          'type': column.type,
+          'comment': column.comment
+        } for column in results.data_table.cols()]
+    }
+
+  def fetch_result_metadata(self):
+    pass 
+
+  def cancel(self):
+    pass
+
+  def get_log(self):
+    pass
+  
+  def progress(self):
+    pass  
+
+
+class SparkApi(): # Pig, DBquery, Phoenix... 
+  
+  def __init__(self, user):
+    pass
+  
+  def execute(self):
+    pass
+
+  def check_status(self):
+    pass
+
+  def fetch_result(self):
+    pass
+
+  def cancel(self):
+    pass
+
+
+def execute(request):
+  response = {'status': -1}
+
+  notebook = json.loads(request.POST.get('notebook', '{}'))
+  snippet = json.loads(request.POST.get('snippet', '{}'))
+
+  try:
+    response['handle'] = HS2Api(request.user).execute(notebook, snippet)
+    response['status'] = 0
+  except Exception, e:
+    raise PopupException(e, title=_('Error while accessing query server'))
+    response['error'] = force_unicode(str(e))
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def check_status(request):
+  response = {'status': -1}
+
+  notebook = json.loads(request.POST.get('notebook', '{}'))
+  snippet = json.loads(request.POST.get('snippet', '{}'))
+
+  try:
+    response['query_status'] = HS2Api(request.user).check_status(notebook, snippet)
+    response['status'] = 0
+  except Exception, e:
+    raise PopupException(e, title=_('Error while accessing query server'))
+    response['error'] = force_unicode(str(e))
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def fetch_result(request):
+  response = {'status': -1}
+
+  notebook = json.loads(request.POST.get('notebook', '{}'))
+  snippet = json.loads(request.POST.get('snippet', '{}'))
+
+  try:
+    response['result'] = HS2Api(request.user).fetch_result(notebook, snippet)
+    response['status'] = 0
+  except Exception, e:
+    raise PopupException(e, title=_('Error while accessing query server'))
+    response['error'] = force_unicode(str(e))
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+
+
 @json_error_handler
 def jars(request):
   api = get_api(request.user)
@@ -112,59 +258,59 @@ def job(request, job_id):
 
 
 
-@json_error_handler
-def execute(request, design_id=None):
-  response = {'status': -1, 'message': ''}
-
-  if request.method != 'POST':
-    response['message'] = _('A POST request is required.')
-
-  app_name = get_app_name(request)
-  query_type = beeswax_models.SavedQuery.TYPES_MAPPING[app_name]
-  design = safe_get_design(request, query_type, design_id)
-
-  try:
-    form = get_query_form(request)
-
-    if form.is_valid():
-      #design = save_design(request, SaveForm(), form, query_type, design)
-
-#      query = SQLdesign(form, query_type=query_type)
-#      query_server = dbms.get_query_server_config(request.POST.get('server'))
-#      db = dbms.get(request.user, query_server)
-#      query_history = db.execute_query(query, design)
-#      query_history.last_state = beeswax_models.QueryHistory.STATE.expired.index
-#      query_history.save()
-
-      params = '\n'.join(['%(name)s=%(value)s' % param for param in json.loads(form.cleaned_data['params'])])
-
-      try:
-        api = get_api(request.user)
-
-        results = api.submit_job(
-            form.cleaned_data['appName'],
-            form.cleaned_data['classPath'],
-            data=params,
-            context=None if form.cleaned_data['autoContext'] else form.cleaned_data['context'],
-            sync=False
-        )
-
-        if results['status'] == 'STARTED':
-          response['status'] = 0
-          response['results'] = results
-        else:
-          response['message'] = str(results[1]['result'])
-        response['design'] = design.id
-      except Exception, e:
-        response['message'] = str(e)
-
-    else:
-      response['message'] = _('There was an error with your query: %s' % form.errors)
-      response['errors'] = form.errors
-  except RuntimeError, e:
-    response['message']= str(e)
-
-  return HttpResponse(json.dumps(response), mimetype="application/json")
+#@json_error_handler
+#def execute(request, design_id=None):
+#  response = {'status': -1, 'message': ''}
+#
+#  if request.method != 'POST':
+#    response['message'] = _('A POST request is required.')
+#
+#  app_name = get_app_name(request)
+#  query_type = beeswax_models.SavedQuery.TYPES_MAPPING[app_name]
+#  design = safe_get_design(request, query_type, design_id)
+#
+#  try:
+#    form = get_query_form(request)
+#
+#    if form.is_valid():
+#      #design = save_design(request, SaveForm(), form, query_type, design)
+#
+##      query = SQLdesign(form, query_type=query_type)
+##      query_server = dbms.get_query_server_config(request.POST.get('server'))
+##      db = dbms.get(request.user, query_server)
+##      query_history = db.execute_query(query, design)
+##      query_history.last_state = beeswax_models.QueryHistory.STATE.expired.index
+##      query_history.save()
+#
+#      params = '\n'.join(['%(name)s=%(value)s' % param for param in json.loads(form.cleaned_data['params'])])
+#
+#      try:
+#        api = get_api(request.user)
+#
+#        results = api.submit_job(
+#            form.cleaned_data['appName'],
+#            form.cleaned_data['classPath'],
+#            data=params,
+#            context=None if form.cleaned_data['autoContext'] else form.cleaned_data['context'],
+#            sync=False
+#        )
+#
+#        if results['status'] == 'STARTED':
+#          response['status'] = 0
+#          response['results'] = results
+#        else:
+#          response['message'] = str(results[1]['result'])
+#        response['design'] = design.id
+#      except Exception, e:
+#        response['message'] = str(e)
+#
+#    else:
+#      response['message'] = _('There was an error with your query: %s' % form.errors)
+#      response['errors'] = form.errors
+#  except RuntimeError, e:
+#    response['message']= str(e)
+#
+#  return HttpResponse(json.dumps(response), mimetype="application/json")
 
 
 @json_error_handler

+ 50 - 0
apps/spark/src/spark/templates/editor.mako

@@ -30,6 +30,15 @@ ${ common.navbar('editor') }
     <div class="span12">
 
       <div class="card card-home">
+      
+        <li>
+          <ul data-bind="template: { name: 'notebook', foreach: notebooks }"></ul>
+          
+           <a href="javascript: void(0)" data-bind="click: newNotebook">
+             <i class="fa fa-plus" title="${ _('Add') }"></i>
+           </a>          
+        </li>
+      
         <div id="snippets"></div>
 
         <div class="question">
@@ -48,6 +57,36 @@ ${ common.navbar('editor') }
   </div>
 </div>
 
+
+<script type="text/html" id="notebook">
+  <strong data-bind="text: id"></strong>
+  <li>
+    <ul data-bind="template: { name: 'snippet', foreach: snippets }"></ul>
+  </li>
+  
+  <a href="javascript: void(0)" data-bind="click: newSnippet">
+    <i class="fa fa-plus" title="${ _('Add') }"></i>
+  </a>
+</script>
+
+
+<script type="text/html" id="snippet">
+  <strong data-bind="text: id"></strong>
+  <strong data-bind="text: type"></strong>
+  <textarea data-bind="value: statement"></textarea>
+  
+  <a href="javascript: void(0)" data-bind="click: execute">
+    <i class="fa fa-play" title="${ _('Go') }"></i>
+  </a>
+  
+  <strong data-bind="text: ko.mapping.toJSON(result.meta)"></strong>
+  <li data-bind="foreach: result.data">
+    <ul data-bind="text: ko.mapping.toJSON($data)"></ul>
+  </li>  
+</script>
+
+
+
 <textarea id="tempEditor"></textarea>
 
 <link rel="stylesheet" href="/static/ext/css/codemirror.css">
@@ -60,8 +99,19 @@ ${ common.navbar('editor') }
 <script src="/static/ext/js/codemirror-markdown.js"></script>
 <script src="/static/ext/js/markdown.min.js"></script>
 
+
+<script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
+<script src="/spark/static/js/spark.vm.js" type="text/javascript" charset="utf-8"></script>
+
+
 <script type="text/javascript" charset="utf-8">
 
+  viewModel = new EditorViewModel(${ notebooks_json | n,unicode });
+  ko.applyBindings(viewModel);
+  viewModel.init();
+
+
   var mainCodeMirror, tempCodeMirror, tempCodeMirrorUpdateFn;
 
 

+ 4 - 1
apps/spark/src/spark/urls.py

@@ -36,8 +36,11 @@ urlpatterns = patterns('spark.views',
 
 # APIs
 urlpatterns += patterns('spark.api',
-  url(r'^api/jars$', 'jars', name='jars'),
   url(r'^api/execute$', 'execute', name='execute'),
+  url(r'^api/check_status', 'check_status', name='check_status'),
+  url(r'^api/fetch_result$', 'fetch_result', name='fetch_result'),
+                        
+  url(r'^api/jars$', 'jars', name='jars'),  
   url(r'^api/contexts$', 'contexts', name='contexts'),
   url(r'^api/job/(?P<job_id>.+)$', 'job', name='job'),
   url(r'^api/create_context$', 'create_context', name='create_context'),

+ 3 - 1
apps/spark/src/spark/views.py

@@ -42,7 +42,9 @@ LOG = logging.getLogger(__name__)
 
 @view_error_handler
 def editor(request):
-  return render('editor.mako', request, {})
+  return render('editor.mako', request, {
+      'notebooks_json': json.dumps([{'snippets': [{'type': 'table', 'result': {}}]}])
+  })
 
 @view_error_handler
 def list_jobs(request):

+ 140 - 335
apps/spark/static/js/spark.vm.js

@@ -14,371 +14,176 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-var SparkParameter = function (property) {
-  var self = this;
+function s4() {
+  return Math.floor((1 + Math.random()) * 0x10000)
+      .toString(16)
+      .substring(1);
+}
 
-  self.name = ko.observable(property.name);
-  self.value = ko.observable(property.value);
-};
+function UUID() {
+  return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
+}
 
-function sparkViewModel() {
-  var self = this;
 
-  self.appNames = ko.observableArray(); // List of jars
-  self.selectedAppName = ko.observable(0);
-  self.autoContext = ko.observable(true);
-  self.contexts = ko.observableArray(); // List of contexts
-  self.selectedContext = ko.observable(0);
-  self.classPath = ko.observable('');
+var Result = function (snippet, result) {
+  var self = this;
 
-  self.autoContext.forEditing = ko.computed({
-    read: function() {
-        return this.autoContext().toString();
-    },
-    write: function(newValue) {
-         this.autoContext(newValue === "true");
-    },
-    owner: this
-  });
+  self.id = ko.observable(typeof result != "undefined" && result.id != "undefined" && result.id != null ? result.id : UUID());
+  self.type = ko.observable('table');
+  self.handle = ko.observable({});
+  self.meta = ko.observableArray();
+  self.data = ko.observableArray();
+  
+  self.clear = function() {
+	//self.handle = ko.observable({});
+    self.meta.removeAll();
+    self.data.removeAll();
+  };  
+}
 
-  self.query = ko.mapping.fromJS({
-    'id': -1,
-    'jobId': null,
-    'name': null,
-    'description': null,
-    'errors': [],
-    'appName': '',
-    'classPath': '',
-    'context': '',
-    'autoContext': true,
-    'params': []
-  });
 
-  self.rows = ko.observableArray();
-  self.resultsEmpty = ko.observable(false);
+var Snippet = function (notebook, snippet) {
+  var self = this;
+  
+  self.id = ko.observable(typeof snippet.id != "undefined" && snippet.id != null ? snippet.id : UUID());
+  self.type = ko.observable('hive-sql');
+  self.statement = ko.observable('');
+  self.status = ko.observable('finished');
+  
+  self.result = new Result(snippet, snippet.result);
+  
+  // init()
+  // checkStatus()
+  
+  
+  self.execute = function() {
+	$(".jHueNotify").hide();
+	logGA('/execute/' + self.type());	  
+    
+	self.result.clear();
+    
+    $.post("/spark/api/execute", {
+        notebook: ko.mapping.toJSON(notebook),
+        snippet: ko.mapping.toJSON(self)
+      }, function (data) {
+        if (data.status == 0) {
+          $.each(data.handle, function(key, val) {
+        	 self.result.handle()[key] = val;
+          });
 
-  self.appName = ko.computed({
-    'read': function() {
-      if (self.appNames().length > 0) {
-        return self.appNames()[self.selectedAppName()];
-      } else {
-        return null;
-      }
-    },
-    'write': function(value) {
-      var filtered = $.each(self.appNames(), function(index, appName) {
-        if (appName.name() == value) {
-          self.selectedAppName(index);
+          self.status('running');
+          self.checkStatus();
         }
-      });
-    }
-  });
-
-  self.context = ko.computed({
-    'read': function() {
-      if (self.contexts().length > 0) {
-        return self.contexts()[self.selectedContext()];
-      } else {
-        return null;
-      }
-    },
-    'write': function(value) {
-      var filtered = $.each(self.contexts(), function(index, context) {
-        if (context.name() == value) {
-          self.selectedContext(index);
+        else {
+          $(document).trigger("error", data.message);
         }
-      });
-    }
-  });
-
-  self.updateResults = function(results) {
-    self.rows.removeAll();
-    var newRows = [];
-    // Is a list of map
-    if ($.inArray($.type(results), ['array', 'object']) != -1) {
-      $.each(results, function(key, value) {
-        newRows.push([key, value]);
-      });
-    } else {
-      newRows.push([0, results]);
-    }
-    self.rows(newRows);
-  };
-
-  self.updateAppNames = function(appNames) {
-    var newAppNames = [];
-    $.each(appNames, function(key, value) {
-      newAppNames.push({
-        'name': ko.observable(key),
-        'nice_name': ko.observable(key)
-      });
+    }).fail(function (xhr, textStatus, errorThrown) {
+      $(document).trigger("error", xhr.responseText);
+    });    
+  };
+
+  self.checkStatus = function() {
+    $.post("/spark/api/check_status", {
+       notebook: ko.mapping.toJSON(notebook),
+       snippet: ko.mapping.toJSON(self)
+	  }, function (data) {
+	    if (data.status == 0) {
+          self.status(data.query_status);
+            
+          if (self.status() == 'running') {
+            setTimeout(self.checkStatus, 1000);            	
+          } else {
+        	self.fetchResult();
+          }
+	    }
+	    else {
+	      $(document).trigger("error", data.message);
+	    }
+	}).fail(function (xhr, textStatus, errorThrown) {
+     $(document).trigger("error", xhr.responseText);
     });
-    self.appNames(newAppNames);
-
-    // Load back appName or guess
-    if (self.query.appName()) {
-      viewModel.setAppName( self.query.appName());
-    }
   };
 
-  self.updateContexts = function(contexts) {
-    var newContexts = [];
-    $.each(contexts, function(index, value) {
-      newContexts.push(createDropdownItem(value));
-    });
-    self.contexts(newContexts);
+  self.fetchResult = function() {
+    $.post("/spark/api/fetch_result", {
+        notebook: ko.mapping.toJSON(notebook),
+        snippet: ko.mapping.toJSON(self)
+ 	  }, function (data) {
+ 	    if (data.status == 0) {
+ 	      self.result.meta(data.result.meta);           
+          self.result.data(data.result.data);
 
-    var last = newContexts.length > 0 ? newContexts[0].name() : null;
-    if (last) {
-      self.context(last);
-    }
+          // move resultsets to n rows
+          // check if N rows fetched...
+ 	    }
+ 	    else {
+ 	      $(document).trigger("error", data.message);
+ 	    }
+ 	}).fail(function (xhr, textStatus, errorThrown) {
+      $(document).trigger("error", xhr.responseText);
+     });
   };
 
-  self.addParam = function() {
-    self.query.params.push(new SparkParameter({name: "", value: ""}));
-  };
+  self.fetchResultMetadata = function() {
+	  
+  }
+  
+  self.cancel = function() {
 
-  self.removeParam = function() {
-    self.query.params.remove(this);
   };
+}
 
-  function createDropdownItem(item) {
-    return {
-      'name': ko.observable(item),
-      'nice_name': ko.observable(item)
-    };
-  };
 
-  self.loadDesign = function(design) {
-    self.query.id(design.id);
-    self.query.name(design.name);
-    self.query.description(design.desc);
 
-    self.query.appName(design.appName);
-    self.query.classPath(design.classPath);
-    self.query.autoContext(design.autoContext);
-    self.query.params(design.params);
+var Notebook = function (vm, notebook) {
+  var self = this;
 
-    self.appName(design.appName);
-    self.chooseAppName(self.appName());
-    self.autoContext(design.autoContext);
-    self.context(design.context);
-    self.chooseContext(design.context);
-    self.classPath(design.classPath);
-  };
+  self.id = ko.observable(typeof notebook.id != "undefined" && notebook.id != null ? notebook.id : UUID());
+  self.snippets = ko.observableArray();
 
-  self.chooseAppName = function(value, e) {
-    $.each(self.appNames(), function(index, appName) {
-      if (appName.name() == value.name()) {
-        self.selectedAppName(index);
-      }
-    });
-  };
-
-  self.setAppName = function(name) {
-    $.each(self.appNames(), function(index, appName) {
-      if (appName.name() == name) {
-        self.appName(name);
-        self.selectedAppName(index);
-      }
-    });
-  };
+  self.addSnippet = function(snippet) {
+	self.snippets.push(new Snippet(self, snippet));
+  }  
 
-  self.chooseContext = function(value, e) {
-    $.each(self.contexts(), function(index, context) {
-      if (context.name() == value.name()) {
-        self.selectedContext(index);
-      }
+  self.newSnippet = function() {
+    self.snippets.push(new Snippet(self, {}));
+  }  
+  
+  if (notebook.snippets) {
+    $.each(notebook.snippets, function(index, snippet) {
+      self.addSnippet(snippet);
     });
-  };
-
-  var error_fn = function(jqXHR, status, errorThrown) {
-    try {
-      $(document).trigger('server.error', $.parseJSON(jqXHR.responseText));
-    } catch(e) {
-      $(document).trigger('server.unmanageable_error', jqXHR.responseText);
-    }
-  };
-
-  self.saveQuery = function() {
-    var self = this;
-    if (self.query.name()) {
-      var data = ko.mapping.toJS(self.query);
-      data['saveform-name'] = data['name'];
-      data['saveform-desc'] = data['description'];
-      data['query-appName'] = self.appName().name;
-      data['query-classPath'] = self.classPath();
-      data['query-autoContext'] = self.autoContext();
-      data['query-context'] = self.context() ? self.context().name : '';
-      data['query-params'] = ko.toJSON(self.query.params());
+  }
+}
 
-      var url = '/spark/api/save_query/';
-      if (self.query.id() && self.query.id() != -1) {
-        url += self.query.id() + '/';
-      }
-      var request = {
-        url: url,
-        dataType: 'json',
-        type: 'POST',
-        success: function(data) {
-          if (data.status == 0) {
-            if (self.query.id() == -1) {
-              self.query.id(data.design_id);
-              $(document).trigger('savedas.query', data);
-            } else {
-              $(document).trigger('saved.query', data);
-            }
-          } else {
-            self.query.errors.push(data.message);
-          }
-        },
-        error: function() {
-          $(document).trigger('error.query');
-        },
-        data: data
-      };
-      $.ajax(request);
-    }
-  };
 
-  self.executeQuery = function() {
-    var data = ko.mapping.toJS(self.query);
-    data.appName = self.appName().name;
-    data.classPath = self.classPath();
-    data.autoContext = self.autoContext();
-    data.context = self.context() ? self.context().name : '';
-    data.params = ko.toJSON(self.query.params());
-    self.resultsEmpty(false);
-    var request = {
-      url: '/spark/api/execute',
-      dataType: 'json',
-      type: 'POST',
-      success: function(data) {
-        self.query.errors.removeAll();
-        self.rows.removeAll();
-        if (data.status == 0) {
-          $(document).trigger('execute.query', data);
-          self.query.id(data.design);
-          self.query.jobId(data.results.result.jobId);
-          window.location.hash = 'jobId=' + data.results.result.jobId;
-          self.query.context(data.results.result.context);
-          self.checkQueryStatus();
-        } else {
-          self.query.errors.push(data.message);
-        }
-      },
-      error: error_fn,
-      data: data
-    };
-    $.ajax(request);
-  };
+function EditorViewModel(notebooks) {
+  var self = this;
 
-  self.checkQueryStatus = function() {
-    var timerId = 0;
+  self.notebooks = ko.observableArray();
+  
+  self.init = function() {
+	$.each(notebooks, function(index, notebook) {
+	  self.loadNotebook(notebook);
+	});
+  }
 
-    var request = {
-      url: '/spark/api/job/' + self.query.jobId(),
-      dataType: 'json',
-      type: 'GET',
-      success: function(data) {
-        // Script finished
-        if (data.results.status == 'OK' || data.results.status == 'ERROR') {
-          clearInterval(timerId);
-          if (data.results.status == 'OK') {
-            self.updateResults(data.results.result);
-            self.resultsEmpty(data.results.result.length > 0);
-          } else {
-            self.query.errors.push(data.results.ERROR.message);
-          }
-          $(document).trigger('executed.query', data);
-        }
-      },
-      error: error_fn
-    };
+  self.loadNotebook = function(notebook) {
+    self.notebooks.push(new Notebook(self, notebook));
+  }
 
-    timerId = setInterval(function(){
-      $.ajax(request);
-    }, 1000);
-  };
+  self.newNotebook = function() {
+	self.notebooks.push(new Notebook(self, {}));
+  }
+  
+  self.save = function() {
 
-  self.openQuery = function(jobId) {
-    self.query.jobId(jobId);
-    $(document).trigger('execute.query');
-    self.checkQueryStatus();
-  };
-
-  self.fetchAppNames = function() {
-    var request = {
-      url: '/spark/api/jars',
-      dataType: 'json',
-      type: 'GET',
-      success: function(data) {
-        if (data.error != null) {
-          $.jHueNotify.error(data.error);
-        }
-        else {
-          self.updateAppNames(data.jars);
-        }
-      },
-      error: error_fn
-    };
-    $.ajax(request);
-  };
-
-  self.fetchContexts = function() {
-    var request = {
-      url: '/spark/api/contexts',
-      dataType: 'json',
-      type: 'GET',
-      success: function(data) {
-        if (data.error != null) {
-          $.jHueNotify.error(data.error);
-        }
-        else {
-          self.updateContexts(data.contexts);
-        }
-      },
-      error: error_fn
-    };
-    $.ajax(request);
   };
+}
 
-  self.createContext = function() {
-    var data = $("#createContextForm").serialize(); // Not koified
-    $("#createContextBtn").attr("data-loading-text", $("#createContextBtn").text() + " ...");
-    $("#createContextBtn").button("loading");
-    var request = {
-      url: '/spark/api/create_context',
-      dataType: 'json',
-      type: 'POST',
-      success: function(result) {
-        self.query.errors.removeAll();
-        if (result.status == 'OK') {
-          self.contexts.push(createDropdownItem(result.name));
-          self.context(result.name);
-          self.autoContext(false);
-          $(document).trigger('created.context', data);
-        } else {
-          $(document).trigger('error', result.result);
-        }
-      },
-      error: error_fn,
-      data: data
-    };
-    $.ajax(request);
-  };
 
-  self.showFileChooser = function() {
-    var inputPath = this;
-    var path = inputPath.value().substr(0, inputPath.value().lastIndexOf("/"));
-    $("#filechooser").jHueFileChooser({
-      initialPath: path,
-      onFileChoose: function (filePath) {
-        inputPath.value(filePath);
-        $("#chooseFile").modal("hide");
-      },
-      createFolder: false
-    });
-    $("#chooseFile").modal("show");
-  };
-}
+function logGA(page) {
+  if (typeof trackOnGA == 'function') {
+    trackOnGA('editor/' + page);
+  }
+}