Skip to content

Commit

Permalink
Lab theme handling (#1089)
Browse files Browse the repository at this point in the history
* Lab theme handling

* Add support for query parameters on the TreeHandler
  • Loading branch information
martinRenou committed Feb 14, 2022
1 parent 216c2a6 commit 6be66cc
Show file tree
Hide file tree
Showing 18 changed files with 215 additions and 26 deletions.
10 changes: 8 additions & 2 deletions docs/source/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ the following option:
voila <path-to-notebook> --theme=dark
Or by passing in the query parameter ``voila-theme``, e.g. a URL like ``http://localhost:8867/voila/render/query-strings.ipynb?voila-theme=dark``.

The theme can also be set in the notebook metadata, under ``metadata/voila/theme`` by editing the notebook file manually, or using the metadata editor in for instance the classical notebook
Expand All @@ -35,8 +34,15 @@ The theme can also be set in the notebook metadata, under ``metadata/voila/theme
System administrators who want to disable changing the theme, can pass ``--VoilaConfiguration.allow_theme_override=NO`` or
``--VoilaConfiguration.allow_theme_override=NOTEBOOK`` to disable changing the theme completely, or only allow it from the notebook metadata.

Currently, Voilà supports only **light** and **dark** themes.
Like nbconvert, Voilà supports the **light** and **dark** themes by default, but you can also use custom JupyterLab themes:

.. code-block:: bash
pip install jupyterlab_miami_nights
voila <path-to-notebook> --theme=jupyterlab_miami_nights
.. warning::
Theme are specific to the "lab" template, they will not work for the "classic" template

.. note::
Changing the theme from the notebook metadata may change in the future if this features moves to nbconvert.
Expand Down
4 changes: 3 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ packages = find:
python_requires = >=3.7
install_requires =
jupyter_server>=0.3.0,<2.0.0
jupyterlab_server>=2.3.0,<3
jupyter_client>=6.1.3,<8
nbclient>=0.4.0,<0.6
nbconvert>=6.0.0,<7
nbconvert>=6.4.2,<7
websockets>=9.0
traitlets>=5.0.3,<6

Expand All @@ -60,6 +61,7 @@ visual_test =
scipy
ipympl==0.8.7
ipyvolume
jupyterlab_miami_nights==0.3.2

[options.entry_points]
console_scripts =
Expand Down
6 changes: 3 additions & 3 deletions share/jupyter/voila/templates/base/tree.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@

<ul class="voila-notebooks">
{% if breadcrumbs|length > 1: %}
<li><a href="{{ breadcrumbs[-2][0] }}"><i class="fa fa-folder"></i>..</a></li>
<li><a href="{{ breadcrumbs[-2][0] }}?{{ query }}"><i class="fa fa-folder"></i>..</a></li>
{% endif %}

{% for content in contents.content %}
{% if content.type in ['notebook', 'file'] %}
<li><a href="{{ base_url }}voila/render/{{ content.path }}"><i class="fa fa-book"></i>{{content.name}}</a></li>
<li><a href="{{ base_url }}voila/render/{{ content.path }}?{{ query }}"><i class="fa fa-book"></i>{{content.name}}</a></li>
{% endif %}
{% if content.type == 'directory' %}
<li><a href="{{ base_url }}voila/tree/{{ content.path }}"><i class="fa fa-folder"></i>{{content.name}}</a></li>
<li><a href="{{ base_url }}voila/tree/{{ content.path }}?{{ query }}"><i class="fa fa-folder"></i>{{content.name}}</a></li>
{% endif %}
{% endfor %}
</ul>
Expand Down
3 changes: 2 additions & 1 deletion share/jupyter/voila/templates/lab/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
{{ include_css("static/index.css") }}
{{ include_css("static/theme-light.css") }}
{% else %}
<!-- TODO: Use custom css from theme labextension -->
{{ include_css("static/index.css") }}
{{ include_lab_theme(theme) }}
{% endif %}
{% endblock %}
6 changes: 3 additions & 3 deletions share/jupyter/voila/templates/lab/tree.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@

<ul class="voila-notebooks">
{% if breadcrumbs|length > 1: %}
<li><a href="{{ breadcrumbs[-2][0] }}"><i class="fa fa-folder"></i>..</a></li>
<li><a href="{{ breadcrumbs[-2][0] }}?{{ query }}"><i class="fa fa-folder"></i>..</a></li>
{% endif %}

{% for content in contents.content %}
{% if content.type in ['notebook', 'file'] %}
<li><a href="{{ base_url }}voila/render/{{ content.path }}"><i class="fa fa-book"></i>{{content.name}}</a></li>
<li><a href="{{ base_url }}voila/render/{{ content.path }}?{{ query }}"><i class="fa fa-book"></i>{{content.name}}</a></li>
{% endif %}
{% if content.type == 'directory' %}
<li><a href="{{ base_url }}voila/tree/{{ content.path }}"><i class="fa fa-folder"></i>{{content.name}}</a></li>
<li><a href="{{ base_url }}voila/tree/{{ content.path }}?{{ query }}"><i class="fa fa-folder"></i>{{content.name}}</a></li>
{% endif %}
{% endfor %}
</ul>
Expand Down
2 changes: 1 addition & 1 deletion ui-tests/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = {
['html']
],
use: {
baseURL: 'http://localhost:8866/voila/',
baseURL: 'http://localhost:8866',
video: 'retain-on-failure'
},
// Try one retry as some tests are flaky
Expand Down
149 changes: 137 additions & 12 deletions ui-tests/tests/voila.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,100 @@ test.describe('Voila performance Tests', () => {
test.afterEach(async ({ page, browserName }) => {
await page.close({ runBeforeUnload: true });
});
test('Render tree classic', async ({ page, browserName }, testInfo) => {
const testFunction = async () => {
await page.goto('?voila-template=classic');
// wait for page to load
await page.waitForSelector('.list-header');
};
await addBenchmarkToTest(
'voila-tree-classic',
testFunction,
testInfo,
browserName
);

expect(await page.screenshot()).toMatchSnapshot('voila-tree-classic.png');
});

test('Render tree light theme', async ({ page, browserName }, testInfo) => {
const testFunction = async () => {
await page.goto('');
// wait for page to load
await page.waitForSelector('.list-header');
};
await addBenchmarkToTest(
'voila-tree-light',
testFunction,
testInfo,
browserName
);

expect(await page.screenshot()).toMatchSnapshot('voila-tree-light.png');
});

test('Render tree dark theme', async ({ page, browserName }, testInfo) => {
const testFunction = async () => {
await page.goto('?voila-theme=dark');
// wait for page to load
await page.waitForSelector('.list-header');
};
await addBenchmarkToTest(
'voila-tree-dark',
testFunction,
testInfo,
browserName
);

expect(await page.screenshot()).toMatchSnapshot('voila-tree-dark.png');
});

test('Render tree miami theme', async ({ page, browserName }, testInfo) => {
const testFunction = async () => {
await page.goto('?voila-theme=jupyterlab_miami_nights');
// wait for page to load
await page.waitForSelector('.list-header');
};
await addBenchmarkToTest(
'voila-tree-miami',
testFunction,
testInfo,
browserName
);

expect(await page.screenshot()).toMatchSnapshot('voila-tree-miami.png');
});

test('Render and benchmark basics.ipynb with classic template', async ({
page,
browserName
}, testInfo) => {
const notebookName = 'basics';
const testFunction = async () => {
await page.goto(
`/voila/render/${notebookName}.ipynb?voila-template=classic`
);
// wait for the widgets to load
await page.waitForSelector('span[role="presentation"] >> text=x');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);

// wait for the final MathJax message to be hidden
await page.$('text=Typesetting math: 100%');
await page.waitForSelector('#MathJax_Message', { state: 'hidden' });
expect(await page.screenshot()).toMatchSnapshot(
`${notebookName}-classic.png`
);
});

test('Render and benchmark basics.ipynb', async ({
page,
browserName
}, testInfo) => {
const notebookName = 'basics';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
// wait for the widgets to load
// await page.waitForSelector('.jupyter-widgets');
await page.waitForSelector('span[role="presentation"] >> text=x');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);
Expand All @@ -36,13 +121,53 @@ test.describe('Voila performance Tests', () => {
expect(await page.screenshot()).toMatchSnapshot(`${notebookName}.png`);
});

test('Render basics.ipynb with dark theme', async ({
page,
browserName
}, testInfo) => {
const notebookName = 'basics';
const testFunction = async () => {
await page.goto(`/voila/render/${notebookName}.ipynb?voila-theme=dark`);
// wait for the widgets to load
await page.waitForSelector('span[role="presentation"] >> text=x');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);

// wait for the final MathJax message to be hidden
await page.$('text=Typesetting math: 100%');
await page.waitForSelector('#MathJax_Message', { state: 'hidden' });
expect(await page.screenshot()).toMatchSnapshot(`${notebookName}-dark.png`);
});

test('Render basics.ipynb with miami theme', async ({
page,
browserName
}, testInfo) => {
const notebookName = 'basics';
const testFunction = async () => {
await page.goto(
`/voila/render/${notebookName}.ipynb?voila-theme=jupyterlab_miami_nights`
);
// wait for the widgets to load
await page.waitForSelector('span[role="presentation"] >> text=x');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);

// wait for the final MathJax message to be hidden
await page.$('text=Typesetting math: 100%');
await page.waitForSelector('#MathJax_Message', { state: 'hidden' });
expect(await page.screenshot()).toMatchSnapshot(
`${notebookName}-miami.png`
);
});

test('Render and benchmark bqplot.ipynb', async ({
page,
browserName
}, testInfo) => {
const notebookName = 'bqplot';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
await page.waitForSelector('svg.svg-figure');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);
Expand All @@ -55,7 +180,7 @@ test.describe('Voila performance Tests', () => {
}, testInfo) => {
const notebookName = 'dashboard';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
await page.waitForSelector('svg.svg-figure');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);
Expand All @@ -71,7 +196,7 @@ test.describe('Voila performance Tests', () => {
}, testInfo) => {
const notebookName = 'gridspecLayout';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
await page.waitForSelector(
'button.jupyter-widgets.jupyter-button.widget-button >> text=10'
);
Expand All @@ -86,7 +211,7 @@ test.describe('Voila performance Tests', () => {
}, testInfo) => {
const notebookName = 'interactive';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
await page.waitForSelector('div.widget-slider.widget-hslider');
await page.fill('div.widget-readout', '8.00');
await page.keyboard.down('Enter');
Expand All @@ -104,7 +229,7 @@ test.describe('Voila performance Tests', () => {
}, testInfo) => {
const notebookName = 'ipympl';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
await page.waitForSelector('div.jupyter-matplotlib-figure');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);
Expand All @@ -117,7 +242,7 @@ test.describe('Voila performance Tests', () => {
}, testInfo) => {
const notebookName = 'ipyvolume';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
await page.waitForSelector('canvas');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);
Expand All @@ -130,7 +255,7 @@ test.describe('Voila performance Tests', () => {
}, testInfo) => {
const notebookName = 'multiple_widgets';
const testMultipleWidget = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
await page.waitForSelector(
'button.jupyter-widgets.jupyter-button.widget-button >> text=400'
);
Expand All @@ -149,14 +274,14 @@ test.describe('Voila performance Tests', () => {
}, testInfo) => {
const notebookName = 'query-strings';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
const userName = await page.$$(
'div.jp-RenderedText.jp-OutputArea-output > pre'
);
expect(await userName[1].innerHTML()).toContain('Hi Kim');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);
await page.goto(`render/${notebookName}.ipynb?username=Riley`);
await page.goto(`/voila/render/${notebookName}.ipynb?username=Riley`);
const userName = await page.$$(
'div.jp-RenderedText.jp-OutputArea-output > pre'
);
Expand All @@ -171,7 +296,7 @@ test.describe('Voila performance Tests', () => {
}, testInfo) => {
const notebookName = 'reveal';
const testFunction = async () => {
await page.goto(`render/${notebookName}.ipynb`);
await page.goto(`/voila/render/${notebookName}.ipynb`);
await page.waitForSelector('span[role="presentation"] >> text=x');
};
await addBenchmarkToTest(notebookName, testFunction, testInfo, browserName);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions voila/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
from jupyter_server.utils import url_path_join, run_sync
from jupyter_server.services.config import ConfigManager

from jupyterlab_server.themes_handler import ThemesHandler

from jupyter_client.kernelspec import KernelSpecManager

from jupyter_core.paths import jupyter_config_path, jupyter_path
Expand Down Expand Up @@ -481,6 +483,16 @@ def start(self):
'default_filename': 'index.html'
},
),
(
url_path_join(self.server_url, r'/voila/themes/(.*)'),
ThemesHandler,
{
'themes_url': '/voila/themes',
'path': '',
'labextensions_path': jupyter_path('labextensions'),
'no_cache_paths': ['/']
},
),
(url_path_join(self.server_url, r'/voila/api/shutdown/(.*)'), VoilaShutdownKernelHandler)
])

Expand Down
12 changes: 12 additions & 0 deletions voila/server_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from jupyter_server.utils import url_path_join
from jupyter_server.base.handlers import path_regex, FileFindHandler

from jupyterlab_server.themes_handler import ThemesHandler

from .paths import ROOT, collect_template_paths, collect_static_paths, jupyter_path
from .handler import VoilaHandler
from .treehandler import VoilaTreeHandler
Expand Down Expand Up @@ -67,6 +69,16 @@ def _load_jupyter_server_extension(server_app):
(url_path_join(base_url, '/voila'), VoilaTreeHandler, tree_handler_conf),
(url_path_join(base_url, '/voila/tree' + path_regex), VoilaTreeHandler, tree_handler_conf),
(url_path_join(base_url, '/voila/templates/(.*)'), TemplateStaticFileHandler),
(
url_path_join(base_url, r'/voila/themes/(.*)'),
ThemesHandler,
{
'themes_url': '/voila/themes',
'path': '',
'labextensions_path': jupyter_path('labextensions'),
'no_cache_paths': ['/']
},
),
(url_path_join(base_url, '/voila/static/(.*)'), MultiStaticFileHandler, {'paths': static_paths}),
(url_path_join(base_url, r'/voila/api/shutdown/(.*)'), VoilaShutdownKernelHandler),
(
Expand Down
Loading

0 comments on commit 6be66cc

Please sign in to comment.