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)