diff --git a/share/jupyter/voila/templates/base/error.html b/share/jupyter/voila/templates/base/error.html index 21cbd9142..0af6fdcd5 100644 --- a/share/jupyter/voila/templates/base/error.html +++ b/share/jupyter/voila/templates/base/error.html @@ -1,7 +1,17 @@ {% extends "page.html" %} +{% block stylesheets %} + {{ super() }} + + +{% endblock %} + {% block body %} -
+

{{ status_code }}: {{ status_message }}

{% block error_detail %} diff --git a/share/jupyter/voila/templates/lab/error.html b/share/jupyter/voila/templates/lab/error.html new file mode 100644 index 000000000..8a132454e --- /dev/null +++ b/share/jupyter/voila/templates/lab/error.html @@ -0,0 +1,12 @@ +{% extends "voila/templates/base/error.html" %} + +{% block stylesheets %} + {{ super() }} + + +{% endblock %} diff --git a/share/jupyter/voila/templates/lab/page.html b/share/jupyter/voila/templates/lab/page.html index a3148cdb8..7152c925f 100644 --- a/share/jupyter/voila/templates/lab/page.html +++ b/share/jupyter/voila/templates/lab/page.html @@ -1,14 +1,16 @@ {%- extends 'voila/templates/base/page.html' -%} {% block stylesheets %} -{% if theme == 'dark' %} - {{ include_css("static/index.css") }} - {{ include_css("static/theme-dark.css") }} -{% elif theme == 'light' %} - {{ include_css("static/index.css") }} - {{ include_css("static/theme-light.css") }} -{% else %} - {{ include_css("static/index.css") }} - {{ include_lab_theme(theme) }} +{% if include_css %} + {% if theme == 'dark' %} + {{ include_css("static/index.css") }} + {{ include_css("static/theme-dark.css") }} + {% elif theme == 'light' %} + {{ include_css("static/index.css") }} + {{ include_css("static/theme-light.css") }} + {% else %} + {{ include_css("static/index.css") }} + {{ include_lab_theme(theme) }} + {% endif %} {% endif %} {% endblock %} diff --git a/ui-tests/tests/voila.test.ts b/ui-tests/tests/voila.test.ts index ddaca1412..d9a9ab0e6 100644 --- a/ui-tests/tests/voila.test.ts +++ b/ui-tests/tests/voila.test.ts @@ -161,6 +161,27 @@ test.describe('Voila performance Tests', () => { ); }); + test('Render 404 error', async ({ page }) => { + await page.goto('/voila/render/unknown.ipynb'); + await page.waitForSelector('.voila-error'); + + expect(await page.screenshot()).toMatchSnapshot('404.png'); + }); + + test('Render 404 error with classic template', async ({ page }) => { + await page.goto('/voila/render/unknown.ipynb?voila-template=classic'); + await page.waitForSelector('.voila-error'); + + expect(await page.screenshot()).toMatchSnapshot('404-classic.png'); + }); + + test('Render 404 error with dark theme', async ({ page }) => { + await page.goto('/voila/render/unknown.ipynb?voila-theme=dark'); + await page.waitForSelector('.voila-error'); + + expect(await page.screenshot()).toMatchSnapshot('404-dark.png'); + }); + test('Render and benchmark bqplot.ipynb', async ({ page, browserName diff --git a/ui-tests/tests/voila.test.ts-snapshots/404-classic-linux.png b/ui-tests/tests/voila.test.ts-snapshots/404-classic-linux.png new file mode 100644 index 000000000..9cfb1959a Binary files /dev/null and b/ui-tests/tests/voila.test.ts-snapshots/404-classic-linux.png differ diff --git a/ui-tests/tests/voila.test.ts-snapshots/404-dark-linux.png b/ui-tests/tests/voila.test.ts-snapshots/404-dark-linux.png new file mode 100644 index 000000000..b81e5d451 Binary files /dev/null and b/ui-tests/tests/voila.test.ts-snapshots/404-dark-linux.png differ diff --git a/ui-tests/tests/voila.test.ts-snapshots/404-linux.png b/ui-tests/tests/voila.test.ts-snapshots/404-linux.png new file mode 100644 index 000000000..7ac9b6054 Binary files /dev/null and b/ui-tests/tests/voila.test.ts-snapshots/404-linux.png differ diff --git a/voila/handler.py b/voila/handler.py index 172611706..93de9f1dc 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -23,15 +23,48 @@ from ._version import __version__ from .notebook_renderer import NotebookRenderer from .query_parameters_handler import QueryStringSocketHandler -from .utils import ENV_VARIABLE +from .utils import ENV_VARIABLE, create_include_assets_functions -class VoilaHandler(JupyterHandler): +class BaseVoilaHandler(JupyterHandler): + + def initialize(self, **kwargs): + self.voila_configuration = kwargs['voila_configuration'] + + def render_template(self, name, **ns): + """ Render the Voila HTML template, respecting the theme and nbconvert template. + """ + template_arg = ( + self.get_argument("voila-template", self.voila_configuration.template) + if self.voila_configuration.allow_template_override == "YES" + else self.voila_configuration.template + ) + theme_arg = ( + self.get_argument("voila-theme", self.voila_configuration.theme) + if self.voila_configuration.allow_theme_override == "YES" + else self.voila_configuration.theme + ) + + ns = { + **ns, + **self.template_namespace, + **create_include_assets_functions( + template_arg, self.base_url + ), + "theme": theme_arg + } + + template = self.get_template(name) + return template.render(**ns) + + +class VoilaHandler(BaseVoilaHandler): + def initialize(self, **kwargs): + super().initialize(**kwargs) self.notebook_path = kwargs.pop('notebook_path', []) # should it be [] self.template_paths = kwargs.pop('template_paths', []) self.traitlet_config = kwargs.pop('config', None) - self.voila_configuration = kwargs['voila_configuration'] # we want to avoid starting multiple kernels due to template mistakes self.kernel_started = False diff --git a/voila/treehandler.py b/voila/treehandler.py index deb8e93bc..a8f99c1cd 100644 --- a/voila/treehandler.py +++ b/voila/treehandler.py @@ -10,15 +10,16 @@ from tornado import web -from jupyter_server.base.handlers import JupyterHandler from jupyter_server.utils import url_path_join, url_escape -from .utils import get_server_root_dir, create_include_assets_functions +from .utils import get_server_root_dir +from .handler import BaseVoilaHandler -class VoilaTreeHandler(JupyterHandler): +class VoilaTreeHandler(BaseVoilaHandler): + def initialize(self, **kwargs): - self.voila_configuration = kwargs['voila_configuration'] + super().initialize(**kwargs) self.allowed_extensions = list(self.voila_configuration.extension_language_mapping.keys()) + ['.ipynb'] def get_template(self, name): @@ -58,17 +59,6 @@ def get(self, path=''): page_title = self.generate_page_title(path) contents = cm.get(path) - template_arg = ( - self.get_argument("voila-template", self.voila_configuration.template) - if self.voila_configuration.allow_template_override == "YES" - else self.voila_configuration.template - ) - theme_arg = ( - self.get_argument("voila-theme", self.voila_configuration.theme) - if self.voila_configuration.allow_theme_override == "YES" - else self.voila_configuration.theme - ) - def allowed_content(content): if content['type'] in ['directory', 'notebook']: return True @@ -78,18 +68,16 @@ def allowed_content(content): contents['content'] = sorted(contents['content'], key=lambda i: i['name']) contents['content'] = filter(allowed_content, contents['content']) - include_assets_functions = create_include_assets_functions(template_arg, self.base_url) - - self.write(self.render_template('tree.html', - page_title=page_title, - notebook_path=path, - breadcrumbs=breadcrumbs, - contents=contents, - terminals_available=False, - server_root=get_server_root_dir(self.settings), - theme=theme_arg, - query=self.request.query, - **include_assets_functions)) + self.write(self.render_template( + 'tree.html', + page_title=page_title, + notebook_path=path, + breadcrumbs=breadcrumbs, + contents=contents, + terminals_available=False, + server_root=get_server_root_dir(self.settings), + query=self.request.query, + )) elif cm.file_exists(path): # it's not a directory, we have redirecting to do model = cm.get(path, content=False)