├── .copier-answers.yml ├── .github └── workflows │ ├── build.yml │ ├── check-release.yml │ ├── enforce-label.yml │ ├── prep-release.yml │ ├── publish-release.yml │ └── update-integration-tests.yml ├── .gitignore ├── .prettierignore ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── RELEASE.md ├── babel.config.js ├── doc └── README │ ├── customisation-settings.png │ ├── demo.gif │ └── executor.png ├── install.json ├── jest.config.js ├── jupyterlab_executor └── __init__.py ├── package.json ├── pyproject.toml ├── schema └── executor.json ├── setup.py ├── src ├── __tests__ │ └── jupyterlab_executor.spec.ts ├── command.tsx ├── executor.ts └── index.ts ├── style ├── base.css ├── index.css └── index.js ├── tsconfig.json ├── tsconfig.test.json ├── ui-tests ├── README.md ├── jupyter_server_test_config.py ├── package.json ├── playwright.config.js ├── tests │ └── jupyterlab_executor.spec.ts └── yarn.lock └── yarn.lock /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY 2 | _commit: v4.2.0 3 | _src_path: https://github.com/jupyterlab/extension-template 4 | author_email: gavincyi@gmail.com 5 | author_name: Gavin Chan 6 | has_binder: false 7 | has_settings: false 8 | kind: frontend 9 | labextension_name: jupyterlab_executor 10 | project_short_description: JupyterLab extension of executing the scripts 11 | python_name: jupyterlab_executor 12 | repository: https://github.com/gavincyi/jupyterlab-executor 13 | test: true 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Base Setup 18 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 19 | 20 | - name: Install dependencies 21 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 22 | 23 | - name: Lint the extension 24 | run: | 25 | set -eux 26 | jlpm 27 | jlpm run lint:check 28 | 29 | - name: Test the extension 30 | run: | 31 | set -eux 32 | jlpm run test 33 | 34 | - name: Build the extension 35 | run: | 36 | set -eux 37 | python -m pip install .[test] 38 | 39 | jupyter labextension list 40 | jupyter labextension list 2>&1 | grep -ie "jupyterlab_executor.*OK" 41 | python -m jupyterlab.browser_check 42 | 43 | - name: Package the extension 44 | run: | 45 | set -eux 46 | 47 | pip install build 48 | python -m build 49 | pip uninstall -y "jupyterlab_executor" jupyterlab 50 | 51 | - name: Upload extension packages 52 | uses: actions/upload-artifact@v3 53 | with: 54 | name: extension-artifacts 55 | path: dist/jupyterlab_executor* 56 | if-no-files-found: error 57 | 58 | test_isolated: 59 | needs: build 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - name: Install Python 64 | uses: actions/setup-python@v4 65 | with: 66 | python-version: '3.9' 67 | architecture: 'x64' 68 | - uses: actions/download-artifact@v3 69 | with: 70 | name: extension-artifacts 71 | - name: Install and Test 72 | run: | 73 | set -eux 74 | # Remove NodeJS, twice to take care of system and locally installed node versions. 75 | sudo rm -rf $(which node) 76 | sudo rm -rf $(which node) 77 | 78 | pip install "jupyterlab>=4.0.0,<5" jupyterlab_executor*.whl 79 | 80 | 81 | jupyter labextension list 82 | jupyter labextension list 2>&1 | grep -ie "jupyterlab_executor.*OK" 83 | python -m jupyterlab.browser_check --no-browser-test 84 | 85 | integration-tests: 86 | name: Integration tests 87 | needs: build 88 | runs-on: ubuntu-latest 89 | 90 | env: 91 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers 92 | 93 | steps: 94 | - name: Checkout 95 | uses: actions/checkout@v3 96 | 97 | - name: Base Setup 98 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 99 | 100 | - name: Download extension package 101 | uses: actions/download-artifact@v3 102 | with: 103 | name: extension-artifacts 104 | 105 | - name: Install the extension 106 | run: | 107 | set -eux 108 | python -m pip install "jupyterlab>=4.0.0,<5" jupyterlab_executor*.whl 109 | 110 | - name: Install dependencies 111 | working-directory: ui-tests 112 | env: 113 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 114 | run: jlpm install 115 | 116 | - name: Set up browser cache 117 | uses: actions/cache@v3 118 | with: 119 | path: | 120 | ${{ github.workspace }}/pw-browsers 121 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 122 | 123 | - name: Install browser 124 | run: jlpm playwright install chromium 125 | working-directory: ui-tests 126 | 127 | - name: Execute integration tests 128 | working-directory: ui-tests 129 | run: | 130 | jlpm playwright test 131 | 132 | - name: Upload Playwright Test report 133 | if: always() 134 | uses: actions/upload-artifact@v3 135 | with: 136 | name: jupyterlab_executor-playwright-tests 137 | path: | 138 | ui-tests/test-results 139 | ui-tests/playwright-report 140 | 141 | check_links: 142 | name: Check Links 143 | runs-on: ubuntu-latest 144 | timeout-minutes: 15 145 | steps: 146 | - uses: actions/checkout@v3 147 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 148 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 149 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["*"] 7 | 8 | jobs: 9 | check_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Base Setup 15 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 16 | - name: Check Release 17 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 18 | with: 19 | 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | - name: Upload Distributions 23 | uses: actions/upload-artifact@v3 24 | with: 25 | name: jupyterlab_executor-releaser-dist-${{ github.run_number }} 26 | path: .jupyter_releaser_checkout/dist 27 | -------------------------------------------------------------------------------- /.github/workflows/enforce-label.yml: -------------------------------------------------------------------------------- 1 | name: Enforce PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, edited, synchronize] 6 | jobs: 7 | enforce-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - name: enforce-triage-label 13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 14 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 1: Prep Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: "New Version Specifier" 7 | default: "next" 8 | required: false 9 | branch: 10 | description: "The branch to target" 11 | required: false 12 | post_version_spec: 13 | description: "Post Version Specifier" 14 | required: false 15 | since: 16 | description: "Use PRs with activity since this date or git reference" 17 | required: false 18 | since_last_stable: 19 | description: "Use PRs with activity since the last stable git tag" 20 | required: false 21 | type: boolean 22 | jobs: 23 | prep_release: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 27 | 28 | - name: Prep Release 29 | id: prep-release 30 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 31 | with: 32 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 33 | version_spec: ${{ github.event.inputs.version_spec }} 34 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 35 | branch: ${{ github.event.inputs.branch }} 36 | since: ${{ github.event.inputs.since }} 37 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 38 | 39 | - name: "** Next Step **" 40 | run: | 41 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 2: Publish Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: "The target branch" 7 | required: false 8 | release_url: 9 | description: "The URL of the draft GitHub release" 10 | required: false 11 | steps_to_skip: 12 | description: "Comma separated list of steps to skip" 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | # This is useful if you want to use PyPI trusted publisher 20 | # and NPM provenance 21 | id-token: write 22 | steps: 23 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 24 | 25 | - name: Populate Release 26 | id: populate-release 27 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 28 | with: 29 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 30 | branch: ${{ github.event.inputs.branch }} 31 | release_url: ${{ github.event.inputs.release_url }} 32 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 33 | 34 | - name: Finalize Release 35 | id: finalize-release 36 | env: 37 | # The following are needed if you use legacy PyPI set up 38 | # PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 39 | # PYPI_TOKEN_MAP: ${{ secrets.PYPI_TOKEN_MAP }} 40 | # TWINE_USERNAME: __token__ 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | uses: jupyter-server/jupyter-releaser/.github/actions/finalize-release@v2 43 | with: 44 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 45 | release_url: ${{ steps.populate-release.outputs.release_url }} 46 | 47 | - name: "** Next Step **" 48 | if: ${{ success() }} 49 | run: | 50 | echo "Verify the final release" 51 | echo ${{ steps.finalize-release.outputs.release_url }} 52 | 53 | - name: "** Failure Message **" 54 | if: ${{ failure() }} 55 | run: | 56 | echo "Failed to Publish the Draft Release Url:" 57 | echo ${{ steps.populate-release.outputs.release_url }} 58 | -------------------------------------------------------------------------------- /.github/workflows/update-integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Update Playwright Snapshots 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | update-snapshots: 13 | if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'please update playwright snapshots') }} 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | - name: Configure git to use https 23 | run: git config --global hub.protocol https 24 | 25 | - name: Checkout the branch from the PR that triggered the job 26 | run: hub pr checkout ${{ github.event.issue.number }} 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Base Setup 31 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 32 | 33 | - name: Install dependencies 34 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 35 | 36 | - name: Install extension 37 | run: | 38 | set -eux 39 | jlpm 40 | python -m pip install . 41 | 42 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 43 | with: 44 | github_token: ${{ secrets.GITHUB_TOKEN }} 45 | # Playwright knows how to start JupyterLab server 46 | start_server_script: 'null' 47 | test_folder: ui-tests 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | jupyterlab_executor/labextension 11 | # Version file is handled by hatchling 12 | jupyterlab_executor/_version.py 13 | 14 | # Integration tests 15 | ui-tests/test-results/ 16 | ui-tests/playwright-report/ 17 | 18 | # Created by https://www.gitignore.io/api/python 19 | # Edit at https://www.gitignore.io/?templates=python 20 | 21 | ### Python ### 22 | # Byte-compiled / optimized / DLL files 23 | __pycache__/ 24 | *.py[cod] 25 | *$py.class 26 | 27 | # C extensions 28 | *.so 29 | 30 | # Distribution / packaging 31 | .Python 32 | build/ 33 | develop-eggs/ 34 | dist/ 35 | downloads/ 36 | eggs/ 37 | .eggs/ 38 | lib/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | wheels/ 44 | pip-wheel-metadata/ 45 | share/python-wheels/ 46 | .installed.cfg 47 | *.egg 48 | MANIFEST 49 | 50 | # PyInstaller 51 | # Usually these files are written by a python script from a template 52 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 53 | *.manifest 54 | *.spec 55 | 56 | # Installer logs 57 | pip-log.txt 58 | pip-delete-this-directory.txt 59 | 60 | # Unit test / coverage reports 61 | htmlcov/ 62 | .tox/ 63 | .nox/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | nosetests.xml 68 | coverage/ 69 | coverage.xml 70 | *.cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # celery beat schedule file 91 | celerybeat-schedule 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # Mr Developer 104 | .mr.developer.cfg 105 | .project 106 | .pydevproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # End of https://www.gitignore.io/api/python 120 | 121 | # OSX files 122 | .DS_Store 123 | 124 | # Yarn cache 125 | .yarn/ 126 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | jupyterlab_executor 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Gavin Chan 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean build check check_lint dev-run-jupyterlab-2.x dev-run-jupyterlab-3.x 2 | 3 | clean: 4 | rm -rf venv* lib node_modules *.egg-info jupyterlab_executor/labextension dist build 5 | 6 | venv: 7 | @python3 -m virtualenv venv 8 | venv/bin/python -m pip install -U pip wheel setuptools 9 | venv/bin/python -m pip install jupyterlab build twine 10 | venv/bin/python -m pip install -e . 11 | venv/bin/jupyter labextension develop --overwrite . 12 | 13 | build: 14 | jlpm run build 15 | 16 | check_lint: 17 | jlpm run eslint:check 18 | 19 | check: check_lint 20 | 21 | dev-run: build 22 | jupyter lab 23 | 24 | dist: venv 25 | rm -rf dist build 26 | venv/bin/python -m build -s 27 | venv/bin/python -m build 28 | ls -l dist 29 | 30 | release: dist 31 | venv/bin/python -m twine upload dist/* 32 | 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab-executor 2 | 3 | [![PyPI Release](https://img.shields.io/pypi/v/jupyterlab-executor.svg)](https://pypi.org/project/jupyterlab-executor/) 4 | 5 | ![Github Actions Status](https://github.com/gavincyi/jupyterlab-executor/workflows/Build/badge.svg) 6 | 7 | ![PyPI Downloads](https://img.shields.io/pypi/dm/jupyterlab-executor.svg) 8 | 9 | JupyterLab extension of executing the scripts 10 | 11 | ![demo](doc/README/demo.gif) 12 | 13 | The extension helps the user execute the script in the terminal and provides 14 | multiple common executors, e.g. bash and python. Users can customise the 15 | executors in the settings as well. 16 | 17 | ## Requirements 18 | 19 | * JupyterLab >= 4.0 20 | 21 | ## Install 22 | 23 | The package can be installed via PyPI 24 | 25 | ```bash 26 | pip install jupyterlab_executor 27 | ``` 28 | 29 | ## Customisation 30 | 31 | The executors can be customised from the JupyterLab settings. 32 | 33 | ![Customisation settings](doc/README/customisation-settings.png) 34 | 35 | Alternatively, the customisation JSON file can be appended into the 36 | [users setting directory](https://jupyterlab.readthedocs.io/en/stable/user/directories.html?highlight=%22jupyterlab-settings%22#jupyterlab-user-settings-directory). 37 | The file path should be 38 | `$HOME/.jupyter/lab/user-settings/@gavincyi/jupyterlab-executor/executor.jupyterlab-settings` 39 | and the format is like the following 40 | 41 | ``` 42 | { 43 | "executors": [ 44 | { 45 | "name": "bash", 46 | "command": "bash {path} {args}" 47 | }, 48 | { 49 | "name": "python", 50 | "command": "python {path} {args}" 51 | }, 52 | ... 53 | ] 54 | } 55 | ``` 56 | 57 | The `executors` variable is a list of descriptions, of which 58 | 59 | 1. `name` is the string shown in the dialog 60 | 61 | 2. `command` is the executor command template to run, where `{path}` 62 | is the file path returned by the content manager in the JupyterLab, 63 | and `args` is the arguments passed in by the users. 64 | 65 | The environment variables are always appended at the beginning of the 66 | command. 67 | 68 | For example, the following execution parameters 69 | 70 | ![Execute](doc/README/executor.png) 71 | 72 | run the following command on the terminal 73 | 74 | ``` 75 | PYTHONPATH=. bash test.py --time 1 76 | ``` 77 | 78 | ## Contributing 79 | 80 | ### Roadmap 81 | 82 | The following features are not yet completed but on the roadmap. 83 | 84 | - Support script argument template 85 | 86 | - Support default script arguments 87 | 88 | The above features will come out very soon. 89 | 90 | ### Development install 91 | 92 | Note: You will need NodeJS to build the extension package. 93 | 94 | The `jlpm` command is JupyterLab's pinned version of 95 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 96 | `yarn` or `npm` in lieu of `jlpm` below. 97 | 98 | ```bash 99 | # Clone the repo to your local environment 100 | # Change directory to the jupyterlab_executor directory 101 | # Install jupyterlab 102 | pip install jupyterlab 103 | # Install package in development mode 104 | pip install -e . 105 | # Link your development version of the extension with JupyterLab 106 | jupyter labextension develop . --overwrite 107 | # Rebuild extension Typescript source after making changes 108 | jlpm run build 109 | ``` 110 | 111 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. 112 | 113 | ```bash 114 | # Watch the source directory in one terminal, automatically rebuilding when needed 115 | jlpm run watch 116 | # Run JupyterLab in another terminal 117 | jupyter lab 118 | ``` 119 | 120 | With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). 121 | 122 | By default, the `jlpm run build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: 123 | 124 | ```bash 125 | jupyter lab build --minimize=False 126 | ``` 127 | 128 | ### Uninstall 129 | 130 | ```bash 131 | pip uninstall jupyterlab_executor 132 | ``` 133 | 134 | ### Release 135 | 136 | The release should follow the below steps 137 | 138 | 1. `make clean` 139 | 140 | 2. `make venv` 141 | 142 | 3. Update the version number in `package.json` 143 | 144 | 4. `make release` -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of jupyterlab_executor 2 | 3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 4 | 5 | ## Manual release 6 | 7 | ### Python package 8 | 9 | This extension can be distributed as Python packages. All of the Python 10 | packaging instructions are in the `pyproject.toml` file to wrap your extension in a 11 | Python package. Before generating a package, you first need to install some tools: 12 | 13 | ```bash 14 | pip install build twine hatch 15 | ``` 16 | 17 | Bump the version using `hatch`. By default this will create a tag. 18 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. 19 | 20 | ```bash 21 | hatch version 22 | ``` 23 | 24 | Make sure to clean up all the development files before building the package: 25 | 26 | ```bash 27 | jlpm clean:all 28 | ``` 29 | 30 | You could also clean up the local git repository: 31 | 32 | ```bash 33 | git clean -dfX 34 | ``` 35 | 36 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 37 | 38 | ```bash 39 | python -m build 40 | ``` 41 | 42 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. 43 | 44 | Then to upload the package to PyPI, do: 45 | 46 | ```bash 47 | twine upload dist/* 48 | ``` 49 | 50 | ### NPM package 51 | 52 | To publish the frontend part of the extension as a NPM package, do: 53 | 54 | ```bash 55 | npm login 56 | npm publish --access public 57 | ``` 58 | 59 | ## Automated releases with the Jupyter Releaser 60 | 61 | The extension repository should already be compatible with the Jupyter Releaser. 62 | 63 | Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Add tokens to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository: 68 | - `ADMIN_GITHUB_TOKEN` (with "public_repo" and "repo:status" permissions); see the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) 69 | - `NPM_TOKEN` (with "automation" permission); see the [documentation](https://docs.npmjs.com/creating-and-viewing-access-tokens) 70 | - Set up PyPI 71 | 72 |
Using PyPI trusted publisher (modern way) 73 | 74 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) 75 | - The _workflow name_ is `publish-release.yml` and the _environment_ should be left blank. 76 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/)) 77 | 78 |
79 | 80 |
Using PyPI token (legacy way) 81 | 82 | - If the repo generates PyPI release(s), create a scoped PyPI [token](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github). We recommend using a scoped token for security reasons. 83 | 84 | - You can store the token as `PYPI_TOKEN` in your fork's `Secrets`. 85 | 86 | - Advanced usage: if you are releasing multiple repos, you can create a secret named `PYPI_TOKEN_MAP` instead of `PYPI_TOKEN` that is formatted as follows: 87 | 88 | ```text 89 | owner1/repo1,token1 90 | owner2/repo2,token2 91 | ``` 92 | 93 | If you have multiple Python packages in the same repository, you can point to them as follows: 94 | 95 | ```text 96 | owner1/repo1/path/to/package1,token1 97 | owner1/repo1/path/to/package2,token2 98 | ``` 99 | 100 |
101 | 102 | - Go to the Actions panel 103 | - Run the "Step 1: Prep Release" workflow 104 | - Check the draft changelog 105 | - Run the "Step 2: Publish Release" workflow 106 | 107 | ## Publishing to `conda-forge` 108 | 109 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html 110 | 111 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 112 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /doc/README/customisation-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavincyi/jupyterlab-executor/d0097f51fe24d25d74ae942b2c051d16642c984a/doc/README/customisation-settings.png -------------------------------------------------------------------------------- /doc/README/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavincyi/jupyterlab-executor/d0097f51fe24d25d74ae942b2c051d16642c984a/doc/README/demo.gif -------------------------------------------------------------------------------- /doc/README/executor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavincyi/jupyterlab-executor/d0097f51fe24d25d74ae942b2c051d16642c984a/doc/README/executor.png -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyterlab_executor", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_executor" 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); 2 | 3 | const esModules = [ 4 | '@codemirror', 5 | '@jupyter/ydoc', 6 | '@jupyterlab/', 7 | 'lib0', 8 | 'nanoid', 9 | 'vscode-ws-jsonrpc', 10 | 'y-protocols', 11 | 'y-websocket', 12 | 'yjs' 13 | ].join('|'); 14 | 15 | const baseConfig = jestJupyterLab(__dirname); 16 | 17 | module.exports = { 18 | ...baseConfig, 19 | automock: false, 20 | collectCoverageFrom: [ 21 | 'src/**/*.{ts,tsx}', 22 | '!src/**/*.d.ts', 23 | '!src/**/.ipynb_checkpoints/*' 24 | ], 25 | coverageReporters: ['lcov', 'text'], 26 | testRegex: 'src/.*/.*.spec.ts[x]?$', 27 | transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] 28 | }; 29 | -------------------------------------------------------------------------------- /jupyterlab_executor/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ._version import __version__ 3 | except ImportError: 4 | # Fallback when using the package in dev mode without installing 5 | # in editable mode with pip. It is highly recommended to install 6 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs 7 | import warnings 8 | warnings.warn("Importing 'jupyterlab_executor' outside a proper installation.") 9 | __version__ = "dev" 10 | 11 | 12 | def _jupyter_labextension_paths(): 13 | return [{ 14 | "src": "labextension", 15 | "dest": "@gavincyi/jupyterlab-executor" 16 | }] 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gavincyi/jupyterlab-executor", 3 | "version": "2023.1.1", 4 | "description": "JupyterLab extension of executing the scripts", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/gavincyi/jupyterlab-executor", 11 | "bugs": { 12 | "url": "https://github.com/gavincyi/jupyterlab-executor/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Gavin Chan", 17 | "email": "gavincyi@gmail.com" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 22 | "schema/*.json" 23 | ], 24 | "main": "lib/index.js", 25 | "types": "lib/index.d.ts", 26 | "style": "style/index.css", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/gavincyi/jupyterlab-executor.git" 30 | }, 31 | "workspaces": [ 32 | "ui-tests" 33 | ], 34 | "scripts": { 35 | "build": "jlpm build:lib && jlpm build:labextension:dev", 36 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 37 | "build:labextension": "jupyter labextension build .", 38 | "build:labextension:dev": "jupyter labextension build --development True .", 39 | "build:lib": "tsc --sourceMap", 40 | "build:lib:prod": "tsc", 41 | "clean": "jlpm clean:lib", 42 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 43 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 44 | "clean:labextension": "rimraf jupyterlab_executor/labextension jupyterlab_executor/_version.py", 45 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 46 | "eslint": "jlpm eslint:check --fix", 47 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 48 | "install:extension": "jlpm build", 49 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 50 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 51 | "prettier": "jlpm prettier:base --write --list-different", 52 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 53 | "prettier:check": "jlpm prettier:base --check", 54 | "stylelint": "jlpm stylelint:check --fix", 55 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 56 | "test": "jest --coverage", 57 | "watch": "run-p watch:src watch:labextension", 58 | "watch:src": "tsc -w --sourceMap", 59 | "watch:labextension": "jupyter labextension watch ." 60 | }, 61 | "dependencies": { 62 | "@jupyterlab/application": "^4.0.0", 63 | "@jupyterlab/apputils": "^4.0.0", 64 | "@jupyterlab/filebrowser": "^4.0.0", 65 | "@jupyterlab/terminal": "^4.0.0", 66 | "@phosphor/algorithm": "^1.2.0", 67 | "path": "^0.12.7" 68 | }, 69 | "devDependencies": { 70 | "@jupyterlab/builder": "^4.0.0", 71 | "@jupyterlab/testutils": "^4.0.0", 72 | "@types/jest": "^29.2.0", 73 | "@types/json-schema": "^7.0.11", 74 | "@types/react": "^18.0.26", 75 | "@types/react-addons-linked-state-mixin": "^0.14.22", 76 | "@typescript-eslint/eslint-plugin": "^6.1.0", 77 | "@typescript-eslint/parser": "^6.1.0", 78 | "css-loader": "^6.7.1", 79 | "eslint": "^8.36.0", 80 | "eslint-config-prettier": "^8.8.0", 81 | "eslint-plugin-prettier": "^5.0.0", 82 | "jest": "^29.2.0", 83 | "npm-run-all": "^4.1.5", 84 | "prettier": "^3.0.0", 85 | "rimraf": "^5.0.1", 86 | "source-map-loader": "^1.0.2", 87 | "style-loader": "^3.3.1", 88 | "stylelint": "^15.10.1", 89 | "stylelint-config-recommended": "^13.0.0", 90 | "stylelint-config-standard": "^34.0.0", 91 | "stylelint-csstree-validator": "^3.0.0", 92 | "stylelint-prettier": "^4.0.0", 93 | "typescript": "~5.0.2", 94 | "yjs": "^13.5.0" 95 | }, 96 | "sideEffects": [ 97 | "style/*.css", 98 | "style/index.js" 99 | ], 100 | "styleModule": "style/index.js", 101 | "publishConfig": { 102 | "access": "public" 103 | }, 104 | "jupyterlab": { 105 | "extension": true, 106 | "outputDir": "jupyterlab_executor/labextension", 107 | "schemaDir": "schema" 108 | }, 109 | "eslintIgnore": [ 110 | "node_modules", 111 | "dist", 112 | "coverage", 113 | "**/*.d.ts", 114 | "tests", 115 | "**/__tests__", 116 | "ui-tests" 117 | ], 118 | "eslintConfig": { 119 | "extends": [ 120 | "eslint:recommended", 121 | "plugin:@typescript-eslint/eslint-recommended", 122 | "plugin:@typescript-eslint/recommended", 123 | "plugin:prettier/recommended" 124 | ], 125 | "parser": "@typescript-eslint/parser", 126 | "parserOptions": { 127 | "project": "tsconfig.json", 128 | "sourceType": "module" 129 | }, 130 | "plugins": [ 131 | "@typescript-eslint" 132 | ], 133 | "rules": { 134 | "@typescript-eslint/naming-convention": [ 135 | "error", 136 | { 137 | "selector": "interface", 138 | "format": [ 139 | "PascalCase" 140 | ], 141 | "custom": { 142 | "regex": "^I[A-Z]", 143 | "match": true 144 | } 145 | } 146 | ], 147 | "@typescript-eslint/no-unused-vars": [ 148 | "warn", 149 | { 150 | "args": "none" 151 | } 152 | ], 153 | "@typescript-eslint/no-explicit-any": "off", 154 | "@typescript-eslint/no-namespace": "off", 155 | "@typescript-eslint/no-use-before-define": "off", 156 | "@typescript-eslint/quotes": [ 157 | "error", 158 | "single", 159 | { 160 | "avoidEscape": true, 161 | "allowTemplateLiterals": false 162 | } 163 | ], 164 | "curly": [ 165 | "error", 166 | "all" 167 | ], 168 | "eqeqeq": "error", 169 | "prefer-arrow-callback": "error" 170 | } 171 | }, 172 | "prettier": { 173 | "singleQuote": true, 174 | "trailingComma": "none", 175 | "arrowParens": "avoid", 176 | "endOfLine": "auto", 177 | "overrides": [ 178 | { 179 | "files": "package.json", 180 | "options": { 181 | "tabWidth": 4 182 | } 183 | } 184 | ] 185 | }, 186 | "stylelint": { 187 | "extends": [ 188 | "stylelint-config-recommended", 189 | "stylelint-config-standard", 190 | "stylelint-prettier/recommended" 191 | ], 192 | "plugins": [ 193 | "stylelint-csstree-validator" 194 | ], 195 | "rules": { 196 | "csstree/validator": true, 197 | "property-no-vendor-prefix": null, 198 | "selector-no-vendor-prefix": null, 199 | "value-no-vendor-prefix": null 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "jupyterlab_executor" 7 | readme = "README.md" 8 | license = { file = "LICENSE" } 9 | requires-python = ">=3.8" 10 | classifiers = [ 11 | "Framework :: Jupyter", 12 | "Framework :: Jupyter :: JupyterLab", 13 | "Framework :: Jupyter :: JupyterLab :: 4", 14 | "Framework :: Jupyter :: JupyterLab :: Extensions", 15 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 16 | "License :: OSI Approved :: BSD License", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | ] 24 | dependencies = [ 25 | ] 26 | dynamic = ["version", "description", "authors", "urls", "keywords"] 27 | 28 | [tool.hatch.version] 29 | source = "nodejs" 30 | 31 | [tool.hatch.metadata.hooks.nodejs] 32 | fields = ["description", "authors", "urls"] 33 | 34 | [tool.hatch.build.targets.sdist] 35 | artifacts = ["jupyterlab_executor/labextension"] 36 | exclude = [".github", "binder"] 37 | 38 | [tool.hatch.build.targets.wheel.shared-data] 39 | "jupyterlab_executor/labextension" = "share/jupyter/labextensions/@gavincyi/jupyterlab-executor" 40 | "install.json" = "share/jupyter/labextensions/@gavincyi/jupyterlab-executor/install.json" 41 | 42 | [tool.hatch.build.hooks.version] 43 | path = "jupyterlab_executor/_version.py" 44 | 45 | [tool.hatch.build.hooks.jupyter-builder] 46 | dependencies = ["hatch-jupyter-builder>=0.5"] 47 | build-function = "hatch_jupyter_builder.npm_builder" 48 | ensured-targets = [ 49 | "jupyterlab_executor/labextension/static/style.js", 50 | "jupyterlab_executor/labextension/package.json", 51 | ] 52 | skip-if-exists = ["jupyterlab_executor/labextension/static/style.js"] 53 | 54 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 55 | build_cmd = "build:prod" 56 | npm = ["jlpm"] 57 | 58 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 59 | build_cmd = "install:extension" 60 | npm = ["jlpm"] 61 | source_dir = "src" 62 | build_dir = "jupyterlab_executor/labextension" 63 | 64 | [tool.jupyter-releaser.options] 65 | version_cmd = "hatch version" 66 | 67 | [tool.jupyter-releaser.hooks] 68 | before-build-npm = [ 69 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 70 | "jlpm", 71 | "jlpm build:prod" 72 | ] 73 | before-build-python = ["jlpm clean:all"] 74 | 75 | [tool.check-wheel-contents] 76 | ignore = ["W002"] 77 | -------------------------------------------------------------------------------- /schema/executor.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Executor", 3 | "description": "Settings for the executor plugin.", 4 | "jupyter.lab.menus": { 5 | "context": [ 6 | { 7 | "command": "gavincyi/jupyterlab-executor:execute", 8 | "selector": ".jp-DirListing-item", 9 | "rank": 0 10 | } 11 | ] 12 | }, 13 | "properties": { 14 | "executors": { 15 | "description": "The list of the executors.", 16 | "title": "Executor", 17 | "type": "array", 18 | "items": { "$ref": "#/definitions/executor" }, 19 | "default": [ 20 | { 21 | "name": "bash", 22 | "command": "bash {path} {args}" 23 | }, 24 | { 25 | "name": "python", 26 | "command": "python {path} {args}" 27 | } 28 | ] 29 | } 30 | }, 31 | "type": "object", 32 | "definitions": { 33 | "executor": { 34 | "properties": { 35 | "command": { "type": "string" }, 36 | "name": { "type": "string" } 37 | }, 38 | "required": [ "name", "command" ], 39 | "type": "object" 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __import__("setuptools").setup() 2 | -------------------------------------------------------------------------------- /src/__tests__/jupyterlab_executor.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 3 | */ 4 | 5 | describe('jupyterlab_executor', () => { 6 | it('should be tested', () => { 7 | expect(1 + 1).toEqual(2); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/command.tsx: -------------------------------------------------------------------------------- 1 | import { ReactWidget, UseSignal } from '@jupyterlab/apputils'; 2 | 3 | import { Signal } from '@lumino/signaling'; 4 | 5 | import { PartialJSONObject } from '@lumino/coreutils'; 6 | 7 | import * as React from 'react'; 8 | import { style } from 'typestyle'; 9 | 10 | export interface IExecutor extends PartialJSONObject { 11 | /** 12 | * Executor 13 | */ 14 | 15 | // Command in the format of {path} and {args} 16 | command: string; 17 | 18 | // Name displayed in the widget 19 | name: string; 20 | } 21 | 22 | const wrapperClass = style({ 23 | marginTop: '6px', 24 | marginBottom: '0', 25 | 26 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)' 27 | }); 28 | 29 | const filterInputClass = style({ 30 | boxSizing: 'border-box', 31 | 32 | width: '100%', 33 | height: '2em', 34 | 35 | /* top | right | bottom | left */ 36 | padding: '1px 18px 2px 7px', 37 | 38 | color: 'var(--jp-ui-font-color1)', 39 | fontSize: 'var(--jp-ui-font-size1)', 40 | fontWeight: 300, 41 | 42 | backgroundColor: 'var(--jp-layout-color1)', 43 | 44 | border: 'var(--jp-border-width) solid var(--jp-border-color2)', 45 | borderRadius: '3px', 46 | 47 | $nest: { 48 | '&:active': { 49 | border: 'var(--jp-border-width) solid var(--jp-brand-color1)' 50 | }, 51 | '&:focus': { 52 | border: 'var(--jp-border-width) solid var(--jp-brand-color1)' 53 | } 54 | } 55 | }); 56 | 57 | export class CommandWidget extends ReactWidget { 58 | constructor(path: string, options: IExecutor[]) { 59 | super(); 60 | this._path = path; 61 | this._options = options; 62 | this._selectedExecutor = options[0].command; 63 | } 64 | 65 | getValue(): string { 66 | const command = this._selectedExecutor 67 | .replace('{path}', this._path) 68 | .replace('{args}', this._arguments); 69 | 70 | if (this._environVariables.length === 0) { 71 | return command; 72 | } 73 | 74 | return `${this._environVariables} ${command}`; 75 | } 76 | 77 | protected render(): React.ReactElement { 78 | return ( 79 |
80 | 81 | 93 | 103 | 115 | 127 | 140 |
141 | ); 142 | } 143 | 144 | private _path = ''; 145 | private _selectedExecutor = ''; 146 | private _arguments = ''; 147 | private _environVariables = ''; 148 | private _options = [] as IExecutor[]; 149 | private _signal = new Signal(this); 150 | } -------------------------------------------------------------------------------- /src/executor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | showDialog, 3 | Dialog, 4 | Clipboard, 5 | MainAreaWidget 6 | } from '@jupyterlab/apputils'; 7 | 8 | import { JupyterFrontEnd } from '@jupyterlab/application'; 9 | 10 | import { TerminalManager } from '@jupyterlab/services'; 11 | 12 | import { Terminal } from '@jupyterlab/terminal'; 13 | 14 | import { IExecutor, CommandWidget } from './command'; 15 | 16 | /** 17 | * Show the execution dialog 18 | */ 19 | export function showExecutionDialog( 20 | app: JupyterFrontEnd, 21 | path: string, 22 | options: IExecutor[] 23 | ) { 24 | const dialog = showDialog({ 25 | title: 'Execute', 26 | buttons: [ 27 | Dialog.cancelButton({ label: 'Cancel' }), 28 | Dialog.createButton({ label: 'Copy Command' }), 29 | Dialog.okButton({ label: 'Execute' }) 30 | ], 31 | body: new CommandWidget(path, options) 32 | }); 33 | 34 | dialog.then(async object => { 35 | if (object.button.accept) { 36 | if (object.button.label === 'Execute') { 37 | console.log('Execute the command'); 38 | 39 | const manager = new TerminalManager(); 40 | const s1 = await manager.startNew(); 41 | const term1 = new Terminal(s1, { 42 | initialCommand: `${object.value}` 43 | }); 44 | term1.title.closable = true; 45 | 46 | const widget = new MainAreaWidget({ content: term1 }); 47 | widget.id = `jupyter-executor-${Date.now()}`; 48 | widget.title.label = 'Execute'; 49 | widget.title.closable = true; 50 | 51 | if (!widget.isAttached) { 52 | // Attach the widget to the main work area if it's not there 53 | app.shell.add(widget, 'main'); 54 | } 55 | // Activate the widget 56 | app.shell.activateById(widget.id); 57 | } else if (object.button.label === 'Copy Command') { 58 | Clipboard.copyToSystem(object.value!); 59 | } else { 60 | console.log(`${object.button.label}`); 61 | } 62 | } else { 63 | console.log('Canceled'); 64 | } 65 | }); 66 | 67 | return dialog; 68 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application'; 5 | 6 | import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; 7 | 8 | import { ISettingRegistry } from '@jupyterlab/settingregistry'; 9 | 10 | import { buildIcon } from '@jupyterlab/ui-components'; 11 | 12 | import { IExecutor } from './command'; 13 | 14 | import { showExecutionDialog } from './executor'; 15 | 16 | import path from "path"; 17 | 18 | const PLUGIN_ID = '@gavincyi/jupyterlab-executor:executor'; 19 | 20 | const COMMAND_ID = 'gavincyi/jupyterlab-executor:execute'; 21 | 22 | /** 23 | * Activate the jupyterlab-executor 24 | */ 25 | function activate( 26 | app: JupyterFrontEnd, 27 | factory: IFileBrowserFactory, 28 | settingRegistry: ISettingRegistry 29 | ) { 30 | console.log('JupyterLab extension jupyterlab-executor is activated!'); 31 | const { tracker } = factory; 32 | let executors = [] as IExecutor[]; 33 | 34 | const updateSettings = (settings: ISettingRegistry.ISettings): void => { 35 | executors = settings.composite.executors as IExecutor[]; 36 | }; 37 | 38 | Promise.all([settingRegistry.load(PLUGIN_ID), app.restored]).then( 39 | ([settings]) => { 40 | updateSettings(settings); 41 | settings.changed.connect(updateSettings); 42 | } 43 | ); 44 | 45 | app.commands.addCommand(COMMAND_ID, { 46 | label: 'Execute', 47 | caption: "Execute script", 48 | icon: buildIcon, 49 | execute: () => { 50 | const widget = tracker.currentWidget; 51 | if (!widget) { 52 | return; 53 | } 54 | 55 | // Show the execution dialog 56 | const serverRoot = (app as any)?.paths?.directories?.serverRoot; 57 | const localPath = widget.selectedItems().next()!.value.path; 58 | showExecutionDialog(app, path.join(serverRoot, localPath), executors); 59 | }, 60 | }); 61 | } 62 | 63 | /** 64 | * Initialization data for the jupyterlab-executor extension. 65 | */ 66 | const extension: JupyterFrontEndPlugin = { 67 | id: PLUGIN_ID, 68 | autoStart: true, 69 | requires: [IFileBrowserFactory, ISettingRegistry], 70 | activate: activate 71 | }; 72 | 73 | export default extension; -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | See the JupyterLab Developer Guide for useful CSS Patterns: 3 | 4 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 5 | */ 6 | -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "target": "ES2018" 21 | }, 22 | "include": ["src/*"] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ui-tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Testing 2 | 3 | This folder contains the integration tests of the extension. 4 | 5 | They are defined using [Playwright](https://playwright.dev/docs/intro) test runner 6 | and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. 7 | 8 | The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). 9 | 10 | The JupyterLab server configuration to use for the integration test is defined 11 | in [jupyter_server_test_config.py](./jupyter_server_test_config.py). 12 | 13 | The default configuration will produce video for failing tests and an HTML report. 14 | 15 | > There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). 16 | 17 | ## Run the tests 18 | 19 | > All commands are assumed to be executed from the root directory 20 | 21 | To run the tests, you need to: 22 | 23 | 1. Compile the extension: 24 | 25 | ```sh 26 | jlpm install 27 | jlpm build:prod 28 | ``` 29 | 30 | > Check the extension is installed in JupyterLab. 31 | 32 | 2. Install test dependencies (needed only once): 33 | 34 | ```sh 35 | cd ./ui-tests 36 | jlpm install 37 | jlpm playwright install 38 | cd .. 39 | ``` 40 | 41 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: 42 | 43 | ```sh 44 | cd ./ui-tests 45 | jlpm playwright test 46 | ``` 47 | 48 | Test results will be shown in the terminal. In case of any test failures, the test report 49 | will be opened in your browser at the end of the tests execution; see 50 | [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) 51 | for configuring that behavior. 52 | 53 | ## Update the tests snapshots 54 | 55 | > All commands are assumed to be executed from the root directory 56 | 57 | If you are comparing snapshots to validate your tests, you may need to update 58 | the reference snapshots stored in the repository. To do that, you need to: 59 | 60 | 1. Compile the extension: 61 | 62 | ```sh 63 | jlpm install 64 | jlpm build:prod 65 | ``` 66 | 67 | > Check the extension is installed in JupyterLab. 68 | 69 | 2. Install test dependencies (needed only once): 70 | 71 | ```sh 72 | cd ./ui-tests 73 | jlpm install 74 | jlpm playwright install 75 | cd .. 76 | ``` 77 | 78 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) command: 79 | 80 | ```sh 81 | cd ./ui-tests 82 | jlpm playwright test -u 83 | ``` 84 | 85 | > Some discrepancy may occurs between the snapshots generated on your computer and 86 | > the one generated on the CI. To ease updating the snapshots on a PR, you can 87 | > type `please update playwright snapshots` to trigger the update by a bot on the CI. 88 | > Once the bot has computed new snapshots, it will commit them to the PR branch. 89 | 90 | ## Create tests 91 | 92 | > All commands are assumed to be executed from the root directory 93 | 94 | To create tests, the easiest way is to use the code generator tool of playwright: 95 | 96 | 1. Compile the extension: 97 | 98 | ```sh 99 | jlpm install 100 | jlpm build:prod 101 | ``` 102 | 103 | > Check the extension is installed in JupyterLab. 104 | 105 | 2. Install test dependencies (needed only once): 106 | 107 | ```sh 108 | cd ./ui-tests 109 | jlpm install 110 | jlpm playwright install 111 | cd .. 112 | ``` 113 | 114 | 3. Start the server: 115 | 116 | ```sh 117 | cd ./ui-tests 118 | jlpm start 119 | ``` 120 | 121 | 4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: 122 | 123 | ```sh 124 | cd ./ui-tests 125 | jlpm playwright codegen localhost:8888 126 | ``` 127 | 128 | ## Debug tests 129 | 130 | > All commands are assumed to be executed from the root directory 131 | 132 | To debug tests, a good way is to use the inspector tool of playwright: 133 | 134 | 1. Compile the extension: 135 | 136 | ```sh 137 | jlpm install 138 | jlpm build:prod 139 | ``` 140 | 141 | > Check the extension is installed in JupyterLab. 142 | 143 | 2. Install test dependencies (needed only once): 144 | 145 | ```sh 146 | cd ./ui-tests 147 | jlpm install 148 | jlpm playwright install 149 | cd .. 150 | ``` 151 | 152 | 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): 153 | 154 | ```sh 155 | cd ./ui-tests 156 | jlpm playwright test --debug 157 | ``` 158 | 159 | ## Upgrade Playwright and the browsers 160 | 161 | To update the web browser versions, you must update the package `@playwright/test`: 162 | 163 | ```sh 164 | cd ./ui-tests 165 | jlpm up "@playwright/test" 166 | jlpm playwright install 167 | ``` 168 | -------------------------------------------------------------------------------- /ui-tests/jupyter_server_test_config.py: -------------------------------------------------------------------------------- 1 | """Server configuration for integration tests. 2 | 3 | !! Never use this configuration in production because it 4 | opens the server to the world and provide access to JupyterLab 5 | JavaScript objects through the global window variable. 6 | """ 7 | from jupyterlab.galata import configure_jupyter_server 8 | 9 | configure_jupyter_server(c) 10 | 11 | # Uncomment to set server log level to debug level 12 | # c.ServerApp.log_level = "DEBUG" 13 | -------------------------------------------------------------------------------- /ui-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab_executor-ui-tests", 3 | "version": "1.0.0", 4 | "description": "JupyterLab jupyterlab_executor Integration Tests", 5 | "private": true, 6 | "scripts": { 7 | "start": "jupyter lab --config jupyter_server_test_config.py", 8 | "test": "jlpm playwright test", 9 | "test:update": "jlpm playwright test --update-snapshots" 10 | }, 11 | "devDependencies": { 12 | "@jupyterlab/galata": "^5.0.5", 13 | "@playwright/test": "^1.37.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui-tests/playwright.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for Playwright using default from @jupyterlab/galata 3 | */ 4 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); 5 | 6 | module.exports = { 7 | ...baseConfig, 8 | webServer: { 9 | command: 'jlpm start', 10 | url: 'http://localhost:8888/lab', 11 | timeout: 120 * 1000, 12 | reuseExistingServer: !process.env.CI 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /ui-tests/tests/jupyterlab_executor.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jupyterlab/galata'; 2 | 3 | /** 4 | * Don't load JupyterLab webpage before running the tests. 5 | * This is required to ensure we capture all log messages. 6 | */ 7 | test.use({ autoGoto: false }); 8 | 9 | test('should emit an activation console message', async ({ page }) => { 10 | const logs: string[] = []; 11 | 12 | page.on('console', message => { 13 | logs.push(message.text()); 14 | }); 15 | 16 | await page.goto(); 17 | 18 | expect( 19 | logs.filter(s => s === 'JupyterLab extension jupyterlab_executor is activated!') 20 | ).toHaveLength(1); 21 | }); 22 | -------------------------------------------------------------------------------- /ui-tests/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gavincyi/jupyterlab-executor/d0097f51fe24d25d74ae942b2c051d16642c984a/ui-tests/yarn.lock --------------------------------------------------------------------------------