├── .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 | [](https://github.com/jupyterlab/jupyter_collaboration/actions?query=branch%3Amain++)
4 | [](https://mybinder.org/v2/gh/jupyterlab/jupyter_collaboration/main)
5 | [](https://pypi.org/project/jupyter-collaboration)
6 | [](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 | 
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 |
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 | 
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
--------------------------------------------------------------------------------