mirror of
https://github.com/jupyter/notebook.git
synced 2025-02-05 12:19:58 +08:00
Persistence API,
This is a combination of 10 commits. Enable widget instanciation from front-end. Address @minrk 's review comments. Make API that allows users to persist widget state easily. Added support for view persistence Started adding support for model persistence. Half way there! Finished persistence API. Move persistence code into the widget framework. Fin. Bug fixes
This commit is contained in:
parent
8160308bb2
commit
d466601dbb
@ -101,7 +101,7 @@ define([
|
||||
|
||||
this.last_msg_id = null;
|
||||
this.completer = null;
|
||||
|
||||
this.widget_views = [];
|
||||
|
||||
var config = utils.mergeopt(CodeCell, this.config);
|
||||
Cell.apply(this,[{
|
||||
@ -191,12 +191,19 @@ define([
|
||||
.addClass('widget-subarea')
|
||||
.appendTo(widget_area);
|
||||
this.widget_subarea = widget_subarea;
|
||||
var that = this;
|
||||
var widget_clear_buton = $('<button />')
|
||||
.addClass('close')
|
||||
.html('×')
|
||||
.click(function() {
|
||||
widget_area.slideUp('', function(){ widget_subarea.html(''); });
|
||||
})
|
||||
widget_area.slideUp('', function(){
|
||||
for (var i = 0; i < that.widget_views.length; i++) {
|
||||
that.widget_views[i].remove();
|
||||
}
|
||||
that.widget_views = [];
|
||||
widget_subarea.html('');
|
||||
});
|
||||
})
|
||||
.appendTo(widget_prompt);
|
||||
|
||||
var output = $('<div></div>');
|
||||
@ -210,6 +217,24 @@ define([
|
||||
this.completer = new completer.Completer(this, this.events);
|
||||
};
|
||||
|
||||
/**
|
||||
* Display a widget view in the cell.
|
||||
*/
|
||||
CodeCell.prototype.display_widget_view = function(view_promise) {
|
||||
|
||||
// Display a dummy element
|
||||
var dummy = $('<div/>');
|
||||
this.widget_subarea.append(dummy);
|
||||
|
||||
// Display the view.
|
||||
var that = this;
|
||||
return view_promise.then(function(view) {
|
||||
dummy.replaceWith(view.$el);
|
||||
this.widget_views.push(view);
|
||||
return view;
|
||||
});
|
||||
};
|
||||
|
||||
/** @method bind_events */
|
||||
CodeCell.prototype.bind_events = function () {
|
||||
Cell.prototype.bind_events.apply(this);
|
||||
@ -322,6 +347,10 @@ define([
|
||||
this.active_output_area.clear_output();
|
||||
|
||||
// Clear widget area
|
||||
for (var i = 0; i < this.widget_views.length; i++) {
|
||||
this.widget_views[i].remove();
|
||||
}
|
||||
this.widget_views = [];
|
||||
this.widget_subarea.html('');
|
||||
this.widget_subarea.height('');
|
||||
this.widget_area.height('');
|
||||
|
@ -291,6 +291,13 @@ define([
|
||||
// Firefox 22 broke $(window).on("beforeunload")
|
||||
// I'm not sure why or how.
|
||||
window.onbeforeunload = function (e) {
|
||||
// Raise an event that allows the user to execute custom code on unload
|
||||
try {
|
||||
that.events.trigger('beforeunload.Notebook', {notebook: that});
|
||||
} catch(e) {
|
||||
console.err('Error in "beforeunload.Notebook" event handler.', e);
|
||||
}
|
||||
|
||||
// TODO: Make killing the kernel configurable.
|
||||
var kill_kernel = false;
|
||||
if (kill_kernel) {
|
||||
|
@ -7,7 +7,8 @@ define([
|
||||
"jquery",
|
||||
"base/js/utils",
|
||||
"base/js/namespace",
|
||||
], function (_, Backbone, $, utils, IPython) {
|
||||
"services/kernels/comm"
|
||||
], function (_, Backbone, $, utils, IPython, comm) {
|
||||
"use strict";
|
||||
//--------------------------------------------------------------------
|
||||
// WidgetManager class
|
||||
@ -22,10 +23,11 @@ define([
|
||||
this.keyboard_manager = notebook.keyboard_manager;
|
||||
this.notebook = notebook;
|
||||
this.comm_manager = comm_manager;
|
||||
this.comm_target_name = 'ipython.widget';
|
||||
this._models = {}; /* Dictionary of model ids and model instances */
|
||||
|
||||
// Register with the comm manager.
|
||||
this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
|
||||
this.comm_manager.register_target(this.comm_target_name, $.proxy(this._handle_comm_open, this));
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
@ -53,21 +55,34 @@ define([
|
||||
* Displays a view for a particular model.
|
||||
*/
|
||||
var that = this;
|
||||
var cell = this.get_msg_cell(msg.parent_header.msg_id);
|
||||
if (cell === null) {
|
||||
return Promise.reject(new Error("Could not determine where the display" +
|
||||
" message was from. Widget will not be displayed"));
|
||||
} else if (cell.widget_subarea) {
|
||||
var dummy = $('<div />');
|
||||
cell.widget_subarea.append(dummy);
|
||||
return this.create_view(model, {cell: cell}).then(
|
||||
function(view) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var cell = that.get_msg_cell(msg.parent_header.msg_id);
|
||||
if (cell === null) {
|
||||
reject(new Error("Could not determine where the display" +
|
||||
" message was from. Widget will not be displayed"));
|
||||
} else {
|
||||
return that.display_view_in_cell(cell, model);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
WidgetManager.prototype.display_view_in_cell = function(cell, model) {
|
||||
// Displays a view in a cell.
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (cell.display_widget_view) {
|
||||
cell.display_widget_view(that.create_view(model, {cell: cell}))
|
||||
.then(function(view) {
|
||||
|
||||
that._handle_display_view(view);
|
||||
dummy.replaceWith(view.$el);
|
||||
view.trigger('displayed');
|
||||
return view;
|
||||
}).catch(utils.reject('Could not display view', true));
|
||||
}
|
||||
resolve(view);
|
||||
}, function(error) {
|
||||
reject(new utils.WrappedError('Could not display view', error));
|
||||
});
|
||||
} else {
|
||||
reject(new Error('Cell does not have a `display_widget_view` method.'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
WidgetManager.prototype._handle_display_view = function (view) {
|
||||
@ -238,6 +253,8 @@ define([
|
||||
widget_model.once('comm:close', function () {
|
||||
delete that._models[model_id];
|
||||
});
|
||||
widget_model.name = options.model_name;
|
||||
widget_model.module = options.model_module;
|
||||
return widget_model;
|
||||
|
||||
}, function(error) {
|
||||
@ -249,6 +266,100 @@ define([
|
||||
return model_promise;
|
||||
};
|
||||
|
||||
WidgetManager.prototype.get_state = function(options) {
|
||||
// Get the state of the widget manager.
|
||||
//
|
||||
// This includes all of the widget models and the cells that they are
|
||||
// displayed in.
|
||||
//
|
||||
// Parameters
|
||||
// ----------
|
||||
// options: dictionary
|
||||
// Dictionary of options with the following contents:
|
||||
// only_displayed: (optional) boolean=false
|
||||
// Only return models with one or more displayed views.
|
||||
// not_alive: (optional) boolean=false
|
||||
// Include models that have comms with severed connections.
|
||||
return utils.resolve_promise_dict(function(models) {
|
||||
var state = {};
|
||||
for (var model_id in models) {
|
||||
if (models.hasOwnProperty(model_id)) {
|
||||
var model = models[model_id];
|
||||
|
||||
// If the model has one or more views defined for it,
|
||||
// consider it displayed.
|
||||
var displayed_flag = !(options && options.only_displayed) || Object.keys(model.views).length > 0;
|
||||
var alive_flag = (options && options.not_alive) || model.comm_alive;
|
||||
if (displayed_flag && alive_flag) {
|
||||
state[model.model_id] = {
|
||||
model_name: model.name,
|
||||
model_module: model.module,
|
||||
views: [],
|
||||
};
|
||||
|
||||
// Get the views that are displayed *now*.
|
||||
for (var id in model.views) {
|
||||
if (model.views.hasOwnProperty(id)) {
|
||||
var view = model.views[id];
|
||||
var cell_index = this.notebook.find_cell_index(view.options.cell);
|
||||
state[model.model_id].views.push(cell_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
WidgetManager.prototype.set_state = function(state) {
|
||||
// Set the notebook's state.
|
||||
//
|
||||
// Reconstructs all of the widget models and attempts to redisplay the
|
||||
// widgets in the appropriate cells by cell index.
|
||||
|
||||
// Get the kernel when it's available.
|
||||
var that = this;
|
||||
return (new Promise(function(resolve, reject) {
|
||||
if (that.kernel) {
|
||||
resolve(that.kernel);
|
||||
} else {
|
||||
that.events.on('kernel_created.Session', function(event, data) {
|
||||
resolve(data.kernel);
|
||||
});
|
||||
}
|
||||
})).then(function(kernel) {
|
||||
|
||||
// Recreate all the widget models for the given state.
|
||||
that.widget_models = [];
|
||||
for (var i = 0; i < state.length; i++) {
|
||||
// Recreate a comm using the widget's model id (model_id == comm_id).
|
||||
var new_comm = new comm.Comm(kernel.widget_manager.comm_target_name, state[i].model_id);
|
||||
kernel.comm_manager.register_comm(new_comm);
|
||||
|
||||
// Create the model using the recreated comm. When the model is
|
||||
// created we don't know yet if the comm is valid so set_comm_alive
|
||||
// false. Once we receive the first state push from the back-end
|
||||
// we know the comm is alive.
|
||||
var model = kernel.widget_manager.create_model({
|
||||
comm: new_comm,
|
||||
model_name: state[i].model_name,
|
||||
model_module: state[i].model_module}).then(function(model) {
|
||||
model.set_comm_alive(false);
|
||||
model.request_state();
|
||||
model.received_state.then(function() {
|
||||
model.set_comm_alive(true);
|
||||
});
|
||||
return model;
|
||||
});
|
||||
that.widget_models.push(model);
|
||||
}
|
||||
return Promise.all(that.widget_models);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Backwards compatibility.
|
||||
IPython.WidgetManager = WidgetManager;
|
||||
|
||||
|
@ -32,6 +32,13 @@ define(["widgets/js/manager",
|
||||
this.id = model_id;
|
||||
this.views = {};
|
||||
|
||||
// Promise that is resolved when a state is received
|
||||
// from the back-end.
|
||||
var that = this;
|
||||
this.received_state = new Promise(function(resolve) {
|
||||
that._resolve_received_state = resolve;
|
||||
});
|
||||
|
||||
if (comm !== undefined) {
|
||||
// Remember comm associated with the model.
|
||||
this.comm = comm;
|
||||
@ -40,6 +47,11 @@ define(["widgets/js/manager",
|
||||
// Hook comm messages up to model.
|
||||
comm.on_close($.proxy(this._handle_comm_closed, this));
|
||||
comm.on_msg($.proxy(this._handle_comm_msg, this));
|
||||
|
||||
// Assume the comm is alive.
|
||||
this.set_comm_alive(true);
|
||||
} else {
|
||||
this.set_comm_alive(false);
|
||||
}
|
||||
return Backbone.Model.apply(this);
|
||||
},
|
||||
@ -55,11 +67,34 @@ define(["widgets/js/manager",
|
||||
}
|
||||
},
|
||||
|
||||
_handle_comm_closed: function (msg) {
|
||||
/**
|
||||
* Handle when a widget is closed.
|
||||
request_state: function(callbacks) {
|
||||
/**
|
||||
* Request a state push from the back-end.
|
||||
*/
|
||||
this.trigger('comm:close');
|
||||
if (!this.comm) {
|
||||
console.error("Could not request_state because comm doesn't exist!");
|
||||
return;
|
||||
}
|
||||
this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
|
||||
},
|
||||
|
||||
set_comm_alive: function(alive) {
|
||||
/**
|
||||
* Change the comm_alive state of the model.
|
||||
*/
|
||||
if (this.comm_alive === undefined || this.comm_alive != alive) {
|
||||
this.comm_alive = alive;
|
||||
this.trigger(alive ? 'comm_is_live' : 'comm_is_dead', {model: this});
|
||||
}
|
||||
},
|
||||
|
||||
close: function(comm_closed) {
|
||||
/**
|
||||
* Close model
|
||||
*/
|
||||
if (this.comm && !comm_closed) {
|
||||
this.comm.close();
|
||||
}
|
||||
this.stopListening();
|
||||
this.trigger('destroy', this);
|
||||
delete this.comm.model; // Delete ref so GC will collect widget model.
|
||||
@ -73,6 +108,14 @@ define(["widgets/js/manager",
|
||||
});
|
||||
},
|
||||
|
||||
_handle_comm_closed: function (msg) {
|
||||
/**
|
||||
* Handle when a widget is closed.
|
||||
*/
|
||||
this.trigger('comm:close');
|
||||
this.close(true);
|
||||
},
|
||||
|
||||
_handle_comm_msg: function (msg) {
|
||||
/**
|
||||
* Handle incoming comm msg.
|
||||
@ -104,7 +147,20 @@ define(["widgets/js/manager",
|
||||
} finally {
|
||||
that.state_lock = null;
|
||||
}
|
||||
}).catch(utils.reject("Couldn't set model state", true));
|
||||
that._resolve_received_state();
|
||||
return Promise.resolve();
|
||||
}, utils.reject("Couldn't set model state", true));
|
||||
},
|
||||
|
||||
get_state: function() {
|
||||
// Get the serializable state of the model.
|
||||
state = this.toJSON();
|
||||
for (var key in state) {
|
||||
if (state.hasOwnProperty(key)) {
|
||||
state[key] = this._pack_models(state[key]);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
_handle_status: function (msg, callbacks) {
|
||||
@ -322,6 +378,9 @@ define(["widgets/js/manager",
|
||||
this.on('displayed', function() {
|
||||
this.is_displayed = true;
|
||||
}, this);
|
||||
this.on('remove', function() {
|
||||
delete this.model.views[this.id];
|
||||
}, this);
|
||||
},
|
||||
|
||||
update: function(){
|
||||
@ -387,6 +446,12 @@ define(["widgets/js/manager",
|
||||
} else {
|
||||
this.on('displayed', callback, context);
|
||||
}
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
// Raise a remove event when the view is removed.
|
||||
WidgetView.__super__.remove.apply(this, arguments);
|
||||
this.trigger('remove');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -341,19 +341,26 @@ class Widget(LoggingConfigurable):
|
||||
"""Called when a msg is received from the front-end"""
|
||||
data = msg['content']['data']
|
||||
method = data['method']
|
||||
if not method in ['backbone', 'custom']:
|
||||
self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
|
||||
|
||||
# Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
|
||||
if method == 'backbone' and 'sync_data' in data:
|
||||
sync_data = data['sync_data']
|
||||
self.set_state(sync_data) # handles all methods
|
||||
if method == 'backbone':
|
||||
if 'sync_data' in data:
|
||||
sync_data = data['sync_data']
|
||||
self.set_state(sync_data) # handles all methods
|
||||
|
||||
# Handle a custom msg from the front-end
|
||||
# Handle a state request.
|
||||
elif method == 'request_state':
|
||||
self.send_state()
|
||||
|
||||
# Handle a custom msg from the front-end.
|
||||
elif method == 'custom':
|
||||
if 'content' in data:
|
||||
self._handle_custom_msg(data['content'])
|
||||
|
||||
# Catch remainder.
|
||||
else:
|
||||
self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
|
||||
|
||||
def _handle_custom_msg(self, content):
|
||||
"""Called when a custom msg is received."""
|
||||
self._msg_callbacks(self, content)
|
||||
@ -368,7 +375,7 @@ class Widget(LoggingConfigurable):
|
||||
# Send the state after the user registered callbacks for trait changes
|
||||
# have all fired (allows for user to validate values).
|
||||
if self.comm is not None and name in self.keys:
|
||||
# Make sure this isn't information that the front-end just sent us.
|
||||
# Make sure this isn't information that the front-end just sent us.
|
||||
if self._should_send_property(name, new_value):
|
||||
# Send new state to front-end
|
||||
self.send_state(key=name)
|
||||
|
Loading…
Reference in New Issue
Block a user