├── .copier-answers.yml ├── .github └── workflows │ ├── binder-on-pr.yml │ ├── 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 ├── README.md ├── RELEASE.md ├── babel.config.js ├── binder ├── environment.yml └── postBuild ├── install.json ├── jest.config.js ├── jupyterlab_apod └── __init__.py ├── package.json ├── pyproject.toml ├── setup.py ├── src ├── __tests__ │ └── jupyterlab_apod.spec.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_apod.spec.ts └── yarn.lock └── yarn.lock /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY 2 | _commit: v4.3.3 3 | _src_path: https://github.com/jupyterlab/extension-template 4 | author_email: your@name.org 5 | author_name: Your Name 6 | has_binder: true 7 | has_settings: false 8 | kind: frontend 9 | labextension_name: jupyterlab_apod 10 | project_short_description: Show a random NASA Astronomy Picture of the Day in a JupyterLab 11 | panel. 12 | python_name: jupyterlab_apod 13 | repository: https://github.com/github_username/jupyterlab_apod 14 | test: true 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/binder-on-pr.yml: -------------------------------------------------------------------------------- 1 | name: Binder Badge 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | 6 | jobs: 7 | binder: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - uses: jupyterlab/maintainer-tools/.github/actions/binder-link@v1 13 | with: 14 | github_token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Base Setup 22 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - name: Install dependencies 25 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 26 | 27 | - name: Lint the extension 28 | run: | 29 | set -eux 30 | jlpm 31 | jlpm run lint:check 32 | 33 | - name: Test the extension 34 | run: | 35 | set -eux 36 | jlpm run test 37 | 38 | - name: Build the extension 39 | run: | 40 | set -eux 41 | python -m pip install .[test] 42 | 43 | jupyter labextension list 44 | jupyter labextension list 2>&1 | grep -ie "jupyterlab_apod.*OK" 45 | python -m jupyterlab.browser_check 46 | 47 | - name: Package the extension 48 | run: | 49 | set -eux 50 | 51 | pip install build 52 | python -m build 53 | pip uninstall -y "jupyterlab_apod" jupyterlab 54 | 55 | - name: Upload extension packages 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: extension-artifacts 59 | path: dist/jupyterlab_apod* 60 | if-no-files-found: error 61 | 62 | test_isolated: 63 | needs: build 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - name: Install Python 68 | uses: actions/setup-python@v5 69 | with: 70 | python-version: '3.9' 71 | architecture: 'x64' 72 | - uses: actions/download-artifact@v4 73 | with: 74 | name: extension-artifacts 75 | - name: Install and Test 76 | run: | 77 | set -eux 78 | # Remove NodeJS, twice to take care of system and locally installed node versions. 79 | sudo rm -rf $(which node) 80 | sudo rm -rf $(which node) 81 | 82 | pip install "jupyterlab>=4.0.0,<5" jupyterlab_apod*.whl 83 | 84 | 85 | jupyter labextension list 86 | jupyter labextension list 2>&1 | grep -ie "jupyterlab_apod.*OK" 87 | python -m jupyterlab.browser_check --no-browser-test 88 | 89 | integration-tests: 90 | name: Integration tests 91 | needs: build 92 | runs-on: ubuntu-latest 93 | 94 | env: 95 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers 96 | 97 | steps: 98 | - name: Checkout 99 | uses: actions/checkout@v4 100 | 101 | - name: Base Setup 102 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 103 | 104 | - name: Download extension package 105 | uses: actions/download-artifact@v4 106 | with: 107 | name: extension-artifacts 108 | 109 | - name: Install the extension 110 | run: | 111 | set -eux 112 | python -m pip install "jupyterlab>=4.0.0,<5" jupyterlab_apod*.whl 113 | 114 | - name: Install dependencies 115 | working-directory: ui-tests 116 | env: 117 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0 118 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 119 | run: jlpm install 120 | 121 | - name: Set up browser cache 122 | uses: actions/cache@v4 123 | with: 124 | path: | 125 | ${{ github.workspace }}/pw-browsers 126 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} 127 | 128 | - name: Install browser 129 | run: jlpm playwright install chromium 130 | working-directory: ui-tests 131 | 132 | - name: Execute integration tests 133 | working-directory: ui-tests 134 | run: | 135 | jlpm playwright test 136 | 137 | - name: Upload Playwright Test report 138 | if: always() 139 | uses: actions/upload-artifact@v4 140 | with: 141 | name: jupyterlab_apod-playwright-tests 142 | path: | 143 | ui-tests/test-results 144 | ui-tests/playwright-report 145 | 146 | check_links: 147 | name: Check Links 148 | runs-on: ubuntu-latest 149 | timeout-minutes: 15 150 | steps: 151 | - uses: actions/checkout@v4 152 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 153 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 154 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["*"] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | check_release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Base Setup 19 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 20 | - name: Check Release 21 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 22 | with: 23 | 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: Upload Distributions 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: jupyterlab_apod-releaser-dist-${{ github.run_number }} 30 | path: .jupyter_releaser_checkout/dist 31 | -------------------------------------------------------------------------------- /.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 | # silent: 16 | # description: "Set a placeholder in the changelog and don't publish the release." 17 | # required: false 18 | # type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | # silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | branch: ${{ github.event.inputs.branch }} 43 | since: ${{ github.event.inputs.since }} 44 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 45 | 46 | - name: "** Next Step **" 47 | run: | 48 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 49 | -------------------------------------------------------------------------------- /.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 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v1 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 44 | with: 45 | token: ${{ steps.app-token.outputs.token }} 46 | release_url: ${{ steps.populate-release.outputs.release_url }} 47 | 48 | - name: "** Next Step **" 49 | if: ${{ success() }} 50 | run: | 51 | echo "Verify the final release" 52 | echo ${{ steps.finalize-release.outputs.release_url }} 53 | 54 | - name: "** Failure Message **" 55 | if: ${{ failure() }} 56 | run: | 57 | echo "Failed to Publish the Draft Release Url:" 58 | echo ${{ steps.populate-release.outputs.release_url }} 59 | -------------------------------------------------------------------------------- /.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: > 14 | ( 15 | github.event.issue.author_association == 'OWNER' || 16 | github.event.issue.author_association == 'COLLABORATOR' || 17 | github.event.issue.author_association == 'MEMBER' 18 | ) && github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: React to the triggering comment 23 | run: | 24 | gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions --raw-field 'content=+1' 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | token: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Get PR Info 34 | id: pr 35 | env: 36 | PR_NUMBER: ${{ github.event.issue.number }} 37 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | GH_REPO: ${{ github.repository }} 39 | COMMENT_AT: ${{ github.event.comment.created_at }} 40 | run: | 41 | pr="$(gh api /repos/${GH_REPO}/pulls/${PR_NUMBER})" 42 | head_sha="$(echo "$pr" | jq -r .head.sha)" 43 | pushed_at="$(echo "$pr" | jq -r .pushed_at)" 44 | 45 | if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then 46 | echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)" 47 | exit 1 48 | fi 49 | 50 | echo "head_sha=$head_sha" >> $GITHUB_OUTPUT 51 | 52 | - name: Checkout the branch from the PR that triggered the job 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: gh pr checkout ${{ github.event.issue.number }} 56 | 57 | - name: Validate the fetched branch HEAD revision 58 | env: 59 | EXPECTED_SHA: ${{ steps.pr.outputs.head_sha }} 60 | run: | 61 | actual_sha="$(git rev-parse HEAD)" 62 | 63 | if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then 64 | echo "The HEAD of the checked out branch ($actual_sha) differs from the HEAD commit available at the time when trigger comment was submitted ($EXPECTED_SHA)" 65 | exit 1 66 | fi 67 | 68 | - name: Base Setup 69 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 70 | 71 | - name: Install dependencies 72 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 73 | 74 | - name: Install extension 75 | run: | 76 | set -eux 77 | jlpm 78 | python -m pip install . 79 | 80 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1 81 | with: 82 | github_token: ${{ secrets.GITHUB_TOKEN }} 83 | # Playwright knows how to start JupyterLab server 84 | start_server_script: 'null' 85 | test_folder: ui-tests 86 | npm_client: jlpm 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | jupyterlab_apod/labextension 11 | # Version file is handled by hatchling 12 | jupyterlab_apod/_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_apod 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, Your Name 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab_apod 2 | 3 | [![Github Actions Status](https://github.com/jupyterlab/jupyterlab_apod/workflows/Build/badge.svg)](https://github.com/jupyterlab/jupyterlab_apod/actions/workflows/build.yml) 4 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab/jupyterlab_apod/main?urlpath=lab) 5 | 6 | Show a random NASA Astronomy Picture of the Day in a JupyterLab panel. 7 | 8 | ## Requirements 9 | 10 | - JupyterLab >= 4.0.0 11 | 12 | ## Install 13 | 14 | To install the extension, execute: 15 | 16 | ```bash 17 | pip install jupyterlab_apod 18 | ``` 19 | 20 | ## Uninstall 21 | 22 | To remove the extension, execute: 23 | 24 | ```bash 25 | pip uninstall jupyterlab_apod 26 | ``` 27 | 28 | ## Contributing 29 | 30 | ### Development install 31 | 32 | Note: You will need NodeJS to build the extension package. 33 | 34 | The `jlpm` command is JupyterLab's pinned version of 35 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 36 | `yarn` or `npm` in lieu of `jlpm` below. 37 | 38 | ```bash 39 | # Clone the repo to your local environment 40 | # Change directory to the jupyterlab_apod directory 41 | # Install package in development mode 42 | pip install -e "." 43 | # Link your development version of the extension with JupyterLab 44 | jupyter labextension develop . --overwrite 45 | # Rebuild extension Typescript source after making changes 46 | jlpm build 47 | ``` 48 | 49 | 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. 50 | 51 | ```bash 52 | # Watch the source directory in one terminal, automatically rebuilding when needed 53 | jlpm watch 54 | # Run JupyterLab in another terminal 55 | jupyter lab 56 | ``` 57 | 58 | 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). 59 | 60 | By default, the `jlpm 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: 61 | 62 | ```bash 63 | jupyter lab build --minimize=False 64 | ``` 65 | 66 | ### Development uninstall 67 | 68 | ```bash 69 | pip uninstall jupyterlab_apod 70 | ``` 71 | 72 | In development mode, you will also need to remove the symlink created by `jupyter labextension develop` 73 | command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` 74 | folder is located. Then you can remove the symlink named `jupyterlab_apod` within that folder. 75 | 76 | ### Testing the extension 77 | 78 | #### Frontend tests 79 | 80 | This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. 81 | 82 | To execute them, execute: 83 | 84 | ```sh 85 | jlpm 86 | jlpm test 87 | ``` 88 | 89 | #### Integration tests 90 | 91 | This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). 92 | More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. 93 | 94 | More information are provided within the [ui-tests](./ui-tests/README.md) README. 95 | 96 | ### Packaging the extension 97 | 98 | See [RELEASE](RELEASE.md) 99 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of jupyterlab_apod 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. But 62 | the GitHub repository and the package managers need to be properly set up. Please 63 | follow the instructions of the Jupyter Releaser [checklist](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html). 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Go to the Actions panel 68 | - Run the "Step 1: Prep Release" workflow 69 | - Check the draft changelog 70 | - Run the "Step 2: Publish Release" workflow 71 | 72 | > [!NOTE] 73 | > Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) 74 | > for more information. 75 | 76 | ## Publishing to `conda-forge` 77 | 78 | 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 79 | 80 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 81 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | # a mybinder.org-ready environment for demoing jupyterlab_apod 2 | # this environment may also be used locally on Linux/MacOS/Windows, e.g. 3 | # 4 | # conda env update --file binder/environment.yml 5 | # conda activate jupyterlab-apod-demo 6 | # 7 | name: jupyterlab-apod-demo 8 | 9 | channels: 10 | - conda-forge 11 | 12 | dependencies: 13 | # runtime dependencies 14 | - python >=3.10,<3.11.0a0 15 | - jupyterlab >=4.0.0,<5 16 | # labextension build dependencies 17 | - nodejs >=18,<19 18 | - pip 19 | - wheel 20 | # additional packages for demos 21 | # - ipywidgets 22 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ perform a development install of jupyterlab_apod 3 | 4 | On Binder, this will run _after_ the environment has been fully created from 5 | the environment.yml in this directory. 6 | 7 | This script should also run locally on Linux/MacOS/Windows: 8 | 9 | python3 binder/postBuild 10 | """ 11 | import subprocess 12 | import sys 13 | from pathlib import Path 14 | 15 | 16 | ROOT = Path.cwd() 17 | 18 | def _(*args, **kwargs): 19 | """ Run a command, echoing the args 20 | 21 | fails hard if something goes wrong 22 | """ 23 | print("\n\t", " ".join(args), "\n") 24 | return_code = subprocess.call(args, **kwargs) 25 | if return_code != 0: 26 | print("\nERROR", return_code, " ".join(args)) 27 | sys.exit(return_code) 28 | 29 | # verify the environment is self-consistent before even starting 30 | _(sys.executable, "-m", "pip", "check") 31 | 32 | # install the labextension 33 | _(sys.executable, "-m", "pip", "install", "-e", ".") 34 | _(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".") 35 | 36 | # verify the environment the extension didn't break anything 37 | _(sys.executable, "-m", "pip", "check") 38 | 39 | # list the extensions 40 | _("jupyter", "server", "extension", "list") 41 | 42 | # initially list installed extensions to determine if there are any surprises 43 | _("jupyter", "labextension", "list") 44 | 45 | 46 | print("JupyterLab with jupyterlab_apod is ready to run with:\n") 47 | print("\tjupyter lab\n") 48 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyterlab_apod", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_apod" 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_apod/__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_apod' outside a proper installation.") 9 | __version__ = "dev" 10 | 11 | 12 | def _jupyter_labextension_paths(): 13 | return [{ 14 | "src": "labextension", 15 | "dest": "jupyterlab_apod" 16 | }] 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab_apod", 3 | "version": "0.1.0", 4 | "description": "Show a random NASA Astronomy Picture of the Day in a JupyterLab panel.", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/github_username/jupyterlab_apod", 11 | "bugs": { 12 | "url": "https://github.com/github_username/jupyterlab_apod/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Your Name", 17 | "email": "your@name.org" 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 | "src/**/*.{ts,tsx}" 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/jupyterlab/jupyterlab_apod.git" 30 | }, 31 | "scripts": { 32 | "build": "jlpm build:lib && jlpm build:labextension:dev", 33 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 34 | "build:labextension": "jupyter labextension build .", 35 | "build:labextension:dev": "jupyter labextension build --development True .", 36 | "build:lib": "tsc --sourceMap", 37 | "build:lib:prod": "tsc", 38 | "clean": "jlpm clean:lib", 39 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 40 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 41 | "clean:labextension": "rimraf jupyterlab_apod/labextension jupyterlab_apod/_version.py", 42 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 43 | "eslint": "jlpm eslint:check --fix", 44 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 45 | "install:extension": "jlpm build", 46 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 47 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 48 | "prettier": "jlpm prettier:base --write --list-different", 49 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 50 | "prettier:check": "jlpm prettier:base --check", 51 | "stylelint": "jlpm stylelint:check --fix", 52 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 53 | "test": "jest --coverage", 54 | "watch": "run-p watch:src watch:labextension", 55 | "watch:src": "tsc -w --sourceMap", 56 | "watch:labextension": "jupyter labextension watch ." 57 | }, 58 | "dependencies": { 59 | "@jupyterlab/application": "^4.0.0", 60 | "@jupyterlab/apputils": "^4.0.0", 61 | "@lumino/widgets": "^2.1.1" 62 | }, 63 | "devDependencies": { 64 | "@jupyterlab/builder": "^4.0.0", 65 | "@jupyterlab/testutils": "^4.0.0", 66 | "@types/jest": "^29.2.0", 67 | "@types/json-schema": "^7.0.11", 68 | "@types/react": "^18.0.26", 69 | "@typescript-eslint/eslint-plugin": "^6.1.0", 70 | "@typescript-eslint/parser": "^6.1.0", 71 | "css-loader": "^6.7.1", 72 | "eslint": "^8.36.0", 73 | "eslint-config-prettier": "^8.8.0", 74 | "eslint-plugin-prettier": "^5.0.0", 75 | "jest": "^29.2.0", 76 | "npm-run-all": "^4.1.5", 77 | "prettier": "^3.0.0", 78 | "rimraf": "^5.0.1", 79 | "source-map-loader": "^1.0.2", 80 | "style-loader": "^3.3.1", 81 | "stylelint": "^15.10.1", 82 | "stylelint-config-recommended": "^13.0.0", 83 | "stylelint-config-standard": "^34.0.0", 84 | "stylelint-csstree-validator": "^3.0.0", 85 | "stylelint-prettier": "^4.0.0", 86 | "typescript": "~5.0.2", 87 | "yjs": "^13.5.0" 88 | }, 89 | "resolutions": { 90 | "@rjsf/core": "5.18.2" 91 | }, 92 | "sideEffects": [ 93 | "style/*.css", 94 | "style/index.js" 95 | ], 96 | "styleModule": "style/index.js", 97 | "publishConfig": { 98 | "access": "public" 99 | }, 100 | "jupyterlab": { 101 | "extension": true, 102 | "outputDir": "jupyterlab_apod/labextension" 103 | }, 104 | "eslintIgnore": [ 105 | "node_modules", 106 | "dist", 107 | "coverage", 108 | "**/*.d.ts", 109 | "tests", 110 | "**/__tests__", 111 | "ui-tests" 112 | ], 113 | "eslintConfig": { 114 | "extends": [ 115 | "eslint:recommended", 116 | "plugin:@typescript-eslint/eslint-recommended", 117 | "plugin:@typescript-eslint/recommended", 118 | "plugin:prettier/recommended" 119 | ], 120 | "parser": "@typescript-eslint/parser", 121 | "parserOptions": { 122 | "project": "tsconfig.json", 123 | "sourceType": "module" 124 | }, 125 | "plugins": [ 126 | "@typescript-eslint" 127 | ], 128 | "rules": { 129 | "@typescript-eslint/naming-convention": [ 130 | "error", 131 | { 132 | "selector": "interface", 133 | "format": [ 134 | "PascalCase" 135 | ], 136 | "custom": { 137 | "regex": "^I[A-Z]", 138 | "match": true 139 | } 140 | } 141 | ], 142 | "@typescript-eslint/no-unused-vars": [ 143 | "warn", 144 | { 145 | "args": "none" 146 | } 147 | ], 148 | "@typescript-eslint/no-explicit-any": "off", 149 | "@typescript-eslint/no-namespace": "off", 150 | "@typescript-eslint/no-use-before-define": "off", 151 | "@typescript-eslint/quotes": [ 152 | "error", 153 | "single", 154 | { 155 | "avoidEscape": true, 156 | "allowTemplateLiterals": false 157 | } 158 | ], 159 | "curly": [ 160 | "error", 161 | "all" 162 | ], 163 | "eqeqeq": "error", 164 | "prefer-arrow-callback": "error" 165 | } 166 | }, 167 | "prettier": { 168 | "singleQuote": true, 169 | "trailingComma": "none", 170 | "arrowParens": "avoid", 171 | "endOfLine": "auto", 172 | "overrides": [ 173 | { 174 | "files": "package.json", 175 | "options": { 176 | "tabWidth": 4 177 | } 178 | } 179 | ] 180 | }, 181 | "stylelint": { 182 | "extends": [ 183 | "stylelint-config-recommended", 184 | "stylelint-config-standard", 185 | "stylelint-prettier/recommended" 186 | ], 187 | "plugins": [ 188 | "stylelint-csstree-validator" 189 | ], 190 | "rules": { 191 | "csstree/validator": true, 192 | "property-no-vendor-prefix": null, 193 | "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$", 194 | "selector-no-vendor-prefix": null, 195 | "value-no-vendor-prefix": null 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "jupyterlab_apod" 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 | "Programming Language :: Python :: 3.12", 24 | ] 25 | dependencies = [ 26 | ] 27 | dynamic = ["version", "description", "authors", "urls", "keywords"] 28 | 29 | [tool.hatch.version] 30 | source = "nodejs" 31 | 32 | [tool.hatch.metadata.hooks.nodejs] 33 | fields = ["description", "authors", "urls"] 34 | 35 | [tool.hatch.build.targets.sdist] 36 | artifacts = ["jupyterlab_apod/labextension"] 37 | exclude = [".github", "binder"] 38 | 39 | [tool.hatch.build.targets.wheel.shared-data] 40 | "jupyterlab_apod/labextension" = "share/jupyter/labextensions/jupyterlab_apod" 41 | "install.json" = "share/jupyter/labextensions/jupyterlab_apod/install.json" 42 | 43 | [tool.hatch.build.hooks.version] 44 | path = "jupyterlab_apod/_version.py" 45 | 46 | [tool.hatch.build.hooks.jupyter-builder] 47 | dependencies = ["hatch-jupyter-builder>=0.5"] 48 | build-function = "hatch_jupyter_builder.npm_builder" 49 | ensured-targets = [ 50 | "jupyterlab_apod/labextension/static/style.js", 51 | "jupyterlab_apod/labextension/package.json", 52 | ] 53 | skip-if-exists = ["jupyterlab_apod/labextension/static/style.js"] 54 | 55 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 56 | build_cmd = "build:prod" 57 | npm = ["jlpm"] 58 | 59 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 60 | build_cmd = "install:extension" 61 | npm = ["jlpm"] 62 | source_dir = "src" 63 | build_dir = "jupyterlab_apod/labextension" 64 | 65 | [tool.jupyter-releaser.options] 66 | version_cmd = "hatch version" 67 | 68 | [tool.jupyter-releaser.hooks] 69 | before-build-npm = [ 70 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 71 | "jlpm", 72 | "jlpm build:prod" 73 | ] 74 | before-build-python = ["jlpm clean:all"] 75 | 76 | [tool.check-wheel-contents] 77 | ignore = ["W002"] 78 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __import__("setuptools").setup() 2 | -------------------------------------------------------------------------------- /src/__tests__/jupyterlab_apod.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 3 | */ 4 | 5 | describe('jupyterlab_apod', () => { 6 | it('should be tested', () => { 7 | expect(1 + 1).toEqual(2); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ILayoutRestorer, 3 | JupyterFrontEnd, 4 | JupyterFrontEndPlugin 5 | } from '@jupyterlab/application'; 6 | 7 | import { 8 | ICommandPalette, 9 | MainAreaWidget, 10 | WidgetTracker 11 | } from '@jupyterlab/apputils'; 12 | 13 | import { Widget } from '@lumino/widgets'; 14 | 15 | interface IAPODResponse { 16 | copyright: string; 17 | date: string; 18 | explanation: string; 19 | media_type: 'video' | 'image'; 20 | title: string; 21 | url: string; 22 | } 23 | 24 | class APODWidget extends Widget { 25 | /** 26 | * Construct a new APOD widget. 27 | */ 28 | constructor() { 29 | super(); 30 | 31 | this.addClass('my-apodWidget'); 32 | 33 | // Add an image element to the panel 34 | this.img = document.createElement('img'); 35 | this.node.appendChild(this.img); 36 | 37 | // Add a summary element to the panel 38 | this.summary = document.createElement('p'); 39 | this.node.appendChild(this.summary); 40 | } 41 | 42 | /** 43 | * The image element associated with the widget. 44 | */ 45 | readonly img: HTMLImageElement; 46 | 47 | /** 48 | * The summary text element associated with the widget. 49 | */ 50 | readonly summary: HTMLParagraphElement; 51 | 52 | /** 53 | * Handle update requests for the widget. 54 | */ 55 | async updateAPODImage(): Promise { 56 | const response = await fetch( 57 | `https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=${this.randomDate()}` 58 | ); 59 | 60 | if (!response.ok) { 61 | const data = await response.json(); 62 | if (data.error) { 63 | this.summary.innerText = data.error.message; 64 | } else { 65 | this.summary.innerText = response.statusText; 66 | } 67 | return; 68 | } 69 | 70 | const data = (await response.json()) as IAPODResponse; 71 | 72 | if (data.media_type === 'image') { 73 | // Populate the image 74 | this.img.src = data.url; 75 | this.img.title = data.title; 76 | this.summary.innerText = data.title; 77 | if (data.copyright) { 78 | this.summary.innerText += ` (Copyright ${data.copyright})`; 79 | } 80 | } else { 81 | this.summary.innerText = 'Random APOD fetched was not an image.'; 82 | } 83 | } 84 | 85 | /** 86 | * Get a random date string in YYYY-MM-DD format. 87 | */ 88 | randomDate(): string { 89 | const start = new Date(2010, 1, 1); 90 | const end = new Date(); 91 | const randomDate = new Date( 92 | start.getTime() + Math.random() * (end.getTime() - start.getTime()) 93 | ); 94 | return randomDate.toISOString().slice(0, 10); 95 | } 96 | } 97 | 98 | /** 99 | * Activate the APOD widget extension. 100 | */ 101 | function activate( 102 | app: JupyterFrontEnd, 103 | palette: ICommandPalette, 104 | restorer: ILayoutRestorer | null 105 | ) { 106 | console.log('JupyterLab extension jupyterlab_apod is activated!'); 107 | 108 | // Declare a widget variable 109 | let widget: MainAreaWidget; 110 | 111 | // Add an application command 112 | const command: string = 'apod:open'; 113 | app.commands.addCommand(command, { 114 | label: 'Random Astronomy Picture', 115 | execute: () => { 116 | if (!widget || widget.isDisposed) { 117 | const content = new APODWidget(); 118 | widget = new MainAreaWidget({ content }); 119 | widget.id = 'apod-jupyterlab'; 120 | widget.title.label = 'Astronomy Picture'; 121 | widget.title.closable = true; 122 | } 123 | if (!tracker.has(widget)) { 124 | // Track the state of the widget for later restoration 125 | tracker.add(widget); 126 | } 127 | if (!widget.isAttached) { 128 | // Attach the widget to the main work area if it's not there 129 | app.shell.add(widget, 'main'); 130 | } 131 | widget.content.updateAPODImage(); 132 | 133 | // Activate the widget 134 | app.shell.activateById(widget.id); 135 | } 136 | }); 137 | 138 | // Add the command to the palette. 139 | palette.addItem({ command, category: 'Tutorial' }); 140 | 141 | // Track and restore the widget state 142 | const tracker = new WidgetTracker>({ 143 | namespace: 'apod' 144 | }); 145 | if (restorer) { 146 | restorer.restore(tracker, { 147 | command, 148 | name: () => 'apod' 149 | }); 150 | } 151 | } 152 | 153 | /** 154 | * Initialization data for the jupyterlab_apod extension. 155 | */ 156 | const plugin: JupyterFrontEndPlugin = { 157 | id: 'jupyterlab-apod', 158 | description: 159 | 'Show a random NASA Astronomy Picture of the Day in a JupyterLab panel.', 160 | autoStart: true, 161 | requires: [ICommandPalette], 162 | optional: [ILayoutRestorer], 163 | activate: activate 164 | }; 165 | 166 | export default plugin; 167 | -------------------------------------------------------------------------------- /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 | .my-apodWidget { 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | overflow: auto; 11 | } 12 | -------------------------------------------------------------------------------- /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/main/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_apod-ui-tests", 3 | "version": "1.0.0", 4 | "description": "JupyterLab jupyterlab_apod 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_apod.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_apod is activated!') 20 | ).toHaveLength(1); 21 | }); 22 | -------------------------------------------------------------------------------- /ui-tests/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyterlab_apod/397639e908826072b040aeec20fa050dc11046d4/ui-tests/yarn.lock --------------------------------------------------------------------------------