├── .eslintignore ├── .eslintrc.js ├── .flake8 ├── .gitconfig ├── .github └── workflows │ ├── binder-badge.yml │ ├── check-release.yml │ ├── enforce-label.yml │ ├── license-header.yml │ ├── prep-release.yml │ ├── publish-changelog.yml │ ├── publish-release.yml │ ├── test.yml │ └── update_galata_references.yaml ├── .gitignore ├── .licenserc.yaml ├── .npmignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc ├── .readthedocs.yaml ├── .stylelintrc ├── .yarnrc.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── binder ├── environment.yml ├── jupyter_config.py └── postBuild ├── codecov.yml ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── jupyter_logo.svg │ └── logo-icon.png │ ├── conf.py │ ├── configuration.md │ ├── developer │ ├── architecture.md │ ├── contributing.rst │ ├── javascript_api.rst │ └── python_api.rst │ ├── images │ └── rtc_shared_cursors.png │ └── index.md ├── install.json ├── lerna.json ├── package.json ├── packages ├── collaboration-extension │ ├── README.md │ ├── package.json │ ├── schema │ │ ├── shared-link.json │ │ └── user-menu-bar.json │ ├── src │ │ ├── collaboration.ts │ │ ├── index.ts │ │ └── sharedlink.ts │ ├── style │ │ ├── index.css │ │ └── index.js │ └── tsconfig.json ├── collaboration │ ├── README.md │ ├── package.json │ ├── src │ │ ├── collaboratorspanel.tsx │ │ ├── components.tsx │ │ ├── cursors.ts │ │ ├── index.ts │ │ ├── menu.ts │ │ ├── sharedlink.ts │ │ ├── tokens.ts │ │ ├── userinfopanel.tsx │ │ └── users-item.tsx │ ├── style │ │ ├── base.css │ │ ├── index.css │ │ ├── index.js │ │ ├── menu.css │ │ ├── sharedlink.css │ │ ├── sidepanel.css │ │ └── users-item.css │ └── tsconfig.json ├── collaborative-drive │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── tokens.ts │ └── tsconfig.json ├── docprovider-extension │ ├── README.md │ ├── package.json │ ├── src │ │ ├── executor.ts │ │ ├── filebrowser.ts │ │ ├── forkManager.ts │ │ └── index.ts │ ├── style │ │ ├── index.css │ │ └── index.js │ └── tsconfig.json └── docprovider │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── TimelineSlider.tsx │ ├── __tests__ │ │ ├── forkManager.spec.ts │ │ └── yprovider.spec.ts │ ├── awareness.ts │ ├── component.tsx │ ├── forkManager.ts │ ├── index.ts │ ├── notebookCellExecutor.ts │ ├── requests.ts │ ├── tokens.ts │ ├── ydrive.ts │ └── yprovider.ts │ ├── style │ ├── base.css │ ├── index.css │ ├── index.js │ └── slider.css │ ├── tsconfig.json │ └── tsconfig.test.json ├── projects ├── jupyter-collaboration-ui │ ├── LICENSE │ ├── README.md │ ├── install.json │ ├── jupyter_collaboration_ui │ │ ├── __init__.py │ │ └── _version.py │ ├── pyproject.toml │ └── setup.py ├── jupyter-collaboration │ ├── LICENSE │ ├── README.md │ ├── jupyter_collaboration │ │ ├── __init__.py │ │ └── _version.py │ ├── pyproject.toml │ └── setup.py ├── jupyter-docprovider │ ├── LICENSE │ ├── README.md │ ├── install.json │ ├── jupyter_docprovider │ │ ├── __init__.py │ │ └── _version.py │ ├── pyproject.toml │ └── setup.py └── jupyter-server-ydoc │ ├── LICENSE │ ├── README.md │ ├── jupyter-config │ └── jupyter_server_ydoc.json │ ├── jupyter_server_ydoc │ ├── __init__.py │ ├── _version.py │ ├── app.py │ ├── events │ │ ├── awareness.yaml │ │ ├── fork.yaml │ │ └── session.yaml │ ├── handlers.py │ ├── loaders.py │ ├── pytest_plugin.py │ ├── rooms.py │ ├── stores.py │ ├── test_utils.py │ ├── utils.py │ └── websocketserver.py │ ├── pyproject.toml │ └── setup.py ├── pyproject.toml ├── scripts ├── bump_version.py └── dev_install.py ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── test_app.py ├── test_documents.py ├── test_handlers.py ├── test_loaders.py └── test_rooms.py ├── tsconfig.json ├── tsconfig.test.json ├── typedoc.json ├── ui-tests ├── README.md ├── jupyter_server_test_config.py ├── package.json ├── playwright.config.js ├── playwright.timeline.config.js ├── tests │ ├── collaborationpanel.spec.ts │ ├── collaborationpanel.spec.ts-snapshots │ │ ├── collaboration-icon-linux.png │ │ ├── collaborationPanelCollapsed-linux.png │ │ ├── one-client-with-two-documents-linux.png │ │ ├── three-client-with-document-linux.png │ │ └── three-client-without-document-linux.png │ ├── data │ │ └── OutputExamples.ipynb │ ├── hub-share.spec.ts │ ├── hub-share.spec.ts-snapshots │ │ └── shared-link-dialog-hub-linux.png │ ├── notebook.spec.ts │ ├── notebook.spec.ts-snapshots │ │ ├── initialization-create-notebook-guest-linux.png │ │ ├── initialization-create-notebook-host-linux.png │ │ ├── initialization-open-notebook-guest-linux.png │ │ ├── initialization-open-notebook-host-linux.png │ │ └── ten-clients-add-a-new-cell-linux.png │ ├── timeline-slider.spec.ts │ ├── user-menu.spec.ts │ └── user-menu.spec.ts-snapshots │ │ ├── shared-link-dialog-linux.png │ │ └── shared-link-icon-linux.png └── yarn.lock └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | **/lib 2 | **/node_modules 3 | **/style 4 | **/package.json 5 | **/tsconfig.json 6 | **/tsconfig.test.json 7 | **/*.d.ts 8 | **/test 9 | **/ui-tests 10 | **/labextension 11 | 12 | docs 13 | tests 14 | .eslintrc.js 15 | jupyter-config 16 | jupyter_collaboration 17 | 18 | packages/collaboration/babel.config.js 19 | packages/docprovider/babel.config.js 20 | packages/docprovider/jest.config.js 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended' 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: 'tsconfig.json', 11 | sourceType: 'module' 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | rules: { 15 | '@typescript-eslint/naming-convention': [ 16 | 'error', 17 | { 18 | selector: 'interface', 19 | format: ['PascalCase'], 20 | custom: { 21 | regex: '^I[A-Z]', 22 | match: true 23 | } 24 | } 25 | ], 26 | '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-namespace': 'off', 29 | '@typescript-eslint/no-use-before-define': 'off', 30 | '@typescript-eslint/quotes': [ 31 | 'error', 32 | 'single', 33 | { avoidEscape: true, allowTemplateLiterals: false } 34 | ], 35 | curly: ['error', 'all'], 36 | eqeqeq: 'error', 37 | 'prefer-arrow-callback': 'error' 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501, W503, E402 3 | builtins = c, get_config 4 | exclude = 5 | .cache, 6 | .github, 7 | docs, 8 | setup.py 9 | enable-extensions = G 10 | extend-ignore = 11 | G001, G002, G004, G200, G201, G202, 12 | # black adds spaces around ':' 13 | E203, 14 | per-file-ignores = 15 | # B011: Do not call assert False since python -O removes these calls 16 | # F841 local variable 'foo' is assigned to but never used 17 | tests/*: B011, F841 18 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [blame] 2 | ignoreRevsFile = .git-blame-ignore-revs 3 | -------------------------------------------------------------------------------- /.github/workflows/binder-badge.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/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["*"] 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | check_release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Base Setup 19 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 20 | 21 | - name: Check Release 22 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: Upload Distributions 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: jupyter-releaser-dist-${{ github.run_number }} 30 | path: | 31 | .jupyter_releaser_checkout/dist 32 | -------------------------------------------------------------------------------- /.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/license-header.yml: -------------------------------------------------------------------------------- 1 | name: Fix License Headers 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | header-license-fix: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | - name: Configure git to use https 21 | run: git config --global hub.protocol https 22 | 23 | - name: Checkout the branch from the PR that triggered the job 24 | run: gh pr checkout ${{ github.event.pull_request.number }} 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Fix License Header 29 | uses: apache/skywalking-eyes/header@v0.4.0 30 | with: 31 | mode: fix 32 | 33 | - name: List files changed 34 | id: files-changed 35 | shell: bash -l {0} 36 | run: | 37 | set -ex 38 | export CHANGES=$(git status --porcelain | tee modified.log | wc -l) 39 | cat modified.log 40 | # Remove the log otherwise it will be committed 41 | rm modified.log 42 | 43 | echo "N_CHANGES=${CHANGES}" >> $GITHUB_OUTPUT 44 | 45 | git diff 46 | 47 | - name: Commit any changes 48 | if: steps.files-changed.outputs.N_CHANGES != '0' 49 | shell: bash -l {0} 50 | run: | 51 | git config user.name "github-actions[bot]" 52 | git config user.email "github-actions[bot]@users.noreply.github.com" 53 | 54 | git pull --no-tags 55 | 56 | git add * 57 | git commit -m "Automatic application of license header" 58 | 59 | git config push.default upstream 60 | git push 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.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 | target: ${{ github.event.inputs.target }} 43 | branch: ${{ github.event.inputs.branch }} 44 | since: ${{ github.event.inputs.since }} 45 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 46 | 47 | - name: "** Next Step **" 48 | run: | 49 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 50 | -------------------------------------------------------------------------------- /.github/workflows/publish-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Publish Changelog" 2 | on: 3 | release: 4 | types: [published] 5 | 6 | workflow_dispatch: 7 | inputs: 8 | branch: 9 | description: "The branch to target" 10 | required: false 11 | 12 | jobs: 13 | publish_changelog: 14 | runs-on: ubuntu-latest 15 | environment: release 16 | steps: 17 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 18 | 19 | - uses: actions/create-github-app-token@v1 20 | id: app-token 21 | with: 22 | app-id: ${{ vars.APP_ID }} 23 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 24 | 25 | - name: Publish changelog 26 | id: publish-changelog 27 | uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2 28 | with: 29 | token: ${{ steps.app-token.outputs.token }} 30 | branch: ${{ github.event.inputs.branch }} 31 | 32 | - name: "** Next Step **" 33 | run: | 34 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}" 35 | -------------------------------------------------------------------------------- /.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_galata_references.yaml: -------------------------------------------------------------------------------- 1 | name: Update Galata References 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | defaults: 12 | run: 13 | shell: bash -l {0} 14 | 15 | jobs: 16 | update-snapshots: 17 | if: > 18 | ( 19 | github.event.comment.author_association == 'OWNER' || 20 | github.event.comment.author_association == 'COLLABORATOR' || 21 | github.event.comment.author_association == 'MEMBER' 22 | ) && github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots') 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: React to the triggering comment 26 | run: | 27 | gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions --raw-field 'content=+1' 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Get PR Info 37 | id: pr 38 | env: 39 | PR_NUMBER: ${{ github.event.issue.number }} 40 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | GH_REPO: ${{ github.repository }} 42 | COMMENT_AT: ${{ github.event.comment.created_at }} 43 | run: | 44 | pr="$(gh api /repos/${GH_REPO}/pulls/${PR_NUMBER})" 45 | head_sha="$(echo "$pr" | jq -r .head.sha)" 46 | pushed_at="$(echo "$pr" | jq -r .pushed_at)" 47 | 48 | if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then 49 | echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)" 50 | exit 1 51 | fi 52 | 53 | echo "head_sha=$head_sha" >> $GITHUB_OUTPUT 54 | 55 | - name: Checkout the branch from the PR that triggered the job 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | run: gh pr checkout ${{ github.event.issue.number }} 59 | 60 | - name: Validate the fetched branch HEAD revision 61 | env: 62 | EXPECTED_SHA: ${{ steps.pr.outputs.head_sha }} 63 | run: | 64 | actual_sha="$(git rev-parse HEAD)" 65 | 66 | if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then 67 | 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)" 68 | exit 1 69 | fi 70 | 71 | - name: Base Setup 72 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 73 | 74 | - name: Build the extension 75 | run: yarn dev 76 | 77 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@main 78 | with: 79 | npm_client: jlpm 80 | github_token: ${{ secrets.GITHUB_TOKEN }} 81 | start_server_script: 'null' 82 | test_folder: ui-tests 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | package-lock.json 11 | docs/source/ts 12 | .jupyter 13 | labextension 14 | 15 | # Integration tests 16 | ui-tests/test-results/ 17 | ui-tests/playwright-report/ 18 | 19 | # Created by https://www.gitignore.io/api/python 20 | # Edit at https://www.gitignore.io/?templates=python 21 | 22 | ### Python ### 23 | # Byte-compiled / optimized / DLL files 24 | __pycache__/ 25 | *.py[cod] 26 | *$py.class 27 | 28 | # C extensions 29 | *.so 30 | 31 | # Distribution / packaging 32 | .Python 33 | build/ 34 | develop-eggs/ 35 | dist/ 36 | downloads/ 37 | eggs/ 38 | .eggs/ 39 | lib/ 40 | lib64/ 41 | parts/ 42 | sdist/ 43 | var/ 44 | wheels/ 45 | pip-wheel-metadata/ 46 | share/python-wheels/ 47 | .installed.cfg 48 | *.egg 49 | MANIFEST 50 | 51 | # PyInstaller 52 | # Usually these files are written by a python script from a template 53 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 54 | *.manifest 55 | *.spec 56 | 57 | # Installer logs 58 | pip-log.txt 59 | pip-delete-this-directory.txt 60 | 61 | # Unit test / coverage reports 62 | htmlcov/ 63 | .tox/ 64 | .nox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage/ 70 | coverage.xml 71 | *.cover 72 | .hypothesis/ 73 | .pytest_cache/ 74 | 75 | # Translations 76 | *.mo 77 | *.pot 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # Mr Developer 105 | .mr.developer.cfg 106 | .project 107 | .pydevproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | .dmypy.json 115 | dmypy.json 116 | 117 | # Pyre type checker 118 | .pyre/ 119 | 120 | # End of https://www.gitignore.io/api/python 121 | 122 | # OSX files 123 | .DS_Store 124 | docs/source/changelog.md 125 | 126 | .pnp.* 127 | .yarn/ 128 | !.yarn/patches 129 | !.yarn/plugins 130 | !.yarn/releases 131 | !.yarn/sdks 132 | !.yarn/versions 133 | packages/docprovider/junit.xml 134 | .jupyter_ystore.db 135 | -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- 1 | header: 2 | license: 3 | spdx-id: BSD-3-Clause 4 | copyright-owner: Jupyter Development Team 5 | software-name: JupyterLab 6 | content: | 7 | Copyright (c) Jupyter Development Team. 8 | Distributed under the terms of the Modified BSD License. 9 | 10 | paths-ignore: 11 | - '**/*.ipynb' 12 | - '**/*.json' 13 | - '**/*.md' 14 | - '**/*.svg' 15 | - '**/*.yml' 16 | - '**/*.yaml' 17 | - '**/build' 18 | - '**/lib' 19 | - '**/node_modules' 20 | - '*.map.js' 21 | - '*.bundle.js' 22 | - '**/.*' 23 | - 'binder/postBuild' 24 | - 'coverage' 25 | - 'LICENSE' 26 | - 'yarn.lock' 27 | - '**/_version.py' 28 | 29 | comment: on-failure 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | # skip any check that needs internet access 3 | skip: [prettier, eslint, stylelint] 4 | 5 | default_language_version: 6 | node: system 7 | 8 | repos: 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v4.4.0 11 | hooks: 12 | - id: end-of-file-fixer 13 | # Version bump conflict with this hook 14 | exclude: "^package\\.json$" 15 | - id: check-case-conflict 16 | - id: check-executables-have-shebangs 17 | - id: requirements-txt-fixer 18 | - id: check-added-large-files 19 | - id: check-case-conflict 20 | - id: check-toml 21 | - id: check-yaml 22 | - id: debug-statements 23 | - id: forbid-new-submodules 24 | - id: check-builtin-literals 25 | - id: trailing-whitespace 26 | 27 | - repo: https://github.com/astral-sh/ruff-pre-commit 28 | rev: v0.8.0 29 | hooks: 30 | - id: ruff 31 | args: [ --fix ] 32 | - id: ruff-format 33 | 34 | - repo: https://github.com/PyCQA/doc8 35 | rev: v1.1.1 36 | hooks: 37 | - id: doc8 38 | args: [--max-line-length=200] 39 | stages: [manual] 40 | 41 | - repo: https://github.com/pre-commit/mirrors-mypy 42 | rev: v1.15.0 43 | hooks: 44 | - id: mypy 45 | exclude: "(^binder/jupyter_config\\.py$)|(^scripts/bump_version\\.py$)|(/setup\\.py$)" 46 | args: ["--config-file", "pyproject.toml"] 47 | additional_dependencies: [tornado, pytest, pycrdt-websocket] 48 | stages: [manual] 49 | 50 | - repo: https://github.com/sirosen/check-jsonschema 51 | rev: 0.21.0 52 | hooks: 53 | - id: check-jsonschema 54 | name: "Check GitHub Workflows" 55 | files: ^\.github/workflows/ 56 | types: [yaml] 57 | args: ["--schemafile", "https://json.schemastore.org/github-workflow"] 58 | stages: [manual] 59 | 60 | - repo: local 61 | hooks: 62 | - id: prettier 63 | name: prettier 64 | entry: 'jlpm run prettier' 65 | language: node 66 | types_or: [json, ts, tsx, javascript, jsx, css] 67 | - id: eslint 68 | name: eslint 69 | entry: 'jlpm run eslint' 70 | language: node 71 | types_or: [ts, tsx, javascript, jsx] 72 | - id: stylelint 73 | name: stylelint 74 | entry: 'jlpm run stylelint' 75 | language: node 76 | types: [css] 77 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/lib 2 | **/node_modules 3 | **/style 4 | **/package.json 5 | **/tsconfig.json 6 | **/tsconfig.test.json 7 | **/*.d.ts 8 | **/test 9 | **/labextension 10 | 11 | docs 12 | tests 13 | jupyter-config 14 | jupyter_collaboration 15 | .mypy_cache 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid", 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | nodejs: "18" 11 | 12 | # Build documentation in the docs/ directory with Sphinx 13 | sphinx: 14 | configuration: docs/source/conf.py 15 | 16 | python: 17 | install: 18 | - method: pip 19 | path: . 20 | extra_requirements: 21 | - docs 22 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-standard", 5 | "stylelint-prettier/recommended" 6 | ], 7 | "rules": { 8 | "property-no-vendor-prefix": null, 9 | "selector-no-vendor-prefix": null, 10 | "value-no-vendor-prefix": null, 11 | "selector-class-pattern": null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to JupyterLab Real-Time Collaboration 2 | 3 | If you're reading this section, you're probably interested in contributing to 4 | JupyterLab Real-Time Collaboration. Welcome and thanks for your interest in contributing! 5 | 6 | Please take a look at Contributing to this extension on 7 | [Read the Docs](https://jupyterlab-realtime-collaboration.readthedocs.io/en/latest/developer/contributing.html) or 8 | [Repo docs](docs/source/developer/contributing.rst) (for the latest). 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Licensing terms 2 | 3 | This project is licensed under the terms of the Modified BSD License 4 | (also known as New or Revised or 3-Clause BSD), as follows: 5 | 6 | - Copyright (c) 2021-, Jupyter Development Team 7 | 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | Redistributions in binary form must reproduce the above copyright notice, this 17 | list of conditions and the following disclaimer in the documentation and/or 18 | other materials provided with the distribution. 19 | 20 | Neither the name of the Jupyter Development Team nor the names of its 21 | contributors may be used to endorse or promote products derived from this 22 | software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ## About the Jupyter Development Team 36 | 37 | The Jupyter Development Team is the set of all contributors to the Jupyter project. 38 | This includes all of the Jupyter subprojects. 39 | 40 | The core team that coordinates development on GitHub can be found here: 41 | https://github.com/jupyter/. 42 | 43 | ## Our Copyright Policy 44 | 45 | Jupyter uses a shared copyright model. Each contributor maintains copyright 46 | over their contributions to Jupyter. But, it is important to note that these 47 | contributions are typically only changes to the repositories. Thus, the Jupyter 48 | source code, in its entirety is not the copyright of any single person or 49 | institution. Instead, it is the collective copyright of the entire Jupyter 50 | Development Team. If individual contributors want to maintain a record of what 51 | changes/contributions they have specific copyright on, they should indicate 52 | their copyright in the commit message of the change, when they commit the 53 | change to one of the Jupyter repositories. 54 | 55 | With this in mind, the following banner should be used in any source code file 56 | to indicate the copyright and license terms: 57 | 58 | # Copyright (c) Jupyter Development Team. 59 | # Distributed under the terms of the Modified BSD License. 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jupyter Real-Time Collaboration 2 | 3 | [![Build Status](https://github.com/jupyterlab/jupyter_collaboration/actions/workflows/test.yml/badge.svg?query=branch%3Amain++)](https://github.com/jupyterlab/jupyter_collaboration/actions?query=branch%3Amain++) 4 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab/jupyter_collaboration/main) 5 | [![PyPI](https://img.shields.io/pypi/v/jupyter-collaboration)](https://pypi.org/project/jupyter-collaboration) 6 | [![npm](https://img.shields.io/npm/v/@jupyter/collaboration-extension)](https://www.npmjs.com/package/@jupyter/collaboration-extension) 7 | 8 | JupyterLab Real-Time Collaboration is a Jupyter Server Extension and JupyterLab extensions providing support for [Y documents](https://github.com/jupyter-server/jupyter_ydoc) and adding collaboration UI elements in JupyterLab. 9 | 10 | ![Real-Time Collaboration Demonstration](./docs/source/images/rtc_shared_cursors.png) 11 | 12 | ## Installation and Basic usage 13 | 14 | To install the latest release locally, make sure you have 15 | [pip installed](https://pip.readthedocs.io/en/stable/installing/) and run: 16 | 17 | ```bash 18 | pip install jupyter-collaboration 19 | ``` 20 | 21 | Or using ``conda``/``mamba``: 22 | 23 | ```bash 24 | conda install -c conda-forge jupyter-collaboration 25 | ``` 26 | 27 | ### Testing 28 | 29 | See [CONTRIBUTING](./docs/source/developer/contributing.rst#running-tests). 30 | 31 | ## Contributing 32 | 33 | If you are interested in contributing to the project, see [CONTRIBUTING](./docs/source/developer/contributing.rst). 34 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a jupyter-collaboration Release 2 | 3 | ## Using `jupyter_releaser` 4 | 5 | The recommended way to make a release is to use [`jupyter_releaser`](https://github.com/jupyter-server/jupyter_releaser#checklist-for-adoption). 6 | 7 | ## Version specification 8 | 9 | Here is an example of how version numbers progress through a release process. 10 | Input appropriate specifier into the `jupyter-releaser` workflow dispatch dialog to bump version numbers for this release. 11 | 12 | | Command | Python Version Change | NPM Version change | 13 | | --------- | --------------------- | ---------------------------------- | 14 | | `major` | x.y.z-> (x+1).0.0.a0 | All a.b.c -> a.(b+10).0-alpha.0 | 15 | | `minor` | x.y.z-> x.(y+1).0.a0 | All a.b.c -> a.(b+1).0-alpha.0 | 16 | | `build` | x.y.z.a0-> x.y.z.a1 | All a.b.c-alpha.0 -> a.b.c-alpha.1 | 17 | | `release` | x.y.z.a1-> x.y.z.b0 | All a.b.c-alpha.1 -> a.b.c-beta.0 | 18 | | `release` | x.y.z.b1-> x.y.z.rc0 | All a.b.c-beta.1 -> a.b.c-rc.0 | 19 | | `release` | x.y.z.rc0-> x.y.z | All a.b.c-rc0 -> a.b.c | 20 | | `patch` | x.y.z -> x.y.(z+1) | Changed a.b.c -> a.b.(c+1) | 21 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | name: example-environment 2 | channels: 3 | - conda-forge 4 | - nodefaults 5 | dependencies: 6 | - jupyterlab >=4.0.0 7 | - nodejs >=18,<19 8 | - python >=3.11,<3.12 9 | - yarn >=3,<4 10 | # build 11 | - hatchling >=1.5.0 12 | - hatch-jupyter-builder >=0.3.2 13 | - hatch-nodejs-version 14 | # dependencies 15 | - jupyter_server >=2.0.0 16 | # Use pip to get the the latest version 17 | - pip: 18 | - jupyter_server_fileid >=0.7.0 19 | - jupyter_ydoc >=1.0.0 20 | - ypy-websocket >=0.12.0 21 | -------------------------------------------------------------------------------- /binder/jupyter_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | import logging 4 | 5 | c.ServerApp.log_level = logging.DEBUG 6 | 7 | c.ContentsManager.allow_hidden = True 8 | # Use advance file ID service for out of band rename support 9 | c.FileIdExtension.file_id_manager_class = "jupyter_server_fileid.manager.LocalFileIdManager" 10 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) Jupyter Development Team. 4 | # Distributed under the terms of the Modified BSD License. 5 | 6 | source activate ${NB_PYTHON_PREFIX} 7 | 8 | set -euxo pipefail 9 | 10 | yarn || yarn 11 | 12 | python -m pip install -vv -e . --no-build-isolation 13 | jupyter server extension enable jupyter_collaboration 14 | 15 | mkdir -p ~/.jupyter/ 16 | 17 | cp binder/jupyter_config.py ~/.jupyter/ 18 | 19 | # FIXME until jupyter-server is the default on binder 20 | cp ${NB_PYTHON_PREFIX}/bin/jupyter-lab ${NB_PYTHON_PREFIX}/bin/jupyter-notebook 21 | 22 | jupyter troubleshoot 23 | jupyter notebook --show-config 24 | jupyter lab --show-config 25 | jupyter labextension list 26 | jupyter server extension list 27 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 10 7 | patch: 8 | default: 9 | target: 0% 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # Minimal makefile for Sphinx documentation 5 | # 6 | 7 | # You can set these variables from the command line, and also 8 | # from the environment for the first two. 9 | SPHINXOPTS ?= 10 | SPHINXBUILD ?= sphinx-build 11 | SOURCEDIR = source 12 | BUILDDIR = build 13 | 14 | # Put it first so that "make" without argument is like "make help". 15 | help: 16 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 17 | 18 | .PHONY: help Makefile 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | 25 | clean: 26 | # clean api build as well 27 | -rm -rf "$(SOURCEDIR)/ts" 28 | @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 29 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | rem Copyright (c) Jupyter Development Team. 2 | rem Distributed under the terms of the Modified BSD License. 3 | 4 | @ECHO OFF 5 | 6 | pushd %~dp0 7 | 8 | REM Command file for Sphinx documentation 9 | 10 | if "%SPHINXBUILD%" == "" ( 11 | set SPHINXBUILD=sphinx-build 12 | ) 13 | set SOURCEDIR=source 14 | set BUILDDIR=build 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.https://www.sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | if "%1" == "" goto help 30 | 31 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 32 | goto end 33 | 34 | :help 35 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 36 | 37 | :end 38 | popd 39 | -------------------------------------------------------------------------------- /docs/source/_static/jupyter_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | logo-5.svg 3 | Created using Figma 0.90 4 | 5 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/source/_static/logo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/docs/source/_static/logo-icon.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # Configuration file for the Sphinx documentation builder. 5 | # 6 | # For the full list of built-in configuration values, see the documentation: 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 8 | 9 | import shutil 10 | import time 11 | from pathlib import Path 12 | from subprocess import check_call 13 | 14 | HERE = Path(__file__).parent.resolve() 15 | 16 | # -- Project information ----------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 18 | 19 | project = "jupyter_collaboration" 20 | copyright = f"2022-{time.localtime().tm_year}, Jupyter Development Team" # noqa 21 | author = "Jupyter Development Team" 22 | release = "0.3.0" 23 | 24 | # -- General configuration --------------------------------------------------- 25 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 26 | 27 | extensions = ["myst_parser", "sphinx.ext.autodoc", "sphinxcontrib.mermaid"] 28 | 29 | templates_path = ["_templates"] 30 | exclude_patterns = ["ts/**"] 31 | source_suffix = { 32 | ".rst": "restructuredtext", 33 | ".md": "markdown", 34 | } 35 | 36 | # -- Options for HTML output ------------------------------------------------- 37 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 38 | 39 | html_extra_path = ["ts"] 40 | html_theme = "pydata_sphinx_theme" 41 | html_logo = "_static/jupyter_logo.svg" 42 | html_favicon = "_static/logo-icon.png" 43 | # Theme options are theme-specific and customize the look and feel of a theme 44 | # further. For a list of options available for each theme, see the 45 | # documentation. 46 | # 47 | html_theme_options = { 48 | "logo": { 49 | "text": "Real-Time Collaboration", 50 | "image_dark": "jupyter_logo.svg", 51 | "alt_text": "JupyterLab Real-Time Collaboration", 52 | }, 53 | "icon_links": [ 54 | { 55 | "name": "jupyter.org", 56 | "url": "https://jupyter.org", 57 | "icon": "_static/jupyter_logo.svg", 58 | "type": "local", 59 | } 60 | ], 61 | "github_url": "https://github.com/jupyterlab/jupyter-collaboration", 62 | "use_edit_page_button": True, 63 | "show_toc_level": 1, 64 | "navbar_align": "left", 65 | "navbar_end": ["navbar-icon-links.html"], 66 | "footer_items": ["copyright.html"], 67 | } 68 | 69 | # Output for github to be used in links 70 | html_context = { 71 | "github_user": "jupyterlab", # Username 72 | "github_repo": "jupyter-collaboration", # Repo name 73 | "github_version": "main", # Version 74 | "doc_path": "docs/source", # Path from repo root to the docs folder 75 | "conf_py_path": "/docs/source", # Path in the checkout to the docs root 76 | } 77 | 78 | myst_heading_anchors = 3 79 | 80 | 81 | def setup(app): 82 | # Copy changelog.md file 83 | dest = HERE / "changelog.md" 84 | shutil.copy(str(HERE.parent.parent / "CHANGELOG.md"), str(dest)) 85 | 86 | # Build JavaScript Docs 87 | js = HERE.parent.parent 88 | js_docs = HERE / "ts" / "api" 89 | if js_docs.exists(): 90 | shutil.rmtree(js_docs) 91 | 92 | print("Building JavaScript API docs") 93 | check_call(["jlpm", "install"], cwd=str(js)) 94 | check_call(["jlpm", "run", "docs"], cwd=str(js)) 95 | -------------------------------------------------------------------------------- /docs/source/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | By default, any change made to a document is saved to disk in an SQLite database file called 4 | `.jupyter_ystore.db` in the directory where JupyterLab was launched. This file helps in 5 | preserving the timeline of documents, for instance between JupyterLab sessions, or when a user 6 | looses connection and goes offline for a while. You should never have to touch it, and it is 7 | fine to just ignore it, including in your version control system (don't commit this file). If 8 | you happen to delete it, there shouldn't be any serious consequence either. 9 | 10 | There are a number of settings that you can change: 11 | 12 | ```bash 13 | # To enable or disable RTC (Real-Time Collaboration) (default: False). 14 | # If True, RTC will be disabled. 15 | jupyter lab --YDocExtension.disable_rtc=True 16 | 17 | # The delay of inactivity (in seconds) after which a document is saved to disk (default: 1). 18 | # If None, the document will never be saved. 19 | jupyter lab --YDocExtension.document_save_delay=0.5 20 | 21 | # The period (in seconds) to check for file changes on disk (default: 1). 22 | # If 0, file changes will only be checked when saving. 23 | jupyter lab --YDocExtension.file_poll_interval=2 24 | 25 | # The delay (in seconds) to keep a document in memory in the back-end after all clients disconnect (default: 60). 26 | # If None, the document will be kept in memory forever. 27 | jupyter lab --YDocExtension.document_cleanup_delay=100 28 | 29 | # The YStore class to use for storing Y updates (default: JupyterSQLiteYStore). 30 | jupyter lab --YDocExtension.ystore_class=pycrdt_websocket.ystore.TempFileYStore 31 | ``` 32 | 33 | There is an experimental feature that is currently only supported by the 34 | [Jupyverse](https://github.com/jupyter-server/jupyverse) server 35 | (not yet with [jupyter-server](https://github.com/jupyter-server/jupyter_server), 36 | see the [issue #900](https://github.com/jupyter-server/jupyter_server/issues/900)): 37 | server-side execution. With this, running notebook code cells is not done in the frontend through 38 | the low-level kernel protocol over WebSocket API, but through a high-level REST API. Communication 39 | with the kernel is then delegated to the server, and cell outputs are populated in the notebook 40 | shared document. The frontend gets these outputs changes and shows them live. What this means is 41 | that the notebook state can be recovered even if the frontend disconnects, because cell outputs are 42 | not populated frontend-side but server-side. 43 | 44 | This feature is disabled by default, and can be enabled like so: 45 | ```bash 46 | pip install "jupyterlab>=4.2.0b0" 47 | pip install "jupyverse[jupyterlab, auth]>=0.4.2" 48 | jupyverse --set kernels.require_yjs=true --set jupyterlab.server_side_execution=true 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/source/developer/architecture.md: -------------------------------------------------------------------------------- 1 | # Code Architecture 2 | 3 | ## Current Implementation 4 | 5 | Jupyter Collaboration consists of several Python packages and frontend extensions: 6 | 7 | - **jupyter_server_ydoc**: 8 | A Jupyter Server extension providing core collaborative models. It manages YDocument data structures tied to notebook files and exposes WebSocket endpoints for real-time updates. It integrates CRDTs into Jupyter’s file management and kernel system. 9 | 10 | - **jupyter_collaboration**: 11 | A meta-package that bundles the backend (`jupyter_server_ydoc`) and frontend (JupyterLab and Notebook 7 UI extensions). It connects the collaborative frontend UX (status badges, shared cursors, etc.) with the backend. 12 | 13 | ### Key dependencies: 14 | 15 | - **pycrdt-websocket**: 16 | WebSocket provider library used by the collaboration layer. It runs an async WebSocket server that synchronizes pycrdt documents by managing CRDT updates between clients and the shared server YDoc. 17 | 18 | - **pycrdt-store**: 19 | Persistence layer for CRDT documents. Uses an SQLite-backed store (`.jupyter_ystore.db`) by default to checkpoint document history. Enables autosave and document state recovery after restarts or offline periods. 20 | 21 |
22 | 23 | ```{mermaid} 24 | graph TD 25 | subgraph "Frontend Clients" 26 | JL["JupyterLab Client"] 27 | NB["Notebook Client"] 28 | end 29 | 30 | subgraph "Jupyter Server" 31 | COLLAB["Collaboration Layer"] 32 | WS["WebSocket Provider (pycrdt-websocket)"] 33 | YDOC["Shared YDoc"] 34 | STORE["Persistent Store (pycrdt-store)"] 35 | end 36 | 37 | JL -->|WebSocket| COLLAB 38 | NB -->|WebSocket| COLLAB 39 | COLLAB --> WS 40 | WS --> YDOC 41 | YDOC --> STORE 42 | ``` 43 | 44 |
45 | 46 | ## Early attempts 47 | 48 | Prior to the current implementation based on [Yjs](https://docs.yjs.dev/), other attempts using 49 | different technologies where tried: 50 | 51 | - Attempt based on [Automerge](https://automerge.org/). The code has been archived in that [branch](https://github.com/jupyterlab/jupyter_collaboration/tree/automerge). You can 52 | access the [documentation there](https://jupyterlab-realtime-collaboration.readthedocs.io/en/automerge/). 53 | -------------------------------------------------------------------------------- /docs/source/developer/javascript_api.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) Jupyter Development Team. 2 | .. Distributed under the terms of the Modified BSD License. 3 | 4 | JavaScript API 5 | ============== 6 | 7 | .. this doc exists as a resolvable link target 8 | .. which statically included files are not 9 | 10 | .. meta:: 11 | :http-equiv=refresh: 0;url=../api/index.html 12 | 13 | The JavaScript API reference docs are `here <../api/index.html>`_ 14 | if you are not redirected automatically. 15 | -------------------------------------------------------------------------------- /docs/source/developer/python_api.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) Jupyter Development Team. 2 | .. Distributed under the terms of the Modified BSD License. 3 | 4 | Python API 5 | ========== 6 | 7 | ``jupyter_server_ydoc`` instantiates :any:`YDocExtension` and stores it under ``serverapp.settings`` dictionary, under the ``"jupyter_server_ydoc"`` key. 8 | This instance can be used in other extensions to access the public API methods. 9 | 10 | For example, to access a read-only view of the shared notebook model in your jupyter-server extension, you can use the :any:`get_document` method: 11 | 12 | .. code-block:: 13 | 14 | collaboration = serverapp.settings["jupyter_server_ydoc"] 15 | document = collaboration.get_document( 16 | path='Untitled.ipynb', 17 | content_type="notebook", 18 | file_format="json" 19 | ) 20 | content = document.get() 21 | 22 | 23 | API Reference 24 | ------------- 25 | 26 | .. automodule:: jupyter_server_ydoc.app 27 | :members: 28 | :inherited-members: 29 | 30 | .. automodule:: jupyter_server_ydoc.handlers 31 | :members: 32 | :inherited-members: 33 | -------------------------------------------------------------------------------- /docs/source/images/rtc_shared_cursors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/docs/source/images/rtc_shared_cursors.png -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # Welcome to JupyterLab Real-Time collaboration documentation! 9 | 10 | 11 | From JupyterLab v4, file documents and notebooks have collaborative 12 | editing using the [Yjs shared editing framework](https://github.com/yjs/yjs). 13 | Editors are not collaborative by default; to activate it, install the extension 14 | `jupyter_collaboration`. 15 | 16 | Installation using mamba/conda: 17 | 18 | ```sh 19 | mamba install -c conda-forge jupyter-collaboration 20 | ``` 21 | 22 | Installation using pip: 23 | 24 | ```sh 25 | pip install jupyter-collaboration 26 | ``` 27 | 28 | The new collaborative editing feature enables collaboration in real-time 29 | between multiple clients without user roles. When sharing the URL of a 30 | document to other users, they will have access to the same environment you 31 | are working on (they can e.g. write and execute the cells of a notebook). 32 | 33 | Moreover, you can see the cursors from other users with an anonymous 34 | username, a username that will disappear in a few seconds to make room 35 | for what is essential, the document's content. 36 | 37 | ![Shared cursors](images/rtc_shared_cursors.png) 38 | 39 | A nice improvement from Real Time Collaboration (RTC) is that you don't need to worry 40 | about saving a document anymore. It is automatically taken care of: each change made by 41 | any user to a document is saved after one second by default. You can see it with the dirty indicator 42 | being set after a change, and cleared after saving. This even works if the file is modified 43 | outside of JupyterLab's editor, for instance in the back-end with a third-party editor or 44 | after changing branch in a version control system such as `git`. In this case, the file is 45 | watched and any change will trigger the document update within the next second, by default. 46 | 47 | Something you need to be aware of is that not all editors in JupyterLab support RTC 48 | synchronization. Additionally, opening the same underlying document using different editor 49 | types currently results in a different type of synchronization. 50 | For example, in JupyterLab, you can open a Notebook using the Notebook 51 | editor or a plain text editor, the so-called Editor. Those editors are 52 | not synchronized through RTC because, under the hood, they use a different model to 53 | represent the document's content, what we call `DocumentModel`. If you 54 | modify a Notebook with one editor, it will update the content in the other editor within 55 | one second, going through the file change detection mentioned above. 56 | 57 | Overall, document write access is much more streamlined with RTC. You will never see any warning 58 | message indicating that the file was modified by someone else, and asking if you want to keep 59 | your changes or revert to the saved content. There cannot be any conflict, everyone works in sync 60 | on the same document. 61 | 62 | 63 | ```{toctree} 64 | :maxdepth: 1 65 | :caption: Contents 66 | 67 | configuration 68 | developer/contributing 69 | changelog 70 | ``` 71 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyter_collaboration", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyter_collaboration" 5 | } 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.1.0-rc.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter/real-time-collaboration", 3 | "private": true, 4 | "version": "4.1.0-rc.0", 5 | "description": "JupyterLab Extension enabling Real-Time Collaboration", 6 | "keywords": [ 7 | "jupyter", 8 | "jupyterlab", 9 | "jupyterlab-extension" 10 | ], 11 | "homepage": "https://github.com/jupyterlab/jupyter-collaboration", 12 | "bugs": { 13 | "url": "https://github.com/jupyterlab/jupyter-collaboration/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/jupyterlab/jupyter-collaboration.git" 18 | }, 19 | "license": "BSD-3-Clause", 20 | "author": { 21 | "name": "Jupyter Development Team", 22 | "email": "jupyter@googlegroups.com" 23 | }, 24 | "workspaces": [ 25 | "packages/*" 26 | ], 27 | "scripts": { 28 | "dev": "python scripts/dev_install.py", 29 | "build": "lerna run build", 30 | "build:prod": "lerna run build:prod", 31 | "build:test": "lerna run build:test", 32 | "clean": "lerna run clean", 33 | "clean:lib": "lerna run clean:lib", 34 | "clean:all": "lerna run clean:all", 35 | "docs": "typedoc", 36 | "eslint": "jlpm eslint:check --fix", 37 | "eslint:check": "eslint . --ext .ts,.tsx", 38 | "install:extension": "lerna run install:extension", 39 | "lint": "jlpm prettier && jlpm eslint && jlpm stylelint", 40 | "lint:check": "jlpm prettier:check && jlpm eslint:check", 41 | "prettier": "jlpm prettier:base --write --list-different", 42 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.json,.jsx,.css}\"", 43 | "prettier:check": "jlpm prettier:base --check", 44 | "stylelint": "jlpm stylelint:check --fix", 45 | "stylelint:check": "stylelint --cache \"packages/**/style/**/*.css\"", 46 | "test": "lerna run test", 47 | "test:cov": "lerna run test:cov", 48 | "test:debug": "lerna run test:debug", 49 | "test:debug:watch": "lerna run test:debug:watch", 50 | "watch": "lerna run watch" 51 | }, 52 | "devDependencies": { 53 | "@typescript-eslint/eslint-plugin": "~5.55.0", 54 | "@typescript-eslint/parser": "~5.55.0", 55 | "eslint": "~8.36.0", 56 | "eslint-config-prettier": "~8.7.0", 57 | "eslint-plugin-jest": "~27.2.1", 58 | "eslint-plugin-prettier": "~4.2.1", 59 | "eslint-plugin-react": "~7.32.2", 60 | "lerna": "^6.5.1", 61 | "prettier": "^2.8.4", 62 | "rimraf": "^4.1.2", 63 | "stylelint": "^15.2.0", 64 | "stylelint-config-recommended": "^10.0.0", 65 | "stylelint-config-standard": "^30.0.1", 66 | "stylelint-prettier": "^3.0.0", 67 | "typedoc": "~0.23.28", 68 | "typescript": "~5.1.6" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/collaboration-extension/README.md: -------------------------------------------------------------------------------- 1 | # @jupyter/collaboration-extension 2 | 3 | A JupyterLab package which provides a set of plugins for Real Time Collaboration. 4 | -------------------------------------------------------------------------------- /packages/collaboration-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter/collaboration-extension", 3 | "version": "4.1.0-rc.0", 4 | "description": "JupyterLab - Real-Time Collaboration Extension", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/jupyterlab/jupyter-collaboration", 11 | "bugs": { 12 | "url": "https://github.com/jupyterlab/jupyter-collaboration/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/jupyterlab/jupyter-collaboration.git" 17 | }, 18 | "license": "BSD-3-Clause", 19 | "author": "Project Jupyter", 20 | "sideEffects": [ 21 | "style/*.css", 22 | "style/index.js" 23 | ], 24 | "main": "lib/index.js", 25 | "types": "lib/index.d.ts", 26 | "style": "style/index.css", 27 | "styleModule": "style/index.js", 28 | "directories": { 29 | "lib": "lib/" 30 | }, 31 | "files": [ 32 | "lib/*.d.ts", 33 | "lib/*.js.map", 34 | "lib/*.js", 35 | "schema/*.json", 36 | "style/*.css", 37 | "style/index.js" 38 | ], 39 | "scripts": { 40 | "build": "jlpm run build:lib && jlpm run build:labextension:dev", 41 | "build:lib": "tsc --sourceMap", 42 | "build:lib:prod": "tsc", 43 | "build:prod": "jlpm run clean && jlpm run build:lib:prod && jlpm run build:labextension", 44 | "build:labextension": "jupyter labextension build .", 45 | "build:labextension:dev": "jupyter labextension build --development True .", 46 | "clean": "jlpm run clean:lib", 47 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo node_modules", 48 | "clean:labextension": "rimraf ../../projects/jupyter-collaboration-ui/jupyter_collaboration_ui/labextension", 49 | "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", 50 | "install:extension": "jlpm run build", 51 | "watch": "run-p watch:src watch:labextension", 52 | "watch:src": "tsc -w", 53 | "watch:labextension": "jupyter labextension watch ." 54 | }, 55 | "dependencies": { 56 | "@jupyter/collaboration": "^4.1.0-rc.0", 57 | "@jupyter/collaborative-drive": "^4.1.0-rc.0", 58 | "@jupyter/docprovider": "^4.1.0-rc.0", 59 | "@jupyter/ydoc": "^2.1.3 || ^3.0.0", 60 | "@jupyterlab/application": "^4.4.0", 61 | "@jupyterlab/apputils": "^4.4.0", 62 | "@jupyterlab/codemirror": "^4.4.0", 63 | "@jupyterlab/coreutils": "^6.4.0", 64 | "@jupyterlab/services": "^7.4.0", 65 | "@jupyterlab/statedb": "^4.4.0", 66 | "@jupyterlab/translation": "^4.4.0", 67 | "@jupyterlab/ui-components": "^4.4.0", 68 | "@lumino/widgets": "^2.7.0", 69 | "y-protocols": "^1.0.5", 70 | "y-websocket": "^1.3.15", 71 | "yjs": "^13.5.40" 72 | }, 73 | "devDependencies": { 74 | "@jupyterlab/builder": "^4.4.0", 75 | "@types/react": "~18.3.1", 76 | "npm-run-all": "^4.1.5", 77 | "rimraf": "^4.1.2", 78 | "typescript": "~5.1.6" 79 | }, 80 | "publishConfig": { 81 | "access": "public" 82 | }, 83 | "typedoc": { 84 | "entryPoint": "./src/index.ts", 85 | "readmeFile": "./README.md", 86 | "displayName": "@jupyter/collaboration-extension", 87 | "tsconfig": "./tsconfig.json" 88 | }, 89 | "jupyterlab": { 90 | "extension": true, 91 | "schemaDir": "./schema", 92 | "outputDir": "../../projects/jupyter-collaboration-ui/jupyter_collaboration_ui/labextension", 93 | "sharedPackages": { 94 | "@codemirror/state": { 95 | "bundled": false, 96 | "singleton": true 97 | }, 98 | "@codemirror/view": { 99 | "bundled": false, 100 | "singleton": true 101 | }, 102 | "@jupyter/collaboration": { 103 | "bundled": true, 104 | "singleton": true 105 | }, 106 | "@jupyter/collaborative-drive": { 107 | "bundled": true, 108 | "singleton": true 109 | }, 110 | "@jupyter/docprovider": { 111 | "bundled": true, 112 | "singleton": true 113 | }, 114 | "@jupyter/ydoc": { 115 | "bundled": false, 116 | "singleton": true 117 | }, 118 | "y-protocols": { 119 | "bundled": false, 120 | "singleton": true 121 | }, 122 | "yjs": { 123 | "bundled": false, 124 | "singleton": true 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /packages/collaboration-extension/schema/shared-link.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Shared link", 3 | "description": "Shared link settings", 4 | "jupyter.lab.toolbars": { 5 | "TopBar": [ 6 | { 7 | "name": "@jupyter/collaboration:shared-link", 8 | "command": "collaboration:shared-link", 9 | "rank": 99 10 | } 11 | ] 12 | }, 13 | "properties": {}, 14 | "additionalProperties": false, 15 | "type": "object" 16 | } 17 | -------------------------------------------------------------------------------- /packages/collaboration-extension/schema/user-menu-bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "User Menu Bar", 3 | "description": "User Menu Bar settings.", 4 | "jupyter.lab.toolbars": { 5 | "TopBar": [ 6 | { 7 | "name": "user-menu", 8 | "rank": 100 9 | } 10 | ] 11 | }, 12 | "properties": {}, 13 | "additionalProperties": false, 14 | "type": "object" 15 | } 16 | -------------------------------------------------------------------------------- /packages/collaboration-extension/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | /** 4 | * @packageDocumentation 5 | * @module collaboration-extension 6 | */ 7 | 8 | import { JupyterFrontEndPlugin } from '@jupyterlab/application'; 9 | 10 | import { 11 | userMenuPlugin, 12 | menuBarPlugin, 13 | rtcGlobalAwarenessPlugin, 14 | rtcPanelPlugin, 15 | userEditorCursors 16 | } from './collaboration'; 17 | import { sharedLink } from './sharedlink'; 18 | 19 | /** 20 | * Export the plugins as default. 21 | */ 22 | const plugins: JupyterFrontEndPlugin[] = [ 23 | userMenuPlugin, 24 | menuBarPlugin, 25 | rtcGlobalAwarenessPlugin, 26 | rtcPanelPlugin, 27 | sharedLink, 28 | userEditorCursors 29 | ]; 30 | 31 | export default plugins; 32 | -------------------------------------------------------------------------------- /packages/collaboration-extension/src/sharedlink.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { 5 | JupyterFrontEnd, 6 | JupyterFrontEndPlugin 7 | } from '@jupyterlab/application'; 8 | import { Clipboard, ICommandPalette } from '@jupyterlab/apputils'; 9 | import { ITranslator, nullTranslator } from '@jupyterlab/translation'; 10 | import { shareIcon } from '@jupyterlab/ui-components'; 11 | 12 | import { showSharedLinkDialog } from '@jupyter/collaboration'; 13 | 14 | /** 15 | * The command IDs used by the plugin. 16 | */ 17 | namespace CommandIDs { 18 | export const share = 'collaboration:shared-link'; 19 | } 20 | 21 | /** 22 | * Plugin to share the URL of the running Jupyter Server 23 | */ 24 | export const sharedLink: JupyterFrontEndPlugin = { 25 | id: '@jupyter/collaboration-extension:shared-link', 26 | autoStart: true, 27 | optional: [ICommandPalette, ITranslator], 28 | activate: async ( 29 | app: JupyterFrontEnd, 30 | palette: ICommandPalette | null, 31 | translator: ITranslator | null 32 | ) => { 33 | const { commands } = app; 34 | const trans = (translator ?? nullTranslator).load('collaboration'); 35 | 36 | commands.addCommand(CommandIDs.share, { 37 | label: trans.__('Generate a Shared Link'), 38 | icon: shareIcon, 39 | execute: async () => { 40 | const result = await showSharedLinkDialog({ 41 | translator 42 | }); 43 | if (result.button.accept && result.value) { 44 | Clipboard.copyToSystem(result.value); 45 | } 46 | } 47 | }); 48 | 49 | if (palette) { 50 | palette.addItem({ 51 | command: CommandIDs.share, 52 | category: trans.__('Server') 53 | }); 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /packages/collaboration-extension/style/index.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | @import url('~@jupyter/collaboration/style/index.css'); 7 | -------------------------------------------------------------------------------- /packages/collaboration-extension/style/index.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |----------------------------------------------------------------------------*/ 5 | 6 | import '@jupyter/collaboration/style/index.js'; 7 | -------------------------------------------------------------------------------- /packages/collaboration-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/collaboration/README.md: -------------------------------------------------------------------------------- 1 | # @jupyter/collaboration 2 | 3 | A JupyterLab package which provides a set of widgets for Real Time Collaboration. 4 | -------------------------------------------------------------------------------- /packages/collaboration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter/collaboration", 3 | "version": "4.1.0-rc.0", 4 | "description": "JupyterLab - Real-Time Collaboration Widgets", 5 | "homepage": "https://github.com/jupyterlab/jupyter-collaboration", 6 | "bugs": { 7 | "url": "https://github.com/jupyterlab/jupyter-collaboration/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/jupyterlab/jupyter-collaboration.git" 12 | }, 13 | "license": "BSD-3-Clause", 14 | "author": "Project Jupyter", 15 | "sideEffects": [ 16 | "style/*.css", 17 | "style/index.js" 18 | ], 19 | "main": "lib/index.js", 20 | "types": "lib/index.d.ts", 21 | "style": "style/index.css", 22 | "directories": { 23 | "lib": "lib/" 24 | }, 25 | "files": [ 26 | "lib/*.d.ts", 27 | "lib/*.js.map", 28 | "lib/*.js", 29 | "style/*.css", 30 | "style/index.js" 31 | ], 32 | "scripts": { 33 | "build": "tsc -b", 34 | "build:prod": "jlpm run build", 35 | "clean": "rimraf lib tsconfig.tsbuildinfo", 36 | "clean:lib": "jlpm run clean:all", 37 | "clean:all": "rimraf lib tsconfig.tsbuildinfo node_modules", 38 | "install:extension": "jlpm run build", 39 | "watch": "tsc -b --watch" 40 | }, 41 | "dependencies": { 42 | "@codemirror/state": "^6.2.0", 43 | "@codemirror/view": "^6.7.0", 44 | "@jupyterlab/apputils": "^4.4.0", 45 | "@jupyterlab/coreutils": "^6.4.0", 46 | "@jupyterlab/docregistry": "^4.4.0", 47 | "@jupyterlab/rendermime-interfaces": "^3.12.0", 48 | "@jupyterlab/services": "^7.4.0", 49 | "@jupyterlab/ui-components": "^4.4.0", 50 | "@lumino/coreutils": "^2.2.1", 51 | "@lumino/signaling": "^2.1.4", 52 | "@lumino/virtualdom": "^2.0.3", 53 | "@lumino/widgets": "^2.7.0", 54 | "react": "^18.2.0", 55 | "y-protocols": "^1.0.5", 56 | "yjs": "^13.5.40" 57 | }, 58 | "devDependencies": { 59 | "@types/react": "~18.3.1", 60 | "rimraf": "^4.1.2", 61 | "typescript": "~5.1.6" 62 | }, 63 | "publishConfig": { 64 | "access": "public" 65 | }, 66 | "typedoc": { 67 | "entryPoint": "./src/index.ts", 68 | "readmeFile": "./README.md", 69 | "displayName": "@jupyter/collaboration", 70 | "tsconfig": "./tsconfig.json" 71 | }, 72 | "styleModule": "style/index.js" 73 | } 74 | -------------------------------------------------------------------------------- /packages/collaboration/src/components.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { User } from '@jupyterlab/services'; 5 | import { ReactWidget } from '@jupyterlab/ui-components'; 6 | 7 | import React, { useEffect, useState } from 'react'; 8 | 9 | type UserIconProps = { 10 | /** 11 | * The user manager instance. 12 | */ 13 | userManager: User.IManager; 14 | /** 15 | * An optional onclick handler for the icon. 16 | * 17 | */ 18 | onClick?: () => void; 19 | }; 20 | 21 | /** 22 | * React component for the user icon. 23 | * 24 | * @returns The React component 25 | */ 26 | export function UserIconComponent(props: UserIconProps): JSX.Element { 27 | const { userManager, onClick } = props; 28 | const [user, setUser] = useState(userManager.identity!); 29 | 30 | useEffect(() => { 31 | const updateUser = () => { 32 | setUser(userManager.identity!); 33 | }; 34 | 35 | userManager.userChanged.connect(updateUser); 36 | 37 | return () => { 38 | userManager.userChanged.disconnect(updateUser); 39 | }; 40 | }, [userManager]); 41 | 42 | return ( 43 |
49 | {user.initials} 50 |
51 | ); 52 | } 53 | 54 | type UserDetailsBodyProps = { 55 | /** 56 | * The user manager instance. 57 | **/ 58 | userManager: User.IManager; 59 | }; 60 | 61 | /** 62 | * React widget for the user details. 63 | **/ 64 | export class UserDetailsBody extends ReactWidget { 65 | /** 66 | * Constructs a new user details widget. 67 | */ 68 | constructor(props: UserDetailsBodyProps) { 69 | super(); 70 | this._userManager = props.userManager; 71 | } 72 | 73 | /** 74 | * Get the user modified fields. 75 | */ 76 | getValue(): UserUpdate { 77 | return this._userUpdate; 78 | } 79 | 80 | /** 81 | * Handle change on a field, by updating the user object. 82 | */ 83 | private _onChange = ( 84 | event: React.ChangeEvent, 85 | field: string 86 | ) => { 87 | const updatableFields = (this._userManager.permissions?.[ 88 | 'updatable_fields' 89 | ] || []) as string[]; 90 | if (!updatableFields?.includes(field)) { 91 | return; 92 | } 93 | 94 | this._userUpdate[field as keyof Omit] = 95 | event.target.value; 96 | }; 97 | 98 | render() { 99 | const identity = this._userManager.identity; 100 | if (!identity) { 101 | return
Error loading user info
; 102 | } 103 | const updatableFields = (this._userManager.permissions?.[ 104 | 'updatable_fields' 105 | ] || []) as string[]; 106 | 107 | return ( 108 |
109 | {Object.keys(identity).map((field: string) => { 110 | const id = `jp-UserInfo-Value-${field}`; 111 | return ( 112 |
113 | 114 | ) => 119 | this._onChange(event, field) 120 | } 121 | defaultValue={identity[field] as string} 122 | disabled={!updatableFields?.includes(field)} 123 | /> 124 |
125 | ); 126 | })} 127 |
128 | ); 129 | } 130 | 131 | private _userManager: User.IManager; 132 | private _userUpdate: UserUpdate = {}; 133 | } 134 | 135 | /** 136 | * Type for the user update object. 137 | */ 138 | export type UserUpdate = { 139 | [field in keyof Omit]: string; 140 | }; 141 | -------------------------------------------------------------------------------- /packages/collaboration/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | /** 4 | * @packageDocumentation 5 | * @module collaboration 6 | */ 7 | 8 | export * from './tokens'; 9 | export * from './collaboratorspanel'; 10 | export * from './cursors'; 11 | export * from './menu'; 12 | export * from './sharedlink'; 13 | export * from './userinfopanel'; 14 | export * from './users-item'; 15 | -------------------------------------------------------------------------------- /packages/collaboration/src/menu.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { userIcon } from '@jupyterlab/ui-components'; 5 | import { User } from '@jupyterlab/services'; 6 | import { Menu, MenuBar } from '@lumino/widgets'; 7 | import { h, VirtualElement } from '@lumino/virtualdom'; 8 | 9 | /** 10 | * Custom renderer for the user menu. 11 | */ 12 | export class RendererUserMenu extends MenuBar.Renderer { 13 | private _user: User.IManager; 14 | 15 | /** 16 | * Constructor of the class RendererUserMenu. 17 | * 18 | * @argument user Current user object. 19 | */ 20 | constructor(user: User.IManager) { 21 | super(); 22 | this._user = user; 23 | } 24 | 25 | /** 26 | * Render the virtual element for a menu bar item. 27 | * 28 | * @param data - The data to use for rendering the item. 29 | * 30 | * @returns A virtual element representing the item. 31 | */ 32 | renderItem(data: MenuBar.IRenderData): VirtualElement { 33 | const className = this.createItemClass(data); 34 | const dataset = this.createItemDataset(data); 35 | const aria = this.createItemARIA(data); 36 | return h.li( 37 | { className, dataset, tabindex: '0', onfocus: data.onfocus, ...aria }, 38 | this._createUserIcon(), 39 | this.renderLabel(data), 40 | this.renderIcon(data) 41 | ); 42 | } 43 | 44 | /** 45 | * Render the label element for a menu item. 46 | * 47 | * @param data - The data to use for rendering the label. 48 | * 49 | * @returns A virtual element representing the item label. 50 | */ 51 | renderLabel(data: MenuBar.IRenderData): VirtualElement { 52 | const content = this.formatLabel(data); 53 | return h.div( 54 | { className: 'lm-MenuBar-itemLabel jp-MenuBar-label' }, 55 | content 56 | ); 57 | } 58 | 59 | /** 60 | * Render the user icon element for a menu item. 61 | * 62 | * @returns A virtual element representing the item label. 63 | */ 64 | private _createUserIcon(): VirtualElement { 65 | if (this._user.isReady && this._user.identity!.avatar_url) { 66 | return h.div( 67 | { 68 | className: 'lm-MenuBar-itemIcon jp-MenuBar-imageIcon' 69 | }, 70 | h.img({ src: this._user.identity!.avatar_url }) 71 | ); 72 | } else if (this._user.isReady) { 73 | return h.div( 74 | { 75 | className: 'lm-MenuBar-itemIcon jp-MenuBar-anonymousIcon', 76 | style: { backgroundColor: this._user.identity!.color } 77 | }, 78 | h.span({}, this._user.identity!.initials) 79 | ); 80 | } else { 81 | return h.div( 82 | { 83 | className: 'lm-MenuBar-itemIcon jp-MenuBar-anonymousIcon' 84 | }, 85 | userIcon 86 | ); 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * This menu does not contain anything but we keep it around in case someone uses it. 93 | * Custom lumino Menu for the user menu. 94 | */ 95 | export class UserMenu extends Menu { 96 | constructor(options: UserMenu.IOptions) { 97 | super(options); 98 | } 99 | } 100 | 101 | /** 102 | * Namespace of the UserMenu class. 103 | */ 104 | export namespace UserMenu { 105 | /** 106 | * User menu options interface 107 | */ 108 | export interface IOptions extends Menu.IOptions { 109 | /** 110 | * Current user manager. 111 | */ 112 | user: User.IManager; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/collaboration/src/tokens.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import type { Menu } from '@lumino/widgets'; 5 | import { Token } from '@lumino/coreutils'; 6 | import type { User } from '@jupyterlab/services'; 7 | 8 | /** 9 | * The user menu token. 10 | * 11 | * NOTE: Require this token in your extension to access the user menu 12 | * (top-right menu in JupyterLab's interface). 13 | */ 14 | export const IUserMenu = new Token( 15 | '@jupyter/collaboration:IUserMenu' 16 | ); 17 | 18 | /** 19 | * An interface describing the user menu. 20 | */ 21 | export interface IUserMenu { 22 | /** 23 | * Dispose of the resources held by the menu. 24 | */ 25 | dispose(): void; 26 | 27 | /** 28 | * Test whether the widget has been disposed. 29 | */ 30 | readonly isDisposed: boolean; 31 | 32 | /** 33 | * A read-only array of the menu items in the menu. 34 | */ 35 | readonly items: ReadonlyArray; 36 | 37 | /** 38 | * Add a menu item to the end of the menu. 39 | * 40 | * @param options - The options for creating the menu item. 41 | * 42 | * @returns The menu item added to the menu. 43 | */ 44 | addItem(options: Menu.IItemOptions): Menu.IItem; 45 | 46 | /** 47 | * Insert a menu item into the menu at the specified index. 48 | * 49 | * @param index - The index at which to insert the item. 50 | * 51 | * @param options - The options for creating the menu item. 52 | * 53 | * @returns The menu item added to the menu. 54 | * 55 | * #### Notes 56 | * The index will be clamped to the bounds of the items. 57 | */ 58 | insertItem(index: number, options: Menu.IItemOptions): Menu.IItem; 59 | 60 | /** 61 | * Remove an item from the menu. 62 | * 63 | * @param item - The item to remove from the menu. 64 | * 65 | * #### Notes 66 | * This is a no-op if the item is not in the menu. 67 | */ 68 | removeItem(item: Menu.IItem): void; 69 | } 70 | 71 | /** 72 | * Global awareness for JupyterLab scopped shared data. 73 | */ 74 | export interface ICollaboratorAwareness { 75 | /** 76 | * The User owning theses data. 77 | */ 78 | user: User.IIdentity; 79 | 80 | /** 81 | * The current file/context the user is working on (current panel in main area). 82 | */ 83 | current?: string; 84 | 85 | /** 86 | * The shared documents opened by the user. 87 | */ 88 | documents?: string[]; 89 | } 90 | -------------------------------------------------------------------------------- /packages/collaboration/src/userinfopanel.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { Dialog, ReactWidget, showDialog } from '@jupyterlab/apputils'; 5 | 6 | import { ServerConnection, User } from '@jupyterlab/services'; 7 | 8 | import { URLExt } from '@jupyterlab/coreutils'; 9 | 10 | import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; 11 | 12 | import { Panel } from '@lumino/widgets'; 13 | 14 | import * as React from 'react'; 15 | 16 | import { UserDetailsBody, UserIconComponent } from './components'; 17 | 18 | /** 19 | * The properties for the UserInfoBody. 20 | */ 21 | type UserInfoProps = { 22 | userManager: User.IManager; 23 | trans: IRenderMime.TranslationBundle; 24 | }; 25 | 26 | export class UserInfoPanel extends Panel { 27 | private _profile: User.IManager; 28 | private _body: UserInfoBody | null; 29 | 30 | constructor(options: UserInfoProps) { 31 | super({}); 32 | this.addClass('jp-UserInfoPanel'); 33 | this._profile = options.userManager; 34 | this._body = null; 35 | 36 | if (this._profile.isReady) { 37 | this._body = new UserInfoBody({ 38 | userManager: this._profile, 39 | trans: options.trans 40 | }); 41 | this.addWidget(this._body); 42 | this.update(); 43 | } else { 44 | this._profile.ready 45 | .then(() => { 46 | this._body = new UserInfoBody({ 47 | userManager: this._profile, 48 | trans: options.trans 49 | }); 50 | this.addWidget(this._body); 51 | this.update(); 52 | }) 53 | .catch(e => console.error(e)); 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * A SettingsWidget for the user. 60 | */ 61 | export class UserInfoBody 62 | extends ReactWidget 63 | implements Dialog.IBodyWidget 64 | { 65 | private _userManager: User.IManager; 66 | private _trans: IRenderMime.TranslationBundle; 67 | /** 68 | * Constructs a new settings widget. 69 | */ 70 | constructor(props: UserInfoProps) { 71 | super(); 72 | this._userManager = props.userManager; 73 | this._trans = props.trans; 74 | } 75 | 76 | get user(): User.IManager { 77 | return this._userManager; 78 | } 79 | 80 | set user(user: User.IManager) { 81 | this._userManager = user; 82 | this.update(); 83 | } 84 | 85 | private onClick = () => { 86 | if (!this._userManager.identity) { 87 | return; 88 | } 89 | showDialog({ 90 | body: new UserDetailsBody({ 91 | userManager: this._userManager 92 | }), 93 | title: this._trans.__('User Details') 94 | }).then(async result => { 95 | if (result.button.accept) { 96 | // Call the Jupyter Server API to update the user field 97 | try { 98 | const settings = ServerConnection.makeSettings(); 99 | const url = URLExt.join(settings.baseUrl, '/api/me'); 100 | const body = { 101 | method: 'PATCH', 102 | body: JSON.stringify(result.value) 103 | }; 104 | 105 | let response: Response; 106 | try { 107 | response = await ServerConnection.makeRequest(url, body, settings); 108 | } catch (error) { 109 | throw new ServerConnection.NetworkError(error as Error); 110 | } 111 | 112 | if (!response.ok) { 113 | const errorMsg = this._trans.__('Failed to update user data'); 114 | throw new Error(errorMsg); 115 | } 116 | 117 | // Refresh user information 118 | this._userManager.refreshUser(); 119 | } catch (error) { 120 | console.error(error); 121 | } 122 | } 123 | }); 124 | }; 125 | 126 | render(): JSX.Element { 127 | return ( 128 |
129 | 133 |
134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /packages/collaboration/style/base.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | @import url('./menu.css'); 7 | @import url('./sidepanel.css'); 8 | @import url('./users-item.css'); 9 | @import url('./sharedlink.css'); 10 | 11 | .jp-shared-link-body { 12 | user-select: none; 13 | } 14 | -------------------------------------------------------------------------------- /packages/collaboration/style/index.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | @import url('./base.css'); 7 | -------------------------------------------------------------------------------- /packages/collaboration/style/index.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |----------------------------------------------------------------------------*/ 5 | 6 | import './base.css'; 7 | -------------------------------------------------------------------------------- /packages/collaboration/style/menu.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | .jp-MenuBar-label { 7 | margin-left: 25px; 8 | } 9 | 10 | .jp-MenuBar-anonymousIcon span { 11 | width: 24px; 12 | text-align: center; 13 | fill: var(--jp-ui-font-color1); 14 | color: var(--jp-ui-font-color1); 15 | } 16 | 17 | .jp-MenuBar-anonymousIcon, 18 | .jp-MenuBar-imageIcon { 19 | position: absolute; 20 | top: 1px; 21 | left: 8px; 22 | width: 24px; 23 | height: 24px; 24 | display: flex; 25 | align-items: center; 26 | vertical-align: middle; 27 | border-radius: 100%; 28 | } 29 | 30 | .jp-MenuBar-imageIcon img { 31 | width: 24px; 32 | border-radius: 100%; 33 | fill: var(--jp-ui-font-color1); 34 | color: var(--jp-ui-font-color1); 35 | } 36 | 37 | .jp-UserMenu-caretDownIcon { 38 | height: 22px; 39 | position: relative; 40 | top: 15%; 41 | } 42 | -------------------------------------------------------------------------------- /packages/collaboration/style/sharedlink.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | .jp-shared-link-body { 7 | user-select: none; 8 | } 9 | 10 | .jp-ManageSharesBody-search-container { 11 | margin-bottom: 10px; 12 | } 13 | 14 | .jp-ManageSharesBody-search-input { 15 | width: 100%; 16 | padding: 5px; 17 | margin-top: 5px; 18 | } 19 | 20 | .jp-ManageSharesBody-search-results { 21 | height: 10em; 22 | overflow-y: auto; 23 | border: 1px solid var(--jp-border-color0); 24 | padding: 5px; 25 | flex-shrink: 0; 26 | } 27 | 28 | .jp-ManageSharesBody-user-item { 29 | padding: 5px; 30 | cursor: pointer; 31 | } 32 | 33 | .jp-ManageSharesBody-user-item:hover { 34 | background-color: var(--jp-border-color3); 35 | } 36 | 37 | .jp-ManageSharesBody-selected-users { 38 | margin-top: 10px; 39 | height: 10em; 40 | overflow-y: auto; 41 | border: 1px solid var(--jp-border-color0); 42 | flex-shrink: 0; 43 | } 44 | 45 | .jp-ManageSharesBody-url-input { 46 | width: 100%; 47 | padding: 5px; 48 | margin-top: 10px; 49 | } 50 | 51 | .jp-ManageSharesBody-shares-table { 52 | width: 100%; 53 | } 54 | 55 | .jp-ManageSharesBody-shares-table td:nth-child(2), 56 | .jp-ManageSharesBody-shares-table td:nth-child(3) { 57 | text-align: center; 58 | } 59 | 60 | .jp-Dialog-content:has(.jp-shared-link-body) { 61 | max-height: 750px; 62 | } 63 | -------------------------------------------------------------------------------- /packages/collaboration/style/sidepanel.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /************************************************************ 7 | Main Panel 8 | *************************************************************/ 9 | 10 | .jp-RTCPanel { 11 | min-width: var(--jp-sidebar-min-width) !important; 12 | color: var(--jp-ui-font-color1); 13 | background: var(--jp-layout-color1); 14 | font-size: var(--jp-ui-font-size1); 15 | } 16 | 17 | /************************************************************ 18 | User Info Panel 19 | *************************************************************/ 20 | .jp-UserInfoPanel { 21 | display: flex; 22 | flex-direction: column; 23 | max-height: 140px; 24 | padding-top: 3px; 25 | } 26 | 27 | .jp-UserInfo-Container { 28 | margin: 20px; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | } 33 | 34 | .jp-UserInfo-Icon { 35 | margin: auto; 36 | width: 50px; 37 | height: 50px; 38 | border-radius: 50px; 39 | display: inline-flex; 40 | align-items: center; 41 | } 42 | 43 | .jp-UserInfo-Icon span { 44 | margin: auto; 45 | text-align: center; 46 | font-size: 25px; 47 | fill: var(--jp-ui-font-color1); 48 | color: var(--jp-ui-font-color1); 49 | } 50 | 51 | .jp-UserInfo-Info { 52 | margin: 20px; 53 | display: inline-flex; 54 | flex-direction: column; 55 | } 56 | 57 | .jp-UserInfo-Info label { 58 | font-weight: bold; 59 | fill: var(--jp-ui-font-color1); 60 | color: var(--jp-ui-font-color1); 61 | } 62 | 63 | .jp-UserInfo-Info input { 64 | text-decoration: none; 65 | border-top: none; 66 | border-left: none; 67 | border-right: none; 68 | border-color: var(--jp-ui-font-color1); 69 | border-width: 0.5px; 70 | background-color: transparent; 71 | fill: var(--jp-ui-font-color1); 72 | color: var(--jp-ui-font-color1); 73 | } 74 | 75 | /************************************************************ 76 | Collaborators Info Panel 77 | *************************************************************/ 78 | 79 | .jp-CollaboratorsPanel { 80 | overflow-y: auto; 81 | } 82 | 83 | .jp-CollaboratorsList { 84 | flex-direction: column; 85 | display: flex; 86 | z-index: 1000; 87 | } 88 | 89 | .jp-CollaboratorHeader { 90 | padding: 10px; 91 | display: flex; 92 | align-items: center; 93 | font-size: var(--jp-ui-font-size0); 94 | fill: var(--jp-ui-font-color1); 95 | color: var(--jp-ui-font-color1); 96 | } 97 | 98 | .jp-CollaboratorHeader > span { 99 | padding-left: 7px; 100 | } 101 | 102 | .jp-ClickableCollaborator:hover { 103 | cursor: pointer; 104 | background-color: var(--jp-layout-color2); 105 | fill: var(--jp-ui-font-color0); 106 | color: var(--jp-ui-font-color0); 107 | } 108 | 109 | .jp-CollaboratorHeaderCollapser { 110 | transform: rotate(-90deg); 111 | margin: auto 0; 112 | height: 16px; 113 | } 114 | 115 | .jp-CollaboratorHeader:not(.jp-ClickableCollaborator) .jp-CollaboratorHeaderCollapser { 116 | visibility: hidden; 117 | } 118 | 119 | .jp-CollaboratorHeaderCollapser.jp-mod-expanded { 120 | transform: rotate(0deg); 121 | } 122 | 123 | .jp-CollaboratorIcon { 124 | border-radius: 100%; 125 | padding: 2px; 126 | width: 24px; 127 | height: 24px; 128 | display: flex; 129 | } 130 | 131 | .jp-CollaboratorIcon > span { 132 | text-align: center; 133 | margin: auto; 134 | font-size: 12px; 135 | fill: var(--jp-ui-font-color1); 136 | color: var(--jp-ui-font-color1); 137 | } 138 | 139 | .jp-CollaboratorFiles { 140 | padding-left: 1em; 141 | margin-top: 0; 142 | box-shadow: 0 2px 2px -2px rgb(0 0 0 / 24%); 143 | 144 | } 145 | 146 | /************************************************************ 147 | User Info Details 148 | *************************************************************/ 149 | .jp-UserInfo-Field { 150 | display: flex; 151 | justify-content: space-between; 152 | } 153 | 154 | .jp-UserInfo-Field > label, 155 | .jp-UserInfo-Field > input { 156 | padding: 0.5em 1em; 157 | margin: 0.25em 0; 158 | } 159 | 160 | .jp-UserInfo-Field > label { 161 | font-weight: bold; 162 | } 163 | 164 | .jp-UserInfo-Field > input { 165 | border: none; 166 | } 167 | 168 | .jp-UserInfo-Field > input:not(:disabled) { 169 | cursor: pointer; 170 | background-color: var(--jp-input-background); 171 | } 172 | 173 | .jp-UserInfo-Field > input:focus { 174 | border: solid 1px var(--jp-cell-editor-active-border-color); 175 | } 176 | 177 | .jp-UserInfo-Field > input:focus-visible { 178 | outline: none; 179 | } 180 | -------------------------------------------------------------------------------- /packages/collaboration/style/users-item.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | .jp-toolbar-users-item { 7 | flex-grow: 1; 8 | display: flex; 9 | flex-direction: row; 10 | } 11 | 12 | .jp-toolbar-users-item .jp-MenuBar-anonymousIcon, 13 | .jp-toolbar-users-item .jp-MenuBar-imageIcon { 14 | position: relative; 15 | left: 0; 16 | height: 22px; 17 | width: 22px; 18 | box-sizing: border-box; 19 | cursor: default; 20 | } 21 | -------------------------------------------------------------------------------- /packages/collaboration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/collaborative-drive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter/collaborative-drive", 3 | "version": "4.1.0-rc.0", 4 | "description": "JupyterLab - Collaborative Drive", 5 | "homepage": "https://github.com/jupyterlab/jupyter-collaboration", 6 | "bugs": { 7 | "url": "https://github.com/jupyterlab/jupyter-collaboration/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/jupyterlab/jupyter-collaboration.git" 12 | }, 13 | "license": "BSD-3-Clause", 14 | "author": "Project Jupyter", 15 | "sideEffects": [ 16 | "style/**/*" 17 | ], 18 | "main": "lib/index.js", 19 | "types": "lib/index.d.ts", 20 | "directories": { 21 | "lib": "lib/" 22 | }, 23 | "files": [ 24 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 25 | "schema/*.json", 26 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 27 | "style/index.js" 28 | ], 29 | "scripts": { 30 | "build": "tsc -b", 31 | "build:prod": "jlpm run build", 32 | "build:test": "tsc --build tsconfig.test.json", 33 | "clean": "rimraf lib tsconfig.tsbuildinfo", 34 | "clean:lib": "jlpm run clean:all", 35 | "clean:all": "rimraf lib tsconfig.tsbuildinfo node_modules", 36 | "install:extension": "jlpm run build", 37 | "watch": "tsc -b --watch" 38 | }, 39 | "dependencies": { 40 | "@jupyter/ydoc": "^2.1.3 || ^3.0.0", 41 | "@jupyterlab/services": "^7.4.0", 42 | "@lumino/coreutils": "^2.2.1", 43 | "@lumino/disposable": "^2.1.4" 44 | }, 45 | "devDependencies": { 46 | "rimraf": "^4.1.2", 47 | "typescript": "~5.1.6" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | }, 52 | "typedoc": { 53 | "entryPoint": "./src/index.ts", 54 | "displayName": "@jupyter/collaborative-drive", 55 | "tsconfig": "./tsconfig.json" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/collaborative-drive/src/index.ts: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |----------------------------------------------------------------------------*/ 5 | /** 6 | * @packageDocumentation 7 | * @module collaborative-drive 8 | */ 9 | 10 | export * from './tokens'; 11 | -------------------------------------------------------------------------------- /packages/collaborative-drive/src/tokens.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { IAwareness } from '@jupyter/ydoc'; 5 | import { Contents, SharedDocumentFactory } from '@jupyterlab/services'; 6 | import { IDisposable } from '@lumino/disposable'; 7 | 8 | import { Token } from '@lumino/coreutils'; 9 | 10 | /** 11 | * The collaborative drive. 12 | */ 13 | export const ICollaborativeContentProvider = 14 | new Token( 15 | '@jupyter/collaboration-extension:ICollaborativeContentProvider' 16 | ); 17 | 18 | /** 19 | * The global awareness token. 20 | */ 21 | export const IGlobalAwareness = new Token( 22 | '@jupyter/collaboration:IGlobalAwareness' 23 | ); 24 | 25 | export interface ICollaborativeContentProvider { 26 | /** 27 | * SharedModel factory for the YDrive. 28 | */ 29 | readonly sharedModelFactory: ISharedModelFactory; 30 | 31 | readonly providers: Map; 32 | } 33 | 34 | /** 35 | * Yjs sharedModel factory for real-time collaboration. 36 | */ 37 | export interface ISharedModelFactory extends Contents.ISharedFactory { 38 | /** 39 | * Register a SharedDocumentFactory. 40 | * 41 | * @param type Document type 42 | * @param factory Document factory 43 | */ 44 | registerDocumentFactory( 45 | type: Contents.ContentType, 46 | factory: SharedDocumentFactory 47 | ): void; 48 | 49 | documentFactories: Map; 50 | } 51 | 52 | /** 53 | * An interface for a document provider. 54 | */ 55 | export interface IDocumentProvider extends IDisposable { 56 | /** 57 | * Returns a Promise that resolves when the document provider is ready. 58 | */ 59 | readonly ready: Promise; 60 | } 61 | -------------------------------------------------------------------------------- /packages/collaborative-drive/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/*"], 8 | "exclude": ["src/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/docprovider-extension/README.md: -------------------------------------------------------------------------------- 1 | # @jupyter/docprovider-extension 2 | 3 | A JupyterLab package which provides a set of plugins for collaborative shared models. 4 | -------------------------------------------------------------------------------- /packages/docprovider-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter/docprovider-extension", 3 | "version": "4.1.0-rc.0", 4 | "description": "JupyterLab - Collaborative Shared Models", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/jupyterlab/jupyter-collaboration", 11 | "bugs": { 12 | "url": "https://github.com/jupyterlab/jupyter-collaboration/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/jupyterlab/jupyter-collaboration.git" 17 | }, 18 | "license": "BSD-3-Clause", 19 | "author": "Project Jupyter", 20 | "sideEffects": [ 21 | "style/*.css", 22 | "style/index.js" 23 | ], 24 | "main": "lib/index.js", 25 | "types": "lib/index.d.ts", 26 | "style": "style/index.css", 27 | "styleModule": "style/index.js", 28 | "directories": { 29 | "lib": "lib/" 30 | }, 31 | "files": [ 32 | "lib/*.d.ts", 33 | "lib/*.js.map", 34 | "lib/*.js", 35 | "schema/*.json", 36 | "style/*.css", 37 | "style/index.js" 38 | ], 39 | "scripts": { 40 | "build": "jlpm run build:lib && jlpm run build:labextension:dev", 41 | "build:lib": "tsc --sourceMap", 42 | "build:lib:prod": "tsc", 43 | "build:prod": "jlpm run clean && jlpm run build:lib:prod && jlpm run build:labextension", 44 | "build:labextension": "jupyter labextension build .", 45 | "build:labextension:dev": "jupyter labextension build --development True .", 46 | "clean": "jlpm run clean:lib", 47 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo node_modules", 48 | "clean:labextension": "rimraf ../../projects/jupyter-docprovider/jupyter_docprovider/labextension", 49 | "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", 50 | "install:extension": "jlpm run build", 51 | "watch": "run-p watch:src watch:labextension", 52 | "watch:src": "tsc -w", 53 | "watch:labextension": "jupyter labextension watch ." 54 | }, 55 | "dependencies": { 56 | "@jupyter/collaborative-drive": "^4.1.0-rc.0", 57 | "@jupyter/docprovider": "^4.1.0-rc.0", 58 | "@jupyter/ydoc": "^2.1.3 || ^3.0.0", 59 | "@jupyterlab/application": "^4.4.0", 60 | "@jupyterlab/apputils": "^4.4.0", 61 | "@jupyterlab/docregistry": "^4.4.0", 62 | "@jupyterlab/filebrowser": "^4.4.0", 63 | "@jupyterlab/fileeditor": "^4.4.0", 64 | "@jupyterlab/logconsole": "^4.4.0", 65 | "@jupyterlab/notebook": "^4.4.0", 66 | "@jupyterlab/settingregistry": "^4.4.0", 67 | "@jupyterlab/translation": "^4.4.0", 68 | "@lumino/commands": "^2.3.2", 69 | "y-protocols": "^1.0.5", 70 | "y-websocket": "^1.3.15", 71 | "yjs": "^13.5.40" 72 | }, 73 | "devDependencies": { 74 | "@jupyterlab/builder": "^4.4.0", 75 | "@types/react": "~18.3.1", 76 | "npm-run-all": "^4.1.5", 77 | "rimraf": "^4.1.2", 78 | "typescript": "~5.1.6" 79 | }, 80 | "publishConfig": { 81 | "access": "public" 82 | }, 83 | "typedoc": { 84 | "entryPoint": "./src/index.ts", 85 | "readmeFile": "./README.md", 86 | "displayName": "@jupyter/docprovider-extension", 87 | "tsconfig": "./tsconfig.json" 88 | }, 89 | "jupyterlab": { 90 | "extension": true, 91 | "outputDir": "../../projects/jupyter-docprovider/jupyter_docprovider/labextension", 92 | "disabledExtensions": [ 93 | "@jupyterlab/filebrowser-extension:defaultFileBrowser", 94 | "@jupyterlab/notebook-extension:cell-executor" 95 | ], 96 | "sharedPackages": { 97 | "@codemirror/state": { 98 | "bundled": false, 99 | "singleton": true 100 | }, 101 | "@codemirror/view": { 102 | "bundled": false, 103 | "singleton": true 104 | }, 105 | "@jupyter/collaboration": { 106 | "bundled": false, 107 | "singleton": true 108 | }, 109 | "@jupyter/collaborative-drive": { 110 | "bundled": true, 111 | "singleton": true 112 | }, 113 | "@jupyter/docprovider": { 114 | "bundled": true, 115 | "singleton": true 116 | }, 117 | "@jupyter/ydoc": { 118 | "bundled": false, 119 | "singleton": true 120 | }, 121 | "y-protocols": { 122 | "bundled": false, 123 | "singleton": true 124 | }, 125 | "yjs": { 126 | "bundled": false, 127 | "singleton": true 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /packages/docprovider-extension/src/executor.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | /** 4 | * @packageDocumentation 5 | * @module docprovider-extension 6 | */ 7 | 8 | import { NotebookCellServerExecutor } from '@jupyter/docprovider'; 9 | import { 10 | JupyterFrontEnd, 11 | JupyterFrontEndPlugin 12 | } from '@jupyterlab/application'; 13 | import { PageConfig } from '@jupyterlab/coreutils'; 14 | import { INotebookCellExecutor, runCell } from '@jupyterlab/notebook'; 15 | 16 | export const notebookCellExecutor: JupyterFrontEndPlugin = 17 | { 18 | id: '@jupyter/docprovider-extension:notebook-cell-executor', 19 | description: 20 | 'Add notebook cell executor that uses REST API instead of kernel protocol over WebSocket.', 21 | autoStart: true, 22 | provides: INotebookCellExecutor, 23 | activate: (app: JupyterFrontEnd): INotebookCellExecutor => { 24 | if (PageConfig.getOption('serverSideExecution') === 'true') { 25 | return new NotebookCellServerExecutor({ 26 | serverSettings: app.serviceManager.serverSettings 27 | }); 28 | } 29 | return Object.freeze({ runCell }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/docprovider-extension/src/forkManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { ICollaborativeContentProvider } from '@jupyter/collaborative-drive'; 7 | import { 8 | ForkManager, 9 | IForkManager, 10 | IForkManagerToken 11 | } from '@jupyter/docprovider'; 12 | 13 | import { 14 | JupyterFrontEnd, 15 | JupyterFrontEndPlugin 16 | } from '@jupyterlab/application'; 17 | 18 | export const forkManagerPlugin: JupyterFrontEndPlugin = { 19 | id: '@jupyter/docprovider-extension:forkManager', 20 | autoStart: true, 21 | requires: [ICollaborativeContentProvider], 22 | provides: IForkManagerToken, 23 | activate: ( 24 | app: JupyterFrontEnd, 25 | contentProvider: ICollaborativeContentProvider 26 | ) => { 27 | const eventManager = app.serviceManager.events; 28 | const manager = new ForkManager({ contentProvider, eventManager }); 29 | return manager; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/docprovider-extension/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | /** 4 | * @packageDocumentation 5 | * @module collaboration-extension 6 | */ 7 | 8 | import { JupyterFrontEndPlugin } from '@jupyterlab/application'; 9 | 10 | import { 11 | rtcContentProvider, 12 | yfile, 13 | ynotebook, 14 | logger, 15 | statusBarTimeline 16 | } from './filebrowser'; 17 | import { notebookCellExecutor } from './executor'; 18 | import { forkManagerPlugin } from './forkManager'; 19 | 20 | /** 21 | * Export the plugins as default. 22 | */ 23 | const plugins: JupyterFrontEndPlugin[] = [ 24 | rtcContentProvider, 25 | yfile, 26 | ynotebook, 27 | logger, 28 | notebookCellExecutor, 29 | statusBarTimeline, 30 | forkManagerPlugin 31 | ]; 32 | 33 | export default plugins; 34 | -------------------------------------------------------------------------------- /packages/docprovider-extension/style/index.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | @import url('~@jupyter/collaboration/style/index.css'); 7 | -------------------------------------------------------------------------------- /packages/docprovider-extension/style/index.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |----------------------------------------------------------------------------*/ 5 | 6 | import '@jupyter/collaboration/style/index.js'; 7 | -------------------------------------------------------------------------------- /packages/docprovider-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/docprovider/babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | module.exports = require('@jupyterlab/testing/lib/babel-config'); 7 | -------------------------------------------------------------------------------- /packages/docprovider/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | const jestJupyterLab = require('@jupyterlab/testing/lib/jest-config'); 7 | 8 | const esModules = [ 9 | '@codemirror', 10 | '@microsoft', 11 | 'exenv-es6', 12 | '@jupyter/ydoc', 13 | '@jupyter/react-components', 14 | '@jupyter/web-components', 15 | '@jupyterlab/', 16 | 'lib0', 17 | 'nanoid', 18 | 'vscode-ws-jsonrpc', 19 | 'y-protocols', 20 | 'y-websocket', 21 | 'yjs' 22 | ].join('|'); 23 | 24 | const baseConfig = jestJupyterLab(__dirname); 25 | 26 | module.exports = { 27 | ...baseConfig, 28 | automock: false, 29 | collectCoverageFrom: [ 30 | 'src/**/*.{ts,tsx}', 31 | '!src/**/*.d.ts', 32 | '!src/**/.ipynb_checkpoints/*' 33 | ], 34 | coverageReporters: ['lcov', 'text'], 35 | testRegex: 'src/.*/.*.spec.ts[x]?$', 36 | transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] 37 | }; 38 | -------------------------------------------------------------------------------- /packages/docprovider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter/docprovider", 3 | "version": "4.1.0-rc.0", 4 | "description": "JupyterLab - Document Provider", 5 | "homepage": "https://github.com/jupyterlab/jupyter-collaboration", 6 | "bugs": { 7 | "url": "https://github.com/jupyterlab/jupyter-collaboration/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/jupyterlab/jupyter-collaboration.git" 12 | }, 13 | "license": "BSD-3-Clause", 14 | "author": "Project Jupyter", 15 | "sideEffects": [ 16 | "style/**/*" 17 | ], 18 | "main": "lib/index.js", 19 | "types": "lib/index.d.ts", 20 | "directories": { 21 | "lib": "lib/" 22 | }, 23 | "files": [ 24 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 25 | "schema/*.json", 26 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 27 | "style/index.js" 28 | ], 29 | "scripts": { 30 | "build": "tsc -b", 31 | "build:prod": "jlpm run build", 32 | "build:test": "tsc --build tsconfig.test.json", 33 | "clean": "rimraf lib tsconfig.tsbuildinfo", 34 | "clean:lib": "jlpm run clean:all", 35 | "clean:all": "rimraf lib tsconfig.tsbuildinfo node_modules", 36 | "install:extension": "jlpm run build", 37 | "test": "jest", 38 | "test:cov": "jest --collect-coverage", 39 | "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand", 40 | "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch", 41 | "watch": "tsc -b --watch" 42 | }, 43 | "dependencies": { 44 | "@jupyter/collaborative-drive": "^4.1.0-rc.0", 45 | "@jupyter/ydoc": "^2.1.3 || ^3.0.0", 46 | "@jupyterlab/apputils": "^4.4.0", 47 | "@jupyterlab/cells": "^4.4.0", 48 | "@jupyterlab/coreutils": "^6.4.0", 49 | "@jupyterlab/notebook": "^4.4.0", 50 | "@jupyterlab/services": "^7.4.0", 51 | "@jupyterlab/translation": "^4.4.0", 52 | "@lumino/coreutils": "^2.2.1", 53 | "@lumino/disposable": "^2.1.4", 54 | "@lumino/signaling": "^2.1.4", 55 | "@lumino/widgets": "^2.7.0", 56 | "y-protocols": "^1.0.5", 57 | "y-websocket": "^1.3.15", 58 | "yjs": "^13.5.40" 59 | }, 60 | "devDependencies": { 61 | "@jupyterlab/testing": "^4.4.0", 62 | "@types/jest": "^29.2.0", 63 | "jest": "^29.5.0", 64 | "rimraf": "^4.1.2", 65 | "typescript": "~5.1.6" 66 | }, 67 | "publishConfig": { 68 | "access": "public" 69 | }, 70 | "typedoc": { 71 | "entryPoint": "./src/index.ts", 72 | "displayName": "@jupyter/docprovider", 73 | "tsconfig": "./tsconfig.json" 74 | }, 75 | "jupyterlab": { 76 | "sharedPackages": { 77 | "@jupyter/collaborative-drive": { 78 | "bundled": true, 79 | "singleton": true 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/docprovider/src/TimelineSlider.tsx: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |----------------------------------------------------------------------------*/ 5 | 6 | import { ReactWidget } from '@jupyterlab/apputils'; 7 | import { TimelineSliderComponent } from './component'; 8 | import * as React from 'react'; 9 | import { IForkProvider } from './ydrive'; 10 | 11 | export class TimelineWidget extends ReactWidget { 12 | private apiURL: string; 13 | private provider: IForkProvider; 14 | private contentType: string; 15 | private format: string; 16 | private documentTimelineUrl: string; 17 | 18 | constructor( 19 | apiURL: string, 20 | provider: IForkProvider, 21 | contentType: string, 22 | format: string, 23 | documentTimelineUrl: string 24 | ) { 25 | super(); 26 | this.apiURL = apiURL; 27 | this.provider = provider; 28 | this.contentType = contentType; 29 | this.format = format; 30 | this.documentTimelineUrl = documentTimelineUrl; 31 | this.addClass('jp-timelineSliderWrapper'); 32 | } 33 | 34 | render(): JSX.Element { 35 | return ( 36 | 44 | ); 45 | } 46 | updateContent(apiURL: string, provider: IForkProvider): void { 47 | this.apiURL = apiURL; 48 | this.provider = provider; 49 | this.contentType = this.provider.contentType; 50 | this.format = this.provider.format; 51 | 52 | this.update(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/docprovider/src/__tests__/forkManager.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { ICollaborativeContentProvider } from '@jupyter/collaborative-drive'; 5 | import { 6 | ForkManager, 7 | JUPYTER_COLLABORATION_FORK_EVENTS_URI 8 | } from '../forkManager'; 9 | import { Event } from '@jupyterlab/services'; 10 | import { Signal } from '@lumino/signaling'; 11 | import { requestAPI } from '../requests'; 12 | jest.mock('../requests'); 13 | 14 | const contentProviderMock = { 15 | providers: new Map() 16 | } as ICollaborativeContentProvider; 17 | const stream = new Signal({}); 18 | const eventManagerMock = { 19 | stream: stream as any 20 | } as Event.IManager; 21 | 22 | describe('@jupyter/docprovider', () => { 23 | let manager: ForkManager; 24 | beforeEach(() => { 25 | manager = new ForkManager({ 26 | contentProvider: contentProviderMock, 27 | eventManager: eventManagerMock 28 | }); 29 | }); 30 | describe('forkManager', () => { 31 | it('should have a type', () => { 32 | expect(ForkManager).not.toBeUndefined(); 33 | }); 34 | it('should be able to create instance', () => { 35 | expect(manager).toBeInstanceOf(ForkManager); 36 | }); 37 | it('should be able to create new fork', async () => { 38 | await manager.createFork({ 39 | rootId: 'root-uuid', 40 | synchronize: true, 41 | title: 'my fork label', 42 | description: 'my fork description' 43 | }); 44 | expect(requestAPI).toHaveBeenCalledWith( 45 | 'api/collaboration/fork/root-uuid', 46 | { 47 | method: 'PUT', 48 | body: JSON.stringify({ 49 | title: 'my fork label', 50 | description: 'my fork description', 51 | synchronize: true 52 | }) 53 | } 54 | ); 55 | }); 56 | it('should be able to get all forks', async () => { 57 | await manager.getAllForks('root-uuid'); 58 | expect(requestAPI).toHaveBeenCalledWith( 59 | 'api/collaboration/fork/root-uuid', 60 | { 61 | method: 'GET' 62 | } 63 | ); 64 | }); 65 | it('should be able to get delete forks', async () => { 66 | await manager.deleteFork({ forkId: 'fork-uuid', merge: true }); 67 | expect(requestAPI).toHaveBeenCalledWith( 68 | 'api/collaboration/fork/fork-uuid?merge=true', 69 | { 70 | method: 'DELETE' 71 | } 72 | ); 73 | }); 74 | it('should be able to emit fork added signal', async () => { 75 | const listener = jest.fn(); 76 | manager.forkAdded.connect(listener); 77 | const data = { 78 | schema_id: JUPYTER_COLLABORATION_FORK_EVENTS_URI, 79 | action: 'create' 80 | }; 81 | stream.emit(data); 82 | expect(listener).toHaveBeenCalledWith(manager, data); 83 | }); 84 | it('should be able to emit fork deleted signal', async () => { 85 | const listener = jest.fn(); 86 | manager.forkDeleted.connect(listener); 87 | const data = { 88 | schema_id: JUPYTER_COLLABORATION_FORK_EVENTS_URI, 89 | action: 'delete' 90 | }; 91 | stream.emit(data); 92 | expect(listener).toHaveBeenCalledWith(manager, data); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/docprovider/src/__tests__/yprovider.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { WebSocketProvider } from '../yprovider'; 5 | 6 | describe('@jupyter/docprovider', () => { 7 | describe('docprovider', () => { 8 | it('should have a type', () => { 9 | expect(WebSocketProvider).not.toBeUndefined(); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/docprovider/src/awareness.ts: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |----------------------------------------------------------------------------*/ 5 | 6 | import { User } from '@jupyterlab/services'; 7 | 8 | import { IDisposable } from '@lumino/disposable'; 9 | 10 | import { IAwareness } from '@jupyter/ydoc'; 11 | 12 | import { WebsocketProvider } from 'y-websocket'; 13 | 14 | export interface IContent { 15 | type: string; 16 | body: string; 17 | } 18 | 19 | /** 20 | * A class to provide Yjs synchronization over WebSocket. 21 | * 22 | * We specify custom messages that the server can interpret. For reference please look in yjs_ws_server. 23 | * 24 | */ 25 | export class WebSocketAwarenessProvider 26 | extends WebsocketProvider 27 | implements IDisposable 28 | { 29 | /** 30 | * Construct a new WebSocketAwarenessProvider 31 | * 32 | * @param options The instantiation options for a WebSocketAwarenessProvider 33 | */ 34 | constructor(options: WebSocketAwarenessProvider.IOptions) { 35 | super(options.url, options.roomID, options.awareness.doc, { 36 | awareness: options.awareness 37 | }); 38 | 39 | this._awareness = options.awareness; 40 | 41 | this._user = options.user; 42 | this._user.ready 43 | .then(() => this._onUserChanged(this._user)) 44 | .catch(e => console.error(e)); 45 | this._user.userChanged.connect(this._onUserChanged, this); 46 | } 47 | 48 | get isDisposed(): boolean { 49 | return this._isDisposed; 50 | } 51 | 52 | dispose(): void { 53 | if (this._isDisposed) { 54 | return; 55 | } 56 | 57 | this._user.userChanged.disconnect(this._onUserChanged, this); 58 | this._isDisposed = true; 59 | this.destroy(); 60 | } 61 | 62 | private _onUserChanged(user: User.IManager): void { 63 | this._awareness.setLocalStateField('user', user.identity); 64 | } 65 | 66 | private _isDisposed = false; 67 | private _user: User.IManager; 68 | private _awareness: IAwareness; 69 | } 70 | 71 | /** 72 | * A namespace for WebSocketAwarenessProvider statics. 73 | */ 74 | export namespace WebSocketAwarenessProvider { 75 | /** 76 | * The instantiation options for a WebSocketAwarenessProvider. 77 | */ 78 | export interface IOptions { 79 | /** 80 | * The server URL 81 | */ 82 | url: string; 83 | 84 | /** 85 | * The room ID 86 | */ 87 | roomID: string; 88 | 89 | /** 90 | * The awareness object 91 | */ 92 | awareness: IAwareness; 93 | 94 | /** 95 | * The user data 96 | */ 97 | user: User.IManager; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/docprovider/src/forkManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { ICollaborativeContentProvider } from '@jupyter/collaborative-drive'; 7 | import { URLExt } from '@jupyterlab/coreutils'; 8 | import { Event } from '@jupyterlab/services'; 9 | import { ISignal, Signal } from '@lumino/signaling'; 10 | 11 | import { requestAPI, ROOM_FORK_URL } from './requests'; 12 | import { 13 | IAllForksResponse, 14 | IForkChangedEvent, 15 | IForkCreationResponse, 16 | IForkManager 17 | } from './tokens'; 18 | import { IForkProvider } from './ydrive'; 19 | 20 | export const JUPYTER_COLLABORATION_FORK_EVENTS_URI = 21 | 'https://schema.jupyter.org/jupyter_collaboration/fork/v1'; 22 | 23 | export class ForkManager implements IForkManager { 24 | constructor(options: ForkManager.IOptions) { 25 | const { contentProvider, eventManager } = options; 26 | this._contentProvider = contentProvider; 27 | this._eventManager = eventManager; 28 | this._eventManager.stream.connect(this._handleEvent, this); 29 | } 30 | 31 | get isDisposed(): boolean { 32 | return this._disposed; 33 | } 34 | get forkAdded(): ISignal { 35 | return this._forkAddedSignal; 36 | } 37 | get forkDeleted(): ISignal { 38 | return this._forkDeletedSignal; 39 | } 40 | 41 | dispose(): void { 42 | if (this._disposed) { 43 | return; 44 | } 45 | this._eventManager?.stream.disconnect(this._handleEvent); 46 | this._disposed = true; 47 | } 48 | async createFork(options: { 49 | rootId: string; 50 | synchronize: boolean; 51 | title?: string; 52 | description?: string; 53 | }): Promise { 54 | const { rootId, title, description, synchronize } = options; 55 | const init: RequestInit = { 56 | method: 'PUT', 57 | body: JSON.stringify({ title, description, synchronize }) 58 | }; 59 | const url = URLExt.join(ROOM_FORK_URL, rootId); 60 | const response = await requestAPI(url, init); 61 | return response; 62 | } 63 | 64 | async getAllForks(rootId: string) { 65 | const url = URLExt.join(ROOM_FORK_URL, rootId); 66 | const init = { method: 'GET' }; 67 | const response = await requestAPI(url, init); 68 | return response; 69 | } 70 | 71 | async deleteFork(options: { forkId: string; merge: boolean }): Promise { 72 | const { forkId, merge } = options; 73 | const url = URLExt.join(ROOM_FORK_URL, forkId); 74 | const query = URLExt.objectToQueryString({ merge }); 75 | const init = { method: 'DELETE' }; 76 | await requestAPI(`${url}${query}`, init); 77 | } 78 | getProvider(options: { 79 | documentPath: string; 80 | format: string; 81 | type: string; 82 | }): IForkProvider | undefined { 83 | const { documentPath, format, type } = options; 84 | const contentProvider = this._contentProvider; 85 | if (contentProvider) { 86 | const docPath = documentPath; 87 | const provider = contentProvider.providers.get( 88 | `${format}:${type}:${docPath}` 89 | ); 90 | return provider as IForkProvider | undefined; 91 | } 92 | return; 93 | } 94 | 95 | private _handleEvent(_: Event.IManager, emission: Event.Emission) { 96 | if (emission.schema_id === JUPYTER_COLLABORATION_FORK_EVENTS_URI) { 97 | switch (emission.action) { 98 | case 'create': { 99 | this._forkAddedSignal.emit(emission as any); 100 | break; 101 | } 102 | case 'delete': { 103 | this._forkDeletedSignal.emit(emission as any); 104 | break; 105 | } 106 | default: 107 | break; 108 | } 109 | } 110 | } 111 | 112 | private _disposed = false; 113 | private _contentProvider: ICollaborativeContentProvider | undefined; 114 | private _eventManager: Event.IManager | undefined; 115 | private _forkAddedSignal = new Signal(this); 116 | private _forkDeletedSignal = new Signal(this); 117 | } 118 | 119 | export namespace ForkManager { 120 | export interface IOptions { 121 | contentProvider: ICollaborativeContentProvider; 122 | eventManager: Event.IManager; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/docprovider/src/index.ts: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |----------------------------------------------------------------------------*/ 5 | /** 6 | * @packageDocumentation 7 | * @module docprovider 8 | */ 9 | 10 | export * from './awareness'; 11 | export * from './notebookCellExecutor'; 12 | export * from './requests'; 13 | export * from './ydrive'; 14 | export * from './yprovider'; 15 | export * from './TimelineSlider'; 16 | export * from './tokens'; 17 | export * from './forkManager'; 18 | -------------------------------------------------------------------------------- /packages/docprovider/src/tokens.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { Token } from '@lumino/coreutils'; 7 | import { IDisposable } from '@lumino/disposable'; 8 | import { ISignal } from '@lumino/signaling'; 9 | import { IForkProvider } from './ydrive'; 10 | export interface IForkInfo { 11 | description?: string; 12 | root_roomid: string; 13 | synchronize: boolean; 14 | title?: string; 15 | } 16 | 17 | export interface IForkCreationResponse { 18 | fork_info: IForkInfo; 19 | fork_roomid: string; 20 | sessionId: string; 21 | } 22 | 23 | export interface IAllForksResponse { 24 | [forkId: string]: IForkInfo; 25 | } 26 | 27 | export interface IForkChangedEvent { 28 | fork_info: IForkInfo; 29 | fork_roomid: string; 30 | username?: string; 31 | } 32 | 33 | /** 34 | * Interface representing a Fork Manager that manages forked documents and 35 | * provides signals for fork-related events. 36 | * 37 | * @interface IForkManager 38 | * @extends IDisposable 39 | */ 40 | export interface IForkManager extends IDisposable { 41 | /** 42 | * Get the fork provider of a given document. 43 | * 44 | * @param options.documentPath - The document path including the 45 | * drive prefix. 46 | * @param options.format - Format of the document. 47 | * @param options.type - Content type of the document. 48 | * @returns The fork provider of the document. 49 | */ 50 | getProvider(options: { 51 | documentPath: string; 52 | format: string; 53 | type: string; 54 | }): IForkProvider | undefined; 55 | 56 | /** 57 | * Creates a new fork for a given document. 58 | * 59 | * @param options.rootId - The ID of the root document to fork. 60 | * @param options.synchronize - A flag indicating whether the fork should be kept 61 | * synchronized with the root document. 62 | * @param options.title - An optional label for the fork. 63 | * @param options.description - An optional description for the fork. 64 | * 65 | * @returns A promise that resolves to an `IForkCreationResponse` if the fork 66 | * is created successfully, or `undefined` if the creation fails. 67 | */ 68 | createFork(options: { 69 | rootId: string; 70 | synchronize: boolean; 71 | title?: string; 72 | description?: string; 73 | }): Promise; 74 | 75 | /** 76 | * Retrieves all forks associated with a specific document. 77 | * 78 | * @param documentId - The ID of the document for which forks are to be retrieved. 79 | * 80 | * @returns A promise that resolves to an `IAllForksResponse` containing information about all forks. 81 | */ 82 | getAllForks(documentId: string): Promise; 83 | 84 | /** 85 | * Deletes a specified fork and optionally merges its changes. 86 | * 87 | * @param options - Options for deleting the fork. 88 | * @param options.forkId - The ID of the fork to be deleted. 89 | * @param options.merge - A flag indicating whether changes from the fork should be merged back into the root document. 90 | * 91 | * @returns A promise that resolves when the fork is successfully deleted. 92 | */ 93 | deleteFork(options: { forkId: string; merge: boolean }): Promise; 94 | 95 | /** 96 | * Signal emitted when a new fork is added. 97 | * 98 | * @event forkAdded 99 | * @type ISignal 100 | */ 101 | forkAdded: ISignal; 102 | 103 | /** 104 | * Signal emitted when a fork is deleted. 105 | * 106 | * @event forkDeleted 107 | * @type ISignal 108 | */ 109 | forkDeleted: ISignal; 110 | } 111 | 112 | /** 113 | * Token providing a fork manager instance. 114 | */ 115 | export const IForkManagerToken = new Token( 116 | '@jupyter/docprovider:IForkManagerToken' 117 | ); 118 | -------------------------------------------------------------------------------- /packages/docprovider/style/base.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | @import url('./slider.css'); 7 | -------------------------------------------------------------------------------- /packages/docprovider/style/index.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | @import url('./base.css'); 7 | -------------------------------------------------------------------------------- /packages/docprovider/style/index.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |----------------------------------------------------------------------------*/ 5 | 6 | import './base.css'; 7 | -------------------------------------------------------------------------------- /packages/docprovider/style/slider.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | .jp-timelineSliderWrapper .jp-sliderContainer{ 7 | display: flex; 8 | align-items: center; 9 | } 10 | 11 | .jp-Slider { 12 | height: 4.5px 13 | } 14 | 15 | #jp-slider-status-bar { 16 | display: flex; 17 | } 18 | 19 | .jp-timestampDisplay { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | gap: 6px; 24 | } 25 | 26 | .jp-restoreBtnContainer { 27 | width: 192px; 28 | } 29 | 30 | .jp-ToolbarButtonComponent.jp-restoreBtn { 31 | cursor: pointer; 32 | color: var(--jp-layout-color2); 33 | width: 100%; 34 | background: var(--jp-accept-color-normal) 35 | } 36 | -------------------------------------------------------------------------------- /packages/docprovider/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/*"], 8 | "exclude": ["src/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/docprovider/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.test.json", 3 | "include": ["src/*", "test/*"] 4 | } 5 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration-ui/LICENSE: -------------------------------------------------------------------------------- 1 | # Licensing terms 2 | 3 | This project is licensed under the terms of the Modified BSD License 4 | (also known as New or Revised or 3-Clause BSD), as follows: 5 | 6 | - Copyright (c) 2021-, Jupyter Development Team 7 | 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | Redistributions in binary form must reproduce the above copyright notice, this 17 | list of conditions and the following disclaimer in the documentation and/or 18 | other materials provided with the distribution. 19 | 20 | Neither the name of the Jupyter Development Team nor the names of its 21 | contributors may be used to endorse or promote products derived from this 22 | software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ## About the Jupyter Development Team 36 | 37 | The Jupyter Development Team is the set of all contributors to the Jupyter project. 38 | This includes all of the Jupyter subprojects. 39 | 40 | The core team that coordinates development on GitHub can be found here: 41 | https://github.com/jupyter/. 42 | 43 | ## Our Copyright Policy 44 | 45 | Jupyter uses a shared copyright model. Each contributor maintains copyright 46 | over their contributions to Jupyter. But, it is important to note that these 47 | contributions are typically only changes to the repositories. Thus, the Jupyter 48 | source code, in its entirety is not the copyright of any single person or 49 | institution. Instead, it is the collective copyright of the entire Jupyter 50 | Development Team. If individual contributors want to maintain a record of what 51 | changes/contributions they have specific copyright on, they should indicate 52 | their copyright in the commit message of the change, when they commit the 53 | change to one of the Jupyter repositories. 54 | 55 | With this in mind, the following banner should be used in any source code file 56 | to indicate the copyright and license terms: 57 | 58 | # Copyright (c) Jupyter Development Team. 59 | # Distributed under the terms of the Modified BSD License. 60 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration-ui/README.md: -------------------------------------------------------------------------------- 1 | # jupyter-collaboration-ui 2 | 3 | JupyterLab/Jupyter Notebook 7+ extension providing user interface integration for real time collaboration. 4 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration-ui/install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyter-collaboration-ui", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyter-collaboration-ui" 5 | } 6 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration-ui/jupyter_collaboration_ui/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from ._version import __version__ # noqa 5 | 6 | 7 | def _jupyter_labextension_paths(): 8 | return [{"src": "labextension", "dest": "@jupyter/collaboration-extension"}] 9 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration-ui/jupyter_collaboration_ui/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.1.0rc0" 2 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration-ui/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | [build-system] 5 | build-backend = "hatchling.build" 6 | requires = ["hatchling>=1.4.0", "jupyterlab>=4.0.0"] 7 | 8 | [project] 9 | name = "jupyter-collaboration-ui" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | requires-python = ">=3.8" 13 | description = "JupyterLab/Jupyter Notebook 7+ extension providing user interface integration for real time collaboration" 14 | classifiers = [ 15 | "Intended Audience :: Developers", 16 | "Intended Audience :: System Administrators", 17 | "Intended Audience :: Science/Research", 18 | "License :: OSI Approved :: BSD License", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Framework :: Jupyter", 26 | "Framework :: Jupyter :: JupyterLab", 27 | "Framework :: Jupyter :: JupyterLab :: 4", 28 | "Framework :: Jupyter :: JupyterLab :: Extensions", 29 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 30 | ] 31 | authors = [ 32 | { name = "Jupyter Development Team", email = "jupyter@googlegroups.com" }, 33 | ] 34 | dynamic = ["version"] 35 | 36 | [project.urls] 37 | Documentation = "https://jupyterlab-realtime-collaboration.readthedocs.io/" 38 | Repository = "https://github.com/jupyterlab/jupyter-collaboration" 39 | 40 | [tool.hatch.version] 41 | path = "jupyter_collaboration_ui/_version.py" 42 | 43 | [tool.hatch.build.targets.sdist] 44 | artifacts = ["jupyter_collaboration_ui/labextension"] 45 | exclude = ["/.github", "/binder", "node_modules"] 46 | 47 | [tool.hatch.build.targets.sdist.force-include] 48 | "../../packages" = "packages" 49 | 50 | [tool.hatch.build.targets.wheel.shared-data] 51 | "jupyter_collaboration_ui/labextension" = "share/jupyter/labextensions/@jupyter/collaboration-extension" 52 | "install.json" = "share/jupyter/labextensions/@jupyter/collaboration-extension/install.json" 53 | 54 | [tool.hatch.build.hooks.jupyter-builder] 55 | dependencies = ["hatch-jupyter-builder>=0.5"] 56 | build-function = "hatch_jupyter_builder.npm_builder" 57 | ensured-targets = [ 58 | "jupyter_collaboration_ui/labextension/static/style.js", 59 | "jupyter_collaboration_ui/labextension/package.json", 60 | ] 61 | skip-if-exists = ["jupyter_collaboration_ui/labextension/static/style.js"] 62 | 63 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 64 | npm = ["jlpm"] 65 | path = "../.." 66 | build_cmd = "build:prod" 67 | editable_build_cmd = "install:extension" 68 | 69 | [tool.check-wheel-contents] 70 | ignore = ["W002"] 71 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration-ui/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # setup.py shim for use with applications that require it. 5 | __import__("setuptools").setup() 6 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration/LICENSE: -------------------------------------------------------------------------------- 1 | # Licensing terms 2 | 3 | This project is licensed under the terms of the Modified BSD License 4 | (also known as New or Revised or 3-Clause BSD), as follows: 5 | 6 | - Copyright (c) 2021-, Jupyter Development Team 7 | 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | Redistributions in binary form must reproduce the above copyright notice, this 17 | list of conditions and the following disclaimer in the documentation and/or 18 | other materials provided with the distribution. 19 | 20 | Neither the name of the Jupyter Development Team nor the names of its 21 | contributors may be used to endorse or promote products derived from this 22 | software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ## About the Jupyter Development Team 36 | 37 | The Jupyter Development Team is the set of all contributors to the Jupyter project. 38 | This includes all of the Jupyter subprojects. 39 | 40 | The core team that coordinates development on GitHub can be found here: 41 | https://github.com/jupyter/. 42 | 43 | ## Our Copyright Policy 44 | 45 | Jupyter uses a shared copyright model. Each contributor maintains copyright 46 | over their contributions to Jupyter. But, it is important to note that these 47 | contributions are typically only changes to the repositories. Thus, the Jupyter 48 | source code, in its entirety is not the copyright of any single person or 49 | institution. Instead, it is the collective copyright of the entire Jupyter 50 | Development Team. If individual contributors want to maintain a record of what 51 | changes/contributions they have specific copyright on, they should indicate 52 | their copyright in the commit message of the change, when they commit the 53 | change to one of the Jupyter repositories. 54 | 55 | With this in mind, the following banner should be used in any source code file 56 | to indicate the copyright and license terms: 57 | 58 | # Copyright (c) Jupyter Development Team. 59 | # Distributed under the terms of the Modified BSD License. 60 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration/README.md: -------------------------------------------------------------------------------- 1 | # jupyter-collaboration 2 | 3 | A meta-package for: 4 | - jupyter-collaboration-ui 5 | - jupyter-docprovider 6 | - jupyter-server-ydoc 7 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration/jupyter_collaboration/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from ._version import __version__ # noqa 5 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration/jupyter_collaboration/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.1.0rc0" 2 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | [build-system] 5 | build-backend = "hatchling.build" 6 | requires = ["hatchling>=1.4.0"] 7 | 8 | [project] 9 | name = "jupyter-collaboration" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | requires-python = ">=3.8" 13 | description = "JupyterLab/Jupyter Notebook 7+ Real Time Collaboration extension (metapackage)" 14 | classifiers = [ 15 | "Intended Audience :: Developers", 16 | "Intended Audience :: System Administrators", 17 | "Intended Audience :: Science/Research", 18 | "License :: OSI Approved :: BSD License", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Framework :: Jupyter", 26 | "Framework :: Jupyter :: JupyterLab", 27 | "Framework :: Jupyter :: JupyterLab :: 4", 28 | "Framework :: Jupyter :: JupyterLab :: Extensions", 29 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 30 | ] 31 | dynamic = ["version"] 32 | dependencies = [ 33 | "jupyter_collaboration_ui>=2.1.0rc0,<3", 34 | "jupyter_docprovider>=2.1.0rc0,<3", 35 | "jupyter_server_ydoc>=2.1.0rc0,<3", 36 | "jupyterlab>=4.4.0,<5.0.0", 37 | ] 38 | 39 | [tool.hatch.version] 40 | path = "jupyter_collaboration/_version.py" 41 | 42 | [tool.check-wheel-contents] 43 | ignore = ["W002"] 44 | -------------------------------------------------------------------------------- /projects/jupyter-collaboration/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # setup.py shim for use with applications that require it. 5 | __import__("setuptools").setup() 6 | -------------------------------------------------------------------------------- /projects/jupyter-docprovider/LICENSE: -------------------------------------------------------------------------------- 1 | # Licensing terms 2 | 3 | This project is licensed under the terms of the Modified BSD License 4 | (also known as New or Revised or 3-Clause BSD), as follows: 5 | 6 | - Copyright (c) 2021-, Jupyter Development Team 7 | 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | Redistributions in binary form must reproduce the above copyright notice, this 17 | list of conditions and the following disclaimer in the documentation and/or 18 | other materials provided with the distribution. 19 | 20 | Neither the name of the Jupyter Development Team nor the names of its 21 | contributors may be used to endorse or promote products derived from this 22 | software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ## About the Jupyter Development Team 36 | 37 | The Jupyter Development Team is the set of all contributors to the Jupyter project. 38 | This includes all of the Jupyter subprojects. 39 | 40 | The core team that coordinates development on GitHub can be found here: 41 | https://github.com/jupyter/. 42 | 43 | ## Our Copyright Policy 44 | 45 | Jupyter uses a shared copyright model. Each contributor maintains copyright 46 | over their contributions to Jupyter. But, it is important to note that these 47 | contributions are typically only changes to the repositories. Thus, the Jupyter 48 | source code, in its entirety is not the copyright of any single person or 49 | institution. Instead, it is the collective copyright of the entire Jupyter 50 | Development Team. If individual contributors want to maintain a record of what 51 | changes/contributions they have specific copyright on, they should indicate 52 | their copyright in the commit message of the change, when they commit the 53 | change to one of the Jupyter repositories. 54 | 55 | With this in mind, the following banner should be used in any source code file 56 | to indicate the copyright and license terms: 57 | 58 | # Copyright (c) Jupyter Development Team. 59 | # Distributed under the terms of the Modified BSD License. 60 | -------------------------------------------------------------------------------- /projects/jupyter-docprovider/README.md: -------------------------------------------------------------------------------- 1 | # jupyter-docprovider 2 | 3 | JupyterLab/Jupyter Notebook 7+ extension integrating collaborative shared models. 4 | 5 | The collaborative shared models are used for both: 6 | - real time collaboration, and 7 | - server-side execution of notebooks 8 | -------------------------------------------------------------------------------- /projects/jupyter-docprovider/install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyter-docprovider", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyter-docprovider" 5 | } 6 | -------------------------------------------------------------------------------- /projects/jupyter-docprovider/jupyter_docprovider/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from ._version import __version__ # noqa 5 | 6 | 7 | def _jupyter_labextension_paths(): 8 | return [{"src": "labextension", "dest": "@jupyter/docprovider-extension"}] 9 | -------------------------------------------------------------------------------- /projects/jupyter-docprovider/jupyter_docprovider/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.1.0rc0" 2 | -------------------------------------------------------------------------------- /projects/jupyter-docprovider/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | [build-system] 5 | build-backend = "hatchling.build" 6 | requires = ["hatchling>=1.4.0", "jupyterlab>=4.0.0"] 7 | 8 | [project] 9 | name = "jupyter-docprovider" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | requires-python = ">=3.8" 13 | description = "JupyterLab/Jupyter Notebook 7+ extension integrating collaborative shared models." 14 | classifiers = [ 15 | "Intended Audience :: Developers", 16 | "Intended Audience :: System Administrators", 17 | "Intended Audience :: Science/Research", 18 | "License :: OSI Approved :: BSD License", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Framework :: Jupyter", 26 | "Framework :: Jupyter :: JupyterLab", 27 | "Framework :: Jupyter :: JupyterLab :: 4", 28 | "Framework :: Jupyter :: JupyterLab :: Extensions", 29 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 30 | ] 31 | authors = [ 32 | { name = "Jupyter Development Team", email = "jupyter@googlegroups.com" }, 33 | ] 34 | dynamic = ["version"] 35 | 36 | [project.urls] 37 | Documentation = "https://jupyterlab-realtime-collaboration.readthedocs.io/" 38 | Repository = "https://github.com/jupyterlab/jupyter-collaboration" 39 | 40 | [tool.hatch.version] 41 | path = "jupyter_docprovider/_version.py" 42 | 43 | [tool.hatch.build.targets.sdist] 44 | artifacts = ["jupyter_docprovider/labextension"] 45 | exclude = ["/.github", "/binder", "node_modules"] 46 | 47 | [tool.hatch.build.targets.sdist.force-include] 48 | "../../packages/docprovider" = "packages/docprovider" 49 | "../../packages/docprovider-extension" = "packages/docprovider-extension" 50 | 51 | [tool.hatch.build.targets.wheel.shared-data] 52 | "jupyter_docprovider/labextension" = "share/jupyter/labextensions/@jupyter/docprovider-extension" 53 | "install.json" = "share/jupyter/labextensions/@jupyter/docprovider-extension/install.json" 54 | 55 | [tool.hatch.build.hooks.jupyter-builder] 56 | dependencies = ["hatch-jupyter-builder>=0.5"] 57 | build-function = "hatch_jupyter_builder.npm_builder" 58 | ensured-targets = [ 59 | "jupyter_docprovider/labextension/static/style.js", 60 | "jupyter_docprovider/labextension/package.json", 61 | ] 62 | skip-if-exists = ["jupyter_docprovider/labextension/static/style.js"] 63 | 64 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 65 | npm = ["jlpm"] 66 | path = "../.." 67 | build_cmd = "build:prod" 68 | editable_build_cmd = "install:extension" 69 | 70 | [tool.check-wheel-contents] 71 | ignore = ["W002"] 72 | -------------------------------------------------------------------------------- /projects/jupyter-docprovider/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # setup.py shim for use with applications that require it. 5 | __import__("setuptools").setup() 6 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/LICENSE: -------------------------------------------------------------------------------- 1 | # Licensing terms 2 | 3 | This project is licensed under the terms of the Modified BSD License 4 | (also known as New or Revised or 3-Clause BSD), as follows: 5 | 6 | - Copyright (c) 2021-, Jupyter Development Team 7 | 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | Redistributions in binary form must reproduce the above copyright notice, this 17 | list of conditions and the following disclaimer in the documentation and/or 18 | other materials provided with the distribution. 19 | 20 | Neither the name of the Jupyter Development Team nor the names of its 21 | contributors may be used to endorse or promote products derived from this 22 | software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ## About the Jupyter Development Team 36 | 37 | The Jupyter Development Team is the set of all contributors to the Jupyter project. 38 | This includes all of the Jupyter subprojects. 39 | 40 | The core team that coordinates development on GitHub can be found here: 41 | https://github.com/jupyter/. 42 | 43 | ## Our Copyright Policy 44 | 45 | Jupyter uses a shared copyright model. Each contributor maintains copyright 46 | over their contributions to Jupyter. But, it is important to note that these 47 | contributions are typically only changes to the repositories. Thus, the Jupyter 48 | source code, in its entirety is not the copyright of any single person or 49 | institution. Instead, it is the collective copyright of the entire Jupyter 50 | Development Team. If individual contributors want to maintain a record of what 51 | changes/contributions they have specific copyright on, they should indicate 52 | their copyright in the commit message of the change, when they commit the 53 | change to one of the Jupyter repositories. 54 | 55 | With this in mind, the following banner should be used in any source code file 56 | to indicate the copyright and license terms: 57 | 58 | # Copyright (c) Jupyter Development Team. 59 | # Distributed under the terms of the Modified BSD License. 60 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/README.md: -------------------------------------------------------------------------------- 1 | # jupyter-server-ydoc 2 | 3 | jupyter-server extension integrating collaborative shared models. 4 | 5 | The collaborative shared models are used for both: 6 | - real time collaboration, and 7 | - server-side execution of notebooks 8 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter-config/jupyter_server_ydoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyter_server_ydoc": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter_server_ydoc/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from typing import Any, Dict, List 5 | 6 | from ._version import __version__ # noqa 7 | from .app import YDocExtension 8 | 9 | 10 | def _jupyter_server_extension_points() -> List[Dict[str, Any]]: 11 | return [{"module": "jupyter_server_ydoc", "app": YDocExtension}] 12 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter_server_ydoc/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.1.0rc0" 2 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter_server_ydoc/events/awareness.yaml: -------------------------------------------------------------------------------- 1 | "$id": https://schema.jupyter.org/jupyter_collaboration/awareness/v1 2 | "$schema": "http://json-schema.org/draft-07/schema" 3 | version: "1" 4 | title: Collaborative awareness events 5 | personal-data: true 6 | description: | 7 | Awareness events emitted from server-side during a collaborative session. 8 | type: object 9 | required: 10 | - roomid 11 | - username 12 | - action 13 | properties: 14 | roomid: 15 | type: string 16 | description: | 17 | Room ID. Usually composed by the file type, format and ID. 18 | username: 19 | type: string 20 | description: | 21 | The name of the user who joined or left room. 22 | action: 23 | enum: 24 | - join 25 | - leave 26 | description: | 27 | Possible values: 28 | 1. join 29 | 2. leave 30 | msg: 31 | type: string 32 | description: | 33 | Optional event message. 34 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter_server_ydoc/events/fork.yaml: -------------------------------------------------------------------------------- 1 | "$id": https://schema.jupyter.org/jupyter_collaboration/fork/v1 2 | "$schema": "http://json-schema.org/draft-07/schema" 3 | version: "1" 4 | title: Collaborative fork events 5 | personal-data: true 6 | description: | 7 | Fork events emitted from server-side during a collaborative session. 8 | type: object 9 | required: 10 | - fork_roomid 11 | - fork_info 12 | - username 13 | - action 14 | properties: 15 | fork_roomid: 16 | type: string 17 | description: | 18 | Fork root room ID. 19 | fork_info: 20 | type: object 21 | description: | 22 | Fork root room information. 23 | required: 24 | - root_roomid 25 | - synchronize 26 | - title 27 | - description 28 | properties: 29 | root_roomid: 30 | type: string 31 | description: | 32 | Root room ID. Usually composed by the file type, format and ID. 33 | synchronize: 34 | type: boolean 35 | description: | 36 | Whether the fork is kept in sync with the root. 37 | title: 38 | type: string 39 | description: | 40 | The title of the fork. 41 | description: 42 | type: string 43 | description: | 44 | The description of the fork. 45 | username: 46 | type: string 47 | description: | 48 | The name of the user who created or deleted the fork. 49 | action: 50 | enum: 51 | - create 52 | - delete 53 | description: | 54 | Possible values: 55 | 1. create 56 | 2. delete 57 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter_server_ydoc/events/session.yaml: -------------------------------------------------------------------------------- 1 | "$id": https://schema.jupyter.org/jupyter_collaboration/session/v1 2 | "$schema": "http://json-schema.org/draft-07/schema" 3 | version: "1" 4 | title: Collaborative session events 5 | personal-data: true 6 | description: | 7 | Events emitted server-side during a collaborative session. 8 | type: object 9 | required: 10 | - level 11 | - room 12 | - path 13 | properties: 14 | level: 15 | enum: 16 | - INFO 17 | - DEBUG 18 | - WARNING 19 | - ERROR 20 | - CRITICAL 21 | description: | 22 | Message type. 23 | room: 24 | type: string 25 | description: | 26 | Room ID. Usually composed by the file type, format and ID. 27 | path: 28 | type: string 29 | description: | 30 | File path. 31 | store: 32 | type: string 33 | description: | 34 | The store used to track the document history. 35 | action: 36 | enum: 37 | - initialize 38 | - load 39 | - save 40 | - overwrite 41 | - clean 42 | description: | 43 | Action performed in a room during a collaborative session. 44 | Possible values: 45 | 1. initialize 46 | Initialize a room by loading the content from the contents manager or a store. 47 | 2. load 48 | Load the content from the contents manager. 49 | 3. save 50 | Save the content with the contents manager. 51 | 4. overwrite 52 | Overwrite the content in a room with content from the contents manager. 53 | This can happen when multiple rooms access the same file or when a user 54 | modifies the file outside Jupyter Server (e.g. using a different app). 55 | 5. clean 56 | Clean the room once is empty (aka there is no more users connected to it). 57 | msg: 58 | type: string 59 | description: | 60 | Event message. 61 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter_server_ydoc/stores.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from pycrdt_websocket.ystore import SQLiteYStore as _SQLiteYStore 5 | from pycrdt_websocket.ystore import TempFileYStore as _TempFileYStore 6 | from traitlets import Int, Unicode 7 | from traitlets.config import LoggingConfigurable 8 | 9 | 10 | class TempFileYStore(_TempFileYStore): 11 | prefix_dir = "jupyter_ystore_" 12 | 13 | 14 | class SQLiteYStoreMetaclass(type(LoggingConfigurable), type(_SQLiteYStore)): # type: ignore 15 | pass 16 | 17 | 18 | class SQLiteYStore(LoggingConfigurable, _SQLiteYStore, metaclass=SQLiteYStoreMetaclass): 19 | db_path = Unicode( 20 | ".jupyter_ystore.db", 21 | config=True, 22 | help="""The path to the YStore database. Defaults to '.jupyter_ystore.db' in the current 23 | directory.""", 24 | ) 25 | 26 | document_ttl = Int( 27 | None, 28 | allow_none=True, 29 | config=True, 30 | help="""The document time-to-live in seconds. Defaults to None (document history is never 31 | cleared).""", 32 | ) 33 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter_server_ydoc/test_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from __future__ import annotations 5 | 6 | from datetime import datetime 7 | from typing import Any 8 | 9 | from anyio import Lock 10 | from jupyter_server import _tz as tz 11 | 12 | 13 | class FakeFileIDManager: 14 | def __init__(self, mapping: dict): 15 | self.mapping = mapping 16 | 17 | def get_path(self, id: str) -> str: 18 | return self.mapping[id] 19 | 20 | def move(self, id: str, new_path: str) -> None: 21 | self.mapping[id] = new_path 22 | 23 | 24 | class FakeContentsManager: 25 | def __init__(self, model: dict): 26 | self.model = { 27 | "name": "", 28 | "path": "", 29 | "last_modified": datetime(1970, 1, 1, 0, 0, tzinfo=tz.UTC), 30 | "created": datetime(1970, 1, 1, 0, 0, tzinfo=tz.UTC), 31 | "content": None, 32 | "format": None, 33 | "mimetype": None, 34 | "size": 0, 35 | "writable": False, 36 | } 37 | self.model.update(model) 38 | 39 | self.actions: list[str] = [] 40 | 41 | def get( 42 | self, path: str, content: bool = True, format: str | None = None, type: str | None = None 43 | ) -> dict: 44 | self.actions.append("get") 45 | return self.model 46 | 47 | def save(self, model: dict[str, Any], path: str) -> dict: 48 | self.actions.append("save") 49 | return self.model 50 | 51 | def save_content(self, model: dict[str, Any], path: str) -> dict: 52 | self.actions.append("save_content") 53 | return self.model 54 | 55 | 56 | class FakeEventLogger: 57 | def emit(self, schema_id: str, data: dict) -> None: 58 | print(data) 59 | 60 | 61 | class Websocket: 62 | def __init__(self, websocket: Any, path: str): 63 | self._websocket = websocket 64 | self._path = path 65 | self._send_lock = Lock() 66 | 67 | @property 68 | def path(self) -> str: 69 | return self._path 70 | 71 | def __aiter__(self): 72 | return self 73 | 74 | async def __anext__(self) -> bytes: 75 | try: 76 | message = await self.recv() 77 | except Exception: 78 | raise StopAsyncIteration() 79 | return message 80 | 81 | async def send(self, message: bytes) -> None: 82 | async with self._send_lock: 83 | await self._websocket.send_bytes(message) 84 | 85 | async def recv(self) -> bytes: 86 | b = await self._websocket.receive_bytes() 87 | return bytes(b) 88 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/jupyter_server_ydoc/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from enum import Enum, IntEnum 5 | from pathlib import Path 6 | from typing import Tuple 7 | 8 | EVENTS_FOLDER_PATH = Path(__file__).parent / "events" 9 | JUPYTER_COLLABORATION_EVENTS_URI = "https://schema.jupyter.org/jupyter_collaboration/session/v1" 10 | EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "session.yaml" 11 | JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI = ( 12 | "https://schema.jupyter.org/jupyter_collaboration/awareness/v1" 13 | ) 14 | JUPYTER_COLLABORATION_FORK_EVENTS_URI = "https://schema.jupyter.org/jupyter_collaboration/fork/v1" 15 | AWARENESS_EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "awareness.yaml" 16 | FORK_EVENTS_SCHEMA_PATH = EVENTS_FOLDER_PATH / "fork.yaml" 17 | 18 | 19 | class MessageType(IntEnum): 20 | SYNC = 0 21 | AWARENESS = 1 22 | RAW = 2 23 | CHAT = 125 24 | 25 | 26 | class LogLevel(Enum): 27 | INFO = "INFO" 28 | DEBUG = "DEBUG" 29 | WARNING = "WARNING" 30 | ERROR = "ERROR" 31 | CRITICAL = "CRITICAL" 32 | 33 | 34 | class OutOfBandChanges(Exception): 35 | pass 36 | 37 | 38 | class ReadError(Exception): 39 | pass 40 | 41 | 42 | class WriteError(Exception): 43 | pass 44 | 45 | 46 | def decode_file_path(path: str) -> Tuple[str, str, str]: 47 | """ 48 | Decodes a file path. The file path is composed by the format, 49 | content type, and path or file id separated by ':'. 50 | 51 | Parameters: 52 | path (str): File path. 53 | 54 | Returns: 55 | components (Tuple[str, str, str]): A tuple with the format, 56 | content type, and path or file id. 57 | """ 58 | format, file_type, file_id = path.split(":", 2) 59 | return (format, file_type, file_id) 60 | 61 | 62 | def encode_file_path(format: str, file_type: str, file_id: str) -> str: 63 | """ 64 | Encodes a file path. The file path is composed by the format, 65 | content type, and path or file id separated by ':'. 66 | 67 | Parameters: 68 | format (str): File format. 69 | type (str): Content type. 70 | path (str): Path or file id. 71 | 72 | Returns: 73 | path (str): File path. 74 | """ 75 | return f"{format}:{file_type}:{file_id}" 76 | 77 | 78 | def room_id_from_encoded_path(encoded_path: str) -> str: 79 | """Transforms the encoded path into a stable room identifier.""" 80 | return encoded_path.split("/")[-1] 81 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | [build-system] 5 | build-backend = "hatchling.build" 6 | requires = ["hatchling>=1.4.0"] 7 | 8 | [project] 9 | name = "jupyter-server-ydoc" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | requires-python = ">=3.8" 13 | description = "jupyter-server extension integrating collaborative shared models." 14 | classifiers = [ 15 | "Intended Audience :: Developers", 16 | "Intended Audience :: System Administrators", 17 | "Intended Audience :: Science/Research", 18 | "License :: OSI Approved :: BSD License", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Framework :: Jupyter" 26 | ] 27 | authors = [ 28 | { name = "Jupyter Development Team", email = "jupyter@googlegroups.com" }, 29 | ] 30 | dependencies = [ 31 | "jupyter_server>=2.15.0,<3.0.0", 32 | "jupyter_ydoc>=2.1.2,<4.0.0,!=3.0.0,!=3.0.1", 33 | "pycrdt", 34 | "pycrdt-websocket>=0.15.0,<0.16.0", 35 | "jupyter_events>=0.11.0", 36 | "jupyter_server_fileid>=0.7.0,<1", 37 | "jsonschema>=4.18.0" 38 | ] 39 | dynamic = ["version"] 40 | 41 | [project.optional-dependencies] 42 | test = [ 43 | "coverage", 44 | "dirty-equals", 45 | "jupyter_server[test]>=2.15.0", 46 | "jupyter_server_fileid[test]", 47 | "pytest>=7.0", 48 | "pytest-cov", 49 | "anyio", 50 | "httpx-ws >=0.5.2", 51 | "importlib_metadata >=4.8.3; python_version<'3.10'", 52 | ] 53 | 54 | [tool.hatch.version] 55 | path = "jupyter_server_ydoc/_version.py" 56 | 57 | [tool.hatch.build.targets.sdist] 58 | exclude = ["/.github", "/binder", "node_modules"] 59 | 60 | [tool.hatch.build.targets.wheel.shared-data] 61 | "jupyter-config/jupyter_server_ydoc.json" = "etc/jupyter/jupyter_server_config.d/jupyter_collaboration.json" 62 | 63 | [tool.hatch.build.targets.sdist.force-include] 64 | "../../tests" = "tests" 65 | 66 | [tool.check-wheel-contents] 67 | ignore = ["W002"] 68 | -------------------------------------------------------------------------------- /projects/jupyter-server-ydoc/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # setup.py shim for use with applications that require it. 5 | __import__("setuptools").setup() 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | [build-system] 5 | build-backend = "hatchling.build" 6 | requires = ["hatchling>=1.4.0", "hatch-nodejs-version", "jupyterlab>=4.0.0"] 7 | 8 | [project] 9 | name = "jupyter-collaboration-monorepo" 10 | requires-python = ">=3.8" 11 | dependencies = [ 12 | # jupyter-server extensions 13 | "jupyter-server-ydoc @ {root:uri}/projects/jupyter-server-ydoc", 14 | # jupyterlab/notebook frontend extensions 15 | "jupyter-collaboration-ui @ {root:uri}/projects/jupyter-collaboration-ui", 16 | "jupyter-docprovider @ {root:uri}/projects/jupyter-docprovider", 17 | # the metapackage 18 | "jupyter-collaboration @ {root:uri}/projects/jupyter-collaboration", 19 | ] 20 | dynamic = ["version"] 21 | 22 | [project.optional-dependencies] 23 | dev = [ 24 | "click", 25 | "pre-commit", 26 | "jupyter_releaser", 27 | "tomlkit" 28 | ] 29 | test = [ 30 | "jupyter-server-ydoc[test] @ {root:uri}/projects/jupyter-server-ydoc", 31 | ] 32 | docs = [ 33 | "jupyterlab>=4.4.0", 34 | "sphinx", 35 | "myst-parser", 36 | "pydata-sphinx-theme", 37 | "sphinxcontrib-mermaid" 38 | ] 39 | 40 | [tool.ruff] 41 | line-length = 100 42 | exclude = ["binder"] 43 | 44 | [tool.hatch.version] 45 | source = "nodejs" 46 | 47 | [tool.hatch.build] 48 | packages = [ 49 | "projects/jupyter-server-ydoc", 50 | "projects/jupyter-collaboration-ui", 51 | "projects/jupyter-docprovider", 52 | "projects/jupyter-collaboration" 53 | ] 54 | 55 | [tool.hatch.metadata] 56 | allow-direct-references = true 57 | 58 | [tool.jupyter-releaser] 59 | skip = ["check-links", "check-python"] 60 | 61 | [tool.jupyter-releaser.options] 62 | # `--skip-if-dirty` is a workaround for https://github.com/jupyter-server/jupyter_releaser/issues/567 63 | version-cmd = "cd ../.. && python scripts/bump_version.py --force --skip-if-dirty" 64 | python_packages = [ 65 | "projects/jupyter-server-ydoc:jupyter-server-ydoc", 66 | "projects/jupyter-collaboration-ui:jupyter-collaboration-ui", 67 | "projects/jupyter-docprovider:jupyter-docprovider", 68 | "projects/jupyter-collaboration:jupyter-collaboration" 69 | ] 70 | 71 | [tool.jupyter-releaser.hooks] 72 | before-build-npm = [ 73 | "YARN_ENABLE_IMMUTABLE_INSTALLS=0 jlpm build:prod" 74 | ] 75 | before-build-python = [ 76 | "jlpm clean:all", 77 | # Build the assets 78 | "jlpm build:prod", 79 | # Clean the build artifacts to not include them in sdist 80 | "jlpm clean:lib" 81 | ] 82 | before-bump-version = [ 83 | "python -m pip install -U jupyterlab tomlkit", 84 | "jlpm" 85 | ] 86 | 87 | [tool.pytest.ini_options] 88 | addopts = "-raXs --durations 10 --color=yes --doctest-modules" 89 | testpaths = ["tests/"] 90 | timeout = 300 91 | # Restore this setting to debug failures 92 | # timeout_method = "thread" 93 | filterwarnings = [ 94 | "error", 95 | # From tornado 96 | "ignore:unclosed None: 10 | subprocess.run(cmd.split(" "), check=True, cwd=cwd) 11 | 12 | 13 | def install_dev() -> None: 14 | install_build_deps = "python -m pip install jupyterlab>=4.4.0,<5" 15 | install_js_deps = "jlpm install" 16 | 17 | python_package_prefix = "projects" 18 | python_packages = ["jupyter-collaboration-ui", "jupyter-docprovider", "jupyter-server-ydoc"] 19 | 20 | execute(install_build_deps) 21 | execute(install_js_deps) 22 | 23 | for py_package in python_packages: 24 | real_package_name = py_package.replace("-", "_") 25 | execute(f"pip uninstall {real_package_name} -y") 26 | execute(f"pip install -e {python_package_prefix}/{py_package}[test]") 27 | 28 | # List of server extensions 29 | if py_package in ["jupyter-server-ydoc"]: 30 | execute(f"jupyter server extension enable {real_package_name}") 31 | 32 | # List of jupyterlab extensions 33 | if py_package in ["jupyter-collaboration-ui", "jupyter-docprovider"]: 34 | execute( 35 | f"jupyter labextension develop --overwrite {python_package_prefix}/{py_package} --overwrite" 36 | ) 37 | 38 | 39 | if __name__ == "__main__": 40 | install_dev() 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | # setup.py shim for use with applications that require it. 5 | __import__("setuptools").setup() 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | pytest_plugins = [ 5 | "jupyter_server.pytest_plugin", 6 | "jupyter_server_fileid.pytest_plugin", 7 | "jupyter_server_ydoc.pytest_plugin", 8 | ] 9 | -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from __future__ import annotations 5 | 6 | import nbformat 7 | import pytest 8 | from jupyter_server_ydoc.pytest_plugin import rtc_create_SQLite_store_factory 9 | from jupyter_server_ydoc.stores import SQLiteYStore, TempFileYStore 10 | 11 | 12 | def test_default_settings(jp_serverapp): 13 | settings = jp_serverapp.web_app.settings["jupyter_server_ydoc_config"] 14 | 15 | assert settings["disable_rtc"] is False 16 | assert settings["file_poll_interval"] == 1 17 | assert settings["document_cleanup_delay"] == 60 18 | assert settings["document_save_delay"] == 1 19 | assert settings["ystore_class"] == SQLiteYStore 20 | 21 | 22 | def test_settings_should_disable_rtc(jp_configurable_serverapp): 23 | argv = ["--YDocExtension.disable_rtc=True"] 24 | 25 | app = jp_configurable_serverapp(argv=argv) 26 | settings = app.web_app.settings["jupyter_server_ydoc_config"] 27 | 28 | assert settings["disable_rtc"] is True 29 | 30 | 31 | def test_settings_should_change_file_poll(jp_configurable_serverapp): 32 | argv = ["--YDocExtension.file_poll_interval=2"] 33 | 34 | app = jp_configurable_serverapp(argv=argv) 35 | settings = app.web_app.settings["jupyter_server_ydoc_config"] 36 | 37 | assert settings["file_poll_interval"] == 2 38 | 39 | 40 | def test_settings_should_change_document_cleanup(jp_configurable_serverapp): 41 | argv = ["--YDocExtension.document_cleanup_delay=10"] 42 | 43 | app = jp_configurable_serverapp(argv=argv) 44 | settings = app.web_app.settings["jupyter_server_ydoc_config"] 45 | 46 | assert settings["document_cleanup_delay"] == 10 47 | 48 | 49 | def test_settings_should_change_save_delay(jp_configurable_serverapp): 50 | argv = ["--YDocExtension.document_save_delay=10"] 51 | 52 | app = jp_configurable_serverapp(argv=argv) 53 | settings = app.web_app.settings["jupyter_server_ydoc_config"] 54 | 55 | assert settings["document_save_delay"] == 10 56 | 57 | 58 | def test_settings_should_change_ystore_class(jp_configurable_serverapp): 59 | argv = ["--YDocExtension.ystore_class=jupyter_server_ydoc.stores.TempFileYStore"] 60 | 61 | app = jp_configurable_serverapp(argv=argv) 62 | settings = app.web_app.settings["jupyter_server_ydoc_config"] 63 | 64 | assert settings["ystore_class"] == TempFileYStore 65 | 66 | 67 | async def test_document_ttl_from_settings(rtc_create_mock_document_room, jp_configurable_serverapp): 68 | argv = ["--SQLiteYStore.document_ttl=3600"] 69 | 70 | app = jp_configurable_serverapp(argv=argv) 71 | 72 | id = "test-id" 73 | content = "test_ttl" 74 | rtc_create_SQLite_store = rtc_create_SQLite_store_factory(app) 75 | store = await rtc_create_SQLite_store("file", id, content) 76 | 77 | assert store.document_ttl == 3600 78 | 79 | 80 | @pytest.mark.parametrize("copy", [True, False]) 81 | async def test_get_document_file(rtc_create_file, jp_serverapp, copy): 82 | path, content = await rtc_create_file("test.txt", "test", store=True) 83 | collaboration = jp_serverapp.web_app.settings["jupyter_server_ydoc"] 84 | document = await collaboration.get_document( 85 | path=path, content_type="file", file_format="text", copy=copy 86 | ) 87 | assert document.get() == content == "test" 88 | await collaboration.stop_extension() 89 | 90 | 91 | @pytest.mark.parametrize("copy", [True, False]) 92 | async def test_get_document_notebook(rtc_create_notebook, jp_serverapp, copy): 93 | nb = nbformat.v4.new_notebook( 94 | cells=[nbformat.v4.new_code_cell(source="1+1", execution_count=99)] 95 | ) 96 | nb_content = nbformat.writes(nb, version=4) 97 | path, _ = await rtc_create_notebook("test.ipynb", nb_content, store=True) 98 | collaboration = jp_serverapp.web_app.settings["jupyter_server_ydoc"] 99 | document = await collaboration.get_document( 100 | path=path, content_type="notebook", file_format="json", copy=copy 101 | ) 102 | doc = document.get() 103 | assert len(doc["cells"]) == 1 104 | cell = doc["cells"][0] 105 | assert cell["source"] == "1+1" 106 | assert cell["execution_count"] == 99 107 | await collaboration.stop_extension() 108 | 109 | 110 | async def test_get_document_file_copy_is_independent( 111 | rtc_create_file, jp_serverapp, rtc_fetch_session 112 | ): 113 | path, content = await rtc_create_file("test.txt", "test", store=True) 114 | collaboration = jp_serverapp.web_app.settings["jupyter_server_ydoc"] 115 | document = await collaboration.get_document( 116 | path=path, content_type="file", file_format="text", copy=True 117 | ) 118 | document.set("other") 119 | fresh_copy = await collaboration.get_document( 120 | path=path, content_type="file", file_format="text" 121 | ) 122 | assert fresh_copy.get() == "test" 123 | await collaboration.stop_extension() 124 | -------------------------------------------------------------------------------- /tests/test_documents.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import sys 5 | from time import time 6 | 7 | if sys.version_info < (3, 10): 8 | from importlib_metadata import entry_points 9 | else: 10 | from importlib.metadata import entry_points 11 | 12 | import pytest 13 | from anyio import create_task_group, sleep 14 | from jupyter_server_ydoc.test_utils import Websocket 15 | from pycrdt_websocket import WebsocketProvider 16 | 17 | jupyter_ydocs = {ep.name: ep.load() for ep in entry_points(group="jupyter_ydoc")} 18 | 19 | 20 | @pytest.fixture 21 | def rtc_document_save_delay(): 22 | return 0.5 23 | 24 | 25 | async def test_dirty( 26 | rtc_create_file, 27 | rtc_connect_doc_client, 28 | rtc_document_save_delay, 29 | ): 30 | file_format = "text" 31 | file_type = "file" 32 | file_path = "dummy.txt" 33 | await rtc_create_file(file_path) 34 | jupyter_ydoc = jupyter_ydocs[file_type]() 35 | 36 | websocket, room_name = await rtc_connect_doc_client(file_format, file_type, file_path) 37 | async with websocket as ws, WebsocketProvider(jupyter_ydoc.ydoc, Websocket(ws, room_name)): 38 | for _ in range(2): 39 | jupyter_ydoc.dirty = True 40 | await sleep(rtc_document_save_delay * 1.5) 41 | assert not jupyter_ydoc.dirty 42 | 43 | 44 | async def cleanup(jp_serverapp): 45 | # workaround for a shutdown issue of aiosqlite, see 46 | # https://github.com/jupyterlab/jupyter-collaboration/issues/252 47 | await jp_serverapp.web_app.settings["jupyter_server_ydoc"].stop_extension() 48 | # workaround `jupyter_server_fileid` manager accessing database on GC 49 | del jp_serverapp.web_app.settings["file_id_manager"] 50 | 51 | 52 | async def test_room_concurrent_initialization( 53 | jp_serverapp, 54 | rtc_create_file, 55 | rtc_connect_doc_client, 56 | ): 57 | file_format = "text" 58 | file_type = "file" 59 | file_path = "dummy.txt" 60 | await rtc_create_file(file_path) 61 | 62 | async def connect(file_format, file_type, file_path): 63 | websocket, room_name = await rtc_connect_doc_client(file_format, file_type, file_path) 64 | async with websocket: 65 | pass 66 | 67 | t0 = time() 68 | async with create_task_group() as tg: 69 | tg.start_soon(connect, file_format, file_type, file_path) 70 | tg.start_soon(connect, file_format, file_type, file_path) 71 | t1 = time() 72 | assert t1 - t0 < 0.5 73 | 74 | await cleanup(jp_serverapp) 75 | 76 | 77 | async def test_room_sequential_opening( 78 | jp_serverapp, 79 | rtc_create_file, 80 | rtc_connect_doc_client, 81 | ): 82 | file_format = "text" 83 | file_type = "file" 84 | file_path = "dummy.txt" 85 | await rtc_create_file(file_path) 86 | 87 | async def connect(file_format, file_type, file_path): 88 | t0 = time() 89 | websocket, room_name = await rtc_connect_doc_client(file_format, file_type, file_path) 90 | async with websocket: 91 | pass 92 | t1 = time() 93 | return t1 - t0 94 | 95 | dt = await connect(file_format, file_type, file_path) 96 | assert dt < 1 97 | dt = await connect(file_format, file_type, file_path) 98 | assert dt < 1 99 | 100 | await cleanup(jp_serverapp) 101 | -------------------------------------------------------------------------------- /tests/test_loaders.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from __future__ import annotations 5 | 6 | import asyncio 7 | from datetime import datetime, timedelta, timezone 8 | 9 | from jupyter_server_ydoc.loaders import FileLoader, FileLoaderMapping 10 | from jupyter_server_ydoc.test_utils import FakeContentsManager, FakeFileIDManager 11 | 12 | 13 | async def test_FileLoader_with_watcher(): 14 | id = "file-4567" 15 | path = "myfile.txt" 16 | paths = {} 17 | paths[id] = path 18 | 19 | cm = FakeContentsManager({"last_modified": datetime.now(timezone.utc)}) 20 | loader = FileLoader( 21 | id, 22 | FakeFileIDManager(paths), 23 | cm, 24 | poll_interval=0.1, 25 | ) 26 | await loader.load_content("text", "file") 27 | 28 | triggered = False 29 | 30 | async def trigger(): 31 | nonlocal triggered 32 | triggered = True 33 | 34 | loader.observe("test", trigger) 35 | 36 | cm.model["last_modified"] = datetime.now(timezone.utc) + timedelta(seconds=1) 37 | 38 | await asyncio.sleep(0.15) 39 | 40 | try: 41 | assert triggered 42 | finally: 43 | await loader.clean() 44 | 45 | 46 | async def test_FileLoader_without_watcher(): 47 | id = "file-4567" 48 | path = "myfile.txt" 49 | paths = {} 50 | paths[id] = path 51 | 52 | cm = FakeContentsManager({"last_modified": datetime.now(timezone.utc)}) 53 | loader = FileLoader( 54 | id, 55 | FakeFileIDManager(paths), 56 | cm, 57 | ) 58 | await loader.load_content("text", "file") 59 | 60 | triggered = False 61 | 62 | async def trigger(): 63 | nonlocal triggered 64 | triggered = True 65 | 66 | loader.observe("test", trigger) 67 | 68 | cm.model["last_modified"] = datetime.now(timezone.utc) + timedelta(seconds=1) 69 | 70 | await loader.maybe_notify() 71 | 72 | try: 73 | assert triggered 74 | finally: 75 | await loader.clean() 76 | 77 | 78 | async def test_FileLoaderMapping_with_watcher(): 79 | id = "file-4567" 80 | path = "myfile.txt" 81 | paths = {} 82 | paths[id] = path 83 | 84 | cm = FakeContentsManager({"last_modified": datetime.now(timezone.utc)}) 85 | 86 | map = FileLoaderMapping( 87 | {"contents_manager": cm, "file_id_manager": FakeFileIDManager(paths)}, 88 | file_poll_interval=1.0, 89 | ) 90 | 91 | loader = map[id] 92 | await loader.load_content("text", "file") 93 | 94 | triggered = False 95 | 96 | async def trigger(): 97 | nonlocal triggered 98 | triggered = True 99 | 100 | loader.observe("test", trigger) 101 | 102 | # Clear map (and its loader) before updating => triggered should be False 103 | await map.clear() 104 | cm.model["last_modified"] = datetime.now(timezone.utc) 105 | 106 | await asyncio.sleep(0.15) 107 | 108 | assert not triggered 109 | -------------------------------------------------------------------------------- /tests/test_rooms.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | from __future__ import annotations 5 | 6 | import asyncio 7 | 8 | from jupyter_ydoc import YUnicode 9 | 10 | 11 | async def test_should_initialize_document_room_without_store(rtc_create_mock_document_room): 12 | content = "test" 13 | _, _, room = rtc_create_mock_document_room("test-id", "test.txt", content) 14 | 15 | await room.initialize() 16 | assert room._document.source == content 17 | 18 | 19 | async def test_should_initialize_document_room_from_store( 20 | rtc_create_SQLite_store, rtc_create_mock_document_room 21 | ): 22 | # TODO: We don't know for sure if it is taking the content from the store. 23 | # If the content from the store is different than the content from disk, 24 | # the room will initialize with the content from disk and overwrite the document 25 | 26 | id = "test-id" 27 | content = "test" 28 | store = await rtc_create_SQLite_store("file", id, content) 29 | _, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, store=store) 30 | 31 | await room.initialize() 32 | assert room._document.source == content 33 | 34 | 35 | async def test_should_overwrite_the_store(rtc_create_SQLite_store, rtc_create_mock_document_room): 36 | id = "test-id" 37 | content = "test" 38 | store = await rtc_create_SQLite_store("file", id, "whatever") 39 | _, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, store=store) 40 | 41 | await room.initialize() 42 | assert room._document.source == content 43 | 44 | doc = YUnicode() 45 | await store.apply_updates(doc.ydoc) 46 | 47 | assert doc.source == content 48 | 49 | 50 | async def test_defined_save_delay_should_save_content_after_document_change( 51 | rtc_create_mock_document_room, 52 | ): 53 | content = "test" 54 | cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=0.01) 55 | 56 | await room.initialize() 57 | room._document.source = "Test 2" 58 | 59 | # Wait for a bit more than the poll_interval 60 | await asyncio.sleep(0.15) 61 | 62 | assert "save" in cm.actions 63 | 64 | 65 | async def test_undefined_save_delay_should_not_save_content_after_document_change( 66 | rtc_create_mock_document_room, 67 | ): 68 | content = "test" 69 | cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=None) 70 | 71 | await room.initialize() 72 | room._document.source = "Test 2" 73 | 74 | # Wait for a bit more than the poll_interval 75 | await asyncio.sleep(0.15) 76 | 77 | assert "save" not in cm.actions 78 | 79 | 80 | async def test_should_not_save_content_when_all_clients_have_autosave_disabled( 81 | rtc_create_mock_document_room, 82 | ): 83 | content = "test" 84 | cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=0.01) 85 | 86 | # Disable autosave for all existing clients 87 | for state in room.awareness._states.values(): 88 | if state is not None: 89 | state["autosave"] = False 90 | 91 | # Inject a dummy client with autosave disabled 92 | room.awareness._states[9999] = {"autosave": False} 93 | 94 | await room.initialize() 95 | room._document.source = "Test 2" 96 | 97 | await asyncio.sleep(0.15) 98 | 99 | assert "save" not in cm.actions 100 | 101 | 102 | async def test_should_save_content_when_at_least_one_client_has_autosave_enabled( 103 | rtc_create_mock_document_room, 104 | ): 105 | content = "test" 106 | cm, _, room = rtc_create_mock_document_room("test-id", "test.txt", content, save_delay=0.01) 107 | 108 | # Disable autosave for all existing clients 109 | for state in room.awareness._states.values(): 110 | if state is not None: 111 | state["autosave"] = False 112 | 113 | # Inject a dummy client with autosave enabled 114 | room.awareness._states[10000] = {"autosave": True} 115 | 116 | await room.initialize() 117 | room._document.source = "Test 2" 118 | 119 | await asyncio.sleep(0.15) 120 | 121 | assert "save" in cm.actions 122 | 123 | 124 | # The following test should be restored when package versions are fixed. 125 | 126 | # async def test_document_path(rtc_create_mock_document_room): 127 | # id = "test-id" 128 | # path = "test.txt" 129 | # new_path = "test2.txt" 130 | 131 | # _, loader, room = rtc_create_mock_document_room(id, path, "") 132 | 133 | # await room.initialize() 134 | # assert room._document.path == path 135 | 136 | # # Update the path 137 | # loader._file_id_manager.move(id, new_path) 138 | 139 | # # Wait for a bit more than the poll_interval 140 | # await asyncio.sleep(0.15) 141 | 142 | # assert room._document.path == new_path 143 | -------------------------------------------------------------------------------- /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 | "strict": true, 17 | "strictNullChecks": true, 18 | "target": "es2018", 19 | "types": [], 20 | "lib": ["DOM", "ES2018", "ES2020.Intl"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "noImplicitAny": true, 5 | "noEmitOnError": true, 6 | "noUnusedLocals": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "target": "ES2018", 10 | "outDir": "lib", 11 | "lib": ["DOM", "DOM.iterable"], 12 | "types": ["jest", "node"], 13 | "jsx": "react", 14 | "resolveJsonModule": true, 15 | "esModuleInterop": true, 16 | "strictNullChecks": true, 17 | "skipLibCheck": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPointStrategy": "packages", 3 | "entryPoints": [ 4 | "packages/collaboration", 5 | "packages/collaborative-drive", 6 | "packages/docprovider" 7 | ], 8 | "externalSymbolLinkMappings": { 9 | "@codemirror/state": { 10 | "Extension": "https://codemirror.net/docs/ref/#state.Extension" 11 | }, 12 | "@jupyter/ydoc": { 13 | "DocumentChange": "https://jupyter-ydoc.readthedocs.io/en/latest/api/types/DocumentChange.html", 14 | "YDocument": "https://jupyter-ydoc.readthedocs.io/en/latest/api/classes/YDocument-1.html" 15 | }, 16 | "@jupyterlab/services": { 17 | "Contents.IFetchOptions": "https://jupyterlab.readthedocs.io/en/latest/api/interfaces/services.Contents.IFetchOptions.html", 18 | "Contents.IModel": "https://jupyterlab.readthedocs.io/en/latest/api/interfaces/services.Contents.IModel.html", 19 | "Contents.ISharedFactory": "https://jupyterlab.readthedocs.io/en/latest/api/interfaces/services.Contents.ISharedFactory.html", 20 | "Contents.ISharedFactoryOptions": "https://jupyterlab.readthedocs.io/en/latest/api/interfaces/services.Contents.ISharedFactoryOptions.html", 21 | "Drive": "https://jupyterlab.readthedocs.io/en/latest/api/classes/services.Drive-1.html", 22 | "User.IManager": "https://jupyterlab.readthedocs.io/en/latest/api/interfaces/services.User.IManager.html", 23 | "User.IIdentity": "https://jupyterlab.readthedocs.io/en/latest/api/interfaces/services.User.IIdentity.html" 24 | }, 25 | "@jupyterlab/translation": { 26 | "TranslationBundle": "https://jupyterlab.readthedocs.io/en/latest/api/types/translation.TranslationBundle.html" 27 | }, 28 | "@jupyterlab/ui-components": { 29 | "ReactWidget": "https://jupyterlab.readthedocs.io/en/latest/api/modules/ui_components.html#ReactWidget" 30 | }, 31 | "@lumino/coreutils": { 32 | "Token": "https://lumino.readthedocs.io/en/latest/api/classes/coreutils.Token.html" 33 | }, 34 | "@lumino/disposable": { 35 | "IDisposable": "https://lumino.readthedocs.io/en/latest/api/interfaces/disposable.IDisposable.html" 36 | }, 37 | "@lumino/virtualdom": { 38 | "VirtualElement": "https://lumino.readthedocs.io/en/stable/api/modules/virtualdom.VirtualElement.html" 39 | }, 40 | "@lumino/widgets": { 41 | "Menu": "https://lumino.readthedocs.io/en/stable/api/classes/widgets.Menu-1.html", 42 | "Menu.IItem": "https://lumino.readthedocs.io/en/stable/api/interfaces/widgets.Menu.IItem.html", 43 | "Menu.IItemOptions": "https://lumino.readthedocs.io/en/stable/api/interfaces/widgets.Menu.IItemOptions.html", 44 | "MenuBar.Renderer": "https://lumino.readthedocs.io/en/stable/api/classes/widgets.MenuBar.Renderer.html", 45 | "MenuBar.IRenderData": "https://lumino.readthedocs.io/en/stable/api/interfaces/widgets.MenuBar.IRenderData.html", 46 | "Panel": "https://lumino.readthedocs.io/en/stable/api/classes/widgets.Panel-1.html" 47 | }, 48 | "y-protocols": { 49 | "Awareness": "https://docs.yjs.dev/api/about-awareness" 50 | }, 51 | "yjs": { 52 | "Text": "https://docs.yjs.dev/api/shared-types/y.text" 53 | } 54 | }, 55 | "githubPages": false, 56 | "navigationLinks": { 57 | "GitHub": "https://github.com/jupyterlab/jupyter_collaboration", 58 | "Jupyter": "https://jupyter.org" 59 | }, 60 | "titleLink": "https://jupyterlab-realtime-collaboration.readthedocs.io/en/latest", 61 | "out": "docs/source/ts/api" 62 | } 63 | -------------------------------------------------------------------------------- /ui-tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Testing 2 | 3 | This folder contains the integration tests of *jupyter_collaboration*. 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 | ## Run the tests 16 | 17 | > All commands are assumed to be executed from the root directory 18 | 19 | To run the tests, you need to: 20 | 21 | 1. Compile the project: 22 | 23 | ```sh 24 | jlpm install 25 | jlpm build:prod 26 | ``` 27 | 28 | 2. Install test dependencies (needed only once): 29 | 30 | ```sh 31 | cd ./ui-tests 32 | jlpm install 33 | jlpm playwright install 34 | cd .. 35 | ``` 36 | 37 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: 38 | 39 | ```sh 40 | cd ./ui-tests 41 | jlpm playwright test 42 | ``` 43 | 44 | Test results will be shown in the terminal. In case of any test failures, the test report 45 | will be opened in your browser at the end of the tests execution; see 46 | [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) 47 | for configuring that behavior. 48 | 49 | ## Update the tests snapshots 50 | 51 | > All commands are assumed to be executed from the root directory 52 | 53 | If you are comparing snapshots to validate your tests, you may need to update 54 | the reference snapshots stored in the repository. To do that, you need to: 55 | 56 | 1. Compile the project: 57 | 58 | ```sh 59 | jlpm install 60 | jlpm build:prod 61 | ``` 62 | 63 | 2. Install test dependencies (needed only once): 64 | 65 | ```sh 66 | cd ./ui-tests 67 | jlpm install 68 | jlpm playwright install 69 | cd .. 70 | ``` 71 | 72 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) command: 73 | 74 | ```sh 75 | cd ./ui-tests 76 | jlpm playwright test -u 77 | ``` 78 | 79 | ## Create tests 80 | 81 | > All commands are assumed to be executed from the root directory 82 | 83 | To create tests, the easiest way is to use the code generator tool of playwright: 84 | 85 | 1. Compile the extension: 86 | 87 | ```sh 88 | jlpm install 89 | jlpm build:prod 90 | ``` 91 | 92 | > Check the extension is installed in JupyterLab. 93 | 94 | 2. Install test dependencies (needed only once): 95 | 96 | ```sh 97 | cd ./ui-tests 98 | jlpm install 99 | jlpm playwright install 100 | cd .. 101 | ``` 102 | 103 | 3. Execute the [Playwright code generator](https://playwright.dev/docs/codegen): 104 | 105 | ```sh 106 | cd ./ui-tests 107 | jlpm playwright codegen localhost:8888 108 | ``` 109 | 110 | ## Debug tests 111 | 112 | > All commands are assumed to be executed from the root directory 113 | 114 | To debug tests, a good way is to use the inspector tool of playwright: 115 | 116 | 1. Compile the extension: 117 | 118 | ```sh 119 | jlpm install 120 | jlpm build:prod 121 | ``` 122 | 123 | > Check the extension is installed in JupyterLab. 124 | 125 | 2. Install test dependencies (needed only once): 126 | 127 | ```sh 128 | cd ./ui-tests 129 | jlpm install 130 | jlpm playwright install 131 | cd .. 132 | ``` 133 | 134 | 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): 135 | 136 | ```sh 137 | cd ./ui-tests 138 | PWDEBUG=1 jlpm playwright test 139 | ``` 140 | -------------------------------------------------------------------------------- /ui-tests/jupyter_server_test_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | """Server configuration for integration tests. 5 | 6 | !! Never use this configuration in production because it 7 | opens the server to the world and provide access to JupyterLab 8 | JavaScript objects through the global window variable. 9 | """ 10 | 11 | from typing import Any 12 | 13 | from jupyterlab.galata import configure_jupyter_server 14 | 15 | c: Any 16 | configure_jupyter_server(c) # noqa 17 | 18 | # Uncomment to set server log level to debug level 19 | # c.ServerApp.log_level = "DEBUG" 20 | -------------------------------------------------------------------------------- /ui-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyter-collaboration-ui-tests", 3 | "version": "1.0.0", 4 | "description": "Jupyter collaboration Integration Tests", 5 | "private": true, 6 | "scripts": { 7 | "start": "jupyter lab --config jupyter_server_test_config.py", 8 | "start:timeline": "jupyter lab --config jupyter_server_test_config.py --ServerApp.base_url=/api/collaboration/timeline/", 9 | "test": "npx playwright test --config=playwright.config.js", 10 | "test:timeline": "npx playwright test --config=playwright.timeline.config.js", 11 | "test:update": "npx playwright test --update-snapshots" 12 | }, 13 | "devDependencies": { 14 | "@jupyterlab/galata": "^5.3.0", 15 | "@jupyterlab/services": "^7.1.5", 16 | "@playwright/test": "^1.35.0" 17 | }, 18 | "resolutions": { 19 | "@playwright/test": "1.35.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui-tests/playwright.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /** 7 | * Configuration for Playwright using default from @jupyterlab/galata 8 | */ 9 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); 10 | 11 | module.exports = { 12 | ...baseConfig, 13 | // Force one worker as global awareness will contaminate parallel tests. 14 | workers: 1, 15 | webServer: { 16 | command: 'jlpm start', 17 | url: 'http://localhost:8888/lab', 18 | timeout: 120 * 1000, 19 | reuseExistingServer: !process.env.CI 20 | }, 21 | expect: { 22 | toMatchSnapshot: { 23 | maxDiffPixelRatio: 0.01 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /ui-tests/playwright.timeline.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | /** 7 | * Configuration for Playwright using default from @jupyterlab/galata 8 | */ 9 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); 10 | 11 | module.exports = { 12 | ...baseConfig, 13 | workers: 1, 14 | webServer: { 15 | command: 'jlpm start:timeline', 16 | url: 'http://localhost:8888/api/collaboration/timeline/lab', 17 | timeout: 120 * 1000, 18 | reuseExistingServer: !process.env.CI 19 | }, 20 | expect: { 21 | toMatchSnapshot: { 22 | maxDiffPixelRatio: 0.01 23 | } 24 | }, 25 | projects: [ 26 | { 27 | name: 'timeline-tests', 28 | testMatch: 'tests/**/timeline-*.spec.ts', 29 | testIgnore: '**/.ipynb_checkpoints/**', 30 | timeout: 120 * 1000 31 | } 32 | ] 33 | }; 34 | -------------------------------------------------------------------------------- /ui-tests/tests/collaborationpanel.spec.ts-snapshots/collaboration-icon-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/collaborationpanel.spec.ts-snapshots/collaboration-icon-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/collaborationpanel.spec.ts-snapshots/collaborationPanelCollapsed-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/collaborationpanel.spec.ts-snapshots/collaborationPanelCollapsed-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/collaborationpanel.spec.ts-snapshots/one-client-with-two-documents-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/collaborationpanel.spec.ts-snapshots/one-client-with-two-documents-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/collaborationpanel.spec.ts-snapshots/three-client-with-document-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/collaborationpanel.spec.ts-snapshots/three-client-with-document-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/collaborationpanel.spec.ts-snapshots/three-client-without-document-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/collaborationpanel.spec.ts-snapshots/three-client-without-document-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/hub-share.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { expect, test } from '@jupyterlab/galata'; 7 | 8 | test.use( { 9 | permissions: ['clipboard-read'] 10 | }); 11 | 12 | test('should open JupyterHub sharing dialog', async ({ page }) => { 13 | // Mock PageConfig 14 | page.evaluate(() => { 15 | document.body.dataset['hubUser'] = 'jovyan'; 16 | document.body.dataset['hubServerUser'] = 'jovyan'; 17 | document.body.dataset['hubServerName'] = 'my-server'; 18 | document.body.dataset['hubHost'] = 'localhost'; 19 | document.body.dataset['hubPrefix'] = '/hub/'; 20 | }); 21 | 22 | // Mock JupyterHub requests 23 | await page.route('/hub/api', (route) => { 24 | return route.fulfill({ 25 | status: 200, 26 | body: JSON.stringify({version: '5.0.0'}), 27 | contentType: 'application/json' 28 | }); 29 | }); 30 | await page.route('/hub/api/user', (route) => { 31 | return route.fulfill({ 32 | status: 200, 33 | body: JSON.stringify({ 34 | scopes: ['read:users:name', 'shares!user', 'list:users', 'list:groups'] 35 | }), 36 | contentType: 'application/json' 37 | }); 38 | }); 39 | await page.route(/\/hub\/api\/users.*/, (route) => { 40 | return route.fulfill({ 41 | status: 200, 42 | body: JSON.stringify([ 43 | { 44 | name: 'test-user-1', 45 | kind: 'user' 46 | }, 47 | { 48 | name: 'test-user-2', 49 | kind: 'user' 50 | } 51 | ]), 52 | contentType: 'application/json' 53 | }); 54 | }); 55 | await page.route('/hub/api/groups', (route) => { 56 | return route.fulfill({ 57 | status: 200, 58 | body: JSON.stringify([ 59 | { 60 | name: 'test-group-1', 61 | kind: 'group' 62 | }, 63 | { 64 | name: 'test-group-2', 65 | kind: 'group' 66 | } 67 | ]), 68 | contentType: 'application/json' 69 | }); 70 | }); 71 | await page.route('/hub/api/shares/jovyan/my-server', (route) => { 72 | return route.fulfill({ 73 | status: 200, 74 | body: JSON.stringify({ 75 | items: [ 76 | { 77 | created_at: '2025-05-01', 78 | user: { 79 | name: "test-user-1" 80 | } 81 | } 82 | ]}), 83 | contentType: 'application/json' 84 | }); 85 | }); 86 | 87 | const sharedLinkButton = page.locator('jp-button[data-command="collaboration:shared-link"]'); 88 | await sharedLinkButton.click(); 89 | const dialog = page.locator('.jp-Dialog').first(); 90 | await expect(dialog).toBeVisible(); 91 | 92 | // Wait for user results to load. 93 | await page.waitForSelector('.jp-ManageSharesBody-user-item'); 94 | 95 | expect(await dialog.locator('.jp-Dialog-content').screenshot()).toMatchSnapshot( 96 | 'shared-link-dialog-hub.png' 97 | ); 98 | 99 | // Copy the link 100 | await dialog.locator('.jp-mod-accept').click(); 101 | await expect(dialog).not.toBeVisible(); 102 | 103 | let clipboardText = await page.evaluate(() => navigator.clipboard.readText()); 104 | expect(clipboardText).toBe('http://localhost:8888/lab/tree/tests-hub-share-should-open-JupyterHub-sharing-dialog'); 105 | }); 106 | -------------------------------------------------------------------------------- /ui-tests/tests/hub-share.spec.ts-snapshots/shared-link-dialog-hub-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/hub-share.spec.ts-snapshots/shared-link-dialog-hub-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/notebook.spec.ts-snapshots/initialization-create-notebook-guest-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/notebook.spec.ts-snapshots/initialization-create-notebook-guest-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/notebook.spec.ts-snapshots/initialization-create-notebook-host-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/notebook.spec.ts-snapshots/initialization-create-notebook-host-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/notebook.spec.ts-snapshots/initialization-open-notebook-guest-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/notebook.spec.ts-snapshots/initialization-open-notebook-guest-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/notebook.spec.ts-snapshots/initialization-open-notebook-host-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/notebook.spec.ts-snapshots/initialization-open-notebook-host-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/notebook.spec.ts-snapshots/ten-clients-add-a-new-cell-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/notebook.spec.ts-snapshots/ten-clients-add-a-new-cell-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/timeline-slider.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { expect, test } from '@jupyterlab/galata'; 7 | import { Page } from '@playwright/test'; 8 | 9 | async function capturePageErrors(page: Page) { 10 | const pageErrors: string[] = []; 11 | page.on('pageerror', (error) => pageErrors.push(error.message)); 12 | return pageErrors; 13 | } 14 | 15 | async function openNotebook(page: Page, notebookPath: string) { 16 | await page.click('text=File'); 17 | await page.click('.lm-Menu-itemLabel:text("Open from Path…")'); 18 | await page.fill( 19 | 'input[placeholder="/path/relative/to/jlab/root"]', 20 | notebookPath 21 | ); 22 | await page.click('.jp-Dialog-buttonLabel:text("Open")'); 23 | await page.waitForSelector('.jp-Notebook', { state: 'visible' }); 24 | } 25 | 26 | 27 | const isTimelineEnv = process.env.TIMELINE_FEATURE || "0"; 28 | const isTimeline = parseInt(isTimelineEnv) 29 | 30 | test.describe('Timeline Slider', () => { 31 | 32 | if (isTimeline) { 33 | test.use({ autoGoto: false }); 34 | } 35 | test('should fail if there are console errors when opening from path', async ({ page, tmpPath }) => { 36 | if (isTimeline) { 37 | console.log('Skipping this test.'); 38 | return; 39 | } 40 | const pageErrors = await capturePageErrors(page); 41 | 42 | await page.notebook.createNew(); 43 | await page.notebook.close(); 44 | 45 | await openNotebook(page, `${tmpPath}/Untitled.ipynb`); 46 | 47 | expect(pageErrors).toHaveLength(0); 48 | }); 49 | 50 | test('should display in status bar without console errors when baseUrl is set', async ({ page, baseURL }) => { 51 | 52 | if (!isTimeline) { 53 | console.log('Skipping this test.'); 54 | return; 55 | } 56 | 57 | await page.goto('http://localhost:8888/api/collaboration/timeline') 58 | 59 | const pageErrors = await capturePageErrors(page); 60 | 61 | await page.notebook.createNew(); 62 | 63 | const historyIcon = page.locator('.jp-mod-highlighted[title="Document Timeline"]'); 64 | await expect(historyIcon).toBeVisible(); 65 | 66 | await historyIcon.click(); 67 | 68 | const slider = page.locator('.jp-timestampDisplay'); 69 | await expect(slider).toBeVisible(); 70 | 71 | expect(pageErrors).toHaveLength(0); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /ui-tests/tests/user-menu.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Jupyter Development Team. 3 | * Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | import { IJupyterLabPageFixture, expect, test } from '@jupyterlab/galata'; 7 | import type { Locator } from '@playwright/test'; 8 | 9 | test.use( { 10 | permissions: ['clipboard-read'] 11 | }); 12 | 13 | const openDialog = async (page: IJupyterLabPageFixture): Promise => { 14 | const sharedLinkButton = page.locator('jp-button[data-command="collaboration:shared-link"]'); 15 | await sharedLinkButton.click(); 16 | await expect(page.locator('.jp-Dialog')).toBeVisible(); 17 | return page.locator('.jp-Dialog').first(); 18 | }; 19 | 20 | test('the top bar should contain one user menu and one share button', async ({ page }) => { 21 | const shareButton = page.locator('#jp-top-bar jp-button[data-command="collaboration:shared-link"]'); 22 | await expect(shareButton).toHaveCount(1); 23 | const userMenu = page.locator('#jp-top-bar .jp-MenuBar-anonymousIcon'); 24 | await expect(userMenu).toHaveCount(1); 25 | }); 26 | 27 | test('should open dialog when clicking on the shared link button', async ({ page }) => { 28 | const sharedLinkButton = page.locator('jp-button[data-command="collaboration:shared-link"]'); 29 | 30 | expect(await sharedLinkButton.screenshot()).toMatchSnapshot( 31 | 'shared-link-icon.png' 32 | ); 33 | 34 | await sharedLinkButton.click(); 35 | await expect(page.locator('.jp-Dialog')).toBeVisible(); 36 | 37 | expect(await page.locator('.jp-Dialog > div ').screenshot()).toMatchSnapshot( 38 | 'shared-link-dialog.png' 39 | ); 40 | }); 41 | 42 | test('should close the shared link dialog on cancel', async ({ page }) => { 43 | const dialog = await openDialog(page); 44 | 45 | await dialog.locator('.jp-mod-reject').click(); 46 | await expect(dialog).not.toBeVisible(); 47 | }); 48 | 49 | test('should copy the shared link in clipboard', async ({ page }) => { 50 | const dialog = await openDialog(page); 51 | 52 | // copy the link 53 | await dialog.locator('.jp-mod-accept').click(); 54 | await expect(dialog).not.toBeVisible(); 55 | 56 | let clipboardText1 = await page.evaluate(() => navigator.clipboard.readText()); 57 | expect(clipboardText1).toBe('http://localhost:8888/lab/tree/tests-user-menu-should-copy-the-shared-link-in-clipboard'); 58 | }); 59 | 60 | test('should copy the shared link with filepath', async ({ page }) => { 61 | 62 | await page.notebook.createNew(); 63 | const dialog = await openDialog(page); 64 | 65 | // copy the link 66 | await dialog.locator('.jp-mod-accept').click(); 67 | await expect(dialog).not.toBeVisible(); 68 | 69 | let clipboardText1 = await page.evaluate(() => navigator.clipboard.readText()); 70 | expect(clipboardText1).toBe('http://localhost:8888/lab/tree/tests-user-menu-should-copy-the-shared-link-with-filepath/Untitled.ipynb'); 71 | }); 72 | 73 | 74 | /* TODO: Add test using token in URL, probably using playwright projects */ 75 | 76 | // test('should copy the shared link in clipboard with token', async ({ page }) => { 77 | // const dialog = await openDialog(page); 78 | 79 | // // click the token checkbox 80 | // await dialog.locator('input[type="checkbox"]').click(); 81 | // expect(await page.locator('.jp-Dialog').screenshot()).toMatchSnapshot( 82 | // 'shared-link-dialog-token.png' 83 | // ); 84 | 85 | // //copy the link with token 86 | // await dialog.locator('.jp-mod-accept').click(); 87 | // await expect(dialog).not.toBeVisible(); 88 | 89 | // let clipboardText1 = await page.evaluate(() => navigator.clipboard.readText()); 90 | // expect(clipboardText1).toBe('http://localhost:8888/lab/tree/tests-user-menu-should-copy-the-shared-link-in-clipboard'); 91 | // }); 92 | -------------------------------------------------------------------------------- /ui-tests/tests/user-menu.spec.ts-snapshots/shared-link-dialog-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/user-menu.spec.ts-snapshots/shared-link-dialog-linux.png -------------------------------------------------------------------------------- /ui-tests/tests/user-menu.spec.ts-snapshots/shared-link-icon-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyter-collaboration/7a3986a5be99e258497cc4b51a7755824137b107/ui-tests/tests/user-menu.spec.ts-snapshots/shared-link-icon-linux.png --------------------------------------------------------------------------------