diff --git a/.binder/environment.yml b/.binder/environment.yml index 030174f6f..7a0c648ae 100644 --- a/.binder/environment.yml +++ b/.binder/environment.yml @@ -2,7 +2,7 @@ name: voila channels: - conda-forge dependencies: - - jupyterlab=3 + - jupyterlab=3.6 - ipywidgets=8 - ipyvolume - bqplot @@ -10,3 +10,4 @@ dependencies: - ipympl - xleaflet=0.16.0 - xeus-cling=0.13.0 + - python=3.10 diff --git a/.eslintrc.js b/.eslintrc.js index 63280eb70..86246ee12 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,7 @@ module.exports = { ], '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-namespace': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-use-before-define': 'off', diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6b918a7a..8de6e8ccc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: - name: Install Dependencies run: | - python -m pip install -U jupyterlab~=3.0 jupyter_packaging~=0.10 + python -m pip install -U jupyterlab~=4.0 jupyter_packaging~=0.10 "notebook<7" - name: Install the Voilà Preview JupyterLab extension run: | diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml index 2ba0cac7d..6c2d0da54 100644 --- a/.github/workflows/check-release.yml +++ b/.github/workflows/check-release.yml @@ -20,14 +20,6 @@ jobs: - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - name: Upgrade packaging dependencies - run: | - pip install --upgrade jupyter-packaging~=0.10 --user - - - name: Install Dependencies - run: | - pip install . - - name: Check Release uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 198ecaebd..e2f28d28b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,7 +31,7 @@ jobs: - name: Create the conda environment shell: bash -l {0} - run: mamba install -q python=${{ matrix.python_version }} pip jupyterlab_pygments==0.1.0 pytest-cov pytest-rerunfailures nodejs yarn=1 ipywidgets matplotlib xeus-cling "traitlets>=5.0.3,<6" + run: mamba install -q python=${{ matrix.python_version }} pip jupyterlab_pygments==0.1.0 pytest-cov pytest-rerunfailures nodejs=18 yarn ipywidgets matplotlib xeus-cling "traitlets>=5.0.3,<6" ipykernel - name: Install dependencies shell: bash -l {0} @@ -51,13 +51,18 @@ jobs: - name: Run tests shell: bash -l {0} run: | - VOILA_TEST_XEUS_CLING=1 py.test tests/ --async-test-timeout=240 --reruns 2 --reruns-delay 1 + VOILA_TEST_XEUS_CLING=1 py.test tests/app --async-test-timeout=240 --reruns 2 --reruns-delay 1 + VOILA_TEST_XEUS_CLING=1 py.test tests/server --async-test-timeout=240 --reruns 2 --reruns-delay 1 --trace + py.test tests/execute_output_test.py voila --help # Making sure we can run `voila --help` # tests if voila sends a 'heartbeat' to avoid proxies from closing an apparently stale connection # Note that wget is the only easily available software that has a read-timeout voila tests/notebooks/sleep10seconds.ipynb --port=8878 --VoilaConfiguration.http_keep_alive_timeout=2 & sleep 2 wget --read-timeout=5 --tries=1 http://localhost:8878 + # Test nbconvert < 7.6.0 + python -m pip install "nbconvert<7.6.0" + VOILA_TEST_XEUS_CLING=1 py.test tests/app/image_inlining_test.py test-osx: runs-on: ${{ matrix.os }} @@ -65,7 +70,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-10.15] + os: [macos-12] python_version: ['3.8', '3.9', '3.10', '3.11'] steps: @@ -79,7 +84,7 @@ jobs: - name: Create the conda environment shell: bash -l {0} - run: mamba install -q python=${{ matrix.python_version }} pip jupyterlab_pygments==0.1.0 pytest-cov pytest-rerunfailures nodejs yarn=1 ipywidgets matplotlib xeus-cling openssl=1.1.1l "traitlets>=5.0.3,<6" + run: mamba install -q python=${{ matrix.python_version }} pip jupyterlab_pygments==0.1.0 pytest-cov pytest-rerunfailures nodejs=18 yarn ipywidgets matplotlib xeus-cling "traitlets>=5.0.3,<6" ipykernel - name: Install dependencies shell: bash -l {0} @@ -87,20 +92,16 @@ jobs: whereis python python --version yarn install --network-timeout 100000 - python -m pip install ".[test]" + python -m pip install ".[test,dev]" (cd tests/test_template; pip install .) (cd tests/skip_template; pip install .) - name: Run tests shell: bash -l {0} run: | - py.test tests/ --async-test-timeout=240 --reruns 2 --reruns-delay 1 - voila --help # Making sure we can run `voila --help` - # tests if voila sends a 'heartbeat' to avoid proxies from closing an apparently stale connection - # Note that wget is the only easily available software that has a read-timeout - voila tests/notebooks/sleep10seconds.ipynb --port=8878 --VoilaConfiguration.http_keep_alive_timeout=2 & - sleep 2 - wget --read-timeout=5 --tries=1 http://localhost:8878 + py.test tests/app --async-test-timeout=240 --reruns 2 --reruns-delay 1 + py.test tests/server --async-test-timeout=240 --reruns 2 --reruns-delay 1 --trace + py.test tests/execute_output_test.py test-win: runs-on: ${{ matrix.os }} @@ -120,11 +121,11 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: '16' + node-version: '18' - name: Install dependencies run: | - python -m pip install jupyterlab_pygments==0.1.0 pytest-cov pytest-rereunfailures ipywidgets matplotlib traitlets + python -m pip install jupyterlab_pygments==0.1.0 pytest-cov pytest-rerunfailures ipywidgets matplotlib traitlets ipykernel yarn install --network-timeout 100000 python -m pip install ".[test]" cd tests/test_template @@ -135,4 +136,6 @@ jobs: - name: Run test run: | set VOILA_TEST_DEBUG=1 - py.test tests/ --async-test-timeout=240 --reruns 2 --reruns-delay 1 + py.test tests/app --async-test-timeout=240 --reruns 2 --reruns-delay 1 + py.test tests/server --async-test-timeout=240 --reruns 2 --reruns-delay 1 --trace + py.test tests/execute_output_test.py diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index caf5d956a..bc3c1ab07 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | - python -m pip install setuptools jupyter_packaging~=0.10 "jupyterlab>=3,<4" build + python -m pip install setuptools jupyter_packaging~=0.10 "jupyterlab>=4,<5" build - name: Build pypi distributions run: | @@ -58,9 +58,9 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos, windows] - python: ['3.7', '3.10'] + python: ['3.8', '3.10'] include: - - python: '3.7' + - python: '3.8' dist: 'voila*.tar.gz' - python: '3.10' dist: 'voila*.whl' @@ -82,7 +82,7 @@ jobs: path: ./dist - name: Install the prerequisites run: | - ${{ matrix.py_cmd }} -m pip install pip wheel jupyterlab~=3.0 notebook~=6.4 + ${{ matrix.py_cmd }} -m pip install pip wheel "jupyterlab>=4.0.0,<5" notebook~=6.4 - name: Install the package run: | cd dist diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index d60cdad83..1a486575d 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -20,12 +20,13 @@ jobs: - name: Install dependencies run: | python -m pip install -r requirements-visual-test.txt + python -m pip install jupyterlab_miami_nights --no-deps python -m pip install ".[test]" jlpm jlpm build jupyter labextension develop . --overwrite cd ui-tests - jlpm install --frozen-lockfile + jlpm install - name: Launch Voila run: | @@ -34,7 +35,7 @@ jobs: working-directory: ui-tests - name: Install browser - run: jlpm playwright install chromium + run: npx playwright install chromium working-directory: ui-tests - name: Wait for Voila @@ -47,37 +48,6 @@ jobs: run: jlpm run test working-directory: ui-tests - - uses: iterative/setup-cml@v1 - - - name: Publish Results - env: - REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPORT: ./benchmark-results/voila-benchmark.md - shell: bash - run: | - cd ui-tests - - # Publish image to cml.dev - echo "" >> ${REPORT} - cml-publish ./benchmark-results/voila-benchmark.png --md >> ${REPORT} - echo "" >> ${REPORT} - - # Test if metadata have changed - export METADATA_DIFF="/tmp/metadata.diff" - diff -u <(jq --sort-keys .metadata benchmark-results/voila-benchmark.json) <(jq --sort-keys .metadata voila-benchmark-expected.json) > ${METADATA_DIFF} || true - if [[ -s ${METADATA_DIFF} ]]; then - echo "
:exclamation: Test metadata have changed" >> ${REPORT} - echo "" >> ${REPORT} - echo "\`\`\`diff" >> ${REPORT} - cat ${METADATA_DIFF} >> ${REPORT} - echo "\`\`\`" >> ${REPORT} - echo "" >> ${REPORT} - echo "
" >> ${REPORT} - fi - - # Save PR number for comment publication - echo "${{ github.event.number }}" > ./benchmark-results/NR - - name: Upload Playwright Test assets if: always() uses: actions/upload-artifact@v2 diff --git a/.github/workflows/update_galata_references.yaml b/.github/workflows/update_galata_references.yaml index c2bafd204..76171b165 100644 --- a/.github/workflows/update_galata_references.yaml +++ b/.github/workflows/update_galata_references.yaml @@ -17,8 +17,15 @@ jobs: name: Update Galata References if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'update galata references') }} runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + node-version: [16.x] steps: + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Checkout uses: actions/checkout@v2 with: @@ -40,7 +47,7 @@ jobs: jlpm build jupyter labextension develop . --overwrite cd ui-tests - jlpm install --frozen-lockfile + jlpm install - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@main with: diff --git a/.gitignore b/.gitignore index 251b0cf31..089503701 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ config.rst package-lock.json share/jupyter/voila/templates/base/static/*voila.js +share/jupyter/voila/templates/base/static/*treepage.js share/jupyter/voila/templates/base/static/*voila-style.js share/jupyter/voila/templates/base/static/*.woff share/jupyter/voila/templates/base/static/*.woff2 @@ -38,6 +39,8 @@ share/jupyter/voila/templates/base/static/*.svg share/jupyter/voila/templates/base/static/*.ttf share/jupyter/voila/templates/base/static/labvariables.css share/jupyter/voila/templates/base/static/materialcolors.css +share/jupyter/voila/templates/base/static/*.LICENSE.txt + lib @@ -48,3 +51,9 @@ ui-tests/playwright-report ui-tests/test-results ui-tests/benchmark-results ui-tests/jlab_root + +.yarn/ + +share/jupyter/voila/schemas +share/jupyter/voila/themes +share/jupyter/voila/style.js diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12b6ffe30..da8258513 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,12 +25,12 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.7.0 hooks: - id: black - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.255 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.281 hooks: - id: ruff args: ['--fix'] diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 000000000..fe1125f54 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,3 @@ +enableImmutableInstalls: false + +nodeLinker: node-modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c3a4aa24..5ba9e6722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,87 @@ +## 0.5.0a5 + +([Full Changelog](https://github.com/voila-dashboards/voila/compare/@voila-dashboards/jupyterlab-preview@2.3.0-alpha.4...ea11d94f9ee4eece6f517e1d8ef6191e4334c927)) + +### Enhancements made + +- JupyterLab 4 support [#1343](https://github.com/voila-dashboards/voila/pull/1343) ([@trungleduc](https://github.com/trungleduc)) +- Separate Tornado handlers into their own class [#1330](https://github.com/voila-dashboards/voila/pull/1330) ([@davidbrochart](https://github.com/davidbrochart)) + +### Bugs fixed + +- Do not clean up kernel resources after execution [#1334](https://github.com/voila-dashboards/voila/pull/1334) ([@martinRenou](https://github.com/martinRenou)) +- Add nbconvert version check [#1333](https://github.com/voila-dashboards/voila/pull/1333) ([@trungleduc](https://github.com/trungleduc)) +- Inject react-dom in the shared scope [#1320](https://github.com/voila-dashboards/voila/pull/1320) ([@martinRenou](https://github.com/martinRenou)) + +### Maintenance and upkeep improvements + +- Install yarn\<3 in dev env [#1328](https://github.com/voila-dashboards/voila/pull/1328) ([@davidbrochart](https://github.com/davidbrochart)) + +### API and Breaking Changes + +- JupyterLab 4 support [#1343](https://github.com/voila-dashboards/voila/pull/1343) ([@trungleduc](https://github.com/trungleduc)) + +### Other merged PRs + +- Fix typo \[skip ci\] [#1326](https://github.com/voila-dashboards/voila/pull/1326) ([@davidbrochart](https://github.com/davidbrochart)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/voila-dashboards/voila/graphs/contributors?from=2023-04-19&to=2023-07-06&type=c)) + +[@bsyouness](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Absyouness+updated%3A2023-04-19..2023-07-06&type=Issues) | [@davidbrochart](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Adavidbrochart+updated%3A2023-04-19..2023-07-06&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Agithub-actions+updated%3A2023-04-19..2023-07-06&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Ajtpio+updated%3A2023-04-19..2023-07-06&type=Issues) | [@maartenbreddels](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Amaartenbreddels+updated%3A2023-04-19..2023-07-06&type=Issues) | [@martinRenou](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3AmartinRenou+updated%3A2023-04-19..2023-07-06&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Apre-commit-ci+updated%3A2023-04-19..2023-07-06&type=Issues) | [@SylvainCorlay](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3ASylvainCorlay+updated%3A2023-04-19..2023-07-06&type=Issues) | [@trungleduc](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Atrungleduc+updated%3A2023-04-19..2023-07-06&type=Issues) + + + +## 0.5.0a4 + +([Full Changelog](https://github.com/voila-dashboards/voila/compare/v0.5.0a3...6663f0b1c4f8abe09a6c8fee10899a7e2da59a53)) + +### Enhancements made + +- Migrating to `jupyter-server` 2, `jupyter_client` 7.x [#1308](https://github.com/voila-dashboards/voila/pull/1308) ([@trungleduc](https://github.com/trungleduc)) + +### Maintenance and upkeep improvements + +- Update `galata` bot [#1318](https://github.com/voila-dashboards/voila/pull/1318) ([@trungleduc](https://github.com/trungleduc)) +- Set `cell-index` attribute as a string [#1317](https://github.com/voila-dashboards/voila/pull/1317) ([@jtpio](https://github.com/jtpio)) +- Remove unused `_get_query_string` function [#1316](https://github.com/voila-dashboards/voila/pull/1316) ([@jtpio](https://github.com/jtpio)) +- Clean up the check release workflow [#1310](https://github.com/voila-dashboards/voila/pull/1310) ([@jtpio](https://github.com/jtpio)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/voila-dashboards/voila/graphs/contributors?from=2023-03-23&to=2023-04-19&type=c)) + +[@github-actions](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Agithub-actions+updated%3A2023-03-23..2023-04-19&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Ajtpio+updated%3A2023-03-23..2023-04-19&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Apre-commit-ci+updated%3A2023-03-23..2023-04-19&type=Issues) | [@trungleduc](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Atrungleduc+updated%3A2023-03-23..2023-04-19&type=Issues) + +## 0.5.0a3 + +([Full Changelog](https://github.com/voila-dashboards/voila/compare/v0.5.0a2...34a52e7deb65a8e4f8e8a4512ac66b36c8cc733f)) + +### Enhancements made + +- Make use of JupyterLab mimetype renderers [#1249](https://github.com/voila-dashboards/voila/pull/1249) ([@martinRenou](https://github.com/martinRenou)) + +### Bugs fixed + +- Reveal template: Use proper routine for including JS asset [#1301](https://github.com/voila-dashboards/voila/pull/1301) ([@martinRenou](https://github.com/martinRenou)) + +### Maintenance and upkeep improvements + +- Add `pre-commit` hook [#1306](https://github.com/voila-dashboards/voila/pull/1306) ([@trungleduc](https://github.com/trungleduc)) +- Bump webpack from 5.75.0 to 5.76.0 [#1302](https://github.com/voila-dashboards/voila/pull/1302) ([@dependabot](https://github.com/dependabot)) +- Bump vega from 5.20.2 to 5.23.0 in /ui-tests [#1297](https://github.com/voila-dashboards/voila/pull/1297) ([@dependabot](https://github.com/dependabot)) +- Refactor styling [#1268](https://github.com/voila-dashboards/voila/pull/1268) ([@martinRenou](https://github.com/martinRenou)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/voila-dashboards/voila/graphs/contributors?from=2023-03-06&to=2023-03-23&type=c)) + +[@davidbrochart](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Adavidbrochart+updated%3A2023-03-06..2023-03-23&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Adependabot+updated%3A2023-03-06..2023-03-23&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Agithub-actions+updated%3A2023-03-06..2023-03-23&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Ajtpio+updated%3A2023-03-06..2023-03-23&type=Issues) | [@maartenbreddels](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Amaartenbreddels+updated%3A2023-03-06..2023-03-23&type=Issues) | [@martinRenou](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3AmartinRenou+updated%3A2023-03-06..2023-03-23&type=Issues) | [@philippjfr](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Aphilippjfr+updated%3A2023-03-06..2023-03-23&type=Issues) | [@trungleduc](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Atrungleduc+updated%3A2023-03-06..2023-03-23&type=Issues) + ## 0.5.0a2 ([Full Changelog](https://github.com/voila-dashboards/voila/compare/@voila-dashboards/jupyterlab-preview@2.3.0-alpha.1...ac93bd1a2b13cf2cbbf61b899a025520d9c6a769)) @@ -25,8 +106,6 @@ [@12rambau](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3A12rambau+updated%3A2023-02-21..2023-03-06&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Agithub-actions+updated%3A2023-02-21..2023-03-06&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Ajtpio+updated%3A2023-02-21..2023-03-06&type=Issues) | [@martinRenou](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3AmartinRenou+updated%3A2023-02-21..2023-03-06&type=Issues) | [@trungleduc](https://github.com/search?q=repo%3Avoila-dashboards%2Fvoila+involves%3Atrungleduc+updated%3A2023-02-21..2023-03-06&type=Issues) - - ## 0.5.0a1 ([Full Changelog](https://github.com/voila-dashboards/voila/compare/@voila-dashboards/jupyterlab-preview@2.3.0-alpha.0...45d595ff0d698f37dda6ed6f482e1e17ec9e9f5d)) diff --git a/docs/source/_static/main_stylesheet.css b/docs/source/_static/main_stylesheet.css index 8c07d18d8..eed575bac 100644 --- a/docs/source/_static/main_stylesheet.css +++ b/docs/source/_static/main_stylesheet.css @@ -6,3 +6,12 @@ .wy-nav-content img { margin: 5px auto 25px; } + +/*use voila palette for title coloring*/ +html[data-theme="light"] { + --pst-color-primary: #5dbcaf; +} + +html[data-theme="dark"] { + --pst-color-primary: #5dbcaf; +} diff --git a/docs/source/contribute.rst b/docs/source/contribute.rst index e7157a0d7..80c315b92 100755 --- a/docs/source/contribute.rst +++ b/docs/source/contribute.rst @@ -31,7 +31,7 @@ First, you need to fork the project. Then setup your environment: .. code-block:: bash # create a new conda environment - conda create -n voila -c conda-forge notebook jupyterlab nodejs yarn pip + conda create -n voila -c conda-forge notebook jupyterlab nodejs "yarn<3" pip conda activate voila # download voila from your GitHub fork diff --git a/docs/source/customize.rst b/docs/source/customize.rst index f74779dd1..e11b844d8 100755 --- a/docs/source/customize.rst +++ b/docs/source/customize.rst @@ -24,7 +24,7 @@ the following option: voila --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``. +Or by passing in the query parameter ``theme``, e.g. a URL like ``http://localhost:8867/voila/render/query-strings.ipynb?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 @@ -73,7 +73,7 @@ For example, to use the `gridstack --template=gridstack -Or by passing in the query parameter ``voila-template``, e.g. a URL like ``http://localhost:8867/voila/render/query-strings.ipynb?voila-template=material`` (Note that this requires installing voila-material). +Or by passing in the query parameter ``template``, e.g. a URL like ``http://localhost:8867/voila/render/query-strings.ipynb?template=material`` (Note that this requires installing voila-material). The template can also set in the notebook metadata, under ``metadata/voila/template`` by editing the notebook file manually, or using the metadata editor in for instance the classical notebook @@ -584,3 +584,18 @@ By default, Voilà does not have an execution timeout, meaning there is no limit voila --VoilaExecutor.timeout=30 your_notebook.ipynb With this setting, if any cell takes longer than 30 seconds to run, a ``TimeoutError`` will be raised. You can further customize this behavior using the ``VoilaExecutor.timeout_func`` and ``VoilaExecutor.interrupt_on_timeout`` options. + +Customizing the Voila Preview widget +========================================= + +By using the `layout customization system `_ of JupyterLab, users can configure the position of the Voila preview widget to open it in a different area than `main`. + +``Voila Preview`` is the setting key of the preview widget. For example, the following configuration will open this widget in the right panel of JupyterLab + +.. code-block:: javascript + + "layout": { + "multiple": { + "Voila Preview": { "area": "right" } + } + } diff --git a/docs/source/deploy.rst b/docs/source/deploy.rst index ad58ca322..d418580cd 100644 --- a/docs/source/deploy.rst +++ b/docs/source/deploy.rst @@ -81,41 +81,48 @@ Customizing Voilà on Binder To specify different options (such as the theme and template), create a ``jupyter_config.json`` file at the root of the repository with the following content: - .. code:: json +.. code:: json - { - "VoilaConfiguration": { - "theme": "dark", - "template": "gridstack" - } - } + { + "VoilaConfiguration": { + "theme": "dark", + "template": "gridstack" + } + } An example can be found in the `voila-demo `__ repository. -Deployment on Heroku --------------------- +Deployment on Railway +--------------------- + +.. note:: -Heroku.com is an attractive option if you want to try out deployment for -free. You have limited computing hours, however the app will also -automatically shutdown if it is idle. + Heroku.com was the suggested option for free deployment but since `November 28th 2022 `__, free + product plans have been removed from the platform. The process described in + this section remain valid for other services. -The general steps for deployment at Heroku can be found -`here `__. +`Railway.app `__ is an attractive option if you want to try +out deployment for free. You have limited computing hours, however the app will +also automatically shutdown if it is idle. + +The general steps for deployment at Railway can be found +`here `__. High level instructions, specific to Voilà can be found below: -1. Follow the steps of the official documentation to install the heroku - cli and login on your machine. -2. Add a file named runtime.txt to the project directory with a +1. Follow the steps of the official documentation to install the Railway + CLI and login on your machine. + +2. Add a file named runtime.txt to the project directory with a `valid Python runtime `__: .. code:: text python-3.10.4 -3. Add a file named Procfile to the project directory with the +3. Add a file named ``Procfile`` to the project directory with the following content if you want to show all notebooks: .. code:: text @@ -128,7 +135,7 @@ High level instructions, specific to Voilà can be found below: web: voila --port=$PORT --no-browser --Voila.ip=0.0.0.0 your_notebook.ipynb -4. Initialize your git repo and commit your code. At minimum you need to commit +4. Initialize a git repo and commit your code. At minimum you need to commit your notebooks, requirements.txt, runtime.txt, and the Procfile. .. code:: bash @@ -137,24 +144,23 @@ High level instructions, specific to Voilà can be found below: git add git commit -m "my message" -5. Create an Heroku instance and push the code. +5. Create an Railway instance and push the code. .. code:: bash - heroku create - git push heroku master + railway init 6. Open your web app .. code:: bash - heroku open + railway up --detach To resolve issues, it is useful to see the logs of your application. You can do this by running: - .. code:: bash +.. code:: bash - heroku logs --tail + railway up Deployment on Google App Engine @@ -218,96 +224,96 @@ Steps 1. SSH into the server: - .. code:: text + .. code:: text - ssh ubuntu@ + ssh ubuntu@ 2. Install nginx: - .. code:: text + .. code:: text - sudo apt install nginx + sudo apt install nginx 3. To check that ``nginx`` is correctly installed: - .. code:: text + .. code:: text - sudo systemctl status nginx + sudo systemctl status nginx 4. Create the file ``/etc/nginx/sites-enabled/yourdomain.com`` with the following content: - .. code:: text + .. code:: text - server { - listen 80; - server_name yourdomain.com; - proxy_buffering off; - location / { - proxy_pass http://localhost:8866; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_read_timeout 86400; - } - - client_max_body_size 100M; - error_log /var/log/nginx/error.log; - } + server { + listen 80; + server_name yourdomain.com; + proxy_buffering off; + location / { + proxy_pass http://localhost:8866; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + } + + client_max_body_size 100M; + error_log /var/log/nginx/error.log; + } 5. Enable and start the ``nginx`` service: - .. code:: text + .. code:: text - sudo systemctl enable nginx.service - sudo systemctl start nginx.service + sudo systemctl enable nginx.service + sudo systemctl start nginx.service 6. Install pip: - .. code:: text + .. code:: text - sudo apt update && sudo apt install python3-pip + sudo apt update && sudo apt install python3-pip 7. Follow the instructions in `Setup an example project`_, and install the dependencies: - .. code:: text - - sudo python3 -m pip install -r requirements.txt - -8. Create a new systemd service for running Voilà in ``/usr/lib/systemd/system/voila.service``. -The service will ensure Voilà is automatically restarted on startup: + .. code:: text - .. code:: text + sudo python3 -m pip install -r requirements.txt - [Unit] - Description=Voila +8. Create a new systemd service for running Voilà in ``/usr/lib/systemd/system/voila.service``. The service will ensure Voilà is automatically restarted on startup: - [Service] - Type=simple - PIDFile=/run/voila.pid - ExecStart=voila --no-browser voila/notebooks/basics.ipynb - User=ubuntu - WorkingDirectory=/home/ubuntu/ - Restart=always - RestartSec=10 + .. code:: text - [Install] - WantedBy=multi-user.target + [Unit] + Description=Voila + + [Service] + Type=simple + PIDFile=/run/voila.pid + ExecStart=voila --no-browser voila/notebooks/basics.ipynb + User=ubuntu + WorkingDirectory=/home/ubuntu/ + Restart=always + RestartSec=10 + + [Install] + WantedBy=multi-user.target In this example Voilà is started with ``voila --no-browser voila/notebooks/basics.ipynb`` to serve a single notebook. You can edit the command to change this behavior and the notebooks Voilà is serving. 9. Enable and start the ``voila`` service: - .. code:: text + .. code:: text - sudo systemctl enable voila.service - sudo systemctl start voila.service + sudo systemctl enable voila.service + sudo systemctl start voila.service .. note:: + To check the logs for Voilà: .. code:: text @@ -322,45 +328,45 @@ Enable HTTPS with Let's Encrypt 1. Install ``certbot``: - .. code:: text + .. code:: text - sudo add-apt-repository ppa:certbot/certbot - sudo apt update - sudo apt install python-certbot-nginx + sudo add-apt-repository ppa:certbot/certbot + sudo apt update + sudo apt install python-certbot-nginx 2. Obtain the certificates from Let's Encrypt. The ``--nginx`` flag will edit the nginx configuration automatically: - .. code:: text - - sudo certbot --nginx -d yourdomain.com + .. code:: text + + sudo certbot --nginx -d yourdomain.com 3. ``/etc/nginx/sites-enabled/yourdomain.com`` should now contain a few more entries: - .. code :: text - - $ cat /etc/nginx/sites-enabled/yourdomain.com + .. code :: text - ... - listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - ... + $ cat /etc/nginx/sites-enabled/yourdomain.com + + ... + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + ... 4. Visit ``yourdomain.com`` to access the Voilà applications over HTTPS. 5. To automatically renew the certificates (they expire after 90 days), open the ``crontab`` file: - .. code :: text + .. code :: text - crontab -e + crontab -e -And add the following line: + And add the following line: - .. code :: text + .. code :: text - 0 12 * * * /usr/bin/certbot renew --quiet + 0 12 * * * /usr/bin/certbot renew --quiet For more information, you can also follow `the guide on the nginx blog `__. diff --git a/notebooks/bokeh.ipynb b/notebooks/bokeh.ipynb new file mode 100644 index 000000000..387c70189 --- /dev/null +++ b/notebooks/bokeh.ipynb @@ -0,0 +1,133 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "## Serving a Bokeh application in Voila" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "import os\n", + "\n", + "from bokeh.layouts import column\n", + "from bokeh.models import ColumnDataSource, Slider\n", + "from bokeh.plotting import figure\n", + "from bokeh.themes import Theme\n", + "from bokeh.io import show, output_notebook\n", + "\n", + "from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature\n", + "\n", + "output_notebook()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Serving a Bokeh app in Voila is the same as serving it in JupyterLab, see https://docs.bokeh.org/en/latest/docs/user_guide/output/jupyter.html#jupyterlab" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def bkapp(doc):\n", + " df = sea_surface_temperature.copy()\n", + " source = ColumnDataSource(data=df)\n", + "\n", + " plot = figure(x_axis_type='datetime', y_range=(0, 25),\n", + " y_axis_label='Temperature (Celsius)',\n", + " title=\"Sea Surface Temperature at 43.18, -70.43\")\n", + " plot.line('time', 'temperature', source=source)\n", + "\n", + " def callback(attr, old, new):\n", + " if new == 0:\n", + " data = df\n", + " else:\n", + " data = df.rolling('{0}D'.format(new)).mean()\n", + " source.data = ColumnDataSource.from_df(data)\n", + "\n", + " slider = Slider(start=0, end=30, value=0, step=1, title=\"Smoothing by N Days\")\n", + " slider.on_change('value', callback)\n", + "\n", + " doc.add_root(column(slider, plot))\n", + "\n", + " doc.theme = Theme(json=yaml.load(\"\"\"\n", + " attrs:\n", + " Figure:\n", + " background_fill_color: \"#DDDDDD\"\n", + " outline_line_color: white\n", + " toolbar_location: above\n", + " height: 500\n", + " width: 800\n", + " Grid:\n", + " grid_line_dash: [6, 4]\n", + " grid_line_color: white\n", + " \"\"\", Loader=yaml.FullLoader))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: You need to specify the Voila URL to Bokeh, by passing it to the `show` method:\n", + "\n", + "```python\n", + "import os\n", + "from urllib.parse import urlparse\n", + "\n", + "url = urlparse(os.environ.get(\"VOILA_REQUEST_URL\"))\n", + "\n", + "show(bkapp, notebook_url=f\"{url.scheme}://{url.netloc}\")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from urllib.parse import urlparse\n", + "\n", + "url = urlparse(os.environ.get(\"VOILA_REQUEST_URL\"))\n", + "\n", + "show(bkapp, notebook_url=f\"{url.scheme}://{url.netloc}\")" + ] + } + ], + "metadata": { + "anaconda-cloud": {}, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/mimerenderers.ipynb b/notebooks/mimerenderers.ipynb index e8116a73e..1ddf44ed9 100644 --- a/notebooks/mimerenderers.ipynb +++ b/notebooks/mimerenderers.ipynb @@ -1,143 +1,473 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ff203dda-d0f3-48a8-95d5-587fbe1acae8", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import display\n", + "from IPython.display import (\n", + " HTML, Image, Latex, Math, Markdown, SVG\n", + ")" + ] + }, { "cell_type": "markdown", + "id": "83934e25-4c2a-4521-b128-f4da77793fe8", "metadata": {}, "source": [ - "# JSON" + "## Text" ] }, { "cell_type": "code", "execution_count": null, + "id": "4c0ef0c2-a7c2-4aee-b047-d009e9794ef9", "metadata": {}, "outputs": [], "source": [ - "from ipywidgets import Button, Output\n", - "from IPython import display\n", - "\n", - "button = Button(description='Output JSON')\n", - "output = Output()\n", - "obj = {\n", - " \"abcde\": 1234,\n", - " \"nested\": list(range(10))\n", - "}\n", - "\n", - "@output.capture()\n", - "def on_click(change):\n", - " display.display(display.JSON(obj))\n", - " \n", - " \n", - "button.on_click(on_click)\n", - "display.display(button)\n", - "output" + "text = \"\"\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam urna\n", + "libero, dictum a egestas non, placerat vel neque. In imperdiet iaculis fermentum. \n", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia \n", + "Curae; Cras augue tortor, tristique vitae varius nec, dictum eu lectus. Pellentesque \n", + "id eleifend eros. In non odio in lorem iaculis sollicitudin. In faucibus ante ut \n", + "arcu fringilla interdum. Maecenas elit nulla, imperdiet nec blandit et, consequat \n", + "ut elit.\"\"\"\n", + "print(text)" ] }, { "cell_type": "code", "execution_count": null, + "id": "9f841613-56a3-4b9f-a342-929f77534159", "metadata": {}, "outputs": [], "source": [ - "display.JSON(obj)" + "import sys; print('this is stderr', file=sys.stderr)" ] }, { "cell_type": "markdown", + "id": "ff9bcbcb-8b11-4215-a5bd-a82265ab63f2", "metadata": {}, "source": [ - "# Fasta" + "## HTML" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "317c2b55-881f-436c-8cc5-1753e8ebb4b6", + "metadata": {}, + "outputs": [], + "source": [ + "div = HTML('
')\n", + "div" + ] + }, + { + "cell_type": "markdown", + "id": "e410f2d6-d6be-4dc5-b3fa-80ee61ac30ec", + "metadata": {}, + "source": [ + "## Markdown" ] }, { "cell_type": "code", "execution_count": null, + "id": "8088840b-ebec-4964-b823-4864330aa021", "metadata": {}, "outputs": [], "source": [ - "fasta_button = Button(description='Output FASTA')\n", - "fasta_output = Output()\n", + "md = Markdown(\"\"\"\n", + "### Subtitle\n", "\n", + "This is some *markdown* text with math $F=ma$.\n", "\n", - "def Fasta(data=''):\n", - " bundle = {}\n", - " bundle['application/vnd.fasta.fasta'] = data\n", - " bundle['text/plain'] = data\n", - " display.display(bundle, raw=True)\n", - " \n", - " \n", - "@fasta_output.capture()\n", - "def on_click(change):\n", - " Fasta(\"\"\">SEQUENCE_1\n", - "MTEITAAMVKELRESTGAGMMDCKNALSETNGDFDKAVQLLREKGLGKAAKKADRLAAEG\n", - "LVSVKVSDDFTIAAMRPSYLSYEDLDMTFVENEYKALVAELEKENEERRRLKDPNKPEHK\n", - "IPQFASRKQLSDAILKEAEEKIKEELKAQGKPEKIWDNIIPGKMNSFIADNSQLDSKLTL\n", - "MGQFYVMDDKKTVEQVIAEKEKEFGGKIKIVEFICFEVGEGLEKKTEDFAAEVAAQL\n", - ">SEQUENCE_2\n", - "SATVSEINSETDFVAKNDQFIALTKDTTAHIQSNSLQSVEELHSSTINGVKFEEYLKSQI\n", - "ATIGENLVVRRFATLKAGANGVVNGYIHTNGRVGVVIAAACDSAEVASKSRDLLRQICMH\"\"\")\n", + "\"\"\")\n", + "md" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de749ce9-0531-4151-b2c5-6100706ddd59", + "metadata": {}, + "outputs": [], + "source": [ + "display(md)" + ] + }, + { + "cell_type": "markdown", + "id": "a4d62188-c67b-4058-a679-a08f6ad0ad87", + "metadata": {}, + "source": [ + "## LaTeX" + ] + }, + { + "cell_type": "markdown", + "id": "09eb8496-cea0-496a-97de-f887ce924d3d", + "metadata": {}, + "source": [ + "Examples LaTeX in a markdown cell:\n", "\n", "\n", - "fasta_button.on_click(on_click)\n", - "display.display(fasta_button)\n", - "fasta_output" + "\\begin{align}\n", + "\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} & = \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\\\ \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n", + "\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n", + "\\nabla \\cdot \\vec{\\mathbf{B}} & = 0\n", + "\\end{align}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e59f7a97-9ad5-424f-ba99-2f69deba04a1", + "metadata": {}, + "outputs": [], + "source": [ + "math = Latex(\"$F=ma$\")\n", + "math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f14b258-22e0-465d-9b86-d134e7d4a07c", + "metadata": {}, + "outputs": [], + "source": [ + "maxwells = Latex(r\"\"\"\n", + "\\begin{align}\n", + "\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} & = \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\\\ \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n", + "\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n", + "\\nabla \\cdot \\vec{\\mathbf{B}} & = 0\n", + "\\end{align}\n", + "\"\"\")\n", + "maxwells" + ] + }, + { + "cell_type": "markdown", + "id": "cefadf9b-3bab-451e-963d-38cee15c05ea", + "metadata": {}, + "source": [ + "## PDF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0654a20f-1c14-47fd-a8b2-4de3fcbe6379", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from IPython.display import set_matplotlib_formats\n", + "set_matplotlib_formats('pdf')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e472026e-1019-4ef1-aae7-190dfa27c6a6", + "metadata": {}, + "outputs": [], + "source": [ + "plt.scatter(np.random.rand(20), np.random.rand(20), c=np.random.rand(20))" ] }, { "cell_type": "markdown", + "id": "52d8c23c-85e9-4a58-b2ab-a1686f5efaf3", "metadata": {}, "source": [ - "# GeoJSON" + "## Image" ] }, { "cell_type": "code", "execution_count": null, + "id": "e6b7d130-7033-4783-bec1-031beabe66ef", "metadata": {}, "outputs": [], "source": [ - "from IPython.display import GeoJSON\n", + "img = Image(\"https://apod.nasa.gov/apod/image/1707/GreatWallMilkyWay_Yu_1686.jpg\")\n", + "img" + ] + }, + { + "cell_type": "markdown", + "id": "f0063685-9296-4149-abfb-5d0e15cc0b3c", + "metadata": {}, + "source": [ + "## SVG" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "959bbce1-1bbb-45a4-8373-d3994bab420d", + "metadata": {}, + "outputs": [], + "source": [ + "svg_source = \"\"\"\n", + "\n", + " \n", + "\n", + "\"\"\"\n", + "svg = SVG(svg_source)\n", + "svg" + ] + }, + { + "cell_type": "markdown", + "id": "41009292-8e51-4fde-90d2-804a52aa6d7f", + "metadata": {}, + "source": [ + "## HTML Tables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbc0850d-bce6-4b6d-901e-c6ea1b857898", + "metadata": {}, + "outputs": [], + "source": [ + "from vega_datasets import data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "384e0e32-001d-4e40-bda7-4c76578fe123", + "metadata": {}, + "outputs": [], + "source": [ + "df = data.cars()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "702d284d-fefb-4b68-a327-ab311179c991", + "metadata": {}, + "outputs": [], + "source": [ + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "5355a073-8ab7-46f9-87c8-04f2a2c4af7c", + "metadata": {}, + "source": [ + "## Vega" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba337247-3bfa-4f70-b067-b25d4236660c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from IPython.display import display\n", + "import pandas as pd\n", + "\n", + "def Vega(spec):\n", + " bundle = {}\n", + " bundle['application/vnd.vega.v5+json'] = spec\n", + " display(bundle, raw=True)\n", + "\n", + "def VegaLite(spec):\n", + " bundle = {}\n", + " bundle['application/vnd.vegalite.v4+json'] = spec\n", + " display(bundle, raw=True)\n", + "\n", + "Vega({\n", + " \"$schema\": \"https://vega.github.io/schema/vega/v5.0.json\",\n", + " \"width\": 400,\n", + " \"height\": 200,\n", + " \"padding\": 5,\n", "\n", + " \"data\": [\n", + " {\n", + " \"name\": \"table\",\n", + " \"values\": [\n", + " {\"category\": \"A\", \"amount\": 28},\n", + " {\"category\": \"B\", \"amount\": 55},\n", + " {\"category\": \"C\", \"amount\": 43},\n", + " {\"category\": \"D\", \"amount\": 91},\n", + " {\"category\": \"E\", \"amount\": 81},\n", + " {\"category\": \"F\", \"amount\": 53},\n", + " {\"category\": \"G\", \"amount\": 19},\n", + " {\"category\": \"H\", \"amount\": 87}\n", + " ]\n", + " }\n", + " ],\n", "\n", - "geojson_button = Button(description='Output GeoJSON')\n", - "geojson_output = Output()\n", + " \"signals\": [\n", + " {\n", + " \"name\": \"tooltip\",\n", + " \"value\": {},\n", + " \"on\": [\n", + " {\"events\": \"rect:mouseover\", \"update\": \"datum\"},\n", + " {\"events\": \"rect:mouseout\", \"update\": \"{}\"}\n", + " ]\n", + " }\n", + " ],\n", "\n", - " \n", - "@geojson_output.capture()\n", - "def on_click(change):\n", - " obj = GeoJSON({\n", - " \"type\": \"Feature\",\n", - " \"geometry\": {\n", - " \"type\": \"Point\",\n", - " \"coordinates\": [-118.4563712, 34.0163116]\n", + " \"scales\": [\n", + " {\n", + " \"name\": \"xscale\",\n", + " \"type\": \"band\",\n", + " \"domain\": {\"data\": \"table\", \"field\": \"category\"},\n", + " \"range\": \"width\",\n", + " \"padding\": 0.05,\n", + " \"round\": True\n", + " },\n", + " {\n", + " \"name\": \"yscale\",\n", + " \"domain\": {\"data\": \"table\", \"field\": \"amount\"},\n", + " \"nice\": True,\n", + " \"range\": \"height\"\n", + " }\n", + " ],\n", + "\n", + " \"axes\": [\n", + " { \"orient\": \"bottom\", \"scale\": \"xscale\" },\n", + " { \"orient\": \"left\", \"scale\": \"yscale\" }\n", + " ],\n", + "\n", + " \"marks\": [\n", + " {\n", + " \"type\": \"rect\",\n", + " \"from\": {\"data\":\"table\"},\n", + " \"encode\": {\n", + " \"enter\": {\n", + " \"x\": {\"scale\": \"xscale\", \"field\": \"category\"},\n", + " \"width\": {\"scale\": \"xscale\", \"band\": 1},\n", + " \"y\": {\"scale\": \"yscale\", \"field\": \"amount\"},\n", + " \"y2\": {\"scale\": \"yscale\", \"value\": 0}\n", + " },\n", + " \"update\": {\n", + " \"fill\": {\"value\": \"steelblue\"}\n", + " },\n", + " \"hover\": {\n", + " \"fill\": {\"value\": \"red\"}\n", + " }\n", + " }\n", + " },\n", + " {\n", + " \"type\": \"text\",\n", + " \"encode\": {\n", + " \"enter\": {\n", + " \"align\": {\"value\": \"center\"},\n", + " \"baseline\": {\"value\": \"bottom\"},\n", + " \"fill\": {\"value\": \"#333\"}\n", + " },\n", + " \"update\": {\n", + " \"x\": {\"scale\": \"xscale\", \"signal\": \"tooltip.category\", \"band\": 0.5},\n", + " \"y\": {\"scale\": \"yscale\", \"signal\": \"tooltip.amount\", \"offset\": -2},\n", + " \"text\": {\"signal\": \"tooltip.amount\"},\n", + " \"fillOpacity\": [\n", + " {\"test\": \"datum === tooltip\", \"value\": 0},\n", + " {\"value\": 1}\n", + " ]\n", " }\n", - " })\n", - " display.display(obj)\n", - " \n", + " }\n", + " }\n", + " ]\n", + "})" + ] + }, + { + "cell_type": "markdown", + "id": "be6347a4-f418-49d2-a83e-ba0ef6366505", + "metadata": {}, + "source": [ + "## GeoJSON" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67a6bc44-7588-4ae0-8eae-0428b6428f80", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from IPython.display import GeoJSON, JSON\n", "\n", - "geojson_button.on_click(on_click)\n", - "display.display(geojson_button)\n", - "geojson_output" + "data = {\n", + " \"type\": \"Feature\",\n", + " \"geometry\": {\n", + " \"type\": \"Point\",\n", + " \"coordinates\": [-118.4563712, 34.0163116]\n", + " }\n", + "}\n", + "\n", + "GeoJSON(data)" ] }, { "cell_type": "code", "execution_count": null, + "id": "1a1a2fc6", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "JSON(data)" + ] + }, + { + "cell_type": "markdown", + "id": "dc69fbe1", + "metadata": {}, + "source": [ + "# Fasta" + ] }, { "cell_type": "code", "execution_count": null, + "id": "85730759", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "def Fasta(data=''):\n", + " bundle = {}\n", + " bundle['application/vnd.fasta.fasta'] = data\n", + " bundle['text/plain'] = data\n", + " display(bundle, raw=True)\n", + "\n", + "\n", + "Fasta(\"\"\">SEQUENCE_1\n", + "MTEITAAMVKELRESTGAGMMDCKNALSETNGDFDKAVQLLREKGLGKAAKKADRLAAEG\n", + "LVSVKVSDDFTIAAMRPSYLSYEDLDMTFVENEYKALVAELEKENEERRRLKDPNKPEHK\n", + "IPQFASRKQLSDAILKEAEEKIKEELKAQGKPEKIWDNIIPGKMNSFIADNSQLDSKLTL\n", + "MGQFYVMDDKKTVEQVIAEKEKEFGGKIKIVEFICFEVGEGLEKKTEDFAAEVAAQL\n", + ">SEQUENCE_2\n", + "SATVSEINSETDFVAKNDQFIALTKDTTAHIQSNSLQSVEELHSSTINGVKFEEYLKSQI\n", + "ATIGENLVVRRFATLKAGANGVVNGYIHTNGRVGVVIAAACDSAEVASKSRDLLRQICMH\"\"\")" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -151,7 +481,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.1" + "version": "3.10.6" } }, "nbformat": 4, diff --git a/notebooks/yaml.ipynb b/notebooks/yaml.ipynb new file mode 100644 index 000000000..7f3fb2d6f --- /dev/null +++ b/notebooks/yaml.ipynb @@ -0,0 +1,54 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "d956aba2-61bc-43b8-810c-82aa31b2af44", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Markdown, clear_output\n", + "from ipywidgets import ToggleButton, Output\n", + "import json\n", + "import yaml\n", + "\n", + "STR_JSON=\"\"\"{\"hey\": {\n", + " \"1\": \"hi\",\n", + " \"2\": \"yo\",\n", + " \"a\": {\n", + " \"b\": [\n", + " {\"value\": \"3\"},\n", + " {\"value\": \"5\"},\n", + " {\"value\": \"5\"}\n", + " ]\n", + " }\n", + "}}\"\"\"\n", + "\n", + "parsed = json.loads(STR_JSON) \n", + "s = yaml.dump(parsed, indent=2)\n", + "display(Markdown(\"\\n```yaml\\n\" + s + \"\\n```\")) " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/package.json b/package.json index 5e8865738..e8739c9b5 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,8 @@ "build:prod": "lerna run build:prod", "build:test": "lerna run build:test", "clean": "lerna run clean", - "eslint": "eslint . --ext .ts,.tsx,.js,.jsx --fix", - "eslint:check": "eslint . --ext .ts,.tsx,.js,.jsx", - "install": "lerna bootstrap", + "eslint": "eslint . --ext .ts,.tsx --fix", + "eslint:check": "eslint . --ext .ts,.tsx", "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "prettier --list-different \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "test": "lerna run test" diff --git a/packages/jupyterlab-preview/package.json b/packages/jupyterlab-preview/package.json index c2a64b077..fb305b49f 100644 --- a/packages/jupyterlab-preview/package.json +++ b/packages/jupyterlab-preview/package.json @@ -1,6 +1,6 @@ { "name": "@voila-dashboards/jupyterlab-preview", - "version": "2.3.0-alpha.2", + "version": "2.3.0-alpha.5", "description": "A JupyterLab preview extension for Voilà", "keywords": [ "jupyter", @@ -46,29 +46,31 @@ "watch:src": "tsc -w" }, "dependencies": { - "@jupyterlab/application": "^3.0.0", - "@jupyterlab/apputils": "^3.0.0", - "@jupyterlab/coreutils": "^5.0.0", - "@jupyterlab/docregistry": "^3.0.0", - "@jupyterlab/fileeditor": "^3.0.0", - "@jupyterlab/mainmenu": "^3.0.0", - "@jupyterlab/notebook": "^3.0.0", - "@jupyterlab/settingregistry": "^3.0.0", - "@jupyterlab/ui-components": "^3.0.0", - "@lumino/coreutils": "^1.5.3", - "@lumino/signaling": "^1.4.3", - "react": "^17.0.1", - "react-dom": "^17.0.1" + "@jupyterlab/application": "^4.0.0", + "@jupyterlab/apputils": "^4.0.0", + "@jupyterlab/coreutils": "^6.0.0", + "@jupyterlab/docregistry": "^4.0.0", + "@jupyterlab/fileeditor": "^4.0.0", + "@jupyterlab/mainmenu": "^4.0.0", + "@jupyterlab/notebook": "^4.0.0", + "@jupyterlab/settingregistry": "^4.0.0", + "@jupyterlab/ui-components": "^4.0.0", + "@lumino/coreutils": "^2.0.0", + "@lumino/signaling": "^2.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { "@babel/core": "^7.10.2", "@babel/preset-env": "^7.10.2", - "@jupyterlab/builder": "^3.0.0", - "@jupyterlab/testutils": "^3.0.0", - "@types/react": "^17.0.0", - "@types/react-dom": "^17.0.0", + "@jupyterlab/builder": "^4.0.0", + "@jupyterlab/testutils": "^4.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "npm-run-all": "^4.1.5", "rimraf": "^2.6.1", - "typescript": "~4.1.3" + "source-map-loader": "~1.0.2", + "typescript": "~5.0.2" }, "jupyterlab": { "extension": true, diff --git a/packages/jupyterlab-preview/src/index.ts b/packages/jupyterlab-preview/src/index.ts index b61cae2a7..69eabea66 100644 --- a/packages/jupyterlab-preview/src/index.ts +++ b/packages/jupyterlab-preview/src/index.ts @@ -106,7 +106,6 @@ const extension: JupyterFrontEndPlugin = { const tracker = new WidgetTracker({ namespace: 'voila-preview' }); - if (restorer) { restorer.restore(tracker, { command: 'docmanager:open', @@ -143,7 +142,7 @@ const extension: JupyterFrontEndPlugin = { } const factory = new VoilaPreviewFactory(getVoilaUrl, { - name: 'Voila-preview', + name: 'Voila Preview', fileTypes: ['notebook'], modelName: 'notebook' }); @@ -191,7 +190,7 @@ const extension: JupyterFrontEndPlugin = { } commands.execute('docmanager:open', { path: context.path, - factory: 'Voila-preview', + factory: 'Voila Preview', options: { mode: 'split-right' } diff --git a/packages/voila/package.json b/packages/voila/package.json index 0f866204f..3f0b83c17 100644 --- a/packages/voila/package.json +++ b/packages/voila/package.json @@ -1,67 +1,68 @@ { "name": "@voila-dashboards/voila", - "version": "0.5.0-alpha.2", + "version": "0.5.0-alpha.5", "description": "The Voilà Frontend", "author": "Voilà contributors", "license": "BSD-3-Clause", "main": "lib/index.js", "browserslist": ">0.8%, not ie 11, not op_mini all, not dead", "dependencies": { - "@jupyter-widgets/base": "^6.0.1", - "@jupyter-widgets/controls": "^5.0.1", - "@jupyter-widgets/jupyterlab-manager": "^5.0.3", - "@jupyter-widgets/output": "^6.0.1", - "@jupyterlab/application": "^3.0.0", - "@jupyterlab/apputils": "^3.0.0", - "@jupyterlab/coreutils": "^5.0.0", - "@jupyterlab/docregistry": "^3.0.0", - "@jupyterlab/json-extension": "^3.0.0", - "@jupyterlab/logconsole": "^3.0.0", - "@jupyterlab/mainmenu": "^3.0.0", - "@jupyterlab/markdownviewer-extension": "^3.5.0", - "@jupyterlab/mathjax2-extension": "^3.0.0", - "@jupyterlab/nbformat": "^3.0.0", - "@jupyterlab/notebook": "^3.0.0", - "@jupyterlab/outputarea": "^3.0.0", - "@jupyterlab/rendermime": "^3.0.0", - "@jupyterlab/rendermime-extension": "^3.0.0", - "@jupyterlab/services": "^6.1.8", - "@jupyterlab/settingregistry": "^3.0.0", - "@jupyterlab/translation": "^3.0.0", - "@jupyterlab/ui-components": "^3.0.0", - "@lumino/algorithm": "^1.6.2", - "@lumino/commands": "^1.15.2", - "@lumino/coreutils": "^1.8.2", - "@lumino/disposable": "^1.7.2", - "@lumino/domutils": "^1.5.2", - "@lumino/dragdrop": "^1.10.2", - "@lumino/messaging": "^1.7.2", - "@lumino/properties": "^1.5.2", - "@lumino/signaling": "^1.7.2", - "@lumino/virtualdom": "^1.11.2", - "@lumino/widgets": "^1.26.2", - "react": "^17.0.1" + "@jupyter-widgets/base": "^6.0.5", + "@jupyter-widgets/jupyterlab-manager": "^5.0.8", + "@jupyterlab/application": "^4.0.0", + "@jupyterlab/apputils": "^4.0.0", + "@jupyterlab/apputils-extension": "^4.0.0", + "@jupyterlab/codemirror": "^4.0.3", + "@jupyterlab/codemirror-extension": "^4.0.0", + "@jupyterlab/coreutils": "^6.0.0", + "@jupyterlab/docregistry": "^4.0.0", + "@jupyterlab/javascript-extension": "^4.0.0", + "@jupyterlab/json-extension": "^4.0.0", + "@jupyterlab/logconsole": "^4.0.0", + "@jupyterlab/mainmenu": "^4.0.0", + "@jupyterlab/markdownviewer-extension": "^4.0.0", + "@jupyterlab/markedparser-extension": "^4.0.0", + "@jupyterlab/mathjax-extension": "^4.0.0", + "@jupyterlab/nbformat": "^4.0.0", + "@jupyterlab/notebook": "^4.0.0", + "@jupyterlab/outputarea": "^4.0.0", + "@jupyterlab/rendermime": "^4.0.0", + "@jupyterlab/rendermime-extension": "^4.0.0", + "@jupyterlab/services": "^7.0.0", + "@jupyterlab/settingregistry": "^4.0.0", + "@jupyterlab/theme-dark-extension": "^4.0.2", + "@jupyterlab/theme-light-extension": "^4.0.2", + "@jupyterlab/translation": "^4.0.0", + "@jupyterlab/ui-components": "^4.0.0", + "@jupyterlab/vega5-extension": "^4.0.0", + "@lumino/algorithm": "^2.0.0", + "@lumino/commands": "^2.0.0", + "@lumino/coreutils": "^2.0.0", + "@lumino/datagrid": "^2.1.2", + "@lumino/disposable": "^2.0.0", + "@lumino/domutils": "^2.0.0", + "@lumino/dragdrop": "^2.0.0", + "@lumino/messaging": "^2.0.0", + "@lumino/properties": "^2.0.0", + "@lumino/signaling": "^2.0.0", + "@lumino/virtualdom": "^2.0.0", + "@lumino/widgets": "^2.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "style-mod": "^4.0.3" }, "devDependencies": { - "@babel/core": "^7.2.2", - "@babel/preset-env": "^7.3.1", - "@jupyterlab/builder": "^3.0.0", - "@types/node": "^18.8.3", - "babel-loader": "^8.0.5", + "@jupyterlab/builder": "^4.0.0", + "@types/node": "~18.8.3", "css-loader": "^6.7.2", - "file-loader": "^6.2.0", "fs-extra": "^9.1.0", "glob": "~7.1.6", - "mini-css-extract-plugin": "~0.9.0", "npm-run-all": "^4.1.5", "p-limit": "^2.2.2", - "raw-loader": "^4.0.2", "rimraf": "^3.0.2", - "sass-loader": "^13.2.0", - "style-loader": "^2.0.0", - "svg-url-loader": "^7.1.1", - "typescript": "~4.1.3", - "url-loader": "^4.1.1", + "style-loader": "~3.3.1", + "tsc-watch": "^6.0.0", + "typescript": "~5.0.2", "watch": "^1.0.2", "webpack": "^5.24.1", "webpack-bundle-analyzer": "^4.4.0", @@ -73,10 +74,11 @@ "build": "npm run build:lib && webpack --mode=development", "build:lib": "tsc", "build:prod": "npm run build:lib && webpack --mode=production", - "clean": "jlpm run clean:lib && rimraf build", + "clean": "jlpm run clean:lib && jlpm run clean:asset && rimraf build", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", + "clean:asset": "rimraf ../../share/jupyter/voila/schemas ../../share/jupyter/voila/themes ../../share/jupyter/voila/style.js", "test": "echo \"Error: no test specified\" && exit 1", - "watch": "npm-run-all -p watch:*", + "watch": "tsc-watch --onSuccess \"webpack --mode=development\"", "watch:lib": "tsc -w", "watch:bundle": "webpack --watch --mode=development" } diff --git a/packages/voila/src/app.ts b/packages/voila/src/app.ts index b9aa0ca32..3e1c7ea67 100644 --- a/packages/voila/src/app.ts +++ b/packages/voila/src/app.ts @@ -1,3 +1,5 @@ +import { PromiseDelegate } from '@lumino/coreutils'; + import { JupyterFrontEnd, JupyterFrontEndPlugin, @@ -8,6 +10,8 @@ import { PageConfig } from '@jupyterlab/coreutils'; import { IRenderMime } from '@jupyterlab/rendermime'; +import { KernelWidgetManager } from '@jupyter-widgets/jupyterlab-manager'; + import { IShell, VoilaShell } from './shell'; const PACKAGE = require('../package.json'); @@ -113,6 +117,27 @@ export class VoilaApp extends JupyterFrontEnd { this.registerPluginModule(mod); }); } + + /** + * A promise that resolves when the Voila Widget Manager is created + */ + get widgetManagerPromise(): PromiseDelegate { + return this._widgetManagerPromise; + } + + set widgetManager(manager: KernelWidgetManager | null) { + this._widgetManager = manager; + if (this._widgetManager) { + this._widgetManagerPromise.resolve(this._widgetManager); + } + } + + get widgetManager(): KernelWidgetManager | null { + return this._widgetManager; + } + + private _widgetManager: KernelWidgetManager | null = null; + private _widgetManagerPromise = new PromiseDelegate(); } /** diff --git a/packages/voila/src/global.d.ts b/packages/voila/src/global.d.ts index 668cb973e..6584727b9 100644 --- a/packages/voila/src/global.d.ts +++ b/packages/voila/src/global.d.ts @@ -4,3 +4,6 @@ declare function __webpack_init_sharing__(arg: any); declare var _JUPYTERLAB; declare var __webpack_share_scopes__: any; declare var jupyterapp: any; +declare var themeLoaded: boolean; +declare var cellLoaded: boolean; +declare function voila_finish(); diff --git a/packages/voila/src/index.ts b/packages/voila/src/index.ts index 08dab445c..c786492a7 100644 --- a/packages/voila/src/index.ts +++ b/packages/voila/src/index.ts @@ -8,7 +8,6 @@ ****************************************************************************/ export * from './app'; -export * from './manager'; export * from './shell'; -export * from './output'; -export * from './plugins'; +export * from './voilaplugins'; +export * from './tools'; diff --git a/packages/voila/src/main.ts b/packages/voila/src/main.ts index b31779f49..de48b3df0 100644 --- a/packages/voila/src/main.ts +++ b/packages/voila/src/main.ts @@ -5,55 +5,30 @@ * Distributed under the terms of the BSD 3-Clause License. * * * * The full license is in the file LICENSE, distributed with this software. * + * Copyright (c) Jupyter Development Team. * + * Distributed under the terms of the Modified BSD License. * ****************************************************************************/ - -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -// Inspired by: https://github.com/jupyterlab/jupyterlab/blob/master/dev_mode/index.js +import './sharedscope'; import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { VoilaApp } from './app'; +import plugins from './voilaplugins'; +import { VoilaServiceManager } from './services/servicemanager'; import { VoilaShell } from './shell'; -import plugins from './plugins'; - -function loadScript(url: string): Promise { - return new Promise((resolve, reject) => { - const newScript = document.createElement('script'); - newScript.onerror = reject; - newScript.onload = resolve; - newScript.async = true; - document.head.appendChild(newScript); - newScript.src = url; - }); -} +import { + IFederatedExtensionData, + activePlugins, + createModule, + loadComponent +} from './tools'; -async function loadComponent(url: string, scope: string): Promise { - await loadScript(url); +//Inspired by: https://github.com/jupyterlab/jupyterlab/blob/master/dev_mode/index.js - // From MIT-licensed https://github.com/module-federation/module-federation-examples/blob/af043acd6be1718ee195b2511adf6011fba4233c/advanced-api/dynamic-remotes/app1/src/App.js#L6-L12 - // eslint-disable-next-line no-undef - await __webpack_init_sharing__('default'); - const container = window._JUPYTERLAB[scope]; - // Initialize the container, it may provide shared modules and may need ours - // eslint-disable-next-line no-undef - await container.init(__webpack_share_scopes__.default); -} - -async function createModule(scope: string, module: string) { - try { - const factory = await window._JUPYTERLAB[scope].get(module); - return factory(); - } catch (e) { - console.warn( - `Failed to create module: package: ${scope}; module: ${module}` - ); - throw e; - } -} - -const disabled = ['@jupyter-widgets/jupyterlab-manager']; +const disabled = [ + '@jupyter-widgets/jupyterlab-manager:plugin', + '@jupyter-widgets/jupyterlab-manager:saveWidgetState' +]; /** * The main function @@ -61,51 +36,25 @@ const disabled = ['@jupyter-widgets/jupyterlab-manager']; async function main() { const mods = [ // @jupyterlab plugins - require('@jupyterlab/markdownviewer-extension'), - require('@jupyterlab/mathjax2-extension'), + require('@jupyterlab/codemirror-extension').default.filter( + (p: any) => p.id === '@jupyterlab/codemirror-extension:languages' + ), + require('@jupyterlab/markedparser-extension'), require('@jupyterlab/rendermime-extension'), - // TODO: add the settings endpoint to re-enable the theme plugins? - // This would also need the theme manager plugin and settings - // require('@jupyterlab/theme-light-extension'), - // require('@jupyterlab/theme-dark-extension'), + require('@jupyterlab/theme-light-extension'), + require('@jupyterlab/theme-dark-extension'), plugins ]; - const mimeExtensions = [require('@jupyterlab/json-extension')]; - - /** - * Iterate over active plugins in an extension. - * - * #### Notes - * This also populates the disabled - */ - function* activePlugins(extension: any) { - // Handle commonjs or es2015 modules - let exports; - if (Object.prototype.hasOwnProperty.call(extension, '__esModule')) { - exports = extension.default; - } else { - // CommonJS exports. - exports = extension; - } - - const plugins = Array.isArray(exports) ? exports : [exports]; - for (const plugin of plugins) { - if ( - PageConfig.Extension.isDisabled(plugin.id) || - disabled.includes(plugin.id) || - disabled.includes(plugin.id.split(':')[0]) - ) { - continue; - } - yield plugin; - } - } + const mimeExtensions = [ + require('@jupyterlab/javascript-extension'), + require('@jupyterlab/json-extension'), + require('@jupyterlab/vega5-extension') + ]; - const extensionData: any[] = JSON.parse( + const extensionData: IFederatedExtensionData[] = JSON.parse( PageConfig.getOption('federated_extensions') ); - const federatedExtensionPromises: Promise[] = []; const federatedMimeExtensionPromises: Promise[] = []; const federatedStylePromises: Promise[] = []; @@ -151,7 +100,7 @@ async function main() { ); federatedExtensions.forEach((p) => { if (p.status === 'fulfilled') { - for (const plugin of activePlugins(p.value)) { + for (const plugin of activePlugins(p.value, disabled)) { mods.push(plugin); } } else { @@ -165,7 +114,7 @@ async function main() { ); federatedMimeExtensions.forEach((p) => { if (p.status === 'fulfilled') { - for (const plugin of activePlugins(p.value)) { + for (const plugin of activePlugins(p.value, disabled)) { mimeExtensions.push(plugin); } } else { @@ -180,10 +129,13 @@ async function main() { console.error((p as PromiseRejectedResult).reason); }); - const app = new VoilaApp({ mimeExtensions, shell: new VoilaShell() }); + const app = new VoilaApp({ + mimeExtensions, + shell: new VoilaShell(), + serviceManager: new VoilaServiceManager() + }); app.registerPluginModules(mods); await app.start(); - window.jupyterapp = app; } diff --git a/packages/voila/src/manager.ts b/packages/voila/src/manager.ts deleted file mode 100644 index aa4d69243..000000000 --- a/packages/voila/src/manager.ts +++ /dev/null @@ -1,123 +0,0 @@ -/*************************************************************************** - * Copyright (c) 2018, Voilà contributors * - * Copyright (c) 2018, QuantStack * - * * - * Distributed under the terms of the BSD 3-Clause License. * - * * - * The full license is in the file LICENSE, distributed with this software. * - ****************************************************************************/ - -import { WidgetRenderer, output } from '@jupyter-widgets/jupyterlab-manager'; - -import { KernelWidgetManager } from '@jupyter-widgets/jupyterlab-manager'; - -import * as base from '@jupyter-widgets/base'; - -import * as controls from '@jupyter-widgets/controls'; - -import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; - -import * as LuminoWidget from '@lumino/widgets'; - -import { MessageLoop } from '@lumino/messaging'; - -import { Widget } from '@lumino/widgets'; - -import { Kernel } from '@jupyterlab/services'; - -import { OutputModel } from './output'; - -const WIDGET_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; - -/** - * A custom widget manager to render widgets with Voila - */ -export class WidgetManager extends KernelWidgetManager { - constructor( - kernel: Kernel.IKernelConnection, - rendermime: IRenderMimeRegistry - ) { - super(kernel, rendermime); - rendermime.addFactory( - { - safe: false, - mimeTypes: [WIDGET_MIMETYPE], - createRenderer: (options) => new WidgetRenderer(options, this as any) - }, - 1 - ); - this._registerWidgets(); - } - - async build_widgets(): Promise { - const tags = document.body.querySelectorAll( - `script[type="${WIDGET_MIMETYPE}"]` - ); - - tags.forEach(async (viewtag) => { - if (!viewtag?.parentElement) { - return; - } - try { - const widgetViewObject = JSON.parse(viewtag.innerHTML); - const { model_id } = widgetViewObject; - const model = await this.get_model(model_id); - const widgetel = document.createElement('div'); - viewtag.parentElement.insertBefore(widgetel, viewtag); - const view = await this.create_view(model); - this.display_view(view, widgetel); - } catch (error) { - // Each widget view tag rendering is wrapped with a try-catch statement. - // - // This fixes issues with widget models that are explicitly "closed" - // but are still referred to in a previous cell output. - // Without the try-catch statement, this error interrupts the loop and - // prevents the rendering of further cells. - // - // This workaround may not be necessary anymore with templates that make use - // of progressive rendering. - console.error(error); - } - }); - } - - async display_view( - view: base.DOMWidgetView, - el: HTMLElement - ): Promise { - if (el) { - LuminoWidget.Widget.attach(view.luminoWidget, el); - } - if (view.el) { - view.el.setAttribute('data-voila-jupyter-widget', ''); - view.el.addEventListener('jupyterWidgetResize', (e: Event) => { - MessageLoop.postMessage( - view.luminoWidget, - LuminoWidget.Widget.ResizeMessage.UnknownSize - ); - }); - } - return view.luminoWidget; - } - - private _registerWidgets(): void { - this.register({ - name: '@jupyter-widgets/base', - version: base.JUPYTER_WIDGETS_VERSION, - exports: base as any - }); - this.register({ - name: '@jupyter-widgets/controls', - version: controls.JUPYTER_CONTROLS_VERSION, - exports: controls as any - }); - this.register({ - name: '@jupyter-widgets/output', - version: output.OUTPUT_WIDGET_VERSION, - exports: { - ...(output as any), - OutputModel - } - }); - } -} diff --git a/packages/voila/src/output.ts b/packages/voila/src/output.ts deleted file mode 100644 index 8436358e8..000000000 --- a/packages/voila/src/output.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { IBackboneModelOptions } from '@jupyter-widgets/base'; -import { LabWidgetManager } from '@jupyter-widgets/jupyterlab-manager'; -import * as outputBase from '@jupyter-widgets/output'; -import * as nbformat from '@jupyterlab/nbformat'; -import { OutputAreaModel } from '@jupyterlab/outputarea'; -import { KernelMessage } from '@jupyterlab/services'; - -/** - * Adapt the upstream output model to Voila using a KernelWidgetManager: - * https://github.com/jupyter-widgets/ipywidgets/blob/58adde2cfbe2f78a8ec6756d38a7c637f5e599f8/python/jupyterlab_widgets/src/output.ts#L22 - * - * TODO: remove when https://github.com/jupyter-widgets/ipywidgets/pull/3561 (or similar) is merged and released in ipywidgets. - */ -export class OutputModel extends outputBase.OutputModel { - defaults(): Backbone.ObjectHash { - return { ...super.defaults(), msg_id: '', outputs: [] }; - } - - initialize( - attributes: Backbone.ObjectHash, - options: IBackboneModelOptions - ): void { - super.initialize(attributes, options); - // The output area model is trusted since widgets are only rendered in trusted contexts. - this._outputs = new OutputAreaModel({ trusted: true }); - - this.listenTo(this, 'change:msg_id', this.reset_msg_id); - this.listenTo(this, 'change:outputs', this.setOutputs); - this.setOutputs(); - } - - /** - * Reset the message id. - */ - reset_msg_id(): void { - const kernel = this.widget_manager.kernel; - const msgId = this.get('msg_id'); - const oldMsgId = this.previous('msg_id'); - - // Clear any old handler. - if (oldMsgId && kernel) { - kernel.removeMessageHook(oldMsgId, this._msgHook); - } - - // Register any new handler. - if (msgId && kernel) { - kernel.registerMessageHook(msgId, this._msgHook); - } - } - - add(msg: KernelMessage.IIOPubMessage): void { - const msgType = msg.header.msg_type; - switch (msgType) { - case 'execute_result': - case 'display_data': - case 'stream': - case 'error': { - const model = msg.content as nbformat.IOutput; - model.output_type = msgType as nbformat.OutputType; - this._outputs.add(model); - break; - } - case 'clear_output': - this.clear_output((msg as KernelMessage.IClearOutputMsg).content.wait); - break; - default: - break; - } - this.set('outputs', this._outputs.toJSON(), { newMessage: true }); - this.save_changes(); - } - - clear_output(wait = false): void { - this._outputs.clear(wait); - } - - get outputs(): OutputAreaModel { - return this._outputs; - } - - setOutputs(model?: any, value?: any, options?: any): void { - if (!(options && options.newMessage)) { - // fromJSON does not clear the existing output - this.clear_output(); - // fromJSON does not copy the message, so we make a deep copy - this._outputs.fromJSON(JSON.parse(JSON.stringify(this.get('outputs')))); - } - } - - widget_manager!: LabWidgetManager; - - private _msgHook = (msg: KernelMessage.IIOPubMessage): boolean => { - this.add(msg); - return false; - }; - - private _outputs!: OutputAreaModel; -} diff --git a/packages/voila/src/plugins/path.ts b/packages/voila/src/plugins/path.ts new file mode 100644 index 000000000..b9a0a8062 --- /dev/null +++ b/packages/voila/src/plugins/path.ts @@ -0,0 +1,28 @@ +/*************************************************************************** + * Copyright (c) 2018, Voilà contributors * + * Copyright (c) 2018, QuantStack * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ****************************************************************************/ +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { VoilaApp } from '../app'; + +/** + * The default paths. + */ +export const pathsPlugin: JupyterFrontEndPlugin = { + id: '@voila-dashboards/voila:paths', + activate: ( + app: JupyterFrontEnd + ): JupyterFrontEnd.IPaths => { + return (app as VoilaApp).paths; + }, + autoStart: true, + provides: JupyterFrontEnd.IPaths +}; diff --git a/packages/voila/src/plugins/themes/index.ts b/packages/voila/src/plugins/themes/index.ts new file mode 100644 index 000000000..706dc7608 --- /dev/null +++ b/packages/voila/src/plugins/themes/index.ts @@ -0,0 +1,87 @@ +/*************************************************************************** + * Copyright (c) 2023, Voilà contributors * + * Copyright (c) 2023, QuantStack * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ****************************************************************************/ +import { StyleModule } from 'style-mod'; +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; +import { IThemeManager } from '@jupyterlab/apputils'; +import { jupyterHighlightStyle } from '@jupyterlab/codemirror'; +import { PageConfig, URLExt } from '@jupyterlab/coreutils'; +import { ThemeManager } from './thememanager'; + +/** + * The voila theme manager provider. + */ +export const themesManagerPlugin: JupyterFrontEndPlugin = { + id: '@voila-dashboards/voila:theme-manager', + description: 'Provides the theme manager.', + requires: [JupyterFrontEnd.IPaths], + activate: ( + app: JupyterFrontEnd, + paths: JupyterFrontEnd.IPaths + ): IThemeManager => { + const host = app.shell; + const url = URLExt.join(PageConfig.getBaseUrl(), paths.urls.themes); + const manager = new ThemeManager({ + host, + url + }); + + let currentTheme: string; + + manager.themeChanged.connect((sender, args) => { + // Set data attributes on the application shell for the current theme. + currentTheme = args.newValue; + if (currentTheme.length > 0) { + document.body.dataset.jpThemeLight = String( + manager.isLight(currentTheme) + ); + document.body.dataset.jpThemeName = currentTheme; + } + }); + + return manager; + }, + autoStart: true, + provides: IThemeManager +}; + +/** + * A plugin to handler the theme + */ +export const themePlugin: JupyterFrontEndPlugin = { + id: '@voila-dashboards/voila:theme', + autoStart: true, + optional: [IThemeManager], + activate: async ( + app: JupyterFrontEnd, + themeManager: IThemeManager | null + ) => { + if (jupyterHighlightStyle.module) { + StyleModule.mount(document, jupyterHighlightStyle.module); + } + + if (!themeManager) { + return; + } + + const labThemeName = PageConfig.getOption('jupyterLabTheme'); + + // default to the light theme if the theme is not specified (empty) + const theme = labThemeName || 'light'; + if (theme !== 'dark' && theme !== 'light') { + await themeManager.setTheme(theme); + } + window.themeLoaded = true; + if (window.cellLoaded) { + window.voila_finish(); + } + } +}; diff --git a/packages/voila/src/plugins/themes/thememanager.ts b/packages/voila/src/plugins/themes/thememanager.ts new file mode 100644 index 000000000..77e674e0d --- /dev/null +++ b/packages/voila/src/plugins/themes/thememanager.ts @@ -0,0 +1,396 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +import { Dialog, IThemeManager, showDialog } from '@jupyterlab/apputils'; +import { IChangedArgs, URLExt } from '@jupyterlab/coreutils'; +import { + ITranslator, + nullTranslator, + TranslationBundle +} from '@jupyterlab/translation'; +import { DisposableDelegate, IDisposable } from '@lumino/disposable'; +import { ISignal, Signal } from '@lumino/signaling'; +import { Widget } from '@lumino/widgets'; + +/** + * The name for the default JupyterLab light theme + */ +const DEFAULT_JUPYTERLAB_LIGHT_THEME = 'JupyterLab Light'; + +/** + * The number of milliseconds between theme loading attempts. + */ +const REQUEST_INTERVAL = 75; + +/** + * The number of times to attempt to load a theme before giving up. + */ +const REQUEST_THRESHOLD = 20; + +type Dict = { [key: string]: T }; + +/** + * A class that provides theme management. This is a simplified + * version of the same class in JupyterLab without depending + * on the setting token. + */ +export class ThemeManager implements IThemeManager { + /** + * Construct a new theme manager. + */ + constructor(options: ThemeManager.IOptions) { + const { host, url } = options; + this.translator = options.translator || nullTranslator; + this._trans = this.translator.load('jupyterlab'); + + this._base = url; + this._host = host; + } + + /** + * Get the name of the current theme. + */ + get theme(): string | null { + return this._current; + } + + /** + * The names of the registered themes. + */ + get themes(): ReadonlyArray { + return Object.keys(this._themes); + } + + /** + * A signal fired when the application theme changes. + */ + get themeChanged(): ISignal> { + return this._themeChanged; + } + + /** + * Get the value of a CSS variable from its key. + * + * @param key - A Jupyterlab CSS variable, without the leading '--jp-'. + * + * @returns value - The current value of the Jupyterlab CSS variable + */ + getCSS(key: string): string { + return ( + this._overrides[key] ?? + getComputedStyle(document.documentElement).getPropertyValue(`--jp-${key}`) + ); + } + + /** + * Load a theme CSS file by path. + * + * @param path - The path of the file to load. + */ + loadCSS(path: string): Promise { + const base = this._base; + const href = URLExt.isLocal(path) ? URLExt.join(base, path) : path; + const links = this._links; + + return new Promise((resolve, reject) => { + const link = document.createElement('link'); + + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + link.setAttribute('href', href); + link.addEventListener('load', () => { + resolve(undefined); + }); + link.addEventListener('error', () => { + reject(`Stylesheet failed to load: ${href}`); + }); + + document.body.appendChild(link); + links.push(link); + + // add any css overrides to document + this.loadCSSOverrides(); + }); + } + + /** + * Loads all current CSS overrides from settings. If an override has been + * removed or is invalid, this function unloads it instead. + */ + loadCSSOverrides(): void { + this._overrides = {}; + } + + /** + * Validate a CSS value w.r.t. a key + * + * @param key - A Jupyterlab CSS variable, without the leading '--jp-'. + * + * @param val - A candidate CSS value + */ + validateCSS(key: string, val: string): boolean { + // determine the css property corresponding to the key + const prop = this._overrideProps[key]; + + if (!prop) { + console.warn( + 'CSS validation failed: could not find property corresponding to key.\n' + + `key: '${key}', val: '${val}'` + ); + return false; + } + + // use built-in validation once we have the corresponding property + if (CSS.supports(prop, val)) { + return true; + } else { + console.warn( + 'CSS validation failed: invalid value.\n' + + `key: '${key}', val: '${val}', prop: '${prop}'` + ); + return false; + } + } + + /** + * Register a theme with the theme manager. + * + * @param theme - The theme to register. + * + * @returns A disposable that can be used to unregister the theme. + */ + register(theme: IThemeManager.ITheme): IDisposable { + const { name } = theme; + const themes = this._themes; + + if (themes[name]) { + throw new Error(`Theme already registered for ${name}`); + } + + themes[name] = theme; + this._themeChanged.emit({ + name: '', + oldValue: null, + newValue: '' + }); + return new DisposableDelegate(() => { + delete themes[name]; + }); + } + + /** + * Set the current theme. + */ + async setTheme(name: string): Promise { + this._requestedTheme = name; + this._loadSettings(); + } + + /** + * Test whether a given theme is light. + */ + isLight(name: string): boolean { + return this._themes[name].isLight; + } + + /** + * Test whether a given theme styles scrollbars, + * and if the user has scrollbar styling enabled. + */ + themeScrollbars(name: string): boolean { + return false; + } + + /** + * Get the display name of the theme. + */ + getDisplayName(name: string): string { + return this._themes[name]?.displayName ?? name; + } + + /** + * Handle the current settings. + */ + private _loadSettings(): void { + const outstanding = this._outstanding; + const pending = this._pending; + const requests = this._requests; + + // If another request is pending, cancel it. + if (pending) { + window.clearTimeout(pending); + this._pending = 0; + } + + const themes = this._themes; + const theme = this._requestedTheme; + + // If another promise is outstanding, wait until it finishes before + // attempting to load the settings. Because outstanding promises cannot + // be aborted, the order in which they occur must be enforced. + if (outstanding) { + outstanding + .then(() => { + this._loadSettings(); + }) + .catch(() => { + this._loadSettings(); + }); + this._outstanding = null; + return; + } + + // Increment the request counter. + requests[theme] = requests[theme] ? requests[theme] + 1 : 1; + + // If the theme exists, load it right away. + if (themes[theme]) { + this._outstanding = this._loadTheme(theme); + delete requests[theme]; + return; + } + + // If the request has taken too long, give up. + if (requests[theme] > REQUEST_THRESHOLD) { + const fallback = DEFAULT_JUPYTERLAB_LIGHT_THEME; + + // Stop tracking the requests for this theme. + delete requests[theme]; + + if (!themes[fallback]) { + this._onError( + this._trans.__( + 'Neither theme %1 nor default %2 loaded.', + theme, + fallback + ) + ); + return; + } + + console.warn(`Could not load theme ${theme}, using default ${fallback}.`); + this._outstanding = this._loadTheme(fallback); + return; + } + + // If the theme does not yet exist, attempt to wait for it. + this._pending = window.setTimeout(() => { + this._loadSettings(); + }, REQUEST_INTERVAL); + } + + /** + * Load the theme. + * + * #### Notes + * This method assumes that the `theme` exists. + */ + private _loadTheme(theme: string): Promise { + const current = this._current; + const links = this._links; + const themes = this._themes; + const splash = new DisposableDelegate(() => undefined); + + // Unload any CSS files that have been loaded. + links.forEach((link) => { + if (link.parentElement) { + link.parentElement.removeChild(link); + } + }); + links.length = 0; + + // Unload the previously loaded theme. + const old = current ? themes[current].unload() : Promise.resolve(); + + return Promise.all([old, themes[theme].load()]) + .then(() => { + this._current = theme; + this._themeChanged.emit({ + name: 'theme', + oldValue: current, + newValue: theme + }); + if (this._host) { + // Need to force a redraw of the app here to avoid a Chrome rendering + // bug that can leave the scrollbars in an invalid state + this._host.hide(); + + // If we hide/show the widget too quickly, no redraw will happen. + // requestAnimationFrame delays until after the next frame render. + requestAnimationFrame(() => { + this._host!.show(); + Private.fitAll(this._host!); + splash.dispose(); + }); + } + }) + .catch((reason) => { + this._onError(reason); + splash.dispose(); + }); + } + + /** + * Handle a theme error. + */ + private _onError(reason: any): void { + void showDialog({ + title: this._trans.__('Error Loading Theme'), + body: String(reason), + buttons: [Dialog.okButton({ label: this._trans.__('OK') })] + }); + } + + protected translator: ITranslator; + private _trans: TranslationBundle; + private _base: string; + private _current: string | null = null; + private _host?: Widget; + private _links: HTMLLinkElement[] = []; + private _overrides: Dict = {}; + private _overrideProps: Dict = {}; + private _outstanding: Promise | null = null; + private _pending = 0; + private _requests: { [theme: string]: number } = {}; + private _themes: { [key: string]: IThemeManager.ITheme } = {}; + private _themeChanged = new Signal>( + this + ); + private _requestedTheme = DEFAULT_JUPYTERLAB_LIGHT_THEME; +} + +export namespace ThemeManager { + /** + * The options used to create a theme manager. + */ + export interface IOptions { + /** + * The host widget for the theme manager. + */ + host?: Widget; + + /** + * The url for local theme loading. + */ + url: string; + + /** + * The application language translator. + */ + translator?: ITranslator; + } +} + +/** + * A namespace for module private data. + */ +namespace Private { + /** + * Fit a widget and all of its children, recursively. + */ + export function fitAll(widget: Widget): void { + for (const child of widget.children()) { + fitAll(child); + } + widget.fit(); + } +} diff --git a/packages/voila/src/plugins/translator.ts b/packages/voila/src/plugins/translator.ts new file mode 100644 index 000000000..8405e9be0 --- /dev/null +++ b/packages/voila/src/plugins/translator.ts @@ -0,0 +1,26 @@ +/*************************************************************************** + * Copyright (c) 2018, Voilà contributors * + * Copyright (c) 2018, QuantStack * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ****************************************************************************/ +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; +import { ITranslator, TranslationManager } from '@jupyterlab/translation'; + +/** + * A simplified Translator + */ +export const translatorPlugin: JupyterFrontEndPlugin = { + id: '@voila-dashboards/voila:translator', + activate: (app: JupyterFrontEnd): ITranslator => { + const translationManager = new TranslationManager(); + return translationManager; + }, + autoStart: true, + provides: ITranslator +}; diff --git a/packages/voila/src/plugins.ts b/packages/voila/src/plugins/widget.ts similarity index 52% rename from packages/voila/src/plugins.ts rename to packages/voila/src/plugins/widget.ts index 38201e1f5..effddcc81 100644 --- a/packages/voila/src/plugins.ts +++ b/packages/voila/src/plugins/widget.ts @@ -20,64 +20,21 @@ import { KernelAPI, ServerConnection } from '@jupyterlab/services'; import { KernelConnection } from '@jupyterlab/services/lib/kernel/default'; -import { ITranslator, TranslationManager } from '@jupyterlab/translation'; +import { + WidgetRenderer, + KernelWidgetManager +} from '@jupyter-widgets/jupyterlab-manager'; import { IJupyterWidgetRegistry, IWidgetRegistryData } from '@jupyter-widgets/base'; -import { VoilaApp } from './app'; +import { VoilaApp } from '../app'; -import { WidgetManager as VoilaWidgetManager } from './manager'; +import { Widget } from '@lumino/widgets'; -/** - * The default paths. - */ -export const pathsPlugin: JupyterFrontEndPlugin = { - id: '@voila-dashboards/voila:paths', - activate: ( - app: JupyterFrontEnd - ): JupyterFrontEnd.IPaths => { - return (app as VoilaApp).paths; - }, - autoStart: true, - provides: JupyterFrontEnd.IPaths -}; - -/** - * A plugin to stop polling the kernels, sessions and kernel specs. - * - * TODO: a cleaner solution would involve a custom ServiceManager to the VoilaApp - * to prevent the default behavior of polling the /api endpoints. - */ -export const stopPollingPlugin: JupyterFrontEndPlugin = { - id: '@voila-dashboards/voila:stop-polling', - autoStart: true, - activate: (app: JupyterFrontEnd): void => { - app.serviceManager.sessions?.ready.then((value) => { - app.serviceManager.sessions['_kernelManager']['_pollModels']?.stop(); - void app.serviceManager.sessions['_pollModels'].stop(); - }); - - app.serviceManager.kernelspecs?.ready.then((value) => { - void app.serviceManager.kernelspecs.dispose(); - }); - } -}; - -/** - * A simplified Translator - */ -export const translatorPlugin: JupyterFrontEndPlugin = { - id: '@voila-dashboards/voila:translator', - activate: (app: JupyterFrontEnd): ITranslator => { - const translationManager = new TranslationManager(); - return translationManager; - }, - autoStart: true, - provides: ITranslator -}; +const WIDGET_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; /** * The Voila widgets manager plugin. @@ -91,6 +48,11 @@ export const widgetManager: JupyterFrontEndPlugin = { app: JupyterFrontEnd, rendermime: IRenderMimeRegistry ): Promise => { + if (!(app instanceof VoilaApp)) { + throw Error( + 'The Voila Widget Manager plugin must be activated in a VoilaApp' + ); + } const baseUrl = PageConfig.getBaseUrl(); const kernelId = PageConfig.getOption('kernelId'); const serverSettings = ServerConnection.makeSettings({ baseUrl }); @@ -104,11 +66,18 @@ export const widgetManager: JupyterFrontEndPlugin = { }; } const kernel = new KernelConnection({ model, serverSettings }); - const manager = new VoilaWidgetManager(kernel, rendermime); - - manager.restored.connect(() => { - void manager.build_widgets(); - }); + const manager = new KernelWidgetManager(kernel, rendermime); + app.widgetManager = manager; + + rendermime.removeMimeType(WIDGET_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_MIMETYPE], + createRenderer: (options) => new WidgetRenderer(options, manager) + }, + -10 + ); window.addEventListener('beforeunload', (e) => { const data = new FormData(); @@ -124,7 +93,9 @@ export const widgetManager: JupyterFrontEndPlugin = { }); return { - registerWidget(data: IWidgetRegistryData): void { + registerWidget: async (data: IWidgetRegistryData) => { + const manager = await app.widgetManagerPromise.promise; + manager.register(data); } }; @@ -132,13 +103,51 @@ export const widgetManager: JupyterFrontEndPlugin = { }; /** - * Export the plugins as default. + * The plugin that renders outputs. */ -const plugins: JupyterFrontEndPlugin[] = [ - pathsPlugin, - stopPollingPlugin, - translatorPlugin, - widgetManager -]; - -export default plugins; +export const renderOutputsPlugin: JupyterFrontEndPlugin = { + id: '@voila-dashboards/voila:render-outputs', + autoStart: true, + requires: [IRenderMimeRegistry, IJupyterWidgetRegistry], + activate: async ( + app: JupyterFrontEnd, + rendermime: IRenderMimeRegistry + ): Promise => { + // Render outputs + const cellOutputs = document.body.querySelectorAll( + 'script[type="application/vnd.voila.cell-output+json"]' + ); + + cellOutputs.forEach(async (cellOutput) => { + const model = JSON.parse(cellOutput.innerHTML); + + const mimeType = rendermime.preferredMimeType(model.data, 'any'); + + if (!mimeType) { + return null; + } + const output = rendermime.createRenderer(mimeType); + output.renderModel(model).catch((error) => { + // Manually append error message to output + const pre = document.createElement('pre'); + pre.textContent = `Javascript Error: ${error.message}`; + output.node.appendChild(pre); + + // Remove mime-type-specific CSS classes + pre.className = 'lm-Widget jp-RenderedText'; + pre.setAttribute('data-mime-type', 'application/vnd.jupyter.stderr'); + }); + + output.addClass('jp-OutputArea-output'); + + if (cellOutput.parentElement) { + const container = cellOutput.parentElement; + + container.removeChild(cellOutput); + + // Attach output + Widget.attach(output, container); + } + }); + } +}; diff --git a/packages/voila/src/services/event.ts b/packages/voila/src/services/event.ts new file mode 100644 index 000000000..3ee72e40c --- /dev/null +++ b/packages/voila/src/services/event.ts @@ -0,0 +1,15 @@ +import { EventManager } from '@jupyterlab/services'; + +/** + * Need https://github.com/jupyterlab/jupyterlab/pull/14770 for a better mock + * + * @export + * @class VoilaEventManager + * @extends {EventManager} + */ +export class VoilaEventManager extends EventManager { + constructor(options: EventManager.IOptions = {}) { + super(options); + this.dispose(); + } +} diff --git a/packages/voila/src/services/kernelspec.ts b/packages/voila/src/services/kernelspec.ts new file mode 100644 index 000000000..dcab024f7 --- /dev/null +++ b/packages/voila/src/services/kernelspec.ts @@ -0,0 +1,24 @@ +import { BaseManager, KernelSpec } from '@jupyterlab/services'; +import { ISpecModels } from '@jupyterlab/services/lib/kernelspec/restapi'; +import { ISignal, Signal } from '@lumino/signaling'; + +export class VoilaKernelSpecManager + extends BaseManager + implements KernelSpec.IManager +{ + specsChanged: ISignal = new Signal(this); + connectionFailure: ISignal = new Signal(this); + readonly specs: ISpecModels | null = null; + + refreshSpecs(): Promise { + return Promise.resolve(); + } + + get isReady(): boolean { + return true; + } + + get ready(): Promise { + return Promise.resolve(); + } +} diff --git a/packages/voila/src/services/servicemanager.ts b/packages/voila/src/services/servicemanager.ts new file mode 100644 index 000000000..ba0220dae --- /dev/null +++ b/packages/voila/src/services/servicemanager.ts @@ -0,0 +1,24 @@ +import { ServiceManager } from '@jupyterlab/services'; +import { VoilaEventManager } from './event'; +import { VoilaUserManager } from './user'; +import { VoilaKernelSpecManager } from './kernelspec'; + +const alwaysTrue = () => true; + +/** + * A custom service manager to disable non used services. + * + * @export + * @class VoilaServiceManager + * @extends {ServiceManager} + */ +export class VoilaServiceManager extends ServiceManager { + constructor(options?: Partial) { + super({ + standby: options?.standby ?? alwaysTrue, + kernelspecs: options?.kernelspecs ?? new VoilaKernelSpecManager({}), + events: options?.events ?? new VoilaEventManager(), + user: options?.user ?? new VoilaUserManager({}) + }); + } +} diff --git a/packages/voila/src/services/user.ts b/packages/voila/src/services/user.ts new file mode 100644 index 000000000..aee0e48be --- /dev/null +++ b/packages/voila/src/services/user.ts @@ -0,0 +1,22 @@ +import { BaseManager, User } from '@jupyterlab/services'; +import { ReadonlyJSONObject } from '@lumino/coreutils'; +import { ISignal, Signal } from '@lumino/signaling'; + +export class VoilaUserManager extends BaseManager implements User.IManager { + userChanged: ISignal = new Signal(this); + connectionFailure: ISignal = new Signal(this); + readonly identity: User.IIdentity | null = null; + readonly permissions: ReadonlyJSONObject | null = null; + + refreshUser(): Promise { + return Promise.resolve(); + } + + get isReady(): boolean { + return true; + } + + get ready(): Promise { + return Promise.resolve(); + } +} diff --git a/packages/voila/src/sharedscope.ts b/packages/voila/src/sharedscope.ts new file mode 100644 index 000000000..26ac31571 --- /dev/null +++ b/packages/voila/src/sharedscope.ts @@ -0,0 +1,15 @@ +import '@lumino/algorithm'; +import '@lumino/application'; +import '@lumino/coreutils'; +import '@lumino/datagrid'; +import '@lumino/disposable'; +import '@lumino/domutils'; +import '@lumino/dragdrop'; +import '@lumino/keyboard'; +import '@lumino/messaging'; +import '@lumino/polling'; +import '@lumino/properties'; +import '@lumino/signaling'; +import '@lumino/virtualdom'; +import '@lumino/widgets'; +import 'react-dom'; diff --git a/packages/voila/src/shell.ts b/packages/voila/src/shell.ts index 9ba0780f0..c890a1803 100644 --- a/packages/voila/src/shell.ts +++ b/packages/voila/src/shell.ts @@ -11,8 +11,6 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { IIterator, iter } from '@lumino/algorithm'; - import { Widget } from '@lumino/widgets'; export type IShell = VoilaShell; @@ -65,7 +63,7 @@ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell { return null; } - widgets(area: IShell.Area): IIterator { - return iter([]); + widgets(area: IShell.Area): IterableIterator { + return [][Symbol.iterator](); } } diff --git a/packages/voila/src/tools.ts b/packages/voila/src/tools.ts new file mode 100644 index 000000000..042b5a748 --- /dev/null +++ b/packages/voila/src/tools.ts @@ -0,0 +1,79 @@ +import { PageConfig } from '@jupyterlab/coreutils'; + +export function loadScript(url: string): Promise { + return new Promise((resolve, reject) => { + const newScript = document.createElement('script'); + newScript.onerror = reject; + newScript.onload = resolve; + newScript.async = true; + document.head.appendChild(newScript); + newScript.src = url; + }); +} + +export async function loadComponent(url: string, scope: string): Promise { + await loadScript(url); + + // From MIT-licensed https://github.com/module-federation/module-federation-examples/blob/af043acd6be1718ee195b2511adf6011fba4233c/advanced-api/dynamic-remotes/app1/src/App.js#L6-L12 + // eslint-disable-next-line no-undef + await __webpack_init_sharing__('default'); + const container = window._JUPYTERLAB[scope]; + // Initialize the container, it may provide shared modules and may need ours + // eslint-disable-next-line no-undef + await container.init(__webpack_share_scopes__.default); +} + +export async function createModule( + scope: string, + module: string +): Promise { + try { + const factory = await window._JUPYTERLAB[scope].get(module); + return factory(); + } catch (e) { + console.warn( + `Failed to create module: package: ${scope}; module: ${module}` + ); + throw e; + } +} + +/** + * Iterate over active plugins in an extension. + * + * #### Notes + * This also populates the disabled + */ +export function* activePlugins( + extension: any, + disabledExtensions: string[] +): Generator { + // Handle commonjs or es2015 modules + let exports; + if (Object.prototype.hasOwnProperty.call(extension, '__esModule')) { + exports = extension.default; + } else { + // CommonJS exports. + exports = extension; + } + + const plugins = Array.isArray(exports) ? exports : [exports]; + for (const plugin of plugins) { + if ( + PageConfig.Extension.isDisabled(plugin.id) || + disabledExtensions.includes(plugin.id) || + disabledExtensions.includes(plugin.id.split(':')[0]) + ) { + continue; + } + yield plugin; + } +} + +export interface IFederatedExtensionData { + name: string; + load: string; + extension?: string; + style?: string; + mimeExtension?: string; +} diff --git a/packages/voila/src/tree.ts b/packages/voila/src/tree.ts new file mode 100644 index 000000000..d3d8bd8ba --- /dev/null +++ b/packages/voila/src/tree.ts @@ -0,0 +1,140 @@ +/*************************************************************************** + * Copyright (c) 2018, Voilà contributors * + * Copyright (c) 2018, QuantStack * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + * Copyright (c) Jupyter Development Team. * + * Distributed under the terms of the Modified BSD License. * + ****************************************************************************/ + +import { PageConfig, URLExt } from '@jupyterlab/coreutils'; + +import { VoilaApp } from './app'; +import { + pathsPlugin, + themePlugin, + themesManagerPlugin, + translatorPlugin +} from './voilaplugins'; +import { VoilaServiceManager } from './services/servicemanager'; +import { VoilaShell } from './shell'; +import { activePlugins, createModule, loadComponent } from './tools'; + +const disabled = [ + '@jupyter-widgets/jupyterlab-manager:plugin', + '@jupyter-widgets/jupyterlab-manager:saveWidgetState', + '@jupyter-widgets/jupyterlab-manager:base-2.0.0', + '@jupyter-widgets/jupyterlab-manager:controls-2.0.0', + '@jupyter-widgets/jupyterlab-manager:output-1.0.0' +]; + +/** + * The main function + */ +async function main() { + const mods = [ + require('@jupyterlab/theme-light-extension'), + require('@jupyterlab/theme-dark-extension'), + pathsPlugin, + translatorPlugin, + themePlugin, + themesManagerPlugin + ]; + + const mimeExtensions: any[] = []; + + const extensionData: any[] = JSON.parse( + PageConfig.getOption('federated_extensions') + ); + + const federatedExtensionPromises: Promise[] = []; + const federatedMimeExtensionPromises: Promise[] = []; + const federatedStylePromises: Promise[] = []; + + const extensions = await Promise.allSettled( + extensionData.map(async (data) => { + await loadComponent( + `${URLExt.join( + PageConfig.getOption('fullLabextensionsUrl'), + data.name, + data.load + )}`, + data.name + ); + return data; + }) + ); + + extensions.forEach((p) => { + if (p.status === 'rejected') { + // There was an error loading the component + console.error(p.reason); + return; + } + + const data = p.value; + if (data.extension) { + federatedExtensionPromises.push(createModule(data.name, data.extension)); + } + if (data.mimeExtension) { + federatedMimeExtensionPromises.push( + createModule(data.name, data.mimeExtension) + ); + } + if (data.style) { + federatedStylePromises.push(createModule(data.name, data.style)); + } + }); + + // Add the federated extensions. + const federatedExtensions = await Promise.allSettled( + federatedExtensionPromises + ); + federatedExtensions.forEach((p) => { + if (p.status === 'fulfilled') { + for (const plugin of activePlugins(p.value, disabled)) { + mods.push(plugin); + } + } else { + console.error(p.reason); + } + }); + + // Add the federated mime extensions. + const federatedMimeExtensions = await Promise.allSettled( + federatedMimeExtensionPromises + ); + federatedMimeExtensions.forEach((p) => { + if (p.status === 'fulfilled') { + for (const plugin of activePlugins(p.value, disabled)) { + mimeExtensions.push(plugin); + } + } else { + console.error(p.reason); + } + }); + + // Load all federated component styles and log errors for any that do not + (await Promise.allSettled(federatedStylePromises)) + .filter(({ status }) => status === 'rejected') + .forEach((p) => { + console.error((p as PromiseRejectedResult).reason); + }); + + const app = new VoilaApp({ + mimeExtensions, + shell: new VoilaShell(), + serviceManager: new VoilaServiceManager() + }); + app.registerPluginModules(mods); + app.started.then(() => { + const el = document.getElementById('voila-tree-main'); + if (el) { + el.style.display = 'unset'; + } + }); + await app.start(); +} +window.addEventListener('load', main); diff --git a/packages/voila/src/treebootstrap.ts b/packages/voila/src/treebootstrap.ts new file mode 100644 index 000000000..ba5dd3118 --- /dev/null +++ b/packages/voila/src/treebootstrap.ts @@ -0,0 +1 @@ +import('./tree.js'); diff --git a/packages/voila/src/voilaplugins.ts b/packages/voila/src/voilaplugins.ts new file mode 100644 index 000000000..7e52e7113 --- /dev/null +++ b/packages/voila/src/voilaplugins.ts @@ -0,0 +1,37 @@ +/*************************************************************************** + * Copyright (c) 2018, Voilà contributors * + * Copyright (c) 2018, QuantStack * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ****************************************************************************/ + +import { JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { pathsPlugin } from './plugins/path'; +import { translatorPlugin } from './plugins/translator'; +import { renderOutputsPlugin, widgetManager } from './plugins/widget'; +import { themePlugin, themesManagerPlugin } from './plugins/themes'; + +/** + * Export the plugins as default. + */ +const plugins: JupyterFrontEndPlugin[] = [ + pathsPlugin, + translatorPlugin, + widgetManager, + renderOutputsPlugin, + themesManagerPlugin, + themePlugin +]; + +export default plugins; + +export { + pathsPlugin, + translatorPlugin, + widgetManager, + renderOutputsPlugin, + themesManagerPlugin, + themePlugin +}; diff --git a/packages/voila/style/index.js b/packages/voila/style/index.js index 4804686b1..770d93357 100644 --- a/packages/voila/style/index.js +++ b/packages/voila/style/index.js @@ -3,3 +3,4 @@ import '@jupyterlab/ui-components/style/index.js'; import '@jupyterlab/apputils/style/index.js'; import '@jupyterlab/rendermime/style/index.js'; import '@jupyterlab/docregistry/style/index.js'; +import '@jupyterlab/markedparser-extension/style/index.js'; diff --git a/packages/voila/webpack.config.js b/packages/voila/webpack.config.js index 4ae1c0a1b..95e3701e9 100644 --- a/packages/voila/webpack.config.js +++ b/packages/voila/webpack.config.js @@ -7,13 +7,16 @@ const path = require('path'); const webpack = require('webpack'); const merge = require('webpack-merge').default; const { ModuleFederationPlugin } = webpack.container; - const Build = require('@jupyterlab/builder').Build; const baseConfig = require('@jupyterlab/builder/lib/webpack.config.base'); const data = require('./package.json'); const names = Object.keys(data.dependencies).filter((name) => { + if (name === 'style-mod') { + return false; + } + const packageData = require(path.join(name, 'package.json')); return packageData.jupyterlab !== undefined; }); @@ -29,13 +32,22 @@ fs.ensureDirSync(buildDir); const libDir = path.resolve(__dirname, 'lib'); fs.copySync(libDir, buildDir); +const assetDir = path.resolve( + __dirname, + '..', + '..', + 'share', + 'jupyter', + 'voila' +); const extras = Build.ensureAssets({ packageNames: names, - output: buildDir + output: assetDir }); // Make a bootstrap entrypoint const entryPoint = path.join(buildDir, 'bootstrap.js'); +const treeEntryPoint = path.join(buildDir, 'treebootstrap.js'); // Also build the style bundle const styleDir = path.resolve(__dirname, 'style'); @@ -60,14 +72,21 @@ const distRoot = path.resolve( module.exports = [ merge(baseConfig, { mode: 'development', - entry: ['./publicpath.js', './' + path.relative(__dirname, entryPoint)], + entry: { + voila: ['./publicpath.js', './' + path.relative(__dirname, entryPoint)], + treepage: [ + './publicpath.js', + './' + path.relative(__dirname, treeEntryPoint) + ] + }, output: { path: distRoot, library: { type: 'var', name: ['_JUPYTERLAB', 'CORE_OUTPUT'] }, - filename: 'voila.js' + filename: '[name].js', + chunkFilename: '[name].voila.js' }, plugins: [ new ModuleFederationPlugin({ diff --git a/pyproject.toml b/pyproject.toml index 9687263a9..43bee8bfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ "hatchling>=1.8.1", - "jupyterlab~=3.0", + "jupyterlab~=4.0", "jupyter_core", ] build-backend = "hatchling.build" @@ -10,7 +10,7 @@ build-backend = "hatchling.build" name = "voila" description = "Voilà turns Jupyter notebooks into standalone web applications" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "Voila Development Team" }, ] @@ -35,9 +35,9 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "jupyter_client>=6.1.3,<=7.4.1", + "jupyter_client>=7.4.4,<9", "jupyter_core>=4.11.0", - "jupyter_server>=1.18,<2.0.0", + "jupyter_server>=2.0.0,<3", "jupyterlab_server>=2.3.0,<3", "nbclient>=0.4.0,<0.8", "nbconvert>=6.4.5,<8", @@ -59,6 +59,7 @@ dev = [ "jupyter_releaser", ] test = [ + "ipykernel", "ipywidgets", "matplotlib", "mock", @@ -66,7 +67,6 @@ test = [ "pandas", "papermill", "pytest", - "pytest-rerunfailures", "pytest-tornasync", ] @@ -82,7 +82,9 @@ path = "voila/_version.py" [tool.hatch.build] artifacts = [ "voila/labextension", - "share/jupyter/voila/templates" + "share/jupyter/voila/templates", + "share/jupyter/voila/schemas", + "share/jupyter/voila/themes" ] [tool.hatch.build.hooks.custom] @@ -93,6 +95,8 @@ path = "hatch_build.py" "voila/labextension" = "share/jupyter/labextensions/@voila-dashboards/jupyterlab-preview" "install.json" = "share/jupyter/labextensions/@voila-dashboards/jupyterlab-preview/install.json" "share/jupyter/voila/templates" = "share/jupyter/voila/templates" +"share/jupyter/voila/schemas" = "share/jupyter/voila/schemas" +"share/jupyter/voila/themes" = "share/jupyter/voila/themes" [tool.hatch.build.targets.sdist] exclude = [ @@ -109,11 +113,13 @@ ensured-targets = [ "voila/labextension/static/style.js", "share/jupyter/voila/templates/base/static/materialcolors.css", "share/jupyter/voila/templates/base/static/labvariables.css", + "share/jupyter/voila/themes/@jupyterlab/theme-dark-extension/index.css" ] skip-if-exists = [ "voila/labextension/static/style.js", "share/jupyter/voila/templates/base/static/materialcolors.css", "share/jupyter/voila/templates/base/static/labvariables.css", + "share/jupyter/voila/themes/@jupyterlab/theme-dark-extension/index.css" ] [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] @@ -136,10 +142,10 @@ skip = [ [tool.jupyter-releaser.hooks] before-bump-version = [ - "python -m pip install hatch jupyterlab~=3.0", + "python -m pip install hatch jupyterlab~=4.0", ] before-build-npm = [ - "python -m pip install jupyterlab~=3.0", + "python -m pip install jupyterlab~=4.0", "jlpm", "jlpm clean", "jlpm build:prod", diff --git a/requirements-visual-test.txt b/requirements-visual-test.txt index 4b3c7c64e..becdee9d0 100644 --- a/requirements-visual-test.txt +++ b/requirements-visual-test.txt @@ -1,5 +1,10 @@ +bokeh bqplot ipympl==0.9.2 -jupyterlab~=3.0 -jupyterlab_miami_nights==0.3.2 +jupyterlab~=4.0 +jupyterlab-fasta +matplotlib scipy +vega_datasets +#TODO Re-enable where these extension are updated. +#jupyterlab_miami_nights==0.3.2 diff --git a/share/jupyter/voila/templates/base/voila_setup.macro.html.j2 b/share/jupyter/voila/templates/base/voila_setup.macro.html.j2 index 0460c5f4a..8da579fab 100644 --- a/share/jupyter/voila/templates/base/voila_setup.macro.html.j2 +++ b/share/jupyter/voila/templates/base/voila_setup.macro.html.j2 @@ -25,6 +25,11 @@ } } window.display_cells = function() { + // TODO Apply the same logic to Voici + if(!window.themeLoaded){ + window.cellLoaded = true; + return; + } // remove the loading element var el = document.getElementById("loading"); if(el){ diff --git a/share/jupyter/voila/templates/classic/index.html.j2 b/share/jupyter/voila/templates/classic/index.html.j2 index ab2a4bb37..805864c41 100644 --- a/share/jupyter/voila/templates/classic/index.html.j2 +++ b/share/jupyter/voila/templates/classic/index.html.j2 @@ -16,7 +16,6 @@ {% endblock jupyter_widgets %} {% block notebook_execute %} - {# Copy so we do not modify the page_config with updates. #} {% set page_config_full = page_config.copy() %} @@ -67,7 +66,7 @@ {%- for cell in nb.cells -%} {% set cellloop = loop %} {%- block any_cell scoped -%} -
+
{{ super() }}
{%- endblock any_cell -%} @@ -82,6 +81,14 @@ {{ super() }} {%- endblock body_footer -%} +{% block data_priority scoped %} +{% if output %} + +{% endif %} +{% endblock data_priority %} + {%- block footer %} {% block footer_js %} {{ voila_setup(resources.base_url, resources.labextensions) }} diff --git a/share/jupyter/voila/templates/lab/index.html.j2 b/share/jupyter/voila/templates/lab/index.html.j2 index fd2a184c8..e09fe4987 100644 --- a/share/jupyter/voila/templates/lab/index.html.j2 +++ b/share/jupyter/voila/templates/lab/index.html.j2 @@ -51,6 +51,7 @@ {% endblock jupyter_widgets %} {%- block body_header -%} + {% if resources.theme == 'dark' %} {% else %} @@ -62,6 +63,14 @@
{%- endblock body_header -%} +{% block data_priority scoped %} +{% if output %} + +{% endif %} +{% endblock data_priority %} + {%- block body_loop -%} {# from this point on, the kernel is started #} @@ -87,7 +96,7 @@ {%- for cell in cell_generator(nb, kernel_id) -%} {% set cellloop = loop %} {%- block any_cell scoped -%} -
+
diff --git a/share/jupyter/voila/templates/lab/tree.html b/share/jupyter/voila/templates/lab/tree.html index ae8fff75d..7f2c0959c 100644 --- a/share/jupyter/voila/templates/lab/tree.html +++ b/share/jupyter/voila/templates/lab/tree.html @@ -70,25 +70,20 @@ {% endblock %} {% block body %} - - {% if frontend == 'voici' %} - {% set openInNewTab = 'target=_blank' %} - - {% if (theme != 'dark' and theme != 'light') %} - -