Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,29 @@ This should automatically enable the extension. If it is not listed in `jupyter
jupyter serverextension enable --py jupyter_offlinenotebook --sys-prefix


Usage
-----

![Offline notebook buttons](./offline-notebook-buttons.png)

You should see up to five new buttons depending on your configuration and where you are running the notebook:
- download the in-memory (browser) state of the notebook
- save the in-memory state of the notebook to local-storage
- load a notebook from local-storage
- open the permanent URL of the repository containing this notebook
- copy the permanent mybinder URL to share this repository

Saving and loading uses the repository ID and the path of the current notebook.

You should always see the `Download` button.
If you are running this on mybinder all buttons should be visible.
See the configuration section below to enable the other buttons on other systems.

If you don't see the buttons check the Javascript console log.

See [example.ipynb](./example.ipynb)


Configuration
-------------

Expand All @@ -36,23 +59,8 @@ This extension can be configured in `jupyter_notebook_config.py` by setting the
A callable that returns the repository reference URL.
Default is the values of the `BINDER_LAUNCH_HOST` and
`BINDER_PERSISTENT_REQUEST` environment variables.



Usage
-----

![Offline notebook buttons](./offline-notebook-buttons.png)

There are three new icons to:
- download the in-memory (browser) state of the notebook
- save the in-memory state of the notebook to local-storage
- load a notebook from local-storage

Saving and loading uses the repository ID and the path of the current notebook.
If you don't see the buttons check the Javascritp console log, it may mean no repository ID was found.

See [example.ipynb](./example.ipynb)
- `binder_repo_label`:
A callable that returns the label used to link to the repository.


**WARNING**
Expand Down
6 changes: 3 additions & 3 deletions example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"source": [
"1. Make some changes to this notebook (or run it to update the output).\n",
"2. Do not save the notebook. You can even disconnect from the Jupyter server or your network.\n",
"3. Click the first button (`first aid kit`). This should prompt you to download the notebook.\n",
"4. Click the middle button (`download`). This should save the current notebook into your browser's [local-storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).\n",
"3. Click the first button (`Download`). This should prompt you to download the notebook.\n",
"4. Click the second button (`cloud download`). This should save the current notebook into your browser's [local-storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).\n",
"5. Start a new instance of Jupyter, and open the original version of this notebook.\n",
"6. Click the third button (`upload`). This should restore the copy of the notebook from your browser's local-storage."
"6. Click the third button (`cloud upload`). This should restore the copy of the notebook from your browser's local-storage."
]
},
{
Expand Down
31 changes: 25 additions & 6 deletions jupyter_offlinenotebook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,29 @@ async def get(self):
to work if the user subsequently goes offline
"""
config = self.settings['offline_notebook_config']
repoid = config.repository_id()
binder_ref_url = config.repository_ref_url()
binder_persistent_url = config.binder_persistent_url()
jcfg = json.dumps({
'repoid': repoid,
'binder_ref_url': binder_ref_url,
'binder_persistent_url': binder_persistent_url,
'repoid': config.repository_id(),
'binder_repo_label': config.repository_label(),
'binder_ref_url': config.repository_ref_url(),
'binder_persistent_url': config.binder_persistent_url(),
})
self.log.debug('OfflineNotebook config:%s ', jcfg)
self.set_header('Content-Type', 'application/json')
self.write(jcfg)


def _repo_label_from_binder_request():
try:
repotype = os.getenv('BINDER_PERSISTENT_REQUEST', '').split('/')[1]
except IndexError:
return ''
if repotype == 'gh':
return 'GitHub'
if repotype == 'gl':
return 'GitLab'
return repotype.capitalize()


class OfflineNotebookConfig(Configurable):
"""
Holds server-side configuration
Expand All @@ -61,6 +71,15 @@ class OfflineNotebookConfig(Configurable):
"""
).tag(config=True)

repository_label = Callable(
default_value=_repo_label_from_binder_request,
help="""
A callable that returns the repository label.
Default is to parse the `BINDER_PERSISTENT_REQUEST` environment
variable.
"""
).tag(config=True)

repository_ref_url = Callable(
default_value=lambda: os.getenv('BINDER_REF_URL', ''),
help="""
Expand Down
111 changes: 60 additions & 51 deletions jupyter_offlinenotebook/static/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@ define([
'base/js/dialog',
'./dexie',
'jquery'
],
function(Jupyter, events, utils, dialog, dexie, $) {
],
function (Jupyter, events, utils, dialog, dexie, $) {
var repoid = null;
var repoLabel = null;
var bindeRefUrl = null;
var binderPersistentUrl = null
var db = null;
var dbname = 'jupyter-offlinenotebook';

var initialise = function() {
$.getJSON(utils.get_body_data('baseUrl') + 'offlinenotebook/config', function(data) {
var initialise = function () {
$.getJSON(utils.get_body_data('baseUrl') + 'offlinenotebook/config', function (data) {
repoid = data['repoid'];
if (repoid) {
console.log('offline-notebook repoid: ' + repoid);
}
else {
console.log('offline-notebook repoid not found, disabled');
}
repoLabel = data['binder_repo_label'] || 'Repo'
console.log('offline-notebook repoLabel: ' + repoLabel);
bindeRefUrl = data['binder_ref_url'];
console.log('offline-notebook bindeRefUrl: ' + bindeRefUrl);
binderPersistentUrl = data['binder_persistent_url']
Expand All @@ -30,63 +33,69 @@ define([
});
}

var getDb = function() {
var getDb = function () {
if (!db) {
db = new dexie(dbname);
// Only define indexed fields. pk: primary key
db.version(1).stores({'offlinenotebook': 'pk,repoid,name,type'});
db.version(1).stores({ 'offlinenotebook': 'pk,repoid,name,type' });
console.log('offline-notebook: Opened IndexedDB');
}
return db;
}

var addButtons = function() {
var downloadAction = Jupyter.actions.register({
var addButtons = function () {
Jupyter.actions.register({
'help': 'Download visible',
'icon' : 'fa-medkit',
'icon': 'fa-download',
'handler': downloadNotebookFromBrowser
}, 'offline-notebook-download', 'offlinenotebook');
var saveAction = Jupyter.actions.register({
Jupyter.actions.register({
'help': 'Save to browser storage',
'icon' : 'fa-download',
'icon': 'fa-cloud-download',
'handler': localstoreSaveNotebook
}, 'offline-notebook-save', 'offlinenotebook');
var loadAction = Jupyter.actions.register({
'help': 'Load from browser storage',
'icon' : 'fa-upload',
Jupyter.actions.register({
'help': 'Restore from browser storage',
'icon': 'fa-cloud-upload',
'handler': localstoreLoadNotebook
}, 'offline-notebook-load', 'offlinenotebook');

var showRepoAction = Jupyter.actions.register({
var repoIcons = {
'GitHub': 'fa-github',
'GitLab': 'fa-gitlab',
'Git': 'fa-git'
}
Jupyter.actions.register({
'help': 'Visit Binder repository',
'icon' : 'fa-external-link',
'icon': repoIcons[repoLabel] || 'fa-external-link',
'handler': openBinderRepo
}, 'offline-notebook-binderrepo', 'offlinenotebook');
var showBinderAction = Jupyter.actions.register({
Jupyter.actions.register({
'help': 'Link to this Binder',
'icon' : 'fa-external-link',
'icon': 'fa-link',
'handler': showBinderLink
}, 'offline-notebook-binderlink', 'offlinenotebook');

var buttons = [
downloadAction
];
var buttons = [{
'action': 'offlinenotebook:offline-notebook-download',
'label': 'Download'
}];
if (repoid) {
buttons.push(saveAction);
buttons.push(loadAction);
buttons.push('offlinenotebook:offline-notebook-save');
buttons.push('offlinenotebook:offline-notebook-load');
}
Jupyter.toolbar.add_buttons_group(buttons);

var binderButtons = []
if (bindeRefUrl) {
binderButtons.push({
'action': showRepoAction,
'label': 'Repo'
'action': 'offlinenotebook:offline-notebook-binderrepo',
'label': repoLabel
});
}
if (binderPersistentUrl) {
binderButtons.push({
'action': showBinderAction,
'action': 'offlinenotebook:offline-notebook-binderlink',
'label': 'Binder'
})
}
Expand All @@ -101,7 +110,7 @@ define([
}
if (!buttons) {
buttons = {
OK: {'class': 'btn-primary'}
OK: { 'class': 'btn-primary' }
};
}
dialog.modal({
Expand All @@ -116,18 +125,18 @@ define([
$('<span/>', {
'text': 'repoid: '
}).append(
$('<b/>', {
'text': repoid
})
));
$('<b/>', {
'text': repoid
})
));
var displayPath = $('<div/>').append(
$('<span/>', {
'text': 'path: '
}).append(
$('<b/>', {
'text': path
})
));
$('<b/>', {
'text': path
})
));
return displayRepoid.append(displayPath);
}

Expand All @@ -148,12 +157,12 @@ define([
'format': 'json',
'type': 'notebook',
'content': nb
}).then(function(key) {
}).then(function (key) {
console.log('offline-notebook saved: ', key);
modalDialog(
'Notebook saved to browser storage',
repopathDisplay);
}).catch(function(e) {
}).catch(function (e) {
var body = repopathDisplay.append(
$('<div/>', {
'text': e
Expand All @@ -162,14 +171,14 @@ define([
'Local storage IndexedDB error',
body,
'alert alert-danger');
throw(e);
throw (e);
});
}

function localstoreLoadNotebook() {
var path = Jupyter.notebook.notebook_path;
var primaryKey = 'repoid:' + repoid + ' path:' + path;
getDb().offlinenotebook.get(primaryKey).then(function(nb) {
getDb().offlinenotebook.get(primaryKey).then(function (nb) {
var repopathDisplay = formatRepoPathforDialog(repoid, path);
if (nb) {
console.log('offline-notebook found ' + primaryKey);
Expand All @@ -196,27 +205,27 @@ define([
repopathDisplay,
'alert alert-danger');
}
}).catch(function(e) {
}).catch(function (e) {
var body = $('<div/>').append(
$('<div/>', {
'text': primaryKey
})).append(
$('<div/>', {
'text': e
}));
$('<div/>', {
'text': e
}));
modalDialog(
'Local storage IndexedDB error',
body,
'alert alert-danger');
throw(e);
throw (e);
});
}

// Download https://jsfiddle.net/koldev/cW7W5/
function downloadNotebookFromBrowser() {
var name = Jupyter.notebook.notebook_name;
var nb = getNotebookFromBrowser();
var blob = new Blob([JSON.stringify(nb)], {type: 'application/json'});
var blob = new Blob([JSON.stringify(nb)], { type: 'application/json' });
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
document.body.appendChild(a);
Expand Down Expand Up @@ -258,11 +267,11 @@ define([
'style': 'flex-grow: 1; margin: 0;'
}));
var button = $('<button/>', {
'title': 'Copy binder link to clipboard',
'data-url': binderUrl
}).click(function() {
copy_link_into_clipboard(this);
})
'title': 'Copy binder link to clipboard',
'data-url': binderUrl
}).click(function () {
copy_link_into_clipboard(this);
})
button.append(
$('<i/>', {
'class': 'fa fa-clipboard'
Expand All @@ -281,4 +290,4 @@ define([
return {
load_ipython_extension: load_ipython_extension
};
});
});
Binary file modified offline-notebook-buttons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.