From 3e2dfa594784567d013bc29803b2e3fb8389af5a Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 31 Aug 2023 09:05:56 -0400 Subject: [PATCH 01/31] Initial provider-consumer sample, WIP. --- compat_4a/.copier-answers.yml | 15 ++ compat_4a/.gitignore | 125 +++++++++++ compat_4a/.prettierignore | 6 + compat_4a/.yarnrc.yml | 1 + compat_4a/README.md | 96 +++++++++ compat_4a/babel.config.js | 1 + compat_4a/install.json | 5 + compat_4a/jest.config.js | 28 +++ compat_4a/package.json | 201 +++++++++++++++++ compat_4a/pyproject.toml | 76 +++++++ compat_4a/setup.py | 1 + compat_4a/src/__tests__/step_counter.spec.ts | 9 + compat_4a/src/index.ts | 51 +++++ compat_4a/step_counter/__init__.py | 16 ++ compat_4a/style/base.css | 5 + compat_4a/style/index.css | 1 + compat_4a/style/index.js | 1 + compat_4a/tsconfig.json | 23 ++ compat_4a/tsconfig.test.json | 6 + compat_4a/ui-tests/README.md | 167 ++++++++++++++ .../ui-tests/jupyter_server_test_config.py | 12 ++ compat_4a/ui-tests/package.json | 15 ++ compat_4a/ui-tests/playwright.config.js | 14 ++ compat_4a/ui-tests/tests/step_counter.spec.ts | 21 ++ compat_4b/.copier-answers.yml | 15 ++ compat_4b/.gitignore | 125 +++++++++++ compat_4b/.prettierignore | 6 + compat_4b/.yarnrc.yml | 1 + compat_4b/README.md | 96 +++++++++ compat_4b/babel.config.js | 1 + compat_4b/install.json | 5 + compat_4b/jest.config.js | 28 +++ compat_4b/package.json | 203 ++++++++++++++++++ compat_4b/pyproject.toml | 76 +++++++ compat_4b/setup.py | 1 + .../__tests__/step_counter_extension.spec.ts | 9 + compat_4b/src/index.ts | 71 ++++++ compat_4b/step_counter_extension/__init__.py | 16 ++ compat_4b/style/base.css | 28 +++ compat_4b/style/index.css | 1 + compat_4b/style/index.js | 1 + compat_4b/tsconfig.json | 23 ++ compat_4b/tsconfig.test.json | 6 + compat_4b/ui-tests/README.md | 167 ++++++++++++++ .../ui-tests/jupyter_server_test_config.py | 12 ++ compat_4b/ui-tests/package.json | 15 ++ compat_4b/ui-tests/playwright.config.js | 14 ++ .../tests/step_counter_extension.spec.ts | 21 ++ 48 files changed, 1837 insertions(+) create mode 100644 compat_4a/.copier-answers.yml create mode 100644 compat_4a/.gitignore create mode 100644 compat_4a/.prettierignore create mode 100644 compat_4a/.yarnrc.yml create mode 100644 compat_4a/README.md create mode 100644 compat_4a/babel.config.js create mode 100644 compat_4a/install.json create mode 100644 compat_4a/jest.config.js create mode 100644 compat_4a/package.json create mode 100644 compat_4a/pyproject.toml create mode 100644 compat_4a/setup.py create mode 100644 compat_4a/src/__tests__/step_counter.spec.ts create mode 100644 compat_4a/src/index.ts create mode 100644 compat_4a/step_counter/__init__.py create mode 100644 compat_4a/style/base.css create mode 100644 compat_4a/style/index.css create mode 100644 compat_4a/style/index.js create mode 100644 compat_4a/tsconfig.json create mode 100644 compat_4a/tsconfig.test.json create mode 100644 compat_4a/ui-tests/README.md create mode 100644 compat_4a/ui-tests/jupyter_server_test_config.py create mode 100644 compat_4a/ui-tests/package.json create mode 100644 compat_4a/ui-tests/playwright.config.js create mode 100644 compat_4a/ui-tests/tests/step_counter.spec.ts create mode 100644 compat_4b/.copier-answers.yml create mode 100644 compat_4b/.gitignore create mode 100644 compat_4b/.prettierignore create mode 100644 compat_4b/.yarnrc.yml create mode 100644 compat_4b/README.md create mode 100644 compat_4b/babel.config.js create mode 100644 compat_4b/install.json create mode 100644 compat_4b/jest.config.js create mode 100644 compat_4b/package.json create mode 100644 compat_4b/pyproject.toml create mode 100644 compat_4b/setup.py create mode 100644 compat_4b/src/__tests__/step_counter_extension.spec.ts create mode 100644 compat_4b/src/index.ts create mode 100644 compat_4b/step_counter_extension/__init__.py create mode 100644 compat_4b/style/base.css create mode 100644 compat_4b/style/index.css create mode 100644 compat_4b/style/index.js create mode 100644 compat_4b/tsconfig.json create mode 100644 compat_4b/tsconfig.test.json create mode 100644 compat_4b/ui-tests/README.md create mode 100644 compat_4b/ui-tests/jupyter_server_test_config.py create mode 100644 compat_4b/ui-tests/package.json create mode 100644 compat_4b/ui-tests/playwright.config.js create mode 100644 compat_4b/ui-tests/tests/step_counter_extension.spec.ts diff --git a/compat_4a/.copier-answers.yml b/compat_4a/.copier-answers.yml new file mode 100644 index 00000000..f74a2b63 --- /dev/null +++ b/compat_4a/.copier-answers.yml @@ -0,0 +1,15 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_commit: v4.2.0 +_src_path: https://github.com/jupyterlab/extension-template +author_email: me@test.com +author_name: My Name +has_binder: false +has_settings: false +kind: frontend +labextension_name: step_counter +project_short_description: Adds a step counter/button, and a step increment provider + (1 of 3 related examples). This extension holds a provider token. +python_name: step_counter +repository: '' +test: true + diff --git a/compat_4a/.gitignore b/compat_4a/.gitignore new file mode 100644 index 00000000..7b505515 --- /dev/null +++ b/compat_4a/.gitignore @@ -0,0 +1,125 @@ +*.bundle.* +lib/ +node_modules/ +*.log +.eslintcache +.stylelintcache +*.egg-info/ +.ipynb_checkpoints +*.tsbuildinfo +step_counter/labextension +# Version file is handled by hatchling +step_counter/_version.py + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage/ +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + +# OSX files +.DS_Store + +# Yarn cache +.yarn/ diff --git a/compat_4a/.prettierignore b/compat_4a/.prettierignore new file mode 100644 index 00000000..69ed6915 --- /dev/null +++ b/compat_4a/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +step_counter diff --git a/compat_4a/.yarnrc.yml b/compat_4a/.yarnrc.yml new file mode 100644 index 00000000..3186f3f0 --- /dev/null +++ b/compat_4a/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/compat_4a/README.md b/compat_4a/README.md new file mode 100644 index 00000000..53e2bda3 --- /dev/null +++ b/compat_4a/README.md @@ -0,0 +1,96 @@ +# step_counter + +[![Github Actions Status](/workflows/Build/badge.svg)](/actions/workflows/build.yml) +Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds a provider token. + +## Requirements + +- JupyterLab >= 4.0.0 + +## Install + +To install the extension, execute: + +```bash +pip install step_counter +``` + +## Uninstall + +To remove the extension, execute: + +```bash +pip uninstall step_counter +``` + +## Contributing + +### Development install + +Note: You will need NodeJS to build the extension package. + +The `jlpm` command is JupyterLab's pinned version of +[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use +`yarn` or `npm` in lieu of `jlpm` below. + +```bash +# Clone the repo to your local environment +# Change directory to the step_counter directory +# Install package in development mode +pip install -e "." +# Link your development version of the extension with JupyterLab +jupyter labextension develop . --overwrite +# Rebuild extension Typescript source after making changes +jlpm build +``` + +You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. + +```bash +# Watch the source directory in one terminal, automatically rebuilding when needed +jlpm watch +# Run JupyterLab in another terminal +jupyter lab +``` + +With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). + +By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: + +```bash +jupyter lab build --minimize=False +``` + +### Development uninstall + +```bash +pip uninstall step_counter +``` + +In development mode, you will also need to remove the symlink created by `jupyter labextension develop` +command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` +folder is located. Then you can remove the symlink named `step_counter` within that folder. + +### Testing the extension + +#### Frontend tests + +This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. + +To execute them, execute: + +```sh +jlpm +jlpm test +``` + +#### Integration tests + +This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). +More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. + +More information are provided within the [ui-tests](./ui-tests/README.md) README. + +### Packaging the extension + +See [RELEASE](RELEASE.md) diff --git a/compat_4a/babel.config.js b/compat_4a/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/compat_4a/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/compat_4a/install.json b/compat_4a/install.json new file mode 100644 index 00000000..c914622f --- /dev/null +++ b/compat_4a/install.json @@ -0,0 +1,5 @@ +{ + "packageManager": "python", + "packageName": "step_counter", + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package step_counter" +} diff --git a/compat_4a/jest.config.js b/compat_4a/jest.config.js new file mode 100644 index 00000000..b0471e66 --- /dev/null +++ b/compat_4a/jest.config.js @@ -0,0 +1,28 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = [ + '@codemirror', + '@jupyter/ydoc', + '@jupyterlab/', + 'lib0', + 'nanoid', + 'vscode-ws-jsonrpc', + 'y-protocols', + 'y-websocket', + 'yjs' +].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] +}; diff --git a/compat_4a/package.json b/compat_4a/package.json new file mode 100644 index 00000000..9d04470a --- /dev/null +++ b/compat_4a/package.json @@ -0,0 +1,201 @@ +{ + "name": "step_counter", + "version": "0.1.0", + "description": "Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds a provider token.", + "keywords": [ + "jupyter", + "jupyterlab", + "jupyterlab-extension" + ], + "homepage": "", + "bugs": { + "url": "/issues" + }, + "license": "BSD-3-Clause", + "author": { + "name": "My Name", + "email": "me@test.com" + }, + "files": [ + "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "repository": { + "type": "git", + "url": ".git" + }, + "workspaces": [ + "ui-tests" + ], + "scripts": { + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", + "build:labextension": "jupyter labextension build .", + "build:labextension:dev": "jupyter labextension build --development True .", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", + "clean:lib": "rimraf lib tsconfig.tsbuildinfo", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf step_counter/labextension step_counter/_version.py", + "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", + "eslint": "jlpm eslint:check --fix", + "eslint:check": "eslint . --cache --ext .ts,.tsx", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", + "watch": "run-p watch:src watch:labextension", + "watch:src": "tsc -w --sourceMap", + "watch:labextension": "jupyter labextension watch ." + }, + "dependencies": { + "@lumino/coreutils": "^2.1.2" + }, + "devDependencies": { + "@jupyterlab/builder": "^4.0.0", + "@jupyterlab/testutils": "^4.0.0", + "@types/jest": "^29.2.0", + "@types/json-schema": "^7.0.11", + "@types/react": "^18.0.26", + "@types/react-addons-linked-state-mixin": "^0.14.22", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^3.0.0", + "rimraf": "^5.0.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^15.10.1", + "stylelint-config-recommended": "^13.0.0", + "stylelint-config-standard": "^34.0.0", + "stylelint-csstree-validator": "^3.0.0", + "stylelint-prettier": "^4.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" + }, + "sideEffects": [ + "style/*.css", + "style/index.js" + ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "extension": true, + "outputDir": "step_counter/labextension", + "sharedPackages": { + "step_counter": { + "bundled": false, + "singleton": true + } + } + }, + "eslintIgnore": [ + "node_modules", + "dist", + "coverage", + "**/*.d.ts", + "tests", + "**/__tests__", + "ui-tests" + ], + "eslintConfig": { + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "interface", + "format": [ + "PascalCase" + ], + "custom": { + "regex": "^I[A-Z]", + "match": true + } + } + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "none" + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/quotes": [ + "error", + "single", + { + "avoidEscape": true, + "allowTemplateLiterals": false + } + ], + "curly": [ + "error", + "all" + ], + "eqeqeq": "error", + "prefer-arrow-callback": "error" + } + }, + "prettier": { + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto", + "overrides": [ + { + "files": "package.json", + "options": { + "tabWidth": 4 + } + } + ] + }, + "stylelint": { + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "plugins": [ + "stylelint-csstree-validator" + ], + "rules": { + "csstree/validator": true, + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } + } +} diff --git a/compat_4a/pyproject.toml b/compat_4a/pyproject.toml new file mode 100644 index 00000000..d05be2e5 --- /dev/null +++ b/compat_4a/pyproject.toml @@ -0,0 +1,76 @@ +[build-system] +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" + +[project] +name = "step_counter" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] + +[tool.hatch.build.targets.sdist] +artifacts = ["step_counter/labextension"] +exclude = [".github", "binder"] + +[tool.hatch.build.targets.wheel.shared-data] +"step_counter/labextension" = "share/jupyter/labextensions/step_counter" +"install.json" = "share/jupyter/labextensions/step_counter/install.json" + +[tool.hatch.build.hooks.version] +path = "step_counter/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "step_counter/labextension/static/style.js", + "step_counter/labextension/package.json", +] +skip-if-exists = ["step_counter/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +build_cmd = "build:prod" +npm = ["jlpm"] + +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "step_counter/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/compat_4a/setup.py b/compat_4a/setup.py new file mode 100644 index 00000000..aefdf20d --- /dev/null +++ b/compat_4a/setup.py @@ -0,0 +1 @@ +__import__("setuptools").setup() diff --git a/compat_4a/src/__tests__/step_counter.spec.ts b/compat_4a/src/__tests__/step_counter.spec.ts new file mode 100644 index 00000000..162b038f --- /dev/null +++ b/compat_4a/src/__tests__/step_counter.spec.ts @@ -0,0 +1,9 @@ +/** + * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests + */ + +describe('step_counter', () => { + it('should be tested', () => { + expect(1 + 1).toEqual(2); + }); +}); diff --git a/compat_4a/src/index.ts b/compat_4a/src/index.ts new file mode 100644 index 00000000..cea6df02 --- /dev/null +++ b/compat_4a/src/index.ts @@ -0,0 +1,51 @@ +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { Token } from '@lumino/coreutils'; + +interface StepCounterItem { + // registerStatusItem(id: string, statusItem: IStatusBar.IItem): IDisposable; + getStepCount(): number; + incrementStepCount(count: number): void; +} + +// TODO define an interface for type checking the associated service +const StepCounter = new Token( + 'step_counter:StepCounter', + 'A service for counting steps.' +); + +class Counter implements StepCounterItem { + + _stepCount: number; + + constructor() { + this._stepCount = 0; + } + + incrementStepCount(count: number) { + this._stepCount += count; + } + + getStepCount() { + return this._stepCount; + } +} + +const plugin: JupyterFrontEndPlugin = { + id: 'step_counter:provider_plugin', + description: 'Provider plugin for the step_counter\'s "counter" service object.', + autoStart: true, + provides: StepCounter, + activate: (app: JupyterFrontEnd) => { + console.log('JupyterLab X1 extension step_counter\'s provider plugin is activated!'); + const counter = new Counter(); + + return counter; + } +}; + +export { StepCounter, StepCounterItem }; +export default plugin; diff --git a/compat_4a/step_counter/__init__.py b/compat_4a/step_counter/__init__.py new file mode 100644 index 00000000..8b569c95 --- /dev/null +++ b/compat_4a/step_counter/__init__.py @@ -0,0 +1,16 @@ +try: + from ._version import __version__ +except ImportError: + # Fallback when using the package in dev mode without installing + # in editable mode with pip. It is highly recommended to install + # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs + import warnings + warnings.warn("Importing 'step_counter' outside a proper installation.") + __version__ = "dev" + + +def _jupyter_labextension_paths(): + return [{ + "src": "labextension", + "dest": "step_counter" + }] diff --git a/compat_4a/style/base.css b/compat_4a/style/base.css new file mode 100644 index 00000000..e11f4577 --- /dev/null +++ b/compat_4a/style/base.css @@ -0,0 +1,5 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ diff --git a/compat_4a/style/index.css b/compat_4a/style/index.css new file mode 100644 index 00000000..8a7ea29e --- /dev/null +++ b/compat_4a/style/index.css @@ -0,0 +1 @@ +@import url('base.css'); diff --git a/compat_4a/style/index.js b/compat_4a/style/index.js new file mode 100644 index 00000000..a028a764 --- /dev/null +++ b/compat_4a/style/index.js @@ -0,0 +1 @@ +import './base.css'; diff --git a/compat_4a/tsconfig.json b/compat_4a/tsconfig.json new file mode 100644 index 00000000..98979175 --- /dev/null +++ b/compat_4a/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "composite": true, + "declaration": true, + "esModuleInterop": true, + "incremental": true, + "jsx": "react", + "module": "esnext", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "preserveWatchOutput": true, + "resolveJsonModule": true, + "outDir": "lib", + "rootDir": "src", + "strict": true, + "strictNullChecks": true, + "target": "ES2018" + }, + "include": ["src/*"] +} diff --git a/compat_4a/tsconfig.test.json b/compat_4a/tsconfig.test.json new file mode 100644 index 00000000..1de37fd0 --- /dev/null +++ b/compat_4a/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/compat_4a/ui-tests/README.md b/compat_4a/ui-tests/README.md new file mode 100644 index 00000000..dbe6e8aa --- /dev/null +++ b/compat_4a/ui-tests/README.md @@ -0,0 +1,167 @@ +# Integration Testing + +This folder contains the integration tests of the extension. + +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. + +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). + +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +> There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). + +## Run the tests + +> All commands are assumed to be executed from the root directory + +To run the tests, you need to: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test +``` + +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the root directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: + +```sh +cd ./ui-tests +jlpm playwright test -u +``` + +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the root directory + +To create tests, the easiest way is to use the code generator tool of playwright: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Start the server: + +```sh +cd ./ui-tests +jlpm start +``` + +4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: + +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 +``` + +## Debug tests + +> All commands are assumed to be executed from the root directory + +To debug tests, a good way is to use the inspector tool of playwright: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): + +```sh +cd ./ui-tests +jlpm playwright test --debug +``` + +## Upgrade Playwright and the browsers + +To update the web browser versions, you must update the package `@playwright/test`: + +```sh +cd ./ui-tests +jlpm up "@playwright/test" +jlpm playwright install +``` diff --git a/compat_4a/ui-tests/jupyter_server_test_config.py b/compat_4a/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..f2a94782 --- /dev/null +++ b/compat_4a/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,12 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/compat_4a/ui-tests/package.json b/compat_4a/ui-tests/package.json new file mode 100644 index 00000000..d87f111b --- /dev/null +++ b/compat_4a/ui-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "step_counter-ui-tests", + "version": "1.0.0", + "description": "JupyterLab step_counter Integration Tests", + "private": true, + "scripts": { + "start": "jupyter lab --config jupyter_server_test_config.py", + "test": "jlpm playwright test", + "test:update": "jlpm playwright test --update-snapshots" + }, + "devDependencies": { + "@jupyterlab/galata": "^5.0.5", + "@playwright/test": "^1.37.0" + } +} diff --git a/compat_4a/ui-tests/playwright.config.js b/compat_4a/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/compat_4a/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/compat_4a/ui-tests/tests/step_counter.spec.ts b/compat_4a/ui-tests/tests/step_counter.spec.ts new file mode 100644 index 00000000..6d1fa924 --- /dev/null +++ b/compat_4a/ui-tests/tests/step_counter.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@jupyterlab/galata'; + +/** + * Don't load JupyterLab webpage before running the tests. + * This is required to ensure we capture all log messages. + */ +test.use({ autoGoto: false }); + +test('should emit an activation console message', async ({ page }) => { + const logs: string[] = []; + + page.on('console', message => { + logs.push(message.text()); + }); + + await page.goto(); + + expect( + logs.filter(s => s === 'JupyterLab extension step_counter is activated!') + ).toHaveLength(1); +}); diff --git a/compat_4b/.copier-answers.yml b/compat_4b/.copier-answers.yml new file mode 100644 index 00000000..b65d6823 --- /dev/null +++ b/compat_4b/.copier-answers.yml @@ -0,0 +1,15 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_commit: v4.2.0 +_src_path: https://github.com/jupyterlab/extension-template +author_email: me@test.com +author_name: My Name +has_binder: false +has_settings: false +kind: frontend +labextension_name: step_counter_extension +project_short_description: Adds a step counter/button, and a step increment provider + (1 of 3 related examples). This extension holds the UI/plugin implementation. +python_name: step_counter_extension +repository: '' +test: true + diff --git a/compat_4b/.gitignore b/compat_4b/.gitignore new file mode 100644 index 00000000..436fdf37 --- /dev/null +++ b/compat_4b/.gitignore @@ -0,0 +1,125 @@ +*.bundle.* +lib/ +node_modules/ +*.log +.eslintcache +.stylelintcache +*.egg-info/ +.ipynb_checkpoints +*.tsbuildinfo +step_counter_extension/labextension +# Version file is handled by hatchling +step_counter_extension/_version.py + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage/ +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + +# OSX files +.DS_Store + +# Yarn cache +.yarn/ diff --git a/compat_4b/.prettierignore b/compat_4b/.prettierignore new file mode 100644 index 00000000..a7b30907 --- /dev/null +++ b/compat_4b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +step_counter_extension diff --git a/compat_4b/.yarnrc.yml b/compat_4b/.yarnrc.yml new file mode 100644 index 00000000..3186f3f0 --- /dev/null +++ b/compat_4b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/compat_4b/README.md b/compat_4b/README.md new file mode 100644 index 00000000..adf74bdf --- /dev/null +++ b/compat_4b/README.md @@ -0,0 +1,96 @@ +# step_counter_extension + +[![Github Actions Status](/workflows/Build/badge.svg)](/actions/workflows/build.yml) +Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds the UI/plugin implementation. + +## Requirements + +- JupyterLab >= 4.0.0 + +## Install + +To install the extension, execute: + +```bash +pip install step_counter_extension +``` + +## Uninstall + +To remove the extension, execute: + +```bash +pip uninstall step_counter_extension +``` + +## Contributing + +### Development install + +Note: You will need NodeJS to build the extension package. + +The `jlpm` command is JupyterLab's pinned version of +[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use +`yarn` or `npm` in lieu of `jlpm` below. + +```bash +# Clone the repo to your local environment +# Change directory to the step_counter_extension directory +# Install package in development mode +pip install -e "." +# Link your development version of the extension with JupyterLab +jupyter labextension develop . --overwrite +# Rebuild extension Typescript source after making changes +jlpm build +``` + +You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. + +```bash +# Watch the source directory in one terminal, automatically rebuilding when needed +jlpm watch +# Run JupyterLab in another terminal +jupyter lab +``` + +With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). + +By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: + +```bash +jupyter lab build --minimize=False +``` + +### Development uninstall + +```bash +pip uninstall step_counter_extension +``` + +In development mode, you will also need to remove the symlink created by `jupyter labextension develop` +command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` +folder is located. Then you can remove the symlink named `step_counter_extension` within that folder. + +### Testing the extension + +#### Frontend tests + +This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. + +To execute them, execute: + +```sh +jlpm +jlpm test +``` + +#### Integration tests + +This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). +More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. + +More information are provided within the [ui-tests](./ui-tests/README.md) README. + +### Packaging the extension + +See [RELEASE](RELEASE.md) diff --git a/compat_4b/babel.config.js b/compat_4b/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/compat_4b/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/compat_4b/install.json b/compat_4b/install.json new file mode 100644 index 00000000..e092856a --- /dev/null +++ b/compat_4b/install.json @@ -0,0 +1,5 @@ +{ + "packageManager": "python", + "packageName": "step_counter_extension", + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package step_counter_extension" +} diff --git a/compat_4b/jest.config.js b/compat_4b/jest.config.js new file mode 100644 index 00000000..b0471e66 --- /dev/null +++ b/compat_4b/jest.config.js @@ -0,0 +1,28 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = [ + '@codemirror', + '@jupyter/ydoc', + '@jupyterlab/', + 'lib0', + 'nanoid', + 'vscode-ws-jsonrpc', + 'y-protocols', + 'y-websocket', + 'yjs' +].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] +}; diff --git a/compat_4b/package.json b/compat_4b/package.json new file mode 100644 index 00000000..8e229017 --- /dev/null +++ b/compat_4b/package.json @@ -0,0 +1,203 @@ +{ + "name": "step_counter_extension", + "version": "0.1.0", + "description": "Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds the UI/plugin implementation.", + "keywords": [ + "jupyter", + "jupyterlab", + "jupyterlab-extension" + ], + "homepage": "", + "bugs": { + "url": "/issues" + }, + "license": "BSD-3-Clause", + "author": { + "name": "My Name", + "email": "me@test.com" + }, + "files": [ + "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "repository": { + "type": "git", + "url": ".git" + }, + "workspaces": [ + "ui-tests" + ], + "scripts": { + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", + "build:labextension": "jupyter labextension build .", + "build:labextension:dev": "jupyter labextension build --development True .", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", + "clean:lib": "rimraf lib tsconfig.tsbuildinfo", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf step_counter_extension/labextension step_counter_extension/_version.py", + "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", + "eslint": "jlpm eslint:check --fix", + "eslint:check": "eslint . --cache --ext .ts,.tsx", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", + "watch": "run-p watch:src watch:labextension", + "watch:src": "tsc -w --sourceMap", + "watch:labextension": "jupyter labextension watch ." + }, + "dependencies": { + "@jupyterlab/application": "^4.0.0", + "@lumino/widgets": "^2.0.0", + "step_counter": "file:./../compat_4a" + }, + "devDependencies": { + "@jupyterlab/builder": "^4.0.0", + "@jupyterlab/testutils": "^4.0.0", + "@types/jest": "^29.2.0", + "@types/json-schema": "^7.0.11", + "@types/react": "^18.0.26", + "@types/react-addons-linked-state-mixin": "^0.14.22", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^3.0.0", + "rimraf": "^5.0.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^15.10.1", + "stylelint-config-recommended": "^13.0.0", + "stylelint-config-standard": "^34.0.0", + "stylelint-csstree-validator": "^3.0.0", + "stylelint-prettier": "^4.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" + }, + "sideEffects": [ + "style/*.css", + "style/index.js" + ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "extension": true, + "outputDir": "step_counter_extension/labextension", + "sharedPackages": { + "step_counter": { + "bundled": false, + "singleton": true + } + } + }, + "eslintIgnore": [ + "node_modules", + "dist", + "coverage", + "**/*.d.ts", + "tests", + "**/__tests__", + "ui-tests" + ], + "eslintConfig": { + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "interface", + "format": [ + "PascalCase" + ], + "custom": { + "regex": "^I[A-Z]", + "match": true + } + } + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "none" + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/quotes": [ + "error", + "single", + { + "avoidEscape": true, + "allowTemplateLiterals": false + } + ], + "curly": [ + "error", + "all" + ], + "eqeqeq": "error", + "prefer-arrow-callback": "error" + } + }, + "prettier": { + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto", + "overrides": [ + { + "files": "package.json", + "options": { + "tabWidth": 4 + } + } + ] + }, + "stylelint": { + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "plugins": [ + "stylelint-csstree-validator" + ], + "rules": { + "csstree/validator": true, + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } + } +} diff --git a/compat_4b/pyproject.toml b/compat_4b/pyproject.toml new file mode 100644 index 00000000..19638b25 --- /dev/null +++ b/compat_4b/pyproject.toml @@ -0,0 +1,76 @@ +[build-system] +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" + +[project] +name = "step_counter_extension" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] + +[tool.hatch.build.targets.sdist] +artifacts = ["step_counter_extension/labextension"] +exclude = [".github", "binder"] + +[tool.hatch.build.targets.wheel.shared-data] +"step_counter_extension/labextension" = "share/jupyter/labextensions/step_counter_extension" +"install.json" = "share/jupyter/labextensions/step_counter_extension/install.json" + +[tool.hatch.build.hooks.version] +path = "step_counter_extension/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "step_counter_extension/labextension/static/style.js", + "step_counter_extension/labextension/package.json", +] +skip-if-exists = ["step_counter_extension/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +build_cmd = "build:prod" +npm = ["jlpm"] + +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "step_counter_extension/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/compat_4b/setup.py b/compat_4b/setup.py new file mode 100644 index 00000000..aefdf20d --- /dev/null +++ b/compat_4b/setup.py @@ -0,0 +1 @@ +__import__("setuptools").setup() diff --git a/compat_4b/src/__tests__/step_counter_extension.spec.ts b/compat_4b/src/__tests__/step_counter_extension.spec.ts new file mode 100644 index 00000000..cecdc9dd --- /dev/null +++ b/compat_4b/src/__tests__/step_counter_extension.spec.ts @@ -0,0 +1,9 @@ +/** + * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests + */ + +describe('step_counter_extension', () => { + it('should be tested', () => { + expect(1 + 1).toEqual(2); + }); +}); diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts new file mode 100644 index 00000000..73c1122d --- /dev/null +++ b/compat_4b/src/index.ts @@ -0,0 +1,71 @@ +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { Widget } from '@lumino/widgets'; + +import { StepCounter} from "step_counter"; + +/** + * StepCounterWidget holds X + */ +class StepCounterWidget extends Widget { + + stepButton: HTMLElement; + stepCountLabel: HTMLElement; + counter: any; + + constructor(counter: any) { + super(); + + this.counter = counter; + + this.node.classList.add('jp-step-container'); + + // Create and add a button to this widget's root node + const stepButton = document.createElement('div'); + stepButton.innerText = 'Take a Step'; + // Add a listener to TODO + stepButton.addEventListener('click', this.takeStep.bind(this)); + stepButton.classList.add('jp-step-button'); + this.node.appendChild(stepButton); + this.stepButton = stepButton; + + const stepCountLabel = document.createElement('p'); + stepCountLabel.classList.add('jp-step-label'); + this.node.appendChild(stepCountLabel); + this.stepCountLabel = stepCountLabel; + + this.updateStepCountDisplay(); + } + + updateStepCountDisplay() { + this.stepCountLabel.innerText = 'Step Count: ' + this.counter.stepCount; + } + + takeStep() { + this.counter.incrementStepCount(1); + this.updateStepCountDisplay(); + } +} + +/** + * Initialization data for the step_counter_extension extension. + */ +const plugin: JupyterFrontEndPlugin = { + id: 'step_counter_extension:plugin', + description: 'Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds the UI/plugin implementation.', + autoStart: true, + requires: [StepCounter], + activate: (app: JupyterFrontEnd, counter: any) => { + console.log('JupyterLab extension step_counter_extension is activated!'); + + // Create a StepCounterWidget and add it to the interface + const stepWidget: StepCounterWidget = new StepCounterWidget(counter); + stepWidget.id = 'JupyterStepWidget'; // Widgets need an id + app.shell.add(stepWidget, 'top'); + } +}; + +export default plugin; diff --git a/compat_4b/step_counter_extension/__init__.py b/compat_4b/step_counter_extension/__init__.py new file mode 100644 index 00000000..a95ae863 --- /dev/null +++ b/compat_4b/step_counter_extension/__init__.py @@ -0,0 +1,16 @@ +try: + from ._version import __version__ +except ImportError: + # Fallback when using the package in dev mode without installing + # in editable mode with pip. It is highly recommended to install + # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs + import warnings + warnings.warn("Importing 'step_counter_extension' outside a proper installation.") + __version__ = "dev" + + +def _jupyter_labextension_paths(): + return [{ + "src": "labextension", + "dest": "step_counter_extension" + }] diff --git a/compat_4b/style/base.css b/compat_4b/style/base.css new file mode 100644 index 00000000..0669ebd5 --- /dev/null +++ b/compat_4b/style/base.css @@ -0,0 +1,28 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ + +.jp-step-container { + display: inline; + user-select: none; +} + +.jp-step-button { + width: 85px; + margin: 4px; + padding: 2px; + text-align: center; + vertical-align: middle; + border-radius: 2px; + background-color: #2296f3; + color: #212121; + display: inline-block; +} + +.jp-step-label { + display: inline-block; + margin: 6px; + vertical-align: middle; +} diff --git a/compat_4b/style/index.css b/compat_4b/style/index.css new file mode 100644 index 00000000..8a7ea29e --- /dev/null +++ b/compat_4b/style/index.css @@ -0,0 +1 @@ +@import url('base.css'); diff --git a/compat_4b/style/index.js b/compat_4b/style/index.js new file mode 100644 index 00000000..a028a764 --- /dev/null +++ b/compat_4b/style/index.js @@ -0,0 +1 @@ +import './base.css'; diff --git a/compat_4b/tsconfig.json b/compat_4b/tsconfig.json new file mode 100644 index 00000000..98979175 --- /dev/null +++ b/compat_4b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "composite": true, + "declaration": true, + "esModuleInterop": true, + "incremental": true, + "jsx": "react", + "module": "esnext", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "preserveWatchOutput": true, + "resolveJsonModule": true, + "outDir": "lib", + "rootDir": "src", + "strict": true, + "strictNullChecks": true, + "target": "ES2018" + }, + "include": ["src/*"] +} diff --git a/compat_4b/tsconfig.test.json b/compat_4b/tsconfig.test.json new file mode 100644 index 00000000..1de37fd0 --- /dev/null +++ b/compat_4b/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/compat_4b/ui-tests/README.md b/compat_4b/ui-tests/README.md new file mode 100644 index 00000000..dbe6e8aa --- /dev/null +++ b/compat_4b/ui-tests/README.md @@ -0,0 +1,167 @@ +# Integration Testing + +This folder contains the integration tests of the extension. + +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. + +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). + +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +> There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). + +## Run the tests + +> All commands are assumed to be executed from the root directory + +To run the tests, you need to: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test +``` + +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the root directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: + +```sh +cd ./ui-tests +jlpm playwright test -u +``` + +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the root directory + +To create tests, the easiest way is to use the code generator tool of playwright: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Start the server: + +```sh +cd ./ui-tests +jlpm start +``` + +4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: + +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 +``` + +## Debug tests + +> All commands are assumed to be executed from the root directory + +To debug tests, a good way is to use the inspector tool of playwright: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): + +```sh +cd ./ui-tests +jlpm playwright test --debug +``` + +## Upgrade Playwright and the browsers + +To update the web browser versions, you must update the package `@playwright/test`: + +```sh +cd ./ui-tests +jlpm up "@playwright/test" +jlpm playwright install +``` diff --git a/compat_4b/ui-tests/jupyter_server_test_config.py b/compat_4b/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..f2a94782 --- /dev/null +++ b/compat_4b/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,12 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/compat_4b/ui-tests/package.json b/compat_4b/ui-tests/package.json new file mode 100644 index 00000000..3ea36608 --- /dev/null +++ b/compat_4b/ui-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "step_counter_extension-ui-tests", + "version": "1.0.0", + "description": "JupyterLab step_counter_extension Integration Tests", + "private": true, + "scripts": { + "start": "jupyter lab --config jupyter_server_test_config.py", + "test": "jlpm playwright test", + "test:update": "jlpm playwright test --update-snapshots" + }, + "devDependencies": { + "@jupyterlab/galata": "^5.0.5", + "@playwright/test": "^1.37.0" + } +} diff --git a/compat_4b/ui-tests/playwright.config.js b/compat_4b/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/compat_4b/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/compat_4b/ui-tests/tests/step_counter_extension.spec.ts b/compat_4b/ui-tests/tests/step_counter_extension.spec.ts new file mode 100644 index 00000000..525e5a35 --- /dev/null +++ b/compat_4b/ui-tests/tests/step_counter_extension.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@jupyterlab/galata'; + +/** + * Don't load JupyterLab webpage before running the tests. + * This is required to ensure we capture all log messages. + */ +test.use({ autoGoto: false }); + +test('should emit an activation console message', async ({ page }) => { + const logs: string[] = []; + + page.on('console', message => { + logs.push(message.text()); + }); + + await page.goto(); + + expect( + logs.filter(s => s === 'JupyterLab extension step_counter_extension is activated!') + ).toHaveLength(1); +}); From 7442fe6dae82260235e7fad48962319b09787e2b Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 31 Aug 2023 09:15:20 -0400 Subject: [PATCH 02/31] Fixed method name. --- compat_4b/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts index 73c1122d..00b91d29 100644 --- a/compat_4b/src/index.ts +++ b/compat_4b/src/index.ts @@ -41,7 +41,7 @@ class StepCounterWidget extends Widget { } updateStepCountDisplay() { - this.stepCountLabel.innerText = 'Step Count: ' + this.counter.stepCount; + this.stepCountLabel.innerText = 'Step Count: ' + this.counter.getStepCount(); } takeStep() { From 0c731cf8d25e1c0283f499b7822654ff9c391d7b Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 12:43:29 -0400 Subject: [PATCH 03/31] Added comments to the provider-consumer samples. --- compat_4a/src/index.ts | 54 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/compat_4a/src/index.ts b/compat_4a/src/index.ts index cea6df02..225e0f0c 100644 --- a/compat_4a/src/index.ts +++ b/compat_4a/src/index.ts @@ -1,3 +1,27 @@ +// This is one of three related extension examples that demonstrate +// JupyterLab's provider-consumer pattern, where plugins can depend +// on and reuse features from one another. The three packages that +// make up the complete example are: +// +// 1. The step_counter package (this one). This holds a token, a +// class and interface that make up a stock implementation of +// the "step_counter" service, and a provider plugin that +// makes an instance of the Counter available to JupyterLab +// as a service object. +// 2. The step_counter_extension package, that holds a UI/interface +// in JupyterLab for users to count their steps that connects +// with/consumes the step_counter service object via a consumer plugin. +// 3. The leap_counter_extension package, that holds an alternate +// way for users to count leaps. Like the step_counter_extension +// package, this holds a UI/interface in JupyterLab, and a consumer +// plugin that also requests/consumes the step_counter service +// object. The leap_counter_extension package demonstrates how +// an unrelated plugin can depend on and reuse features from +// an existing plugin. Users can add install either the +// step_counter_extension, the leap_counter_extension or both +// to get whichever features they prefer (with both reusing +// the step_counter service object). + import { JupyterFrontEnd, JupyterFrontEndPlugin @@ -5,18 +29,34 @@ import { import { Token } from '@lumino/coreutils'; +// The StepCounterItem interface is used as part of JupyterLab's +// provider-consumer pattern. This interface is supplied to the +// token instance (the StepCounter token), and JupyterLab will +// use it to type-check any service-object associated with the +// token that a provider plugin supplies to check that it conforms +// to the interface. interface StepCounterItem { // registerStatusItem(id: string, statusItem: IStatusBar.IItem): IDisposable; getStepCount(): number; incrementStepCount(count: number): void; } -// TODO define an interface for type checking the associated service +// The token is used to identify a particular "service" in +// JupyterLab's extension system (here the StepCounter token +// identifies the example "Step Counter Service", which is used +// to store and increment step count data in JupyterLab). Any +// plugin can use this token in their "requires" or "activates" +// list to request the service object associated with this token! const StepCounter = new Token( 'step_counter:StepCounter', 'A service for counting steps.' ); +// This class holds step count data/utilities. An instance of +// this class will serve as the service object associated with +// the StepCounter token (Other developers can substitute their +// own implementation of a StepCounterItem instead of using this +// one, by becoming a provider of the StepCounter token). class Counter implements StepCounterItem { _stepCount: number; @@ -34,15 +74,27 @@ class Counter implements StepCounterItem { } } +// This plugin is a "provider" in JupyterLab's provider-consumer pattern. +// For a plugin to become a provider, it must list the token it wants to +// provide a service object for in its "provides" list, and then it has +// to return that object (in this case, an instance of the example Counter +// class defined above) from the function supplied as its activate property. +// It also needs to supply the interface (the one the service object +// implements) to JupyterFrontEndPlugin when it's defined. const plugin: JupyterFrontEndPlugin = { id: 'step_counter:provider_plugin', description: 'Provider plugin for the step_counter\'s "counter" service object.', autoStart: true, provides: StepCounter, + // The activate function here will be called by JupyterLab when the plugin loads activate: (app: JupyterFrontEnd) => { console.log('JupyterLab X1 extension step_counter\'s provider plugin is activated!'); const counter = new Counter(); + // Since this plugin "provides" the "StepCounter" service, make sure to + // return the object you want to use as the "service object" here (when + // other plugins request the StepCounter service, it is this object + // that will be supplied) return counter; } }; From f3fe8126fbae3426d9250ac2dcf89738a5694f33 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 13:01:49 -0400 Subject: [PATCH 04/31] Added comments to provider/consumer example. --- compat_4a/src/index.ts | 2 +- compat_4b/src/index.ts | 62 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/compat_4a/src/index.ts b/compat_4a/src/index.ts index 225e0f0c..da9b6eaf 100644 --- a/compat_4a/src/index.ts +++ b/compat_4a/src/index.ts @@ -3,7 +3,7 @@ // on and reuse features from one another. The three packages that // make up the complete example are: // -// 1. The step_counter package (this one). This holds a token, a +// 1. (*) The step_counter package (this one). This holds a token, a // class and interface that make up a stock implementation of // the "step_counter" service, and a provider plugin that // makes an instance of the Counter available to JupyterLab diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts index 00b91d29..cb740aa3 100644 --- a/compat_4b/src/index.ts +++ b/compat_4b/src/index.ts @@ -1,3 +1,28 @@ +// This is one of three related extension examples that demonstrate +// JupyterLab's provider-consumer pattern, where plugins can depend +// on and reuse features from one another. The three packages that +// make up the complete example are: +// +// 1. The step_counter package. This package holds a token, a +// class and interface that make up a stock implementation of +// the "step_counter" service, and a provider plugin that +// makes an instance of the Counter available to JupyterLab +// as a service object. +// 2. (*) The step_counter_extension package (this one), that holds a +// UI/interface in JupyterLab for users to count their steps that +// connects with/consumes the step_counter service object via a +// consumer plugin. +// 3. The leap_counter_extension package, that holds an alternate +// way for users to count leaps. Like the step_counter_extension +// package, this holds a UI/interface in JupyterLab, and a consumer +// plugin that also requests/consumes the step_counter service +// object. The leap_counter_extension package demonstrates how +// an unrelated plugin can depend on and reuse features from +// an existing plugin. Users can add install either the +// step_counter_extension, the leap_counter_extension or both +// to get whichever features they prefer (with both reusing +// the step_counter service object). + import { JupyterFrontEnd, JupyterFrontEndPlugin @@ -7,31 +32,35 @@ import { Widget } from '@lumino/widgets'; import { StepCounter} from "step_counter"; -/** - * StepCounterWidget holds X - */ +// This widget holds the JupyterLab UI/interface that users will +// see and interact with to count and view their steps. class StepCounterWidget extends Widget { stepButton: HTMLElement; stepCountLabel: HTMLElement; counter: any; + // Notice that the constructor for this object takes a "counter" + // argument, which is the service object associated with the StepCounter + // token (which is passed in by the consumer plugin). constructor(counter: any) { super(); this.counter = counter; + // Add styling by using a CSS class this.node.classList.add('jp-step-container'); // Create and add a button to this widget's root node const stepButton = document.createElement('div'); stepButton.innerText = 'Take a Step'; - // Add a listener to TODO + // Add a listener to handle button clicks stepButton.addEventListener('click', this.takeStep.bind(this)); stepButton.classList.add('jp-step-button'); this.node.appendChild(stepButton); this.stepButton = stepButton; + // Add a label to display the step count const stepCountLabel = document.createElement('p'); stepCountLabel.classList.add('jp-step-label'); this.node.appendChild(stepCountLabel); @@ -40,24 +69,43 @@ class StepCounterWidget extends Widget { this.updateStepCountDisplay(); } + // Refresh the displayed step count updateStepCountDisplay() { this.stepCountLabel.innerText = 'Step Count: ' + this.counter.getStepCount(); } + // Increment the step count, by just 1 takeStep() { this.counter.incrementStepCount(1); this.updateStepCountDisplay(); } } -/** - * Initialization data for the step_counter_extension extension. - */ +// This plugin is a "provider" in JupyterLab's provider-consumer pattern. +// For a plugin to become a provider, it must list the token it wants to +// provide a service object for in its "provides" list, and then it has +// to return that object (in this case, an instance of the example Counter +// class defined above) from the function supplied as its activate property. +// It also needs to supply the interface (the one the service object +// implements) to JupyterFrontEndPlugin when it's defined. + +// This plugin is a "consumer" in JupyterLab's provider-consumer pattern. +// The "requires" property of this plugin lists the StepCounter token, which +// requests the service-object associated with that token from JupyterLab, +// and this plugin "consumes" the service object by using it in its own code. +// Whenever you add a "requires" or "optional" service, you need to manually +// add an argument to your plugin's "activate" function. const plugin: JupyterFrontEndPlugin = { id: 'step_counter_extension:plugin', description: 'Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds the UI/plugin implementation.', autoStart: true, requires: [StepCounter], + // The activate function here will be called by JupyterLab when the plugin loads. + // When JupyterLab calls your plugin's activate function, it will always pass + // an application as the first argument, then any required arguments, then any optional + // arguments, so make sure you add arguments for those here when your plugin requests + // any required or optional services. If a required service is missing, your plugin + // won't load. If an optional service is missing, the supplied argument will be null. activate: (app: JupyterFrontEnd, counter: any) => { console.log('JupyterLab extension step_counter_extension is activated!'); From 14350def36ad72fd5e4c1c2f5904ad8e538d1edb Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 13:02:43 -0400 Subject: [PATCH 05/31] Comment cleanup. --- compat_4b/src/index.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts index cb740aa3..b98c6de3 100644 --- a/compat_4b/src/index.ts +++ b/compat_4b/src/index.ts @@ -81,14 +81,6 @@ class StepCounterWidget extends Widget { } } -// This plugin is a "provider" in JupyterLab's provider-consumer pattern. -// For a plugin to become a provider, it must list the token it wants to -// provide a service object for in its "provides" list, and then it has -// to return that object (in this case, an instance of the example Counter -// class defined above) from the function supplied as its activate property. -// It also needs to supply the interface (the one the service object -// implements) to JupyterFrontEndPlugin when it's defined. - // This plugin is a "consumer" in JupyterLab's provider-consumer pattern. // The "requires" property of this plugin lists the StepCounter token, which // requests the service-object associated with that token from JupyterLab, From 2eb6512ec46bfa184cf884d2eb86261d43e814ff Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 14:32:51 -0400 Subject: [PATCH 06/31] Minor formatting. --- compat_4a/src/index.ts | 2 +- compat_4b/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compat_4a/src/index.ts b/compat_4a/src/index.ts index da9b6eaf..25f1fff8 100644 --- a/compat_4a/src/index.ts +++ b/compat_4a/src/index.ts @@ -4,7 +4,7 @@ // make up the complete example are: // // 1. (*) The step_counter package (this one). This holds a token, a -// class and interface that make up a stock implementation of +// class + an interface that make up a stock implementation of // the "step_counter" service, and a provider plugin that // makes an instance of the Counter available to JupyterLab // as a service object. diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts index b98c6de3..09b4d1b1 100644 --- a/compat_4b/src/index.ts +++ b/compat_4b/src/index.ts @@ -4,7 +4,7 @@ // make up the complete example are: // // 1. The step_counter package. This package holds a token, a -// class and interface that make up a stock implementation of +// class + an interface that make up a stock implementation of // the "step_counter" service, and a provider plugin that // makes an instance of the Counter available to JupyterLab // as a service object. From 9d115b5ac71256ec897bd4ce78e4beed950d69b0 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 14:36:20 -0400 Subject: [PATCH 07/31] Updated project description text. --- compat_4b/.copier-answers.yml | 4 ++-- compat_4b/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compat_4b/.copier-answers.yml b/compat_4b/.copier-answers.yml index b65d6823..dbd5d282 100644 --- a/compat_4b/.copier-answers.yml +++ b/compat_4b/.copier-answers.yml @@ -7,8 +7,8 @@ has_binder: false has_settings: false kind: frontend labextension_name: step_counter_extension -project_short_description: Adds a step counter/button, and a step increment provider - (1 of 3 related examples). This extension holds the UI/plugin implementation. +project_short_description: Adds a step counter/button (1 of 3 related examples). + This extension holds the UI/interface. python_name: step_counter_extension repository: '' test: true diff --git a/compat_4b/package.json b/compat_4b/package.json index 8e229017..8f717e18 100644 --- a/compat_4b/package.json +++ b/compat_4b/package.json @@ -1,7 +1,7 @@ { "name": "step_counter_extension", "version": "0.1.0", - "description": "Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds the UI/plugin implementation.", + "description": "Adds a step counter/button (1 of 3 related examples). This extension holds the UI/interface.", "keywords": [ "jupyter", "jupyterlab", From 03f9cc9693eec5ddfb3e95cbaae649a3d7c66ab6 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 14:41:25 -0400 Subject: [PATCH 08/31] Updated project description text. --- compat_4a/.copier-answers.yml | 4 ++-- compat_4a/package.json | 2 +- compat_4a/src/index.ts | 2 +- compat_4b/src/index.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compat_4a/.copier-answers.yml b/compat_4a/.copier-answers.yml index f74a2b63..35ba42aa 100644 --- a/compat_4a/.copier-answers.yml +++ b/compat_4a/.copier-answers.yml @@ -7,8 +7,8 @@ has_binder: false has_settings: false kind: frontend labextension_name: step_counter -project_short_description: Adds a step counter/button, and a step increment provider - (1 of 3 related examples). This extension holds a provider token. +project_short_description: Adds a step_counter service token and a stock + implementation (1 of 3 related examples). python_name: step_counter repository: '' test: true diff --git a/compat_4a/package.json b/compat_4a/package.json index 9d04470a..3d0c79e1 100644 --- a/compat_4a/package.json +++ b/compat_4a/package.json @@ -1,7 +1,7 @@ { "name": "step_counter", "version": "0.1.0", - "description": "Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds a provider token.", + "description": "Adds a step_counter service token and a stock implementation (1 of 3 related examples).", "keywords": [ "jupyter", "jupyterlab", diff --git a/compat_4a/src/index.ts b/compat_4a/src/index.ts index 25f1fff8..eca1d073 100644 --- a/compat_4a/src/index.ts +++ b/compat_4a/src/index.ts @@ -88,7 +88,7 @@ const plugin: JupyterFrontEndPlugin = { provides: StepCounter, // The activate function here will be called by JupyterLab when the plugin loads activate: (app: JupyterFrontEnd) => { - console.log('JupyterLab X1 extension step_counter\'s provider plugin is activated!'); + console.log('JupyterLab extension (step_counter/provider plugin) is activated!'); const counter = new Counter(); // Since this plugin "provides" the "StepCounter" service, make sure to diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts index 09b4d1b1..fb0b4ac5 100644 --- a/compat_4b/src/index.ts +++ b/compat_4b/src/index.ts @@ -89,7 +89,7 @@ class StepCounterWidget extends Widget { // add an argument to your plugin's "activate" function. const plugin: JupyterFrontEndPlugin = { id: 'step_counter_extension:plugin', - description: 'Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds the UI/plugin implementation.', + description: 'Adds a step counter/button (1 of 3 related examples). This extension holds the UI/interface', autoStart: true, requires: [StepCounter], // The activate function here will be called by JupyterLab when the plugin loads. From de11b88d6024e48684806e858f2a22d0bf77d1a5 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 14:48:56 -0400 Subject: [PATCH 09/31] Minor formatting. --- compat_4a/src/index.ts | 2 +- compat_4b/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compat_4a/src/index.ts b/compat_4a/src/index.ts index eca1d073..a96c1ba9 100644 --- a/compat_4a/src/index.ts +++ b/compat_4a/src/index.ts @@ -17,7 +17,7 @@ // plugin that also requests/consumes the step_counter service // object. The leap_counter_extension package demonstrates how // an unrelated plugin can depend on and reuse features from -// an existing plugin. Users can add install either the +// an existing plugin. Users can install either the // step_counter_extension, the leap_counter_extension or both // to get whichever features they prefer (with both reusing // the step_counter service object). diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts index fb0b4ac5..fa70ac62 100644 --- a/compat_4b/src/index.ts +++ b/compat_4b/src/index.ts @@ -18,7 +18,7 @@ // plugin that also requests/consumes the step_counter service // object. The leap_counter_extension package demonstrates how // an unrelated plugin can depend on and reuse features from -// an existing plugin. Users can add install either the +// an existing plugin. Users can install either the // step_counter_extension, the leap_counter_extension or both // to get whichever features they prefer (with both reusing // the step_counter service object). From 7a5c3c025228774733b0a23bf8c9d3a46d7bc38c Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 14:59:13 -0400 Subject: [PATCH 10/31] Minor formatting. --- compat_4a/src/index.ts | 2 +- compat_4b/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compat_4a/src/index.ts b/compat_4a/src/index.ts index a96c1ba9..134d183d 100644 --- a/compat_4a/src/index.ts +++ b/compat_4a/src/index.ts @@ -12,7 +12,7 @@ // in JupyterLab for users to count their steps that connects // with/consumes the step_counter service object via a consumer plugin. // 3. The leap_counter_extension package, that holds an alternate -// way for users to count leaps. Like the step_counter_extension +// way for users to count steps (a leap is 5 steps). Like the step_counter_extension // package, this holds a UI/interface in JupyterLab, and a consumer // plugin that also requests/consumes the step_counter service // object. The leap_counter_extension package demonstrates how diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts index fa70ac62..7f2cacd6 100644 --- a/compat_4b/src/index.ts +++ b/compat_4b/src/index.ts @@ -13,7 +13,7 @@ // connects with/consumes the step_counter service object via a // consumer plugin. // 3. The leap_counter_extension package, that holds an alternate -// way for users to count leaps. Like the step_counter_extension +// way for users to count steps (a leap is 5 steps). Like the step_counter_extension // package, this holds a UI/interface in JupyterLab, and a consumer // plugin that also requests/consumes the step_counter service // object. The leap_counter_extension package demonstrates how From 82d1ad57777544f92364fdd81a5a6475b0eee2fd Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 1 Sep 2023 15:37:33 -0400 Subject: [PATCH 11/31] Added third provider-consumer sub example, an additional consumer. --- compat_4c/.copier-answers.yml | 15 ++ compat_4c/.gitignore | 125 +++++++++++ compat_4c/.prettierignore | 6 + compat_4c/.yarnrc.yml | 1 + compat_4c/README.md | 96 +++++++++ compat_4c/babel.config.js | 1 + compat_4c/install.json | 5 + compat_4c/jest.config.js | 28 +++ compat_4c/leap_counter_extension/__init__.py | 16 ++ compat_4c/package.json | 203 ++++++++++++++++++ compat_4c/pyproject.toml | 76 +++++++ compat_4c/setup.py | 1 + .../__tests__/leap_counter_extension.spec.ts | 9 + compat_4c/src/index.ts | 111 ++++++++++ compat_4c/style/base.css | 28 +++ compat_4c/style/index.css | 1 + compat_4c/style/index.js | 1 + compat_4c/tsconfig.json | 23 ++ compat_4c/tsconfig.test.json | 6 + compat_4c/ui-tests/README.md | 167 ++++++++++++++ .../ui-tests/jupyter_server_test_config.py | 12 ++ compat_4c/ui-tests/package.json | 15 ++ compat_4c/ui-tests/playwright.config.js | 14 ++ .../tests/leap_counter_extension.spec.ts | 21 ++ 24 files changed, 981 insertions(+) create mode 100644 compat_4c/.copier-answers.yml create mode 100644 compat_4c/.gitignore create mode 100644 compat_4c/.prettierignore create mode 100644 compat_4c/.yarnrc.yml create mode 100644 compat_4c/README.md create mode 100644 compat_4c/babel.config.js create mode 100644 compat_4c/install.json create mode 100644 compat_4c/jest.config.js create mode 100644 compat_4c/leap_counter_extension/__init__.py create mode 100644 compat_4c/package.json create mode 100644 compat_4c/pyproject.toml create mode 100644 compat_4c/setup.py create mode 100644 compat_4c/src/__tests__/leap_counter_extension.spec.ts create mode 100644 compat_4c/src/index.ts create mode 100644 compat_4c/style/base.css create mode 100644 compat_4c/style/index.css create mode 100644 compat_4c/style/index.js create mode 100644 compat_4c/tsconfig.json create mode 100644 compat_4c/tsconfig.test.json create mode 100644 compat_4c/ui-tests/README.md create mode 100644 compat_4c/ui-tests/jupyter_server_test_config.py create mode 100644 compat_4c/ui-tests/package.json create mode 100644 compat_4c/ui-tests/playwright.config.js create mode 100644 compat_4c/ui-tests/tests/leap_counter_extension.spec.ts diff --git a/compat_4c/.copier-answers.yml b/compat_4c/.copier-answers.yml new file mode 100644 index 00000000..19f21b17 --- /dev/null +++ b/compat_4c/.copier-answers.yml @@ -0,0 +1,15 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_commit: v4.2.0 +_src_path: https://github.com/jupyterlab/extension-template +author_email: me@test.com +author_name: My Name +has_binder: false +has_settings: false +kind: frontend +labextension_name: leap_counter_extension +project_short_description: Adds a leap counter/button (1 of 3 related examples). This + extension holds the UI/interface. +python_name: leap_counter_extension +repository: '' +test: true + diff --git a/compat_4c/.gitignore b/compat_4c/.gitignore new file mode 100644 index 00000000..08ebd8ec --- /dev/null +++ b/compat_4c/.gitignore @@ -0,0 +1,125 @@ +*.bundle.* +lib/ +node_modules/ +*.log +.eslintcache +.stylelintcache +*.egg-info/ +.ipynb_checkpoints +*.tsbuildinfo +leap_counter_extension/labextension +# Version file is handled by hatchling +leap_counter_extension/_version.py + +# Integration tests +ui-tests/test-results/ +ui-tests/playwright-report/ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage/ +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + +# OSX files +.DS_Store + +# Yarn cache +.yarn/ diff --git a/compat_4c/.prettierignore b/compat_4c/.prettierignore new file mode 100644 index 00000000..5afbf1dd --- /dev/null +++ b/compat_4c/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +leap_counter_extension diff --git a/compat_4c/.yarnrc.yml b/compat_4c/.yarnrc.yml new file mode 100644 index 00000000..3186f3f0 --- /dev/null +++ b/compat_4c/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/compat_4c/README.md b/compat_4c/README.md new file mode 100644 index 00000000..ddcc10fd --- /dev/null +++ b/compat_4c/README.md @@ -0,0 +1,96 @@ +# leap_counter_extension + +[![Github Actions Status](/workflows/Build/badge.svg)](/actions/workflows/build.yml) +Adds a leap counter/button (1 of 3 related examples). This extension holds the UI/interface. + +## Requirements + +- JupyterLab >= 4.0.0 + +## Install + +To install the extension, execute: + +```bash +pip install leap_counter_extension +``` + +## Uninstall + +To remove the extension, execute: + +```bash +pip uninstall leap_counter_extension +``` + +## Contributing + +### Development install + +Note: You will need NodeJS to build the extension package. + +The `jlpm` command is JupyterLab's pinned version of +[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use +`yarn` or `npm` in lieu of `jlpm` below. + +```bash +# Clone the repo to your local environment +# Change directory to the leap_counter_extension directory +# Install package in development mode +pip install -e "." +# Link your development version of the extension with JupyterLab +jupyter labextension develop . --overwrite +# Rebuild extension Typescript source after making changes +jlpm build +``` + +You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. + +```bash +# Watch the source directory in one terminal, automatically rebuilding when needed +jlpm watch +# Run JupyterLab in another terminal +jupyter lab +``` + +With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). + +By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: + +```bash +jupyter lab build --minimize=False +``` + +### Development uninstall + +```bash +pip uninstall leap_counter_extension +``` + +In development mode, you will also need to remove the symlink created by `jupyter labextension develop` +command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` +folder is located. Then you can remove the symlink named `leap_counter_extension` within that folder. + +### Testing the extension + +#### Frontend tests + +This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. + +To execute them, execute: + +```sh +jlpm +jlpm test +``` + +#### Integration tests + +This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). +More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. + +More information are provided within the [ui-tests](./ui-tests/README.md) README. + +### Packaging the extension + +See [RELEASE](RELEASE.md) diff --git a/compat_4c/babel.config.js b/compat_4c/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/compat_4c/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/compat_4c/install.json b/compat_4c/install.json new file mode 100644 index 00000000..194595a5 --- /dev/null +++ b/compat_4c/install.json @@ -0,0 +1,5 @@ +{ + "packageManager": "python", + "packageName": "leap_counter_extension", + "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package leap_counter_extension" +} diff --git a/compat_4c/jest.config.js b/compat_4c/jest.config.js new file mode 100644 index 00000000..b0471e66 --- /dev/null +++ b/compat_4c/jest.config.js @@ -0,0 +1,28 @@ +const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); + +const esModules = [ + '@codemirror', + '@jupyter/ydoc', + '@jupyterlab/', + 'lib0', + 'nanoid', + 'vscode-ws-jsonrpc', + 'y-protocols', + 'y-websocket', + 'yjs' +].join('|'); + +const baseConfig = jestJupyterLab(__dirname); + +module.exports = { + ...baseConfig, + automock: false, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], + coverageReporters: ['lcov', 'text'], + testRegex: 'src/.*/.*.spec.ts[x]?$', + transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] +}; diff --git a/compat_4c/leap_counter_extension/__init__.py b/compat_4c/leap_counter_extension/__init__.py new file mode 100644 index 00000000..18edae05 --- /dev/null +++ b/compat_4c/leap_counter_extension/__init__.py @@ -0,0 +1,16 @@ +try: + from ._version import __version__ +except ImportError: + # Fallback when using the package in dev mode without installing + # in editable mode with pip. It is highly recommended to install + # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs + import warnings + warnings.warn("Importing 'leap_counter_extension' outside a proper installation.") + __version__ = "dev" + + +def _jupyter_labextension_paths(): + return [{ + "src": "labextension", + "dest": "leap_counter_extension" + }] diff --git a/compat_4c/package.json b/compat_4c/package.json new file mode 100644 index 00000000..0177c85b --- /dev/null +++ b/compat_4c/package.json @@ -0,0 +1,203 @@ +{ + "name": "leap_counter_extension", + "version": "0.1.0", + "description": "Adds a leap counter/button (1 of 3 related examples). This extension holds the UI/interface.", + "keywords": [ + "jupyter", + "jupyterlab", + "jupyterlab-extension" + ], + "homepage": "", + "bugs": { + "url": "/issues" + }, + "license": "BSD-3-Clause", + "author": { + "name": "My Name", + "email": "me@test.com" + }, + "files": [ + "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "repository": { + "type": "git", + "url": ".git" + }, + "workspaces": [ + "ui-tests" + ], + "scripts": { + "build": "jlpm build:lib && jlpm build:labextension:dev", + "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", + "build:labextension": "jupyter labextension build .", + "build:labextension:dev": "jupyter labextension build --development True .", + "build:lib": "tsc --sourceMap", + "build:lib:prod": "tsc", + "clean": "jlpm clean:lib", + "clean:lib": "rimraf lib tsconfig.tsbuildinfo", + "clean:lintcache": "rimraf .eslintcache .stylelintcache", + "clean:labextension": "rimraf leap_counter_extension/labextension leap_counter_extension/_version.py", + "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", + "eslint": "jlpm eslint:check --fix", + "eslint:check": "eslint . --cache --ext .ts,.tsx", + "install:extension": "jlpm build", + "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", + "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", + "prettier": "jlpm prettier:base --write --list-different", + "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", + "prettier:check": "jlpm prettier:base --check", + "stylelint": "jlpm stylelint:check --fix", + "stylelint:check": "stylelint --cache \"style/**/*.css\"", + "test": "jest --coverage", + "watch": "run-p watch:src watch:labextension", + "watch:src": "tsc -w --sourceMap", + "watch:labextension": "jupyter labextension watch ." + }, + "dependencies": { + "@jupyterlab/application": "^4.0.0", + "@lumino/widgets": "^2.0.0", + "step_counter": "file:./../compat_4a" + }, + "devDependencies": { + "@jupyterlab/builder": "^4.0.0", + "@jupyterlab/testutils": "^4.0.0", + "@types/jest": "^29.2.0", + "@types/json-schema": "^7.0.11", + "@types/react": "^18.0.26", + "@types/react-addons-linked-state-mixin": "^0.14.22", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^3.0.0", + "rimraf": "^5.0.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^15.10.1", + "stylelint-config-recommended": "^13.0.0", + "stylelint-config-standard": "^34.0.0", + "stylelint-csstree-validator": "^3.0.0", + "stylelint-prettier": "^4.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" + }, + "sideEffects": [ + "style/*.css", + "style/index.js" + ], + "styleModule": "style/index.js", + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "extension": true, + "outputDir": "leap_counter_extension/labextension", + "sharedPackages": { + "step_counter": { + "bundled": false, + "singleton": true + } + } + }, + "eslintIgnore": [ + "node_modules", + "dist", + "coverage", + "**/*.d.ts", + "tests", + "**/__tests__", + "ui-tests" + ], + "eslintConfig": { + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "interface", + "format": [ + "PascalCase" + ], + "custom": { + "regex": "^I[A-Z]", + "match": true + } + } + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "none" + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/quotes": [ + "error", + "single", + { + "avoidEscape": true, + "allowTemplateLiterals": false + } + ], + "curly": [ + "error", + "all" + ], + "eqeqeq": "error", + "prefer-arrow-callback": "error" + } + }, + "prettier": { + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto", + "overrides": [ + { + "files": "package.json", + "options": { + "tabWidth": 4 + } + } + ] + }, + "stylelint": { + "extends": [ + "stylelint-config-recommended", + "stylelint-config-standard", + "stylelint-prettier/recommended" + ], + "plugins": [ + "stylelint-csstree-validator" + ], + "rules": { + "csstree/validator": true, + "property-no-vendor-prefix": null, + "selector-no-vendor-prefix": null, + "value-no-vendor-prefix": null + } + } +} diff --git a/compat_4c/pyproject.toml b/compat_4c/pyproject.toml new file mode 100644 index 00000000..d6f855c6 --- /dev/null +++ b/compat_4c/pyproject.toml @@ -0,0 +1,76 @@ +[build-system] +requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] +build-backend = "hatchling.build" + +[project] +name = "leap_counter_extension" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Framework :: Jupyter", + "Framework :: Jupyter :: JupyterLab", + "Framework :: Jupyter :: JupyterLab :: 4", + "Framework :: Jupyter :: JupyterLab :: Extensions", + "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ +] +dynamic = ["version", "description", "authors", "urls", "keywords"] + +[tool.hatch.version] +source = "nodejs" + +[tool.hatch.metadata.hooks.nodejs] +fields = ["description", "authors", "urls"] + +[tool.hatch.build.targets.sdist] +artifacts = ["leap_counter_extension/labextension"] +exclude = [".github", "binder"] + +[tool.hatch.build.targets.wheel.shared-data] +"leap_counter_extension/labextension" = "share/jupyter/labextensions/leap_counter_extension" +"install.json" = "share/jupyter/labextensions/leap_counter_extension/install.json" + +[tool.hatch.build.hooks.version] +path = "leap_counter_extension/_version.py" + +[tool.hatch.build.hooks.jupyter-builder] +dependencies = ["hatch-jupyter-builder>=0.5"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = [ + "leap_counter_extension/labextension/static/style.js", + "leap_counter_extension/labextension/package.json", +] +skip-if-exists = ["leap_counter_extension/labextension/static/style.js"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +build_cmd = "build:prod" +npm = ["jlpm"] + +[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] +build_cmd = "install:extension" +npm = ["jlpm"] +source_dir = "src" +build_dir = "leap_counter_extension/labextension" + +[tool.jupyter-releaser.options] +version_cmd = "hatch version" + +[tool.jupyter-releaser.hooks] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0,<5'", + "jlpm", + "jlpm build:prod" +] +before-build-python = ["jlpm clean:all"] + +[tool.check-wheel-contents] +ignore = ["W002"] diff --git a/compat_4c/setup.py b/compat_4c/setup.py new file mode 100644 index 00000000..aefdf20d --- /dev/null +++ b/compat_4c/setup.py @@ -0,0 +1 @@ +__import__("setuptools").setup() diff --git a/compat_4c/src/__tests__/leap_counter_extension.spec.ts b/compat_4c/src/__tests__/leap_counter_extension.spec.ts new file mode 100644 index 00000000..12ae1527 --- /dev/null +++ b/compat_4c/src/__tests__/leap_counter_extension.spec.ts @@ -0,0 +1,9 @@ +/** + * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests + */ + +describe('leap_counter_extension', () => { + it('should be tested', () => { + expect(1 + 1).toEqual(2); + }); +}); diff --git a/compat_4c/src/index.ts b/compat_4c/src/index.ts new file mode 100644 index 00000000..502b1244 --- /dev/null +++ b/compat_4c/src/index.ts @@ -0,0 +1,111 @@ +// This is one of three related extension examples that demonstrate +// JupyterLab's provider-consumer pattern, where plugins can depend +// on and reuse features from one another. The three packages that +// make up the complete example are: +// +// 1. The step_counter package. This package holds a token, a +// class + an interface that make up a stock implementation of +// the "step_counter" service, and a provider plugin that +// makes an instance of the Counter available to JupyterLab +// as a service object. +// 2. The step_counter_extension package, that holds a +// UI/interface in JupyterLab for users to count their steps that +// connects with/consumes the step_counter service object via a +// consumer plugin. +// 3. (*) The leap_counter_extension package (this one), that holds an alternate +// way for users to count steps (a leap is 5 steps). Like the step_counter_extension +// package, this holds a UI/interface in JupyterLab, and a consumer +// plugin that also requests/consumes the step_counter service +// object. The leap_counter_extension package demonstrates how +// an unrelated plugin can depend on and reuse features from +// an existing plugin. Users can install either the +// step_counter_extension, the leap_counter_extension or both +// to get whichever features they prefer (with both reusing +// the step_counter service object). + +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { Widget } from '@lumino/widgets'; + +import { StepCounter} from "step_counter"; + +// This widget holds the JupyterLab UI/interface that users will +// see and interact with to count and view their steps. +class LeapCounterWidget extends Widget { + + leapButton: HTMLElement; + combinedStepCountLabel: HTMLElement; + counter: any; + + // Notice that the constructor for this object takes a "counter" + // argument, which is the service object associated with the StepCounter + // token (which is passed in by the consumer plugin). + constructor(counter: any) { + super(); + + this.counter = counter; + + // Add styling by using a CSS class + this.node.classList.add('jp-leap-container'); + + // Create and add a button to this widget's root node + const leapButton = document.createElement('div'); + leapButton.innerText = 'Take a Leap'; + // Add a listener to handle button clicks + leapButton.addEventListener('click', this.takeLeap.bind(this)); + leapButton.classList.add('jp-leap-button'); + this.node.appendChild(leapButton); + this.leapButton = leapButton; + + // Add a label to display the step count + const combinedStepCountLabel = document.createElement('p'); + combinedStepCountLabel.classList.add('jp-combined-step-count-label'); + this.node.appendChild(combinedStepCountLabel); + this.combinedStepCountLabel = combinedStepCountLabel; + + this.updateStepCountDisplay(); + } + + // Refresh the displayed step count + updateStepCountDisplay() { + this.combinedStepCountLabel.innerText = 'Combined Step Count: ' + this.counter.getStepCount(); + } + + // Increment the step count, a leap is 5 steps + takeLeap() { + this.counter.incrementStepCount(5); + this.updateStepCountDisplay(); + } +} + +// This plugin is a "consumer" in JupyterLab's provider-consumer pattern. +// The "requires" property of this plugin lists the StepCounter token, which +// requests the service-object associated with that token from JupyterLab, +// and this plugin "consumes" the service object by using it in its own code. +// Whenever you add a "requires" or "optional" service, you need to manually +// add an argument to your plugin's "activate" function. +const plugin: JupyterFrontEndPlugin = { + id: 'leap_counter_extension:plugin', + description: 'Adds a leap counter/button (1 of 3 related examples). This extension holds the UI/interface', + autoStart: true, + requires: [StepCounter], + // The activate function here will be called by JupyterLab when the plugin loads. + // When JupyterLab calls your plugin's activate function, it will always pass + // an application as the first argument, then any required arguments, then any optional + // arguments, so make sure you add arguments for those here when your plugin requests + // any required or optional services. If a required service is missing, your plugin + // won't load. If an optional service is missing, the supplied argument will be null. + activate: (app: JupyterFrontEnd, counter: any) => { + console.log('JupyterLab extension leap_counter_extension is activated!'); + + // Create a LeapCounterWidget and add it to the interface + const leapWidget: LeapCounterWidget = new LeapCounterWidget(counter); + leapWidget.id = 'JupyterLeapWidget'; // Widgets need an id + app.shell.add(leapWidget, 'top'); + } +}; + +export default plugin; diff --git a/compat_4c/style/base.css b/compat_4c/style/base.css new file mode 100644 index 00000000..764059d6 --- /dev/null +++ b/compat_4c/style/base.css @@ -0,0 +1,28 @@ +/* + See the JupyterLab Developer Guide for useful CSS Patterns: + + https://jupyterlab.readthedocs.io/en/stable/developer/css.html +*/ + +.jp-leap-container { + display: inline; + user-select: none; +} + +.jp-leap-button { + width: 85px; + margin: 4px; + padding: 2px; + text-align: center; + vertical-align: middle; + border-radius: 2px; + background-color: #00eb00; + color: #212121; + display: inline-block; +} + +.jp-combined-step-count-label { + display: inline-block; + margin: 6px; + vertical-align: middle; +} \ No newline at end of file diff --git a/compat_4c/style/index.css b/compat_4c/style/index.css new file mode 100644 index 00000000..8a7ea29e --- /dev/null +++ b/compat_4c/style/index.css @@ -0,0 +1 @@ +@import url('base.css'); diff --git a/compat_4c/style/index.js b/compat_4c/style/index.js new file mode 100644 index 00000000..a028a764 --- /dev/null +++ b/compat_4c/style/index.js @@ -0,0 +1 @@ +import './base.css'; diff --git a/compat_4c/tsconfig.json b/compat_4c/tsconfig.json new file mode 100644 index 00000000..98979175 --- /dev/null +++ b/compat_4c/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "composite": true, + "declaration": true, + "esModuleInterop": true, + "incremental": true, + "jsx": "react", + "module": "esnext", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "preserveWatchOutput": true, + "resolveJsonModule": true, + "outDir": "lib", + "rootDir": "src", + "strict": true, + "strictNullChecks": true, + "target": "ES2018" + }, + "include": ["src/*"] +} diff --git a/compat_4c/tsconfig.test.json b/compat_4c/tsconfig.test.json new file mode 100644 index 00000000..1de37fd0 --- /dev/null +++ b/compat_4c/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/compat_4c/ui-tests/README.md b/compat_4c/ui-tests/README.md new file mode 100644 index 00000000..dbe6e8aa --- /dev/null +++ b/compat_4c/ui-tests/README.md @@ -0,0 +1,167 @@ +# Integration Testing + +This folder contains the integration tests of the extension. + +They are defined using [Playwright](https://playwright.dev/docs/intro) test runner +and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper. + +The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). + +The JupyterLab server configuration to use for the integration test is defined +in [jupyter_server_test_config.py](./jupyter_server_test_config.py). + +The default configuration will produce video for failing tests and an HTML report. + +> There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). + +## Run the tests + +> All commands are assumed to be executed from the root directory + +To run the tests, you need to: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: + +```sh +cd ./ui-tests +jlpm playwright test +``` + +Test results will be shown in the terminal. In case of any test failures, the test report +will be opened in your browser at the end of the tests execution; see +[Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) +for configuring that behavior. + +## Update the tests snapshots + +> All commands are assumed to be executed from the root directory + +If you are comparing snapshots to validate your tests, you may need to update +the reference snapshots stored in the repository. To do that, you need to: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the [Playwright](https://playwright.dev/docs/intro) command: + +```sh +cd ./ui-tests +jlpm playwright test -u +``` + +> Some discrepancy may occurs between the snapshots generated on your computer and +> the one generated on the CI. To ease updating the snapshots on a PR, you can +> type `please update playwright snapshots` to trigger the update by a bot on the CI. +> Once the bot has computed new snapshots, it will commit them to the PR branch. + +## Create tests + +> All commands are assumed to be executed from the root directory + +To create tests, the easiest way is to use the code generator tool of playwright: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Start the server: + +```sh +cd ./ui-tests +jlpm start +``` + +4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: + +```sh +cd ./ui-tests +jlpm playwright codegen localhost:8888 +``` + +## Debug tests + +> All commands are assumed to be executed from the root directory + +To debug tests, a good way is to use the inspector tool of playwright: + +1. Compile the extension: + +```sh +jlpm install +jlpm build:prod +``` + +> Check the extension is installed in JupyterLab. + +2. Install test dependencies (needed only once): + +```sh +cd ./ui-tests +jlpm install +jlpm playwright install +cd .. +``` + +3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): + +```sh +cd ./ui-tests +jlpm playwright test --debug +``` + +## Upgrade Playwright and the browsers + +To update the web browser versions, you must update the package `@playwright/test`: + +```sh +cd ./ui-tests +jlpm up "@playwright/test" +jlpm playwright install +``` diff --git a/compat_4c/ui-tests/jupyter_server_test_config.py b/compat_4c/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..f2a94782 --- /dev/null +++ b/compat_4c/ui-tests/jupyter_server_test_config.py @@ -0,0 +1,12 @@ +"""Server configuration for integration tests. + +!! Never use this configuration in production because it +opens the server to the world and provide access to JupyterLab +JavaScript objects through the global window variable. +""" +from jupyterlab.galata import configure_jupyter_server + +configure_jupyter_server(c) + +# Uncomment to set server log level to debug level +# c.ServerApp.log_level = "DEBUG" diff --git a/compat_4c/ui-tests/package.json b/compat_4c/ui-tests/package.json new file mode 100644 index 00000000..1cde9069 --- /dev/null +++ b/compat_4c/ui-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "leap_counter_extension-ui-tests", + "version": "1.0.0", + "description": "JupyterLab leap_counter_extension Integration Tests", + "private": true, + "scripts": { + "start": "jupyter lab --config jupyter_server_test_config.py", + "test": "jlpm playwright test", + "test:update": "jlpm playwright test --update-snapshots" + }, + "devDependencies": { + "@jupyterlab/galata": "^5.0.5", + "@playwright/test": "^1.37.0" + } +} diff --git a/compat_4c/ui-tests/playwright.config.js b/compat_4c/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/compat_4c/ui-tests/playwright.config.js @@ -0,0 +1,14 @@ +/** + * Configuration for Playwright using default from @jupyterlab/galata + */ +const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); + +module.exports = { + ...baseConfig, + webServer: { + command: 'jlpm start', + url: 'http://localhost:8888/lab', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + } +}; diff --git a/compat_4c/ui-tests/tests/leap_counter_extension.spec.ts b/compat_4c/ui-tests/tests/leap_counter_extension.spec.ts new file mode 100644 index 00000000..b2d2ddca --- /dev/null +++ b/compat_4c/ui-tests/tests/leap_counter_extension.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@jupyterlab/galata'; + +/** + * Don't load JupyterLab webpage before running the tests. + * This is required to ensure we capture all log messages. + */ +test.use({ autoGoto: false }); + +test('should emit an activation console message', async ({ page }) => { + const logs: string[] = []; + + page.on('console', message => { + logs.push(message.text()); + }); + + await page.goto(); + + expect( + logs.filter(s => s === 'JupyterLab extension leap_counter_extension is activated!') + ).toHaveLength(1); +}); From 76438b980a2be1d61d636f3a4757c4d27dac57f7 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 26 Sep 2023 11:27:29 -0400 Subject: [PATCH 12/31] Added step counter signal for display updates. --- compat_4a/src/index.ts | 5 +++++ compat_4b/src/index.ts | 1 + compat_4c/src/index.ts | 1 + 3 files changed, 7 insertions(+) diff --git a/compat_4a/src/index.ts b/compat_4a/src/index.ts index 134d183d..0d1c7c97 100644 --- a/compat_4a/src/index.ts +++ b/compat_4a/src/index.ts @@ -28,6 +28,7 @@ import { } from '@jupyterlab/application'; import { Token } from '@lumino/coreutils'; +import { Signal } from '@lumino/signaling'; // The StepCounterItem interface is used as part of JupyterLab's // provider-consumer pattern. This interface is supplied to the @@ -39,6 +40,7 @@ interface StepCounterItem { // registerStatusItem(id: string, statusItem: IStatusBar.IItem): IDisposable; getStepCount(): number; incrementStepCount(count: number): void; + countChanged: Signal; } // The token is used to identify a particular "service" in @@ -60,13 +62,16 @@ const StepCounter = new Token( class Counter implements StepCounterItem { _stepCount: number; + countChanged: Signal; constructor() { this._stepCount = 0; + this.countChanged = new Signal(this); } incrementStepCount(count: number) { this._stepCount += count; + this.countChanged.emit(this._stepCount); } getStepCount() { diff --git a/compat_4b/src/index.ts b/compat_4b/src/index.ts index 7f2cacd6..a47f2063 100644 --- a/compat_4b/src/index.ts +++ b/compat_4b/src/index.ts @@ -47,6 +47,7 @@ class StepCounterWidget extends Widget { super(); this.counter = counter; + this.counter.countChanged.connect(this.updateStepCountDisplay, this); // Add styling by using a CSS class this.node.classList.add('jp-step-container'); diff --git a/compat_4c/src/index.ts b/compat_4c/src/index.ts index 502b1244..f74dd0c0 100644 --- a/compat_4c/src/index.ts +++ b/compat_4c/src/index.ts @@ -47,6 +47,7 @@ class LeapCounterWidget extends Widget { super(); this.counter = counter; + this.counter.countChanged.connect(this.updateStepCountDisplay, this); // Add styling by using a CSS class this.node.classList.add('jp-leap-container'); From c0865181a1ad39aeaa2e707bf65d10696236e628 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 28 Sep 2023 12:56:52 -0400 Subject: [PATCH 13/31] Update readme --- compat_4b/README.md | 99 +++------------------------------------------ 1 file changed, 6 insertions(+), 93 deletions(-) diff --git a/compat_4b/README.md b/compat_4b/README.md index adf74bdf..34366674 100644 --- a/compat_4b/README.md +++ b/compat_4b/README.md @@ -1,96 +1,9 @@ # step_counter_extension -[![Github Actions Status](/workflows/Build/badge.svg)](/actions/workflows/build.yml) -Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds the UI/plugin implementation. +This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), +and demonstrates Jupyter's provider/consumer pattern (read about this example +on that page). There are three related extensions in this example: -## Requirements - -- JupyterLab >= 4.0.0 - -## Install - -To install the extension, execute: - -```bash -pip install step_counter_extension -``` - -## Uninstall - -To remove the extension, execute: - -```bash -pip uninstall step_counter_extension -``` - -## Contributing - -### Development install - -Note: You will need NodeJS to build the extension package. - -The `jlpm` command is JupyterLab's pinned version of -[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use -`yarn` or `npm` in lieu of `jlpm` below. - -```bash -# Clone the repo to your local environment -# Change directory to the step_counter_extension directory -# Install package in development mode -pip install -e "." -# Link your development version of the extension with JupyterLab -jupyter labextension develop . --overwrite -# Rebuild extension Typescript source after making changes -jlpm build -``` - -You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. - -```bash -# Watch the source directory in one terminal, automatically rebuilding when needed -jlpm watch -# Run JupyterLab in another terminal -jupyter lab -``` - -With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). - -By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: - -```bash -jupyter lab build --minimize=False -``` - -### Development uninstall - -```bash -pip uninstall step_counter_extension -``` - -In development mode, you will also need to remove the symlink created by `jupyter labextension develop` -command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` -folder is located. Then you can remove the symlink named `step_counter_extension` within that folder. - -### Testing the extension - -#### Frontend tests - -This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. - -To execute them, execute: - -```sh -jlpm -jlpm test -``` - -#### Integration tests - -This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). -More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. - -More information are provided within the [ui-tests](./ui-tests/README.md) README. - -### Packaging the extension - -See [RELEASE](RELEASE.md) +- step_counter +- step_counter_extension +- leap_counter_extension From e46c5b95226f6e79000c298cc28e28f0055881cd Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 28 Sep 2023 12:58:31 -0400 Subject: [PATCH 14/31] Renamed folders to match poackage names. --- {compat_4c => leap_counter_extension}/.copier-answers.yml | 0 {compat_4c => leap_counter_extension}/.gitignore | 0 {compat_4c => leap_counter_extension}/.prettierignore | 0 {compat_4a => leap_counter_extension}/.yarnrc.yml | 0 {compat_4c => leap_counter_extension}/README.md | 0 {compat_4a => leap_counter_extension}/babel.config.js | 0 {compat_4c => leap_counter_extension}/install.json | 0 {compat_4a => leap_counter_extension}/jest.config.js | 0 .../leap_counter_extension/__init__.py | 0 {compat_4c => leap_counter_extension}/package.json | 0 {compat_4c => leap_counter_extension}/pyproject.toml | 0 {compat_4a => leap_counter_extension}/setup.py | 0 .../src/__tests__/leap_counter_extension.spec.ts | 0 {compat_4c => leap_counter_extension}/src/index.ts | 0 {compat_4c => leap_counter_extension}/style/base.css | 0 {compat_4a => leap_counter_extension}/style/index.css | 0 {compat_4a => leap_counter_extension}/style/index.js | 0 {compat_4a => leap_counter_extension}/tsconfig.json | 0 {compat_4a => leap_counter_extension}/tsconfig.test.json | 0 {compat_4a => leap_counter_extension}/ui-tests/README.md | 0 .../ui-tests/jupyter_server_test_config.py | 0 {compat_4c => leap_counter_extension}/ui-tests/package.json | 0 .../ui-tests/playwright.config.js | 0 .../ui-tests/tests/leap_counter_extension.spec.ts | 0 {compat_4a => step_counter}/.copier-answers.yml | 0 {compat_4a => step_counter}/.gitignore | 0 {compat_4a => step_counter}/.prettierignore | 0 {compat_4b => step_counter}/.yarnrc.yml | 0 {compat_4a => step_counter}/README.md | 0 {compat_4b => step_counter}/babel.config.js | 0 {compat_4a => step_counter}/install.json | 0 {compat_4b => step_counter}/jest.config.js | 0 {compat_4a => step_counter}/package.json | 0 {compat_4a => step_counter}/pyproject.toml | 0 {compat_4b => step_counter}/setup.py | 0 {compat_4a => step_counter}/src/__tests__/step_counter.spec.ts | 0 {compat_4a => step_counter}/src/index.ts | 0 {compat_4a => step_counter}/step_counter/__init__.py | 0 {compat_4a => step_counter}/style/base.css | 0 {compat_4b => step_counter}/style/index.css | 0 {compat_4b => step_counter}/style/index.js | 0 {compat_4b => step_counter}/tsconfig.json | 0 {compat_4b => step_counter}/tsconfig.test.json | 0 {compat_4b => step_counter}/ui-tests/README.md | 0 .../ui-tests/jupyter_server_test_config.py | 0 {compat_4a => step_counter}/ui-tests/package.json | 0 {compat_4b => step_counter}/ui-tests/playwright.config.js | 0 {compat_4a => step_counter}/ui-tests/tests/step_counter.spec.ts | 0 {compat_4b => step_counter_extension}/.copier-answers.yml | 0 {compat_4b => step_counter_extension}/.gitignore | 0 {compat_4b => step_counter_extension}/.prettierignore | 0 {compat_4c => step_counter_extension}/.yarnrc.yml | 0 {compat_4b => step_counter_extension}/README.md | 0 {compat_4c => step_counter_extension}/babel.config.js | 0 {compat_4b => step_counter_extension}/install.json | 0 {compat_4c => step_counter_extension}/jest.config.js | 0 {compat_4b => step_counter_extension}/package.json | 0 {compat_4b => step_counter_extension}/pyproject.toml | 0 {compat_4c => step_counter_extension}/setup.py | 0 .../src/__tests__/step_counter_extension.spec.ts | 0 {compat_4b => step_counter_extension}/src/index.ts | 0 .../step_counter_extension/__init__.py | 0 {compat_4b => step_counter_extension}/style/base.css | 0 {compat_4c => step_counter_extension}/style/index.css | 0 {compat_4c => step_counter_extension}/style/index.js | 0 {compat_4c => step_counter_extension}/tsconfig.json | 0 {compat_4c => step_counter_extension}/tsconfig.test.json | 0 {compat_4c => step_counter_extension}/ui-tests/README.md | 0 .../ui-tests/jupyter_server_test_config.py | 0 {compat_4b => step_counter_extension}/ui-tests/package.json | 0 .../ui-tests/playwright.config.js | 0 .../ui-tests/tests/step_counter_extension.spec.ts | 0 72 files changed, 0 insertions(+), 0 deletions(-) rename {compat_4c => leap_counter_extension}/.copier-answers.yml (100%) rename {compat_4c => leap_counter_extension}/.gitignore (100%) rename {compat_4c => leap_counter_extension}/.prettierignore (100%) rename {compat_4a => leap_counter_extension}/.yarnrc.yml (100%) rename {compat_4c => leap_counter_extension}/README.md (100%) rename {compat_4a => leap_counter_extension}/babel.config.js (100%) rename {compat_4c => leap_counter_extension}/install.json (100%) rename {compat_4a => leap_counter_extension}/jest.config.js (100%) rename {compat_4c => leap_counter_extension}/leap_counter_extension/__init__.py (100%) rename {compat_4c => leap_counter_extension}/package.json (100%) rename {compat_4c => leap_counter_extension}/pyproject.toml (100%) rename {compat_4a => leap_counter_extension}/setup.py (100%) rename {compat_4c => leap_counter_extension}/src/__tests__/leap_counter_extension.spec.ts (100%) rename {compat_4c => leap_counter_extension}/src/index.ts (100%) rename {compat_4c => leap_counter_extension}/style/base.css (100%) rename {compat_4a => leap_counter_extension}/style/index.css (100%) rename {compat_4a => leap_counter_extension}/style/index.js (100%) rename {compat_4a => leap_counter_extension}/tsconfig.json (100%) rename {compat_4a => leap_counter_extension}/tsconfig.test.json (100%) rename {compat_4a => leap_counter_extension}/ui-tests/README.md (100%) rename {compat_4a => leap_counter_extension}/ui-tests/jupyter_server_test_config.py (100%) rename {compat_4c => leap_counter_extension}/ui-tests/package.json (100%) rename {compat_4a => leap_counter_extension}/ui-tests/playwright.config.js (100%) rename {compat_4c => leap_counter_extension}/ui-tests/tests/leap_counter_extension.spec.ts (100%) rename {compat_4a => step_counter}/.copier-answers.yml (100%) rename {compat_4a => step_counter}/.gitignore (100%) rename {compat_4a => step_counter}/.prettierignore (100%) rename {compat_4b => step_counter}/.yarnrc.yml (100%) rename {compat_4a => step_counter}/README.md (100%) rename {compat_4b => step_counter}/babel.config.js (100%) rename {compat_4a => step_counter}/install.json (100%) rename {compat_4b => step_counter}/jest.config.js (100%) rename {compat_4a => step_counter}/package.json (100%) rename {compat_4a => step_counter}/pyproject.toml (100%) rename {compat_4b => step_counter}/setup.py (100%) rename {compat_4a => step_counter}/src/__tests__/step_counter.spec.ts (100%) rename {compat_4a => step_counter}/src/index.ts (100%) rename {compat_4a => step_counter}/step_counter/__init__.py (100%) rename {compat_4a => step_counter}/style/base.css (100%) rename {compat_4b => step_counter}/style/index.css (100%) rename {compat_4b => step_counter}/style/index.js (100%) rename {compat_4b => step_counter}/tsconfig.json (100%) rename {compat_4b => step_counter}/tsconfig.test.json (100%) rename {compat_4b => step_counter}/ui-tests/README.md (100%) rename {compat_4b => step_counter}/ui-tests/jupyter_server_test_config.py (100%) rename {compat_4a => step_counter}/ui-tests/package.json (100%) rename {compat_4b => step_counter}/ui-tests/playwright.config.js (100%) rename {compat_4a => step_counter}/ui-tests/tests/step_counter.spec.ts (100%) rename {compat_4b => step_counter_extension}/.copier-answers.yml (100%) rename {compat_4b => step_counter_extension}/.gitignore (100%) rename {compat_4b => step_counter_extension}/.prettierignore (100%) rename {compat_4c => step_counter_extension}/.yarnrc.yml (100%) rename {compat_4b => step_counter_extension}/README.md (100%) rename {compat_4c => step_counter_extension}/babel.config.js (100%) rename {compat_4b => step_counter_extension}/install.json (100%) rename {compat_4c => step_counter_extension}/jest.config.js (100%) rename {compat_4b => step_counter_extension}/package.json (100%) rename {compat_4b => step_counter_extension}/pyproject.toml (100%) rename {compat_4c => step_counter_extension}/setup.py (100%) rename {compat_4b => step_counter_extension}/src/__tests__/step_counter_extension.spec.ts (100%) rename {compat_4b => step_counter_extension}/src/index.ts (100%) rename {compat_4b => step_counter_extension}/step_counter_extension/__init__.py (100%) rename {compat_4b => step_counter_extension}/style/base.css (100%) rename {compat_4c => step_counter_extension}/style/index.css (100%) rename {compat_4c => step_counter_extension}/style/index.js (100%) rename {compat_4c => step_counter_extension}/tsconfig.json (100%) rename {compat_4c => step_counter_extension}/tsconfig.test.json (100%) rename {compat_4c => step_counter_extension}/ui-tests/README.md (100%) rename {compat_4c => step_counter_extension}/ui-tests/jupyter_server_test_config.py (100%) rename {compat_4b => step_counter_extension}/ui-tests/package.json (100%) rename {compat_4c => step_counter_extension}/ui-tests/playwright.config.js (100%) rename {compat_4b => step_counter_extension}/ui-tests/tests/step_counter_extension.spec.ts (100%) diff --git a/compat_4c/.copier-answers.yml b/leap_counter_extension/.copier-answers.yml similarity index 100% rename from compat_4c/.copier-answers.yml rename to leap_counter_extension/.copier-answers.yml diff --git a/compat_4c/.gitignore b/leap_counter_extension/.gitignore similarity index 100% rename from compat_4c/.gitignore rename to leap_counter_extension/.gitignore diff --git a/compat_4c/.prettierignore b/leap_counter_extension/.prettierignore similarity index 100% rename from compat_4c/.prettierignore rename to leap_counter_extension/.prettierignore diff --git a/compat_4a/.yarnrc.yml b/leap_counter_extension/.yarnrc.yml similarity index 100% rename from compat_4a/.yarnrc.yml rename to leap_counter_extension/.yarnrc.yml diff --git a/compat_4c/README.md b/leap_counter_extension/README.md similarity index 100% rename from compat_4c/README.md rename to leap_counter_extension/README.md diff --git a/compat_4a/babel.config.js b/leap_counter_extension/babel.config.js similarity index 100% rename from compat_4a/babel.config.js rename to leap_counter_extension/babel.config.js diff --git a/compat_4c/install.json b/leap_counter_extension/install.json similarity index 100% rename from compat_4c/install.json rename to leap_counter_extension/install.json diff --git a/compat_4a/jest.config.js b/leap_counter_extension/jest.config.js similarity index 100% rename from compat_4a/jest.config.js rename to leap_counter_extension/jest.config.js diff --git a/compat_4c/leap_counter_extension/__init__.py b/leap_counter_extension/leap_counter_extension/__init__.py similarity index 100% rename from compat_4c/leap_counter_extension/__init__.py rename to leap_counter_extension/leap_counter_extension/__init__.py diff --git a/compat_4c/package.json b/leap_counter_extension/package.json similarity index 100% rename from compat_4c/package.json rename to leap_counter_extension/package.json diff --git a/compat_4c/pyproject.toml b/leap_counter_extension/pyproject.toml similarity index 100% rename from compat_4c/pyproject.toml rename to leap_counter_extension/pyproject.toml diff --git a/compat_4a/setup.py b/leap_counter_extension/setup.py similarity index 100% rename from compat_4a/setup.py rename to leap_counter_extension/setup.py diff --git a/compat_4c/src/__tests__/leap_counter_extension.spec.ts b/leap_counter_extension/src/__tests__/leap_counter_extension.spec.ts similarity index 100% rename from compat_4c/src/__tests__/leap_counter_extension.spec.ts rename to leap_counter_extension/src/__tests__/leap_counter_extension.spec.ts diff --git a/compat_4c/src/index.ts b/leap_counter_extension/src/index.ts similarity index 100% rename from compat_4c/src/index.ts rename to leap_counter_extension/src/index.ts diff --git a/compat_4c/style/base.css b/leap_counter_extension/style/base.css similarity index 100% rename from compat_4c/style/base.css rename to leap_counter_extension/style/base.css diff --git a/compat_4a/style/index.css b/leap_counter_extension/style/index.css similarity index 100% rename from compat_4a/style/index.css rename to leap_counter_extension/style/index.css diff --git a/compat_4a/style/index.js b/leap_counter_extension/style/index.js similarity index 100% rename from compat_4a/style/index.js rename to leap_counter_extension/style/index.js diff --git a/compat_4a/tsconfig.json b/leap_counter_extension/tsconfig.json similarity index 100% rename from compat_4a/tsconfig.json rename to leap_counter_extension/tsconfig.json diff --git a/compat_4a/tsconfig.test.json b/leap_counter_extension/tsconfig.test.json similarity index 100% rename from compat_4a/tsconfig.test.json rename to leap_counter_extension/tsconfig.test.json diff --git a/compat_4a/ui-tests/README.md b/leap_counter_extension/ui-tests/README.md similarity index 100% rename from compat_4a/ui-tests/README.md rename to leap_counter_extension/ui-tests/README.md diff --git a/compat_4a/ui-tests/jupyter_server_test_config.py b/leap_counter_extension/ui-tests/jupyter_server_test_config.py similarity index 100% rename from compat_4a/ui-tests/jupyter_server_test_config.py rename to leap_counter_extension/ui-tests/jupyter_server_test_config.py diff --git a/compat_4c/ui-tests/package.json b/leap_counter_extension/ui-tests/package.json similarity index 100% rename from compat_4c/ui-tests/package.json rename to leap_counter_extension/ui-tests/package.json diff --git a/compat_4a/ui-tests/playwright.config.js b/leap_counter_extension/ui-tests/playwright.config.js similarity index 100% rename from compat_4a/ui-tests/playwright.config.js rename to leap_counter_extension/ui-tests/playwright.config.js diff --git a/compat_4c/ui-tests/tests/leap_counter_extension.spec.ts b/leap_counter_extension/ui-tests/tests/leap_counter_extension.spec.ts similarity index 100% rename from compat_4c/ui-tests/tests/leap_counter_extension.spec.ts rename to leap_counter_extension/ui-tests/tests/leap_counter_extension.spec.ts diff --git a/compat_4a/.copier-answers.yml b/step_counter/.copier-answers.yml similarity index 100% rename from compat_4a/.copier-answers.yml rename to step_counter/.copier-answers.yml diff --git a/compat_4a/.gitignore b/step_counter/.gitignore similarity index 100% rename from compat_4a/.gitignore rename to step_counter/.gitignore diff --git a/compat_4a/.prettierignore b/step_counter/.prettierignore similarity index 100% rename from compat_4a/.prettierignore rename to step_counter/.prettierignore diff --git a/compat_4b/.yarnrc.yml b/step_counter/.yarnrc.yml similarity index 100% rename from compat_4b/.yarnrc.yml rename to step_counter/.yarnrc.yml diff --git a/compat_4a/README.md b/step_counter/README.md similarity index 100% rename from compat_4a/README.md rename to step_counter/README.md diff --git a/compat_4b/babel.config.js b/step_counter/babel.config.js similarity index 100% rename from compat_4b/babel.config.js rename to step_counter/babel.config.js diff --git a/compat_4a/install.json b/step_counter/install.json similarity index 100% rename from compat_4a/install.json rename to step_counter/install.json diff --git a/compat_4b/jest.config.js b/step_counter/jest.config.js similarity index 100% rename from compat_4b/jest.config.js rename to step_counter/jest.config.js diff --git a/compat_4a/package.json b/step_counter/package.json similarity index 100% rename from compat_4a/package.json rename to step_counter/package.json diff --git a/compat_4a/pyproject.toml b/step_counter/pyproject.toml similarity index 100% rename from compat_4a/pyproject.toml rename to step_counter/pyproject.toml diff --git a/compat_4b/setup.py b/step_counter/setup.py similarity index 100% rename from compat_4b/setup.py rename to step_counter/setup.py diff --git a/compat_4a/src/__tests__/step_counter.spec.ts b/step_counter/src/__tests__/step_counter.spec.ts similarity index 100% rename from compat_4a/src/__tests__/step_counter.spec.ts rename to step_counter/src/__tests__/step_counter.spec.ts diff --git a/compat_4a/src/index.ts b/step_counter/src/index.ts similarity index 100% rename from compat_4a/src/index.ts rename to step_counter/src/index.ts diff --git a/compat_4a/step_counter/__init__.py b/step_counter/step_counter/__init__.py similarity index 100% rename from compat_4a/step_counter/__init__.py rename to step_counter/step_counter/__init__.py diff --git a/compat_4a/style/base.css b/step_counter/style/base.css similarity index 100% rename from compat_4a/style/base.css rename to step_counter/style/base.css diff --git a/compat_4b/style/index.css b/step_counter/style/index.css similarity index 100% rename from compat_4b/style/index.css rename to step_counter/style/index.css diff --git a/compat_4b/style/index.js b/step_counter/style/index.js similarity index 100% rename from compat_4b/style/index.js rename to step_counter/style/index.js diff --git a/compat_4b/tsconfig.json b/step_counter/tsconfig.json similarity index 100% rename from compat_4b/tsconfig.json rename to step_counter/tsconfig.json diff --git a/compat_4b/tsconfig.test.json b/step_counter/tsconfig.test.json similarity index 100% rename from compat_4b/tsconfig.test.json rename to step_counter/tsconfig.test.json diff --git a/compat_4b/ui-tests/README.md b/step_counter/ui-tests/README.md similarity index 100% rename from compat_4b/ui-tests/README.md rename to step_counter/ui-tests/README.md diff --git a/compat_4b/ui-tests/jupyter_server_test_config.py b/step_counter/ui-tests/jupyter_server_test_config.py similarity index 100% rename from compat_4b/ui-tests/jupyter_server_test_config.py rename to step_counter/ui-tests/jupyter_server_test_config.py diff --git a/compat_4a/ui-tests/package.json b/step_counter/ui-tests/package.json similarity index 100% rename from compat_4a/ui-tests/package.json rename to step_counter/ui-tests/package.json diff --git a/compat_4b/ui-tests/playwright.config.js b/step_counter/ui-tests/playwright.config.js similarity index 100% rename from compat_4b/ui-tests/playwright.config.js rename to step_counter/ui-tests/playwright.config.js diff --git a/compat_4a/ui-tests/tests/step_counter.spec.ts b/step_counter/ui-tests/tests/step_counter.spec.ts similarity index 100% rename from compat_4a/ui-tests/tests/step_counter.spec.ts rename to step_counter/ui-tests/tests/step_counter.spec.ts diff --git a/compat_4b/.copier-answers.yml b/step_counter_extension/.copier-answers.yml similarity index 100% rename from compat_4b/.copier-answers.yml rename to step_counter_extension/.copier-answers.yml diff --git a/compat_4b/.gitignore b/step_counter_extension/.gitignore similarity index 100% rename from compat_4b/.gitignore rename to step_counter_extension/.gitignore diff --git a/compat_4b/.prettierignore b/step_counter_extension/.prettierignore similarity index 100% rename from compat_4b/.prettierignore rename to step_counter_extension/.prettierignore diff --git a/compat_4c/.yarnrc.yml b/step_counter_extension/.yarnrc.yml similarity index 100% rename from compat_4c/.yarnrc.yml rename to step_counter_extension/.yarnrc.yml diff --git a/compat_4b/README.md b/step_counter_extension/README.md similarity index 100% rename from compat_4b/README.md rename to step_counter_extension/README.md diff --git a/compat_4c/babel.config.js b/step_counter_extension/babel.config.js similarity index 100% rename from compat_4c/babel.config.js rename to step_counter_extension/babel.config.js diff --git a/compat_4b/install.json b/step_counter_extension/install.json similarity index 100% rename from compat_4b/install.json rename to step_counter_extension/install.json diff --git a/compat_4c/jest.config.js b/step_counter_extension/jest.config.js similarity index 100% rename from compat_4c/jest.config.js rename to step_counter_extension/jest.config.js diff --git a/compat_4b/package.json b/step_counter_extension/package.json similarity index 100% rename from compat_4b/package.json rename to step_counter_extension/package.json diff --git a/compat_4b/pyproject.toml b/step_counter_extension/pyproject.toml similarity index 100% rename from compat_4b/pyproject.toml rename to step_counter_extension/pyproject.toml diff --git a/compat_4c/setup.py b/step_counter_extension/setup.py similarity index 100% rename from compat_4c/setup.py rename to step_counter_extension/setup.py diff --git a/compat_4b/src/__tests__/step_counter_extension.spec.ts b/step_counter_extension/src/__tests__/step_counter_extension.spec.ts similarity index 100% rename from compat_4b/src/__tests__/step_counter_extension.spec.ts rename to step_counter_extension/src/__tests__/step_counter_extension.spec.ts diff --git a/compat_4b/src/index.ts b/step_counter_extension/src/index.ts similarity index 100% rename from compat_4b/src/index.ts rename to step_counter_extension/src/index.ts diff --git a/compat_4b/step_counter_extension/__init__.py b/step_counter_extension/step_counter_extension/__init__.py similarity index 100% rename from compat_4b/step_counter_extension/__init__.py rename to step_counter_extension/step_counter_extension/__init__.py diff --git a/compat_4b/style/base.css b/step_counter_extension/style/base.css similarity index 100% rename from compat_4b/style/base.css rename to step_counter_extension/style/base.css diff --git a/compat_4c/style/index.css b/step_counter_extension/style/index.css similarity index 100% rename from compat_4c/style/index.css rename to step_counter_extension/style/index.css diff --git a/compat_4c/style/index.js b/step_counter_extension/style/index.js similarity index 100% rename from compat_4c/style/index.js rename to step_counter_extension/style/index.js diff --git a/compat_4c/tsconfig.json b/step_counter_extension/tsconfig.json similarity index 100% rename from compat_4c/tsconfig.json rename to step_counter_extension/tsconfig.json diff --git a/compat_4c/tsconfig.test.json b/step_counter_extension/tsconfig.test.json similarity index 100% rename from compat_4c/tsconfig.test.json rename to step_counter_extension/tsconfig.test.json diff --git a/compat_4c/ui-tests/README.md b/step_counter_extension/ui-tests/README.md similarity index 100% rename from compat_4c/ui-tests/README.md rename to step_counter_extension/ui-tests/README.md diff --git a/compat_4c/ui-tests/jupyter_server_test_config.py b/step_counter_extension/ui-tests/jupyter_server_test_config.py similarity index 100% rename from compat_4c/ui-tests/jupyter_server_test_config.py rename to step_counter_extension/ui-tests/jupyter_server_test_config.py diff --git a/compat_4b/ui-tests/package.json b/step_counter_extension/ui-tests/package.json similarity index 100% rename from compat_4b/ui-tests/package.json rename to step_counter_extension/ui-tests/package.json diff --git a/compat_4c/ui-tests/playwright.config.js b/step_counter_extension/ui-tests/playwright.config.js similarity index 100% rename from compat_4c/ui-tests/playwright.config.js rename to step_counter_extension/ui-tests/playwright.config.js diff --git a/compat_4b/ui-tests/tests/step_counter_extension.spec.ts b/step_counter_extension/ui-tests/tests/step_counter_extension.spec.ts similarity index 100% rename from compat_4b/ui-tests/tests/step_counter_extension.spec.ts rename to step_counter_extension/ui-tests/tests/step_counter_extension.spec.ts From 9645adf7411ff449f8b4e3c9bb3815ef8cd1a9b3 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 28 Sep 2023 13:00:48 -0400 Subject: [PATCH 15/31] Updated local dependency paths. --- leap_counter_extension/package.json | 2 +- step_counter_extension/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/leap_counter_extension/package.json b/leap_counter_extension/package.json index 0177c85b..138953e8 100644 --- a/leap_counter_extension/package.json +++ b/leap_counter_extension/package.json @@ -60,7 +60,7 @@ "dependencies": { "@jupyterlab/application": "^4.0.0", "@lumino/widgets": "^2.0.0", - "step_counter": "file:./../compat_4a" + "step_counter": "file:./../step_counter" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", diff --git a/step_counter_extension/package.json b/step_counter_extension/package.json index 8f717e18..a086c996 100644 --- a/step_counter_extension/package.json +++ b/step_counter_extension/package.json @@ -60,7 +60,7 @@ "dependencies": { "@jupyterlab/application": "^4.0.0", "@lumino/widgets": "^2.0.0", - "step_counter": "file:./../compat_4a" + "step_counter": "file:./../step_counter" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", From 8bab7f783a695054b9e5337b0cb628bd981a53a8 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 28 Sep 2023 13:11:50 -0400 Subject: [PATCH 16/31] Updated extension specific READMEs. --- leap_counter_extension/README.md | 124 ++++++++----------------------- step_counter/README.md | 123 ++++++++---------------------- step_counter_extension/README.md | 33 ++++++-- 3 files changed, 87 insertions(+), 193 deletions(-) diff --git a/leap_counter_extension/README.md b/leap_counter_extension/README.md index ddcc10fd..195db078 100644 --- a/leap_counter_extension/README.md +++ b/leap_counter_extension/README.md @@ -1,96 +1,32 @@ # leap_counter_extension -[![Github Actions Status](/workflows/Build/badge.svg)](/actions/workflows/build.yml) -Adds a leap counter/button (1 of 3 related examples). This extension holds the UI/interface. - -## Requirements - -- JupyterLab >= 4.0.0 - -## Install - -To install the extension, execute: - -```bash -pip install leap_counter_extension -``` - -## Uninstall - -To remove the extension, execute: - -```bash -pip uninstall leap_counter_extension -``` - -## Contributing - -### Development install - -Note: You will need NodeJS to build the extension package. - -The `jlpm` command is JupyterLab's pinned version of -[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use -`yarn` or `npm` in lieu of `jlpm` below. - -```bash -# Clone the repo to your local environment -# Change directory to the leap_counter_extension directory -# Install package in development mode -pip install -e "." -# Link your development version of the extension with JupyterLab -jupyter labextension develop . --overwrite -# Rebuild extension Typescript source after making changes -jlpm build -``` - -You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. - -```bash -# Watch the source directory in one terminal, automatically rebuilding when needed -jlpm watch -# Run JupyterLab in another terminal -jupyter lab -``` - -With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). - -By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: - -```bash -jupyter lab build --minimize=False -``` - -### Development uninstall - -```bash -pip uninstall leap_counter_extension -``` - -In development mode, you will also need to remove the symlink created by `jupyter labextension develop` -command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` -folder is located. Then you can remove the symlink named `leap_counter_extension` within that folder. - -### Testing the extension - -#### Frontend tests - -This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. - -To execute them, execute: - -```sh -jlpm -jlpm test -``` - -#### Integration tests - -This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). -More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. - -More information are provided within the [ui-tests](./ui-tests/README.md) README. - -### Packaging the extension - -See [RELEASE](RELEASE.md) +This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), +and demonstrates Jupyter's provider/consumer pattern. You can find +details about this example on that page. + +This is one of three related extension examples that demonstrate +JupyterLab's provider-consumer pattern, where plugins can depend +on and reuse features from one another. The three packages that +make up the complete example are: + + 1. The step_counter package. This package holds a token, a + class + an interface that make up a stock implementation of + the "step_counter" service, and a provider plugin that + makes an instance of the Counter available to JupyterLab + as a service object. + + 2. The step_counter_extension package, that holds a + UI/interface in JupyterLab for users to count their steps that + connects with/consumes the step_counter service object via a + consumer plugin. + + 3. (*) The leap_counter_extension package (this one), that holds an alternate + way for users to count steps (a leap is 5 steps). Like the step_counter_extension + package, this holds a UI/interface in JupyterLab, and a consumer + plugin that also requests/consumes the step_counter service + object. The leap_counter_extension package demonstrates how + an unrelated plugin can depend on and reuse features from + an existing plugin. Users can install either the + step_counter_extension, the leap_counter_extension or both + to get whichever features they prefer (with both reusing + the step_counter service object). diff --git a/step_counter/README.md b/step_counter/README.md index 53e2bda3..fc36a6bc 100644 --- a/step_counter/README.md +++ b/step_counter/README.md @@ -1,96 +1,31 @@ # step_counter -[![Github Actions Status](/workflows/Build/badge.svg)](/actions/workflows/build.yml) -Adds a step counter/button, and a step increment provider (1 of 3 related examples). This extension holds a provider token. - -## Requirements - -- JupyterLab >= 4.0.0 - -## Install - -To install the extension, execute: - -```bash -pip install step_counter -``` - -## Uninstall - -To remove the extension, execute: - -```bash -pip uninstall step_counter -``` - -## Contributing - -### Development install - -Note: You will need NodeJS to build the extension package. - -The `jlpm` command is JupyterLab's pinned version of -[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use -`yarn` or `npm` in lieu of `jlpm` below. - -```bash -# Clone the repo to your local environment -# Change directory to the step_counter directory -# Install package in development mode -pip install -e "." -# Link your development version of the extension with JupyterLab -jupyter labextension develop . --overwrite -# Rebuild extension Typescript source after making changes -jlpm build -``` - -You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. - -```bash -# Watch the source directory in one terminal, automatically rebuilding when needed -jlpm watch -# Run JupyterLab in another terminal -jupyter lab -``` - -With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). - -By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: - -```bash -jupyter lab build --minimize=False -``` - -### Development uninstall - -```bash -pip uninstall step_counter -``` - -In development mode, you will also need to remove the symlink created by `jupyter labextension develop` -command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` -folder is located. Then you can remove the symlink named `step_counter` within that folder. - -### Testing the extension - -#### Frontend tests - -This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. - -To execute them, execute: - -```sh -jlpm -jlpm test -``` - -#### Integration tests - -This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). -More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. - -More information are provided within the [ui-tests](./ui-tests/README.md) README. - -### Packaging the extension - -See [RELEASE](RELEASE.md) +This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), +and demonstrates Jupyter's provider/consumer pattern. You can find +details about this example on that page. + +This is one of three related extension examples that demonstrate +JupyterLab's provider-consumer pattern, where plugins can depend +on and reuse features from one another. The three packages that +make up the complete example are: + + 1. (*) The step_counter package (this one). This holds a token, a + class + an interface that make up a stock implementation of + the "step_counter" service, and a provider plugin that + makes an instance of the Counter available to JupyterLab + as a service object. + + 2. The step_counter_extension package, that holds a UI/interface + in JupyterLab for users to count their steps that connects + with/consumes the step_counter service object via a consumer plugin. + + 3. The leap_counter_extension package, that holds an alternate + way for users to count steps (a leap is 5 steps). Like the step_counter_extension + package, this holds a UI/interface in JupyterLab, and a consumer + plugin that also requests/consumes the step_counter service + object. The leap_counter_extension package demonstrates how + an unrelated plugin can depend on and reuse features from + an existing plugin. Users can install either the + step_counter_extension, the leap_counter_extension or both + to get whichever features they prefer (with both reusing + the step_counter service object). diff --git a/step_counter_extension/README.md b/step_counter_extension/README.md index 34366674..8862cbbf 100644 --- a/step_counter_extension/README.md +++ b/step_counter_extension/README.md @@ -1,9 +1,32 @@ # step_counter_extension This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), -and demonstrates Jupyter's provider/consumer pattern (read about this example -on that page). There are three related extensions in this example: +and demonstrates Jupyter's provider/consumer pattern. You can find +details about this example on that page. -- step_counter -- step_counter_extension -- leap_counter_extension +This is one of three related extension examples that demonstrate +JupyterLab's provider-consumer pattern, where plugins can depend +on and reuse features from one another. The three packages that +make up the complete example are: + + 1. The step_counter package. This package holds a token, a + class + an interface that make up a stock implementation of + the "step_counter" service, and a provider plugin that + makes an instance of the Counter available to JupyterLab + as a service object. + + 2. (*) The step_counter_extension package (this one), that holds a + UI/interface in JupyterLab for users to count their steps that + connects with/consumes the step_counter service object via a + consumer plugin. + + 3. The leap_counter_extension package, that holds an alternate + way for users to count steps (a leap is 5 steps). Like the step_counter_extension + package, this holds a UI/interface in JupyterLab, and a consumer + plugin that also requests/consumes the step_counter service + object. The leap_counter_extension package demonstrates how + an unrelated plugin can depend on and reuse features from + an existing plugin. Users can install either the + step_counter_extension, the leap_counter_extension or both + to get whichever features they prefer (with both reusing + the step_counter service object). From 44238f137ca0a4a0c26727f9e49a0a595487e892 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 28 Sep 2023 15:10:14 -0400 Subject: [PATCH 17/31] Added preview, updated README. --- README.md | 36 ++++++++++++++++++++++++++++++++++++ step_counter/preview.png | Bin 0 -> 111589 bytes 2 files changed, 36 insertions(+) create mode 100644 step_counter/preview.png diff --git a/README.md b/README.md index e28e0c37..fc521dfd 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Start with the [Hello World](hello-world) and then jump to the topic you are int - [Kernel Messaging](kernel-messaging) - [Kernel Output](kernel-output) - [Launcher](launcher) +- [Leap Counter](leap_counter_extension) - [Log Messages](log-messages) - [Main Menu](main-menu) - [Metadata Form](metadata-form) @@ -114,6 +115,8 @@ Start with the [Hello World](hello-world) and then jump to the topic you are int - [Server Hello World](server-extension) - [Settings](settings) - [Signals](signals) +- [Step Counter](step_counter) +- [Step Counter](step_counter_extension) - [State](state) - [Toolbar item](toolbar-button) - [Widgets](widgets) @@ -207,6 +210,17 @@ Start your extension from the Launcher. [![Launcher](launcher/preview.gif)](launcher) +### [Leap Counter Extension](leap_counter_extension) + +Create your own reusable plugin components with Jupyter's "Provider- +Consumer Pattern". This is one of three related extension examples +that demonstrate JupyterLab's provider-consumer pattern, where plugins +can depend on and reuse features from one another. The three packages +that make up the complete example are "step_counter", "step_counter_extension", +and "leap_counter_extension". + +[![Leap counter extension](step_counter/preview.png)](leap_counter_extension) + ### [Log Messages](log-messages) Send a log message to the log console. @@ -261,6 +275,28 @@ Use Signals to allow Widgets communicate with each others. [![Button with Signal](signals/preview.png)](signals) +### [Step Counter](step_counter) + +Create your own reusable plugin components with Jupyter's "Provider- +Consumer Pattern". This is one of three related extension examples +that demonstrate JupyterLab's provider-consumer pattern, where plugins +can depend on and reuse features from one another. The three packages +that make up the complete example are "step_counter", "step_counter_extension", +and "leap_counter_extension". + +[![Step counter](step_counter/preview.png)](step_counter) + +### [Step Counter Extension](step_counter_extension) + +Create your own reusable plugin components with Jupyter's "Provider- +Consumer Pattern". This is one of three related extension examples +that demonstrate JupyterLab's provider-consumer pattern, where plugins +can depend on and reuse features from one another. The three packages +that make up the complete example are "step_counter", "step_counter_extension", +and "leap_counter_extension". + +[![Step counter extension](step_counter/preview.png)](step_counter_extension) + ### [State](state) Use State persistence in an extension. diff --git a/step_counter/preview.png b/step_counter/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..55c1c84478219e2c4618b769af6ad714576c8bcd GIT binary patch literal 111589 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 iSzh7Wr*lDhftSD=|NbaiNA(K$k$$T1q) Date: Thu, 28 Sep 2023 15:28:12 -0400 Subject: [PATCH 18/31] Added singleton package metadata notes. --- step_counter/src/index.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/step_counter/src/index.ts b/step_counter/src/index.ts index 0d1c7c97..d0f52b42 100644 --- a/step_counter/src/index.ts +++ b/step_counter/src/index.ts @@ -104,5 +104,26 @@ const plugin: JupyterFrontEndPlugin = { } }; +// The Counter class here should be marked as a singleton in this project's +// package.json according to the Jupyter extension documentation, both in +// this provider extension and in the consumer extensions. Under the +// "Jupyterlab" key you should have a "singleton" key set to true. +// +// "jupyterlab": { +// "extension": true, +// "outputDir": "step_counter/labextension", +// "sharedPackages": { +// "step_counter": { +// "bundled": false, +// "singleton": true +// } +// } +// }, +// +// Read more about that here: +// https://jupyterlab.readthedocs.io/en/latest/extension/extension_dev.html#providing-a-service +// https://jupyterlab.readthedocs.io/en/latest/extension/extension_dev.html#requiring-a-service +// https://jupyterlab.readthedocs.io/en/latest/extension/extension_dev.html#optionally-using-a-service + export { StepCounter, StepCounterItem }; export default plugin; From 8b3872c548aef1d53e92bca32a2a1952fce91890 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 29 Sep 2023 12:19:53 -0400 Subject: [PATCH 19/31] Update README/fix links. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fc521dfd..58c525d7 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ 1. [Metadata Form](#metadata-form) 1. [MIME Renderer](#mime-renderer) 1. [Notifications](#notifications) + 1. [Step Counter](#step-counter) + 1. [Step Counter Extension](#step-counter-extension) 1. [React Widget](#react-widget) 1. _[Server Hello World](#server-hello-world)_ 1. [Settings](#settings) @@ -116,7 +118,7 @@ Start with the [Hello World](hello-world) and then jump to the topic you are int - [Settings](settings) - [Signals](signals) - [Step Counter](step_counter) -- [Step Counter](step_counter_extension) +- [Step Counter Extension](step_counter_extension) - [State](state) - [Toolbar item](toolbar-button) - [Widgets](widgets) From afee7d7bc79830094b39bec9169ff27a1e50c7a3 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 29 Sep 2023 12:33:16 -0400 Subject: [PATCH 20/31] Updated titles/links. --- README.md | 11 ++++++----- leap_counter_extension/README.md | 2 +- step_counter/README.md | 2 +- step_counter_extension/README.md | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 58c525d7..10b05221 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,14 @@ 1. [Kernel Messaging](#kernel-messaging) 1. [Kernel Output](#kernel-output) 1. [Launcher](#launcher) + 1. [Leap Counter (Reusability 1C)](#leap-counter-reusability-1c) 1. [Log Messages](#log-messages) 1. [Main Menu](#main-menu) 1. [Metadata Form](#metadata-form) 1. [MIME Renderer](#mime-renderer) 1. [Notifications](#notifications) - 1. [Step Counter](#step-counter) - 1. [Step Counter Extension](#step-counter-extension) + 1. [Step Counter (Reusability 1A)](#step-counter-reusability-1a) + 1. [Step Counter Extension (Reusability 1B)](#step-counter-extension-reusability-1b) 1. [React Widget](#react-widget) 1. _[Server Hello World](#server-hello-world)_ 1. [Settings](#settings) @@ -212,7 +213,7 @@ Start your extension from the Launcher. [![Launcher](launcher/preview.gif)](launcher) -### [Leap Counter Extension](leap_counter_extension) +### [Leap Counter (Reusability 1C)](leap_counter_extension) Create your own reusable plugin components with Jupyter's "Provider- Consumer Pattern". This is one of three related extension examples @@ -277,7 +278,7 @@ Use Signals to allow Widgets communicate with each others. [![Button with Signal](signals/preview.png)](signals) -### [Step Counter](step_counter) +### [Step Counter (Reusability 1A)](step_counter) Create your own reusable plugin components with Jupyter's "Provider- Consumer Pattern". This is one of three related extension examples @@ -288,7 +289,7 @@ and "leap_counter_extension". [![Step counter](step_counter/preview.png)](step_counter) -### [Step Counter Extension](step_counter_extension) +### [Step Counter Extension (Reusability 1B)](step_counter_extension) Create your own reusable plugin components with Jupyter's "Provider- Consumer Pattern". This is one of three related extension examples diff --git a/leap_counter_extension/README.md b/leap_counter_extension/README.md index 195db078..785d5252 100644 --- a/leap_counter_extension/README.md +++ b/leap_counter_extension/README.md @@ -1,4 +1,4 @@ -# leap_counter_extension +# leap_counter_extension (Reusability 1C) This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), and demonstrates Jupyter's provider/consumer pattern. You can find diff --git a/step_counter/README.md b/step_counter/README.md index fc36a6bc..924dac04 100644 --- a/step_counter/README.md +++ b/step_counter/README.md @@ -1,4 +1,4 @@ -# step_counter +# step_counter (Reusability 1A) This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), and demonstrates Jupyter's provider/consumer pattern. You can find diff --git a/step_counter_extension/README.md b/step_counter_extension/README.md index 8862cbbf..9bb4b513 100644 --- a/step_counter_extension/README.md +++ b/step_counter_extension/README.md @@ -1,4 +1,4 @@ -# step_counter_extension +# step_counter_extension (Reusability 1B) This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), and demonstrates Jupyter's provider/consumer pattern. You can find From e1a55255011288b0c85fbf77f1cc1e254779b66b Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 29 Sep 2023 12:52:50 -0400 Subject: [PATCH 21/31] Update titles/links in READMEs. --- README.md | 4 ++-- leap_counter_extension/README.md | 2 +- step_counter/README.md | 2 +- step_counter_extension/README.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 10b05221..db0be2a2 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,8 @@ Start with the [Hello World](hello-world) and then jump to the topic you are int - [Server Hello World](server-extension) - [Settings](settings) - [Signals](signals) -- [Step Counter](step_counter) -- [Step Counter Extension](step_counter_extension) +- [Step Counter (Reusability 1A)](step_counter) +- [Step Counter Extension (Reusability 1B)](step_counter_extension) - [State](state) - [Toolbar item](toolbar-button) - [Widgets](widgets) diff --git a/leap_counter_extension/README.md b/leap_counter_extension/README.md index 785d5252..e30401b4 100644 --- a/leap_counter_extension/README.md +++ b/leap_counter_extension/README.md @@ -1,4 +1,4 @@ -# leap_counter_extension (Reusability 1C) +# Leap Counter (Reusability 1C) (leap_counter_extension) This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), and demonstrates Jupyter's provider/consumer pattern. You can find diff --git a/step_counter/README.md b/step_counter/README.md index 924dac04..99ac0d51 100644 --- a/step_counter/README.md +++ b/step_counter/README.md @@ -1,4 +1,4 @@ -# step_counter (Reusability 1A) +# Step Counter (Reusability 1A) (step_counter) This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), and demonstrates Jupyter's provider/consumer pattern. You can find diff --git a/step_counter_extension/README.md b/step_counter_extension/README.md index 9bb4b513..b51604bd 100644 --- a/step_counter_extension/README.md +++ b/step_counter_extension/README.md @@ -1,4 +1,4 @@ -# step_counter_extension (Reusability 1B) +# Step Counter Extension (Reusability 1B) (step_counter_extension) This multi-part example comes from the [Jupyter Plugin System guide](https://jupyterlab.readthedocs.io/en/latest/extension/plugin_system.html), and demonstrates Jupyter's provider/consumer pattern. You can find From 2689a445b8d90d688fdc736ddc45790b7ed0b8eb Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 2 Oct 2023 13:51:51 -0400 Subject: [PATCH 22/31] Updated root package metadata. --- .github/workflows/main.yml | 3 +++ package.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6014ed3a..17ed7def 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,7 @@ jobs: - launcher - kernel-messaging - kernel-output + - leap_counter_extension - log-messages - main-menu - metadata-form @@ -35,6 +36,8 @@ jobs: - settings - signals - state + - step_counter + - step_counter_extension - toolbar-button - widgets os: [ubuntu-latest, macos-latest, windows-latest] diff --git a/package.json b/package.json index 869971b9..3abbb77a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "kernel-messaging", "kernel-output", "launcher", + "leap_counter_extension", "log-messages", "main-menu", "metadata-form", @@ -40,6 +41,8 @@ "settings", "signals", "state", + "step_counter", + "step_counter_extension", "toolbar-button", "widgets" ], From b1452cd53650ca8f255928ea76fa263217418133 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 9 Jun 2025 11:40:44 -0400 Subject: [PATCH 23/31] Workaround for dependency 'any' usage and version conflicts. --- leap_counter_extension/LICENSE | 0 leap_counter_extension/tsconfig.json | 2 +- step_counter/LICENSE | 0 step_counter/package.json | 4 +++- step_counter/tsconfig.json | 2 +- step_counter_extension/LICENSE | 0 step_counter_extension/tsconfig.json | 2 +- 7 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 leap_counter_extension/LICENSE create mode 100644 step_counter/LICENSE create mode 100644 step_counter_extension/LICENSE diff --git a/leap_counter_extension/LICENSE b/leap_counter_extension/LICENSE new file mode 100644 index 00000000..e69de29b diff --git a/leap_counter_extension/tsconfig.json b/leap_counter_extension/tsconfig.json index 98979175..eb930dd2 100644 --- a/leap_counter_extension/tsconfig.json +++ b/leap_counter_extension/tsconfig.json @@ -9,7 +9,7 @@ "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, - "noImplicitAny": true, + "noImplicitAny": false, "noUnusedLocals": true, "preserveWatchOutput": true, "resolveJsonModule": true, diff --git a/step_counter/LICENSE b/step_counter/LICENSE new file mode 100644 index 00000000..e69de29b diff --git a/step_counter/package.json b/step_counter/package.json index 3d0c79e1..f80c1c20 100644 --- a/step_counter/package.json +++ b/step_counter/package.json @@ -63,18 +63,20 @@ "devDependencies": { "@jupyterlab/builder": "^4.0.0", "@jupyterlab/testutils": "^4.0.0", + "@types/entities": "^2.0.2", "@types/jest": "^29.2.0", - "@types/json-schema": "^7.0.11", "@types/react": "^18.0.26", "@types/react-addons-linked-state-mixin": "^0.14.22", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "css-loader": "^6.7.1", + "entities": "^6.0.0", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.2.0", "npm-run-all": "^4.1.5", + "parse5": "^7.3.0", "prettier": "^3.0.0", "rimraf": "^5.0.1", "source-map-loader": "^1.0.2", diff --git a/step_counter/tsconfig.json b/step_counter/tsconfig.json index 98979175..eb930dd2 100644 --- a/step_counter/tsconfig.json +++ b/step_counter/tsconfig.json @@ -9,7 +9,7 @@ "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, - "noImplicitAny": true, + "noImplicitAny": false, "noUnusedLocals": true, "preserveWatchOutput": true, "resolveJsonModule": true, diff --git a/step_counter_extension/LICENSE b/step_counter_extension/LICENSE new file mode 100644 index 00000000..e69de29b diff --git a/step_counter_extension/tsconfig.json b/step_counter_extension/tsconfig.json index 98979175..eb930dd2 100644 --- a/step_counter_extension/tsconfig.json +++ b/step_counter_extension/tsconfig.json @@ -9,7 +9,7 @@ "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, - "noImplicitAny": true, + "noImplicitAny": false, "noUnusedLocals": true, "preserveWatchOutput": true, "resolveJsonModule": true, From e420ec728af24955f234fdb482a35e8580c27138 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 9 Jun 2025 12:18:24 -0400 Subject: [PATCH 24/31] Linting. --- step_counter/README.md | 36 ++++++++++++++++++------------------ step_counter/src/index.ts | 24 +++++++++++++----------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/step_counter/README.md b/step_counter/README.md index 99ac0d51..be2394f1 100644 --- a/step_counter/README.md +++ b/step_counter/README.md @@ -9,23 +9,23 @@ JupyterLab's provider-consumer pattern, where plugins can depend on and reuse features from one another. The three packages that make up the complete example are: - 1. (*) The step_counter package (this one). This holds a token, a - class + an interface that make up a stock implementation of - the "step_counter" service, and a provider plugin that - makes an instance of the Counter available to JupyterLab - as a service object. +1. (\*) The step_counter package (this one). This holds a token, a + class + an interface that make up a stock implementation of + the "step_counter" service, and a provider plugin that + makes an instance of the Counter available to JupyterLab + as a service object. - 2. The step_counter_extension package, that holds a UI/interface - in JupyterLab for users to count their steps that connects - with/consumes the step_counter service object via a consumer plugin. +2. The step_counter_extension package, that holds a UI/interface + in JupyterLab for users to count their steps that connects + with/consumes the step_counter service object via a consumer plugin. - 3. The leap_counter_extension package, that holds an alternate - way for users to count steps (a leap is 5 steps). Like the step_counter_extension - package, this holds a UI/interface in JupyterLab, and a consumer - plugin that also requests/consumes the step_counter service - object. The leap_counter_extension package demonstrates how - an unrelated plugin can depend on and reuse features from - an existing plugin. Users can install either the - step_counter_extension, the leap_counter_extension or both - to get whichever features they prefer (with both reusing - the step_counter service object). +3. The leap_counter_extension package, that holds an alternate + way for users to count steps (a leap is 5 steps). Like the step_counter_extension + package, this holds a UI/interface in JupyterLab, and a consumer + plugin that also requests/consumes the step_counter service + object. The leap_counter_extension package demonstrates how + an unrelated plugin can depend on and reuse features from + an existing plugin. Users can install either the + step_counter_extension, the leap_counter_extension or both + to get whichever features they prefer (with both reusing + the step_counter service object). diff --git a/step_counter/src/index.ts b/step_counter/src/index.ts index d0f52b42..0c32d53a 100644 --- a/step_counter/src/index.ts +++ b/step_counter/src/index.ts @@ -30,13 +30,13 @@ import { import { Token } from '@lumino/coreutils'; import { Signal } from '@lumino/signaling'; -// The StepCounterItem interface is used as part of JupyterLab's +// The IStepCounterItem interface is used as part of JupyterLab's // provider-consumer pattern. This interface is supplied to the // token instance (the StepCounter token), and JupyterLab will // use it to type-check any service-object associated with the // token that a provider plugin supplies to check that it conforms // to the interface. -interface StepCounterItem { +interface IStepCounterItem { // registerStatusItem(id: string, statusItem: IStatusBar.IItem): IDisposable; getStepCount(): number; incrementStepCount(count: number): void; @@ -49,7 +49,7 @@ interface StepCounterItem { // to store and increment step count data in JupyterLab). Any // plugin can use this token in their "requires" or "activates" // list to request the service object associated with this token! -const StepCounter = new Token( +const StepCounter = new Token( 'step_counter:StepCounter', 'A service for counting steps.' ); @@ -57,10 +57,9 @@ const StepCounter = new Token( // This class holds step count data/utilities. An instance of // this class will serve as the service object associated with // the StepCounter token (Other developers can substitute their -// own implementation of a StepCounterItem instead of using this +// own implementation of a IStepCounterItem instead of using this // one, by becoming a provider of the StepCounter token). -class Counter implements StepCounterItem { - +class Counter implements IStepCounterItem { _stepCount: number; countChanged: Signal; @@ -86,14 +85,17 @@ class Counter implements StepCounterItem { // class defined above) from the function supplied as its activate property. // It also needs to supply the interface (the one the service object // implements) to JupyterFrontEndPlugin when it's defined. -const plugin: JupyterFrontEndPlugin = { +const plugin: JupyterFrontEndPlugin = { id: 'step_counter:provider_plugin', - description: 'Provider plugin for the step_counter\'s "counter" service object.', + description: + 'Provider plugin for the step_counter\'s "counter" service object.', autoStart: true, provides: StepCounter, // The activate function here will be called by JupyterLab when the plugin loads activate: (app: JupyterFrontEnd) => { - console.log('JupyterLab extension (step_counter/provider plugin) is activated!'); + console.log( + 'JupyterLab extension (step_counter/provider plugin) is activated!' + ); const counter = new Counter(); // Since this plugin "provides" the "StepCounter" service, make sure to @@ -108,7 +110,7 @@ const plugin: JupyterFrontEndPlugin = { // package.json according to the Jupyter extension documentation, both in // this provider extension and in the consumer extensions. Under the // "Jupyterlab" key you should have a "singleton" key set to true. -// +// // "jupyterlab": { // "extension": true, // "outputDir": "step_counter/labextension", @@ -125,5 +127,5 @@ const plugin: JupyterFrontEndPlugin = { // https://jupyterlab.readthedocs.io/en/latest/extension/extension_dev.html#requiring-a-service // https://jupyterlab.readthedocs.io/en/latest/extension/extension_dev.html#optionally-using-a-service -export { StepCounter, StepCounterItem }; +export { StepCounter, IStepCounterItem }; export default plugin; From ea8a71139ce64e796c41b2638330c7b177dab8b1 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 9 Jun 2025 12:31:54 -0400 Subject: [PATCH 25/31] Fix linting errors for unrelated packages. --- context-menu/src/index.ts | 5 +++-- documents/src/model.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/context-menu/src/index.ts b/context-menu/src/index.ts index 73e86df6..d6fa316d 100644 --- a/context-menu/src/index.ts +++ b/context-menu/src/index.ts @@ -17,8 +17,9 @@ const extension: JupyterFrontEndPlugin = { caption: "Example context menu button for file browser's items.", icon: buildIcon, execute: () => { - const file = factory.tracker.currentWidget?.selectedItems().next() - .value; + const file = factory.tracker.currentWidget + ?.selectedItems() + .next().value; if (file) { showDialog({ diff --git a/documents/src/model.ts b/documents/src/model.ts index 41a2c72a..e53680f3 100644 --- a/documents/src/model.ts +++ b/documents/src/model.ts @@ -395,7 +395,7 @@ export class ExampleDoc extends YDocument { ? data ? JSON.parse(data) : { x: 0, y: 0 } - : data ?? ''; + : (data ?? ''); } /** From 626490b2cc1e8de8abf22740a878f9c88fbb4cf9 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 9 Jun 2025 12:37:35 -0400 Subject: [PATCH 26/31] Modify tsconfigs: Re-enable implicit any checks, add skipLibCheck. --- leap_counter_extension/tsconfig.json | 5 +++-- step_counter/tsconfig.json | 5 +++-- step_counter_extension/tsconfig.json | 5 +++-- step_counter_extension/tsconfig.test.json | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/leap_counter_extension/tsconfig.json b/leap_counter_extension/tsconfig.json index eb930dd2..06185db1 100644 --- a/leap_counter_extension/tsconfig.json +++ b/leap_counter_extension/tsconfig.json @@ -9,7 +9,7 @@ "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, - "noImplicitAny": false, + "noImplicitAny": true, "noUnusedLocals": true, "preserveWatchOutput": true, "resolveJsonModule": true, @@ -17,7 +17,8 @@ "rootDir": "src", "strict": true, "strictNullChecks": true, - "target": "ES2018" + "target": "ES2018", + "skipLibCheck": true }, "include": ["src/*"] } diff --git a/step_counter/tsconfig.json b/step_counter/tsconfig.json index eb930dd2..06185db1 100644 --- a/step_counter/tsconfig.json +++ b/step_counter/tsconfig.json @@ -9,7 +9,7 @@ "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, - "noImplicitAny": false, + "noImplicitAny": true, "noUnusedLocals": true, "preserveWatchOutput": true, "resolveJsonModule": true, @@ -17,7 +17,8 @@ "rootDir": "src", "strict": true, "strictNullChecks": true, - "target": "ES2018" + "target": "ES2018", + "skipLibCheck": true }, "include": ["src/*"] } diff --git a/step_counter_extension/tsconfig.json b/step_counter_extension/tsconfig.json index eb930dd2..06185db1 100644 --- a/step_counter_extension/tsconfig.json +++ b/step_counter_extension/tsconfig.json @@ -9,7 +9,7 @@ "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, - "noImplicitAny": false, + "noImplicitAny": true, "noUnusedLocals": true, "preserveWatchOutput": true, "resolveJsonModule": true, @@ -17,7 +17,8 @@ "rootDir": "src", "strict": true, "strictNullChecks": true, - "target": "ES2018" + "target": "ES2018", + "skipLibCheck": true }, "include": ["src/*"] } diff --git a/step_counter_extension/tsconfig.test.json b/step_counter_extension/tsconfig.test.json index 1de37fd0..b173290e 100644 --- a/step_counter_extension/tsconfig.test.json +++ b/step_counter_extension/tsconfig.test.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig", "compilerOptions": { - "types": ["jest"] + "types": ["jest"], + "skipLibCheck": true } } From bfb24cc29cf9962a709a8dcea29082de2a0e8e87 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 9 Jun 2025 13:21:04 -0400 Subject: [PATCH 27/31] Extra linting. --- leap_counter_extension/README.md | 38 +++++++++---------- leap_counter_extension/src/index.ts | 11 +++--- leap_counter_extension/style/base.css | 30 +++++++-------- .../tests/leap_counter_extension.spec.ts | 4 +- step_counter_extension/README.md | 38 +++++++++---------- step_counter_extension/src/index.ts | 11 +++--- step_counter_extension/style/base.css | 28 +++++++------- .../tests/step_counter_extension.spec.ts | 4 +- 8 files changed, 85 insertions(+), 79 deletions(-) diff --git a/leap_counter_extension/README.md b/leap_counter_extension/README.md index e30401b4..434af89e 100644 --- a/leap_counter_extension/README.md +++ b/leap_counter_extension/README.md @@ -9,24 +9,24 @@ JupyterLab's provider-consumer pattern, where plugins can depend on and reuse features from one another. The three packages that make up the complete example are: - 1. The step_counter package. This package holds a token, a - class + an interface that make up a stock implementation of - the "step_counter" service, and a provider plugin that - makes an instance of the Counter available to JupyterLab - as a service object. +1. The step_counter package. This package holds a token, a + class + an interface that make up a stock implementation of + the "step_counter" service, and a provider plugin that + makes an instance of the Counter available to JupyterLab + as a service object. - 2. The step_counter_extension package, that holds a - UI/interface in JupyterLab for users to count their steps that - connects with/consumes the step_counter service object via a - consumer plugin. +2. The step_counter_extension package, that holds a + UI/interface in JupyterLab for users to count their steps that + connects with/consumes the step_counter service object via a + consumer plugin. - 3. (*) The leap_counter_extension package (this one), that holds an alternate - way for users to count steps (a leap is 5 steps). Like the step_counter_extension - package, this holds a UI/interface in JupyterLab, and a consumer - plugin that also requests/consumes the step_counter service - object. The leap_counter_extension package demonstrates how - an unrelated plugin can depend on and reuse features from - an existing plugin. Users can install either the - step_counter_extension, the leap_counter_extension or both - to get whichever features they prefer (with both reusing - the step_counter service object). +3. (\*) The leap_counter_extension package (this one), that holds an alternate + way for users to count steps (a leap is 5 steps). Like the step_counter_extension + package, this holds a UI/interface in JupyterLab, and a consumer + plugin that also requests/consumes the step_counter service + object. The leap_counter_extension package demonstrates how + an unrelated plugin can depend on and reuse features from + an existing plugin. Users can install either the + step_counter_extension, the leap_counter_extension or both + to get whichever features they prefer (with both reusing + the step_counter service object). diff --git a/leap_counter_extension/src/index.ts b/leap_counter_extension/src/index.ts index f74dd0c0..853daf3e 100644 --- a/leap_counter_extension/src/index.ts +++ b/leap_counter_extension/src/index.ts @@ -30,12 +30,11 @@ import { import { Widget } from '@lumino/widgets'; -import { StepCounter} from "step_counter"; +import { StepCounter } from 'step_counter'; // This widget holds the JupyterLab UI/interface that users will // see and interact with to count and view their steps. class LeapCounterWidget extends Widget { - leapButton: HTMLElement; combinedStepCountLabel: HTMLElement; counter: any; @@ -72,7 +71,8 @@ class LeapCounterWidget extends Widget { // Refresh the displayed step count updateStepCountDisplay() { - this.combinedStepCountLabel.innerText = 'Combined Step Count: ' + this.counter.getStepCount(); + this.combinedStepCountLabel.innerText = + 'Combined Step Count: ' + this.counter.getStepCount(); } // Increment the step count, a leap is 5 steps @@ -90,7 +90,8 @@ class LeapCounterWidget extends Widget { // add an argument to your plugin's "activate" function. const plugin: JupyterFrontEndPlugin = { id: 'leap_counter_extension:plugin', - description: 'Adds a leap counter/button (1 of 3 related examples). This extension holds the UI/interface', + description: + 'Adds a leap counter/button (1 of 3 related examples). This extension holds the UI/interface', autoStart: true, requires: [StepCounter], // The activate function here will be called by JupyterLab when the plugin loads. @@ -104,7 +105,7 @@ const plugin: JupyterFrontEndPlugin = { // Create a LeapCounterWidget and add it to the interface const leapWidget: LeapCounterWidget = new LeapCounterWidget(counter); - leapWidget.id = 'JupyterLeapWidget'; // Widgets need an id + leapWidget.id = 'JupyterLeapWidget'; // Widgets need an id app.shell.add(leapWidget, 'top'); } }; diff --git a/leap_counter_extension/style/base.css b/leap_counter_extension/style/base.css index 764059d6..73679e11 100644 --- a/leap_counter_extension/style/base.css +++ b/leap_counter_extension/style/base.css @@ -5,24 +5,24 @@ */ .jp-leap-container { - display: inline; - user-select: none; + display: inline; + user-select: none; } .jp-leap-button { - width: 85px; - margin: 4px; - padding: 2px; - text-align: center; - vertical-align: middle; - border-radius: 2px; - background-color: #00eb00; - color: #212121; - display: inline-block; + width: 85px; + margin: 4px; + padding: 2px; + text-align: center; + vertical-align: middle; + border-radius: 2px; + background-color: #00eb00; + color: #212121; + display: inline-block; } .jp-combined-step-count-label { - display: inline-block; - margin: 6px; - vertical-align: middle; -} \ No newline at end of file + display: inline-block; + margin: 6px; + vertical-align: middle; +} diff --git a/leap_counter_extension/ui-tests/tests/leap_counter_extension.spec.ts b/leap_counter_extension/ui-tests/tests/leap_counter_extension.spec.ts index b2d2ddca..c63c31a4 100644 --- a/leap_counter_extension/ui-tests/tests/leap_counter_extension.spec.ts +++ b/leap_counter_extension/ui-tests/tests/leap_counter_extension.spec.ts @@ -16,6 +16,8 @@ test('should emit an activation console message', async ({ page }) => { await page.goto(); expect( - logs.filter(s => s === 'JupyterLab extension leap_counter_extension is activated!') + logs.filter( + s => s === 'JupyterLab extension leap_counter_extension is activated!' + ) ).toHaveLength(1); }); diff --git a/step_counter_extension/README.md b/step_counter_extension/README.md index b51604bd..e06abbf9 100644 --- a/step_counter_extension/README.md +++ b/step_counter_extension/README.md @@ -9,24 +9,24 @@ JupyterLab's provider-consumer pattern, where plugins can depend on and reuse features from one another. The three packages that make up the complete example are: - 1. The step_counter package. This package holds a token, a - class + an interface that make up a stock implementation of - the "step_counter" service, and a provider plugin that - makes an instance of the Counter available to JupyterLab - as a service object. +1. The step_counter package. This package holds a token, a + class + an interface that make up a stock implementation of + the "step_counter" service, and a provider plugin that + makes an instance of the Counter available to JupyterLab + as a service object. - 2. (*) The step_counter_extension package (this one), that holds a - UI/interface in JupyterLab for users to count their steps that - connects with/consumes the step_counter service object via a - consumer plugin. +2. (\*) The step_counter_extension package (this one), that holds a + UI/interface in JupyterLab for users to count their steps that + connects with/consumes the step_counter service object via a + consumer plugin. - 3. The leap_counter_extension package, that holds an alternate - way for users to count steps (a leap is 5 steps). Like the step_counter_extension - package, this holds a UI/interface in JupyterLab, and a consumer - plugin that also requests/consumes the step_counter service - object. The leap_counter_extension package demonstrates how - an unrelated plugin can depend on and reuse features from - an existing plugin. Users can install either the - step_counter_extension, the leap_counter_extension or both - to get whichever features they prefer (with both reusing - the step_counter service object). +3. The leap_counter_extension package, that holds an alternate + way for users to count steps (a leap is 5 steps). Like the step_counter_extension + package, this holds a UI/interface in JupyterLab, and a consumer + plugin that also requests/consumes the step_counter service + object. The leap_counter_extension package demonstrates how + an unrelated plugin can depend on and reuse features from + an existing plugin. Users can install either the + step_counter_extension, the leap_counter_extension or both + to get whichever features they prefer (with both reusing + the step_counter service object). diff --git a/step_counter_extension/src/index.ts b/step_counter_extension/src/index.ts index a47f2063..137b7e2d 100644 --- a/step_counter_extension/src/index.ts +++ b/step_counter_extension/src/index.ts @@ -30,12 +30,11 @@ import { import { Widget } from '@lumino/widgets'; -import { StepCounter} from "step_counter"; +import { StepCounter } from 'step_counter'; // This widget holds the JupyterLab UI/interface that users will // see and interact with to count and view their steps. class StepCounterWidget extends Widget { - stepButton: HTMLElement; stepCountLabel: HTMLElement; counter: any; @@ -72,7 +71,8 @@ class StepCounterWidget extends Widget { // Refresh the displayed step count updateStepCountDisplay() { - this.stepCountLabel.innerText = 'Step Count: ' + this.counter.getStepCount(); + this.stepCountLabel.innerText = + 'Step Count: ' + this.counter.getStepCount(); } // Increment the step count, by just 1 @@ -90,7 +90,8 @@ class StepCounterWidget extends Widget { // add an argument to your plugin's "activate" function. const plugin: JupyterFrontEndPlugin = { id: 'step_counter_extension:plugin', - description: 'Adds a step counter/button (1 of 3 related examples). This extension holds the UI/interface', + description: + 'Adds a step counter/button (1 of 3 related examples). This extension holds the UI/interface', autoStart: true, requires: [StepCounter], // The activate function here will be called by JupyterLab when the plugin loads. @@ -104,7 +105,7 @@ const plugin: JupyterFrontEndPlugin = { // Create a StepCounterWidget and add it to the interface const stepWidget: StepCounterWidget = new StepCounterWidget(counter); - stepWidget.id = 'JupyterStepWidget'; // Widgets need an id + stepWidget.id = 'JupyterStepWidget'; // Widgets need an id app.shell.add(stepWidget, 'top'); } }; diff --git a/step_counter_extension/style/base.css b/step_counter_extension/style/base.css index 0669ebd5..73ffbf8a 100644 --- a/step_counter_extension/style/base.css +++ b/step_counter_extension/style/base.css @@ -5,24 +5,24 @@ */ .jp-step-container { - display: inline; - user-select: none; + display: inline; + user-select: none; } .jp-step-button { - width: 85px; - margin: 4px; - padding: 2px; - text-align: center; - vertical-align: middle; - border-radius: 2px; - background-color: #2296f3; - color: #212121; - display: inline-block; + width: 85px; + margin: 4px; + padding: 2px; + text-align: center; + vertical-align: middle; + border-radius: 2px; + background-color: #2296f3; + color: #212121; + display: inline-block; } .jp-step-label { - display: inline-block; - margin: 6px; - vertical-align: middle; + display: inline-block; + margin: 6px; + vertical-align: middle; } diff --git a/step_counter_extension/ui-tests/tests/step_counter_extension.spec.ts b/step_counter_extension/ui-tests/tests/step_counter_extension.spec.ts index 525e5a35..35640eab 100644 --- a/step_counter_extension/ui-tests/tests/step_counter_extension.spec.ts +++ b/step_counter_extension/ui-tests/tests/step_counter_extension.spec.ts @@ -16,6 +16,8 @@ test('should emit an activation console message', async ({ page }) => { await page.goto(); expect( - logs.filter(s => s === 'JupyterLab extension step_counter_extension is activated!') + logs.filter( + s => s === 'JupyterLab extension step_counter_extension is activated!' + ) ).toHaveLength(1); }); From cb58d6c8194a43ef5464a623362c02b6a25903e2 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 9 Jun 2025 14:47:51 -0400 Subject: [PATCH 28/31] Revised to address feedback. --- leap_counter_extension/src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/leap_counter_extension/src/index.ts b/leap_counter_extension/src/index.ts index 853daf3e..8e21123e 100644 --- a/leap_counter_extension/src/index.ts +++ b/leap_counter_extension/src/index.ts @@ -30,19 +30,19 @@ import { import { Widget } from '@lumino/widgets'; -import { StepCounter } from 'step_counter'; +import { IStepCounterItem, StepCounter } from 'step_counter'; // This widget holds the JupyterLab UI/interface that users will // see and interact with to count and view their steps. class LeapCounterWidget extends Widget { - leapButton: HTMLElement; - combinedStepCountLabel: HTMLElement; - counter: any; + private leapButton: HTMLElement; + private combinedStepCountLabel: HTMLElement; + private counter: IStepCounterItem; // Notice that the constructor for this object takes a "counter" // argument, which is the service object associated with the StepCounter // token (which is passed in by the consumer plugin). - constructor(counter: any) { + constructor(counter: IStepCounterItem) { super(); this.counter = counter; @@ -57,8 +57,8 @@ class LeapCounterWidget extends Widget { // Add a listener to handle button clicks leapButton.addEventListener('click', this.takeLeap.bind(this)); leapButton.classList.add('jp-leap-button'); - this.node.appendChild(leapButton); this.leapButton = leapButton; + this.node.appendChild(this.leapButton); // Add a label to display the step count const combinedStepCountLabel = document.createElement('p'); @@ -69,8 +69,8 @@ class LeapCounterWidget extends Widget { this.updateStepCountDisplay(); } - // Refresh the displayed step count updateStepCountDisplay() { + // Refresh the displayed step count this.combinedStepCountLabel.innerText = 'Combined Step Count: ' + this.counter.getStepCount(); } From 101cfd60097eb479b9ddfd2e463d65995b10a174 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 9 Jun 2025 14:58:57 -0400 Subject: [PATCH 29/31] Added explanation of declaration merging. --- step_counter/src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/step_counter/src/index.ts b/step_counter/src/index.ts index 0c32d53a..80939547 100644 --- a/step_counter/src/index.ts +++ b/step_counter/src/index.ts @@ -36,6 +36,14 @@ import { Signal } from '@lumino/signaling'; // use it to type-check any service-object associated with the // token that a provider plugin supplies to check that it conforms // to the interface. +// .... +// Note: JupyterLab frequently uses Typescript declaration merging +// to reduce the number of names used and to simplify certain aspects +// of development, export and usage. Normally, this interface would +// be called StepCounter and would take advantage of declaration merging +// in line with JupyterLab standards, but is named differently here +// to make the differences between these items more explicit to avoid +// confusion for beginners. interface IStepCounterItem { // registerStatusItem(id: string, statusItem: IStatusBar.IItem): IDisposable; getStepCount(): number; From 1e56e657f225f04895ecd56a0830cedb70647f56 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 9 Jun 2025 15:03:49 -0400 Subject: [PATCH 30/31] Add changes from PR feedback. --- step_counter_extension/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/step_counter_extension/src/index.ts b/step_counter_extension/src/index.ts index 137b7e2d..e1e3ee7d 100644 --- a/step_counter_extension/src/index.ts +++ b/step_counter_extension/src/index.ts @@ -30,19 +30,19 @@ import { import { Widget } from '@lumino/widgets'; -import { StepCounter } from 'step_counter'; +import { IStepCounterItem, StepCounter } from 'step_counter'; // This widget holds the JupyterLab UI/interface that users will // see and interact with to count and view their steps. class StepCounterWidget extends Widget { stepButton: HTMLElement; stepCountLabel: HTMLElement; - counter: any; + counter: IStepCounterItem; // Notice that the constructor for this object takes a "counter" // argument, which is the service object associated with the StepCounter // token (which is passed in by the consumer plugin). - constructor(counter: any) { + constructor(counter: IStepCounterItem) { super(); this.counter = counter; @@ -100,7 +100,7 @@ const plugin: JupyterFrontEndPlugin = { // arguments, so make sure you add arguments for those here when your plugin requests // any required or optional services. If a required service is missing, your plugin // won't load. If an optional service is missing, the supplied argument will be null. - activate: (app: JupyterFrontEnd, counter: any) => { + activate: (app: JupyterFrontEnd, counter: IStepCounterItem) => { console.log('JupyterLab extension step_counter_extension is activated!'); // Create a StepCounterWidget and add it to the interface From 7b0f58e0b8ecb6036c8cdf6e9d475f6e644b594c Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Tue, 10 Jun 2025 10:16:50 -0400 Subject: [PATCH 31/31] Add updated version of entities with types. --- leap_counter_extension/package.json | 1 + leap_counter_extension/tsconfig.json | 3 +-- step_counter/package.json | 3 +-- step_counter/tsconfig.json | 3 +-- step_counter_extension/package.json | 1 + step_counter_extension/tsconfig.json | 3 +-- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/leap_counter_extension/package.json b/leap_counter_extension/package.json index 138953e8..5db99dae 100644 --- a/leap_counter_extension/package.json +++ b/leap_counter_extension/package.json @@ -72,6 +72,7 @@ "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "css-loader": "^6.7.1", + "entities": "6.0.1", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/leap_counter_extension/tsconfig.json b/leap_counter_extension/tsconfig.json index 06185db1..98979175 100644 --- a/leap_counter_extension/tsconfig.json +++ b/leap_counter_extension/tsconfig.json @@ -17,8 +17,7 @@ "rootDir": "src", "strict": true, "strictNullChecks": true, - "target": "ES2018", - "skipLibCheck": true + "target": "ES2018" }, "include": ["src/*"] } diff --git a/step_counter/package.json b/step_counter/package.json index f80c1c20..7ff23d37 100644 --- a/step_counter/package.json +++ b/step_counter/package.json @@ -63,14 +63,13 @@ "devDependencies": { "@jupyterlab/builder": "^4.0.0", "@jupyterlab/testutils": "^4.0.0", - "@types/entities": "^2.0.2", "@types/jest": "^29.2.0", "@types/react": "^18.0.26", "@types/react-addons-linked-state-mixin": "^0.14.22", "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "css-loader": "^6.7.1", - "entities": "^6.0.0", + "entities": "6.0.1", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/step_counter/tsconfig.json b/step_counter/tsconfig.json index 06185db1..98979175 100644 --- a/step_counter/tsconfig.json +++ b/step_counter/tsconfig.json @@ -17,8 +17,7 @@ "rootDir": "src", "strict": true, "strictNullChecks": true, - "target": "ES2018", - "skipLibCheck": true + "target": "ES2018" }, "include": ["src/*"] } diff --git a/step_counter_extension/package.json b/step_counter_extension/package.json index a086c996..dca5afd6 100644 --- a/step_counter_extension/package.json +++ b/step_counter_extension/package.json @@ -72,6 +72,7 @@ "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "css-loader": "^6.7.1", + "entities": "6.0.1", "eslint": "^8.36.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/step_counter_extension/tsconfig.json b/step_counter_extension/tsconfig.json index 06185db1..98979175 100644 --- a/step_counter_extension/tsconfig.json +++ b/step_counter_extension/tsconfig.json @@ -17,8 +17,7 @@ "rootDir": "src", "strict": true, "strictNullChecks": true, - "target": "ES2018", - "skipLibCheck": true + "target": "ES2018" }, "include": ["src/*"] }