diff --git a/bower.json b/bower.json index e58ec9279..7ce891988 100644 --- a/bower.json +++ b/bower.json @@ -7,7 +7,7 @@ "bootstrap-tour": "0.9.0", "codemirror": "components/codemirror#~5.22.2", "es6-promise": "~1.0", - "font-awesome": "components/font-awesome#~4.2.0", + "font-awesome": "components/font-awesome#~4.7.0", "google-caja": "5669", "jquery": "components/jquery#~2.0", "jquery-typeahead": "~2.0.0", diff --git a/docs/source/security.rst b/docs/source/security.rst index 4c57ef0f5..f90f9d6da 100644 --- a/docs/source/security.rst +++ b/docs/source/security.rst @@ -31,7 +31,8 @@ When you start a notebook server with token authentication enabled (default), a token is generated to use for authentication. This token is logged to the terminal, so that you can copy/paste the URL into your browser:: - [I 11:59:16.597 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01 + [I 11:59:16.597 NotebookApp] The Jupyter Notebook is running at: + http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01 If the notebook server is going to open your browser automatically diff --git a/notebook/_sysinfo.py b/notebook/_sysinfo.py index ea237bbd2..4e6a36626 100644 --- a/notebook/_sysinfo.py +++ b/notebook/_sysinfo.py @@ -46,11 +46,15 @@ def pkg_commit_hash(pkg_path): while cur_path != par_path: cur_path = par_path if p.exists(p.join(cur_path, '.git')): - proc = subprocess.Popen('git rev-parse --short HEAD', - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=pkg_path, shell=True) - repo_commit, _ = proc.communicate() + try: + proc = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=pkg_path) + repo_commit, _ = proc.communicate() + except OSError: + repo_commit = None + if repo_commit: return 'repository', repo_commit.strip().decode('ascii') else: diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 2c48d5cc1..cf3e183f0 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -39,7 +39,12 @@ from notebook.services.security import csp_report_uri #----------------------------------------------------------------------------- non_alphanum = re.compile(r'[^A-Za-z0-9]') -sys_info = json.dumps(get_sys_info()) +_sys_info_cache = None +def json_sys_info(): + global _sys_info_cache + if _sys_info_cache is None: + _sys_info_cache = json.dumps(get_sys_info()) + return _sys_info_cache def log(): if Application.initialized(): @@ -357,7 +362,7 @@ class IPythonHandler(AuthenticatedHandler): login_available=self.login_available, token_available=bool(self.token or self.one_time_token), static_url=self.static_url, - sys_info=sys_info, + sys_info=json_sys_info(), contents_js_source=self.contents_js_source, version_hash=self.version_hash, ignore_minified_js=self.ignore_minified_js, diff --git a/notebook/base/zmqhandlers.py b/notebook/base/zmqhandlers.py index 66e31eb18..8878948da 100644 --- a/notebook/base/zmqhandlers.py +++ b/notebook/base/zmqhandlers.py @@ -296,5 +296,4 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): self.session = Session(config=self.config) def get_compression_options(self): - # use deflate compress websocket - return {} + return self.settings.get('websocket_compression_options', None) diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 839102cdd..934aaa099 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -93,7 +93,7 @@ from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel, NATIVE_KE from jupyter_client.session import Session from nbformat.sign import NotebookNotary from traitlets import ( - Dict, Unicode, Integer, List, Bool, Bytes, Instance, + Any, Dict, Unicode, Integer, List, Bool, Bytes, Instance, TraitError, Type, Float, observe, default, validate ) from ipython_genutils import py3compat @@ -294,6 +294,7 @@ class NotebookWebApplication(web.Application): handlers.extend(load_handlers('services.nbconvert.handlers')) handlers.extend(load_handlers('services.kernelspecs.handlers')) handlers.extend(load_handlers('services.security.handlers')) + handlers.extend(load_handlers('services.shutdown')) handlers.append( (r"/nbextensions/(.*)", FileFindHandler, { @@ -740,7 +741,18 @@ class NotebookApp(JupyterApp): tornado_settings = Dict(config=True, help="Supply overrides for the tornado.web.Application that the " "Jupyter notebook uses.") - + + websocket_compression_options = Any(None, config=True, + help=""" + Set the tornado compression options for websocket connections. + + This value will be returned from :meth:`WebSocketHandler.get_compression_options`. + None (default) will disable compression. + A dict (even an empty one) will enable compression. + + See the tornado docs for WebSocketHandler.get_compression_options for details. + """ + ) terminado_settings = Dict(config=True, help='Supply overrides for terminado. Currently only supports "shell_command".') @@ -1107,6 +1119,7 @@ class NotebookApp(JupyterApp): def init_webapp(self): """initialize tornado webapp and httpserver""" self.tornado_settings['allow_origin'] = self.allow_origin + self.tornado_settings['websocket_compression_options'] = self.websocket_compression_options if self.allow_origin_pat: self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat) self.tornado_settings['allow_credentials'] = self.allow_credentials @@ -1206,7 +1219,7 @@ class NotebookApp(JupyterApp): log("Terminals not available (error was %s)", e) def init_signal(self): - if not sys.platform.startswith('win') and sys.stdin.isatty(): + if not sys.platform.startswith('win') and sys.stdin and sys.stdin.isatty(): signal.signal(signal.SIGINT, self._handle_sigint) signal.signal(signal.SIGTERM, self._signal_stop) if hasattr(signal, 'SIGUSR1'): @@ -1353,7 +1366,8 @@ class NotebookApp(JupyterApp): "Return the current working directory and the server url information" info = self.contents_manager.info_string() + "\n" info += "%d active kernels \n" % len(self.kernel_manager._kernels) - return info + "The Jupyter Notebook is running at: %s" % self.display_url + # Format the info so that the URL fits on a single line in 80 char display + return info + "The Jupyter Notebook is running at:\n\r%s" % self.display_url def server_info(self): """Return a JSONable dict of information about this server.""" diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py index 92a135b4f..a05253ac3 100644 --- a/notebook/services/contents/filemanager.py +++ b/notebook/services/contents/filemanager.py @@ -281,7 +281,7 @@ class FileContentsManager(FileManagerMixin, ContentsManager): if e.errno == errno.ENOENT: self.log.warning("%s doesn't exist", os_path) else: - self.log.warning("Error stat-ing %s: %s", (os_path, e)) + self.log.warning("Error stat-ing %s: %s", os_path, e) continue if not stat.S_ISREG(st.st_mode) and not stat.S_ISDIR(st.st_mode): diff --git a/notebook/services/contents/tests/test_contents_api.py b/notebook/services/contents/tests/test_contents_api.py index 439dffdfa..990b6ca51 100644 --- a/notebook/services/contents/tests/test_contents_api.py +++ b/notebook/services/contents/tests/test_contents_api.py @@ -249,8 +249,8 @@ class APITest(NotebookTestBase): self.assertEqual(nbnames, expected) nbs = notebooks_only(self.api.list('ordering').json()) - nbnames = [n['name'] for n in nbs] - expected = ['A.ipynb', 'b.ipynb', 'C.ipynb'] + nbnames = {n['name'] for n in nbs} + expected = {'A.ipynb', 'b.ipynb', 'C.ipynb'} self.assertEqual(nbnames, expected) def test_list_dirs(self): diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index e81148e10..569c60426 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -15,7 +15,7 @@ from tornado.ioloop import IOLoop, PeriodicCallback from jupyter_client.session import Session from jupyter_client.multikernelmanager import MultiKernelManager -from traitlets import Dict, List, Unicode, TraitError, Integer, default, validate +from traitlets import Bool, Dict, List, Unicode, TraitError, Integer, default, validate from notebook.utils import to_os_path from notebook._tz import utcnow, isoformat @@ -71,6 +71,16 @@ class MappingKernelManager(MultiKernelManager): help="""The interval (in seconds) on which to check for idle kernels exceeding the cull timeout value.""" ) + cull_connected = Bool(False, config=True, + help="""Whether to consider culling kernels which have one or more connections. + Only effective if cull_idle_timeout is not 0.""" + ) + + cull_busy = Bool(False, config=True, + help="""Whether to consider culling kernels which are busy. + Only effective if cull_idle_timeout is not 0.""" + ) + #------------------------------------------------------------------------- # Methods for managing kernels and sessions #------------------------------------------------------------------------- @@ -273,6 +283,10 @@ class MappingKernelManager(MultiKernelManager): self.cull_kernels, 1000*self.cull_interval, loop) self.log.info("Culling kernels with idle durations > %s seconds at %s second intervals ...", self.cull_idle_timeout, self.cull_interval) + if self.cull_busy: + self.log.info("Culling kernels even if busy") + if self.cull_connected: + self.log.info("Culling kernels even with connected clients") self._culler_callback.start() self._initialized_culler = True @@ -294,8 +308,15 @@ class MappingKernelManager(MultiKernelManager): if kernel.last_activity is not None: dt_now = utcnow() dt_idle = dt_now - kernel.last_activity - if dt_idle > timedelta(seconds=self.cull_idle_timeout): # exceeds timeout, can be culled + # Compute idle properties + is_idle_time = dt_idle > timedelta(seconds=self.cull_idle_timeout) + is_idle_execute = self.cull_busy or (kernel.execution_state != 'busy') + connections = self._kernel_connections.get(kernel_id, 0) + is_idle_connected = self.cull_connected or not connections + # Cull the kernel if all three criteria are met + if (is_idle_time and is_idle_execute and is_idle_connected): idle_duration = int(dt_idle.total_seconds()) - self.log.warning("Culling kernel '%s' (%s) due to %s seconds of inactivity.", kernel.kernel_name, kernel_id, idle_duration) + self.log.warning("Culling '%s' kernel '%s' (%s) with %d connections due to %s seconds of inactivity.", + kernel.execution_state, kernel.kernel_name, kernel_id, connections, idle_duration) self.shutdown_kernel(kernel_id) diff --git a/notebook/services/shutdown.py b/notebook/services/shutdown.py new file mode 100644 index 000000000..78d1f2ad6 --- /dev/null +++ b/notebook/services/shutdown.py @@ -0,0 +1,15 @@ +"""HTTP handler to shut down the notebook server. +""" +from tornado import web, ioloop +from notebook.base.handlers import IPythonHandler + +class ShutdownHandler(IPythonHandler): + @web.authenticated + def post(self): + self.log.info("Shutting down on /api/shutdown request.") + ioloop.IOLoop.current().stop() + + +default_handlers = [ + (r"/api/shutdown", ShutdownHandler), +] diff --git a/notebook/static/base/images/favicon-busy-1.ico b/notebook/static/base/images/favicon-busy-1.ico new file mode 100644 index 000000000..5b46a8226 Binary files /dev/null and b/notebook/static/base/images/favicon-busy-1.ico differ diff --git a/notebook/static/base/images/favicon-busy-2.ico b/notebook/static/base/images/favicon-busy-2.ico new file mode 100644 index 000000000..4a8b841c2 Binary files /dev/null and b/notebook/static/base/images/favicon-busy-2.ico differ diff --git a/notebook/static/base/images/favicon-busy-3.ico b/notebook/static/base/images/favicon-busy-3.ico new file mode 100644 index 000000000..b5edce573 Binary files /dev/null and b/notebook/static/base/images/favicon-busy-3.ico differ diff --git a/notebook/static/base/images/favicon-busy.ico b/notebook/static/base/images/favicon-busy.ico deleted file mode 100644 index 85f9995a4..000000000 Binary files a/notebook/static/base/images/favicon-busy.ico and /dev/null differ diff --git a/notebook/static/base/images/favicon-file.ico b/notebook/static/base/images/favicon-file.ico new file mode 100644 index 000000000..8167018cd Binary files /dev/null and b/notebook/static/base/images/favicon-file.ico differ diff --git a/notebook/static/base/images/favicon-notebook.ico b/notebook/static/base/images/favicon-notebook.ico new file mode 100644 index 000000000..4537e2d98 Binary files /dev/null and b/notebook/static/base/images/favicon-notebook.ico differ diff --git a/notebook/static/base/images/favicon-terminal.ico b/notebook/static/base/images/favicon-terminal.ico new file mode 100644 index 000000000..ace499a33 Binary files /dev/null and b/notebook/static/base/images/favicon-terminal.ico differ diff --git a/notebook/static/base/js/utils.js b/notebook/static/base/js/utils.js index b70b3ceb6..bc53a6c53 100644 --- a/notebook/static/base/js/utils.js +++ b/notebook/static/base/js/utils.js @@ -1017,6 +1017,36 @@ define([ } }; + + // javascript stores text as utf16 and string indices use "code units", + // which stores high-codepoint characters as "surrogate pairs", + // which occupy two indices in the javascript string. + // We need to translate cursor_pos in the protocol (in characters) + // to js offset (with surrogate pairs taking two spots). + function js_idx_to_char_idx (js_idx, text) { + var char_idx = js_idx; + for (var i = 0; i < text.length && i < js_idx; i++) { + var char_code = text.charCodeAt(i); + // check for the first half of a surrogate pair + if (char_code >= 0xD800 && char_code < 0xDC00) { + char_idx -= 1; + } + } + return char_idx; + } + + function char_idx_to_js_idx (char_idx, text) { + var js_idx = char_idx; + for (var i = 0; i < text.length && i < js_idx; i++) { + var char_code = text.charCodeAt(i); + // check for the first half of a surrogate pair + if (char_code >= 0xD800 && char_code < 0xDC00) { + js_idx += 1; + } + } + return js_idx; + } + // Test if a drag'n'drop event contains a file (as opposed to an HTML // element/text from the document) var dnd_contain_file = function(event) { @@ -1051,6 +1081,17 @@ define([ fn(); } } + + var change_favicon = function (src) { + var link = document.createElement('link'), + oldLink = document.getElementById('favicon'); + link.id = 'favicon'; + link.type = 'image/x-icon'; + link.rel = 'shortcut icon'; + link.href = utils.url_path_join(utils.get_body_data('baseUrl'), src); + if (oldLink) document.head.removeChild(oldLink); + document.head.appendChild(link); + }; var utils = { throttle: throttle, @@ -1101,7 +1142,10 @@ define([ format_datetime: format_datetime, datetime_sort_helper: datetime_sort_helper, dnd_contain_file: dnd_contain_file, - _ansispan:_ansispan + js_idx_to_char_idx: js_idx_to_char_idx, + char_idx_to_js_idx: char_idx_to_js_idx, + _ansispan:_ansispan, + change_favicon: change_favicon }; return utils; diff --git a/notebook/static/base/less/page.less b/notebook/static/base/less/page.less index 7a51af6a8..8bac02806 100644 --- a/notebook/static/base/less/page.less +++ b/notebook/static/base/less/page.less @@ -27,6 +27,10 @@ body > #header { z-index: 100; #header-container { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 5px; padding-bottom: 5px; padding-top: 5px; .border-box-sizing(); @@ -57,9 +61,6 @@ body > #header { padding-left: 0px; padding-top: (@navbar-height - @logo_height) / 2; padding-bottom: (@navbar-height - @logo_height) / 2; - @media (max-width: @screen-sm-max){ - margin-left: 10px; - } } @@ -99,8 +100,12 @@ input.ui-button { padding: 0.3em 0.9em; } +span#kernel_logo_widget { + margin: 0 10px; +} + span#login_widget { - float: right; + } span#login_widget > .button, diff --git a/notebook/static/edit/js/savewidget.js b/notebook/static/edit/js/savewidget.js index 31d7cc65d..7df4ed4d6 100644 --- a/notebook/static/edit/js/savewidget.js +++ b/notebook/static/edit/js/savewidget.js @@ -79,6 +79,11 @@ define([ class: "btn-primary", click: function () { var new_name = d.find('input').val(); + if (!new_name) { + // Reset the message + d.find('.rename-message').text("Enter a new filename:"); + return false; + } d.find('.rename-message').text("Renaming..."); d.find('input[type="text"]').prop('disabled', true); that.editor.rename(new_name).then( diff --git a/notebook/static/notebook/js/commandpalette.js b/notebook/static/notebook/js/commandpalette.js index 73a79b2d9..05de2e897 100644 --- a/notebook/static/notebook/js/commandpalette.js +++ b/notebook/static/notebook/js/commandpalette.js @@ -164,7 +164,11 @@ define(function(require){ // now src is the right structure for typeahead input.typeahead({ - emptyTemplate: "No results found for
{{query}}
", + emptyTemplate: function(query) { + return $('
').text("No results found for ").append( + $('').text(query) + ); + }, maxItem: 1e3, minLength: 0, hint: true, diff --git a/notebook/static/notebook/js/completer.js b/notebook/static/notebook/js/completer.js index 8eb92ca2e..21a6a3b32 100644 --- a/notebook/static/notebook/js/completer.js +++ b/notebook/static/notebook/js/completer.js @@ -153,6 +153,8 @@ define([ // one kernel completion came back, finish_completing will be called with the results // we fork here and directly call finish completing if kernel is busy var cursor_pos = this.editor.indexFromPos(cur); + var text = this.editor.getValue(); + cursor_pos = utils.js_idx_to_char_idx(cursor_pos, text); if (this.skip_kernel_completion) { this.finish_completing({ content: { matches: [], @@ -160,7 +162,7 @@ define([ cursor_end: cursor_pos, }}); } else { - this.cell.kernel.complete(this.editor.getValue(), cursor_pos, + this.cell.kernel.complete(text, cursor_pos, $.proxy(this.finish_completing, this) ); } @@ -175,6 +177,7 @@ define([ var start = content.cursor_start; var end = content.cursor_end; var matches = content.matches; + console.log(content); var cur = this.editor.getCursor(); if (end === null) { @@ -187,7 +190,13 @@ define([ } else if (start < 0) { start = end + start; } + } else { + // handle surrogate pairs + var text = this.editor.getValue(); + end = utils.char_idx_to_js_idx(end, text); + start = utils.char_idx_to_js_idx(start, text); } + var results = CodeMirror.contextHint(this.editor); var filtered_results = []; //remove results from context completion diff --git a/notebook/static/notebook/js/notificationarea.js b/notebook/static/notebook/js/notificationarea.js index 121fc6be2..89abc33e0 100644 --- a/notebook/static/notebook/js/notificationarea.js +++ b/notebook/static/notebook/js/notificationarea.js @@ -40,6 +40,23 @@ define([ var $modal_ind_icon = $("#modal_indicator"); var $readonly_ind_icon = $('#readonly-indicator'); var $body = $('body'); + var interval = 0; + + var set_busy_favicon = function(on) { + if (on && !interval) { + var i = 0; + var icons = ['favicon-busy-1.ico', 'favicon-busy-3.ico', 'favicon-busy-3.ico']; + interval = setInterval(function() { + var icon = icons[i % 3]; + utils.change_favicon('/static/base/images/' + icon); + i += 1; + }, 300); + } else { + clearInterval(interval); + utils.change_favicon('/static/base/images/favicon-notebook.ico'); + interval = 0; + } + }; // Listen for the notebook loaded event. Set readonly indicator. this.events.on('notebook_loaded.Notebook', function() { @@ -244,41 +261,30 @@ define([ knw.danger(short, undefined, showMsg); }); - var change_favicon = function (src) { - var link = document.createElement('link'), - oldLink = document.getElementById('favicon'); - link.id = 'favicon'; - link.type = 'image/x-icon'; - link.rel = 'shortcut icon'; - link.href = utils.url_path_join(utils.get_body_data('baseUrl'), src); - if (oldLink) document.head.removeChild(oldLink); - document.head.appendChild(link); - }; - this.events.on('kernel_starting.Kernel kernel_created.Session', function () { // window.document.title='(Starting) '+window.document.title; $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy'); knw.set_message("Kernel starting, please wait..."); - change_favicon('/static/base/images/favicon-busy.ico'); + set_busy_favicon(true); }); this.events.on('kernel_ready.Kernel', function () { // that.save_widget.update_document_title(); $kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle'); knw.info("Kernel ready", 500); - change_favicon('/static/base/images/favicon.ico'); + set_busy_favicon(false); }); this.events.on('kernel_idle.Kernel', function () { // that.save_widget.update_document_title(); $kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle'); - change_favicon('/static/base/images/favicon.ico'); + set_busy_favicon(false); }); this.events.on('kernel_busy.Kernel', function () { // window.document.title='(Busy) '+window.document.title; $kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy'); - change_favicon('/static/base/images/favicon-busy.ico'); + set_busy_favicon(true); }); this.events.on('spec_match_found.Kernel', function (evt, data) { diff --git a/notebook/static/notebook/js/outputarea.js b/notebook/static/notebook/js/outputarea.js index 71c7b5726..be1ffa89d 100644 --- a/notebook/static/notebook/js/outputarea.js +++ b/notebook/static/notebook/js/outputarea.js @@ -77,7 +77,7 @@ define([ this.prompt_overlay.addClass('out_prompt_overlay prompt'); this.prompt_overlay.attr('title', 'click to expand output; double click to hide output'); - this.collapse(); + this.expand(); }; /** @@ -976,6 +976,7 @@ define([ this._display_id_targets = {}; this.trusted = true; this.unscroll_area(); + this.expand(); return; } }; diff --git a/notebook/static/notebook/js/tooltip.js b/notebook/static/notebook/js/tooltip.js index ba93a1d9c..c0d7e11e1 100644 --- a/notebook/static/notebook/js/tooltip.js +++ b/notebook/static/notebook/js/tooltip.js @@ -201,8 +201,8 @@ define([ this.cancel_pending(); var editor = cell.code_mirror; var cursor = editor.getCursor(); - var cursor_pos = editor.indexFromPos(cursor); var text = cell.get_text(); + var cursor_pos = utils.js_idx_to_char_idx(editor.indexFromPos(cursor), text); this._hide_if_no_docstring = hide_if_no_docstring; diff --git a/notebook/static/notebook/less/kernelselector.less b/notebook/static/notebook/less/kernelselector.less index 33c68a34c..fab2cf745 100644 --- a/notebook/static/notebook/less/kernelselector.less +++ b/notebook/static/notebook/less/kernelselector.less @@ -1,6 +1,4 @@ #kernel_logo_widget { - .pull-right(); - .current_kernel_logo { display: none; .navbar-vertical-align(32px); diff --git a/notebook/static/notebook/less/savewidget.less b/notebook/static/notebook/less/savewidget.less index fe37e1980..13f76b4d4 100644 --- a/notebook/static/notebook/less/savewidget.less +++ b/notebook/static/notebook/less/savewidget.less @@ -1,12 +1,15 @@ span.save_widget { - margin-top: 6px; - max-width: 100%; + height: 30px; + margin-top: 4px; display: flex; + justify-content: flex-start; + align-items: baseline; + width: 50%; + flex: 1; span.filename { - height: 1em; + height: 100%; line-height: 1em; - padding: 3px; margin-left: @padding-large-horizontal; border: none; font-size: 146.5%; diff --git a/notebook/templates/edit.html b/notebook/templates/edit.html index 9cda9fbc7..8c69952be 100644 --- a/notebook/templates/edit.html +++ b/notebook/templates/edit.html @@ -2,6 +2,8 @@ {% block title %}{{page_title}}{% endblock %} +{% block favicon %}{% endblock %} + {% block stylesheet %} diff --git a/notebook/templates/notebook.html b/notebook/templates/notebook.html index 53e5ebbb4..faa82cd87 100644 --- a/notebook/templates/notebook.html +++ b/notebook/templates/notebook.html @@ -1,5 +1,7 @@ {% extends "page.html" %} +{% block favicon %}{% endblock %} + {% block stylesheet %} {% if mathjax_url %} @@ -36,7 +38,7 @@ data-notebook-path="{{notebook_path | urlencode}}" {% block headercontainer %} - + diff --git a/notebook/templates/page.html b/notebook/templates/page.html index e40b3147b..147f9a9d2 100644 --- a/notebook/templates/page.html +++ b/notebook/templates/page.html @@ -120,6 +120,9 @@ dir="ltr">
+ {% block headercontainer %} + {% endblock %} + {% block header_buttons %} {% block login_widget %} @@ -135,9 +138,7 @@ dir="ltr"> {% endblock %} {% endblock header_buttons %} - - {% block headercontainer %} - {% endblock %} +
diff --git a/notebook/templates/terminal.html b/notebook/templates/terminal.html index 945e19070..0a3cfd05c 100644 --- a/notebook/templates/terminal.html +++ b/notebook/templates/terminal.html @@ -2,6 +2,8 @@ {% block title %}{{page_title}}{% endblock %} +{% block favicon %}{% endblock %} + {% block bodyclasses %}terminal-app {{super()}}{% endblock %} {% block params %}