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/README.md b/README.md index e28e0c37..db0be2a2 100644 --- a/README.md +++ b/README.md @@ -18,11 +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 (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) @@ -105,6 +108,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 +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 (Reusability 1A)](step_counter) +- [Step Counter Extension (Reusability 1B)](step_counter_extension) - [State](state) - [Toolbar item](toolbar-button) - [Widgets](widgets) @@ -207,6 +213,17 @@ Start your extension from the Launcher. [![Launcher](launcher/preview.gif)](launcher) +### [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 +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 +278,28 @@ Use Signals to allow Widgets communicate with each others. [![Button with Signal](signals/preview.png)](signals) +### [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 +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 (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 +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/leap_counter_extension/.copier-answers.yml b/leap_counter_extension/.copier-answers.yml new file mode 100644 index 00000000..19f21b17 --- /dev/null +++ b/leap_counter_extension/.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/leap_counter_extension/.gitignore b/leap_counter_extension/.gitignore new file mode 100644 index 00000000..08ebd8ec --- /dev/null +++ b/leap_counter_extension/.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/leap_counter_extension/.prettierignore b/leap_counter_extension/.prettierignore new file mode 100644 index 00000000..5afbf1dd --- /dev/null +++ b/leap_counter_extension/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +leap_counter_extension diff --git a/leap_counter_extension/.yarnrc.yml b/leap_counter_extension/.yarnrc.yml new file mode 100644 index 00000000..3186f3f0 --- /dev/null +++ b/leap_counter_extension/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/leap_counter_extension/README.md b/leap_counter_extension/README.md new file mode 100644 index 00000000..e30401b4 --- /dev/null +++ b/leap_counter_extension/README.md @@ -0,0 +1,32 @@ +# 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 +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/leap_counter_extension/babel.config.js b/leap_counter_extension/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/leap_counter_extension/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/leap_counter_extension/install.json b/leap_counter_extension/install.json new file mode 100644 index 00000000..194595a5 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/jest.config.js b/leap_counter_extension/jest.config.js new file mode 100644 index 00000000..b0471e66 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/leap_counter_extension/__init__.py b/leap_counter_extension/leap_counter_extension/__init__.py new file mode 100644 index 00000000..18edae05 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/package.json b/leap_counter_extension/package.json new file mode 100644 index 00000000..138953e8 --- /dev/null +++ b/leap_counter_extension/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:./../step_counter" + }, + "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/leap_counter_extension/pyproject.toml b/leap_counter_extension/pyproject.toml new file mode 100644 index 00000000..d6f855c6 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/setup.py b/leap_counter_extension/setup.py new file mode 100644 index 00000000..aefdf20d --- /dev/null +++ b/leap_counter_extension/setup.py @@ -0,0 +1 @@ +__import__("setuptools").setup() diff --git a/leap_counter_extension/src/__tests__/leap_counter_extension.spec.ts b/leap_counter_extension/src/__tests__/leap_counter_extension.spec.ts new file mode 100644 index 00000000..12ae1527 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/src/index.ts b/leap_counter_extension/src/index.ts new file mode 100644 index 00000000..f74dd0c0 --- /dev/null +++ b/leap_counter_extension/src/index.ts @@ -0,0 +1,112 @@ +// 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; + this.counter.countChanged.connect(this.updateStepCountDisplay, this); + + // 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/leap_counter_extension/style/base.css b/leap_counter_extension/style/base.css new file mode 100644 index 00000000..764059d6 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/style/index.css b/leap_counter_extension/style/index.css new file mode 100644 index 00000000..8a7ea29e --- /dev/null +++ b/leap_counter_extension/style/index.css @@ -0,0 +1 @@ +@import url('base.css'); diff --git a/leap_counter_extension/style/index.js b/leap_counter_extension/style/index.js new file mode 100644 index 00000000..a028a764 --- /dev/null +++ b/leap_counter_extension/style/index.js @@ -0,0 +1 @@ +import './base.css'; diff --git a/leap_counter_extension/tsconfig.json b/leap_counter_extension/tsconfig.json new file mode 100644 index 00000000..98979175 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/tsconfig.test.json b/leap_counter_extension/tsconfig.test.json new file mode 100644 index 00000000..1de37fd0 --- /dev/null +++ b/leap_counter_extension/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/leap_counter_extension/ui-tests/README.md b/leap_counter_extension/ui-tests/README.md new file mode 100644 index 00000000..dbe6e8aa --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/ui-tests/jupyter_server_test_config.py b/leap_counter_extension/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..f2a94782 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/ui-tests/package.json b/leap_counter_extension/ui-tests/package.json new file mode 100644 index 00000000..1cde9069 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/ui-tests/playwright.config.js b/leap_counter_extension/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/leap_counter_extension/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/leap_counter_extension/ui-tests/tests/leap_counter_extension.spec.ts b/leap_counter_extension/ui-tests/tests/leap_counter_extension.spec.ts new file mode 100644 index 00000000..b2d2ddca --- /dev/null +++ b/leap_counter_extension/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); +}); 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" ], diff --git a/step_counter/.copier-answers.yml b/step_counter/.copier-answers.yml new file mode 100644 index 00000000..35ba42aa --- /dev/null +++ b/step_counter/.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 service token and a stock + implementation (1 of 3 related examples). +python_name: step_counter +repository: '' +test: true + diff --git a/step_counter/.gitignore b/step_counter/.gitignore new file mode 100644 index 00000000..7b505515 --- /dev/null +++ b/step_counter/.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/step_counter/.prettierignore b/step_counter/.prettierignore new file mode 100644 index 00000000..69ed6915 --- /dev/null +++ b/step_counter/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +step_counter diff --git a/step_counter/.yarnrc.yml b/step_counter/.yarnrc.yml new file mode 100644 index 00000000..3186f3f0 --- /dev/null +++ b/step_counter/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/step_counter/README.md b/step_counter/README.md new file mode 100644 index 00000000..99ac0d51 --- /dev/null +++ b/step_counter/README.md @@ -0,0 +1,31 @@ +# 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 +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/babel.config.js b/step_counter/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/step_counter/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/step_counter/install.json b/step_counter/install.json new file mode 100644 index 00000000..c914622f --- /dev/null +++ b/step_counter/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/step_counter/jest.config.js b/step_counter/jest.config.js new file mode 100644 index 00000000..b0471e66 --- /dev/null +++ b/step_counter/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/step_counter/package.json b/step_counter/package.json new file mode 100644 index 00000000..3d0c79e1 --- /dev/null +++ b/step_counter/package.json @@ -0,0 +1,201 @@ +{ + "name": "step_counter", + "version": "0.1.0", + "description": "Adds a step_counter service token and a stock implementation (1 of 3 related examples).", + "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/step_counter/preview.png b/step_counter/preview.png new file mode 100644 index 00000000..55c1c844 Binary files /dev/null and b/step_counter/preview.png differ diff --git a/step_counter/pyproject.toml b/step_counter/pyproject.toml new file mode 100644 index 00000000..d05be2e5 --- /dev/null +++ b/step_counter/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/step_counter/setup.py b/step_counter/setup.py new file mode 100644 index 00000000..aefdf20d --- /dev/null +++ b/step_counter/setup.py @@ -0,0 +1 @@ +__import__("setuptools").setup() diff --git a/step_counter/src/__tests__/step_counter.spec.ts b/step_counter/src/__tests__/step_counter.spec.ts new file mode 100644 index 00000000..162b038f --- /dev/null +++ b/step_counter/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/step_counter/src/index.ts b/step_counter/src/index.ts new file mode 100644 index 00000000..d0f52b42 --- /dev/null +++ b/step_counter/src/index.ts @@ -0,0 +1,129 @@ +// 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). + +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} 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 +// 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; + countChanged: Signal; +} + +// 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; + countChanged: Signal; + + constructor() { + this._stepCount = 0; + this.countChanged = new Signal(this); + } + + incrementStepCount(count: number) { + this._stepCount += count; + this.countChanged.emit(this._stepCount); + } + + getStepCount() { + return this._stepCount; + } +} + +// 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 extension (step_counter/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; + } +}; + +// 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; diff --git a/step_counter/step_counter/__init__.py b/step_counter/step_counter/__init__.py new file mode 100644 index 00000000..8b569c95 --- /dev/null +++ b/step_counter/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/step_counter/style/base.css b/step_counter/style/base.css new file mode 100644 index 00000000..e11f4577 --- /dev/null +++ b/step_counter/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/step_counter/style/index.css b/step_counter/style/index.css new file mode 100644 index 00000000..8a7ea29e --- /dev/null +++ b/step_counter/style/index.css @@ -0,0 +1 @@ +@import url('base.css'); diff --git a/step_counter/style/index.js b/step_counter/style/index.js new file mode 100644 index 00000000..a028a764 --- /dev/null +++ b/step_counter/style/index.js @@ -0,0 +1 @@ +import './base.css'; diff --git a/step_counter/tsconfig.json b/step_counter/tsconfig.json new file mode 100644 index 00000000..98979175 --- /dev/null +++ b/step_counter/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/step_counter/tsconfig.test.json b/step_counter/tsconfig.test.json new file mode 100644 index 00000000..1de37fd0 --- /dev/null +++ b/step_counter/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/step_counter/ui-tests/README.md b/step_counter/ui-tests/README.md new file mode 100644 index 00000000..dbe6e8aa --- /dev/null +++ b/step_counter/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/step_counter/ui-tests/jupyter_server_test_config.py b/step_counter/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..f2a94782 --- /dev/null +++ b/step_counter/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/step_counter/ui-tests/package.json b/step_counter/ui-tests/package.json new file mode 100644 index 00000000..d87f111b --- /dev/null +++ b/step_counter/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/step_counter/ui-tests/playwright.config.js b/step_counter/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/step_counter/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/step_counter/ui-tests/tests/step_counter.spec.ts b/step_counter/ui-tests/tests/step_counter.spec.ts new file mode 100644 index 00000000..6d1fa924 --- /dev/null +++ b/step_counter/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/step_counter_extension/.copier-answers.yml b/step_counter_extension/.copier-answers.yml new file mode 100644 index 00000000..dbd5d282 --- /dev/null +++ b/step_counter_extension/.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 (1 of 3 related examples). + This extension holds the UI/interface. +python_name: step_counter_extension +repository: '' +test: true + diff --git a/step_counter_extension/.gitignore b/step_counter_extension/.gitignore new file mode 100644 index 00000000..436fdf37 --- /dev/null +++ b/step_counter_extension/.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/step_counter_extension/.prettierignore b/step_counter_extension/.prettierignore new file mode 100644 index 00000000..a7b30907 --- /dev/null +++ b/step_counter_extension/.prettierignore @@ -0,0 +1,6 @@ +node_modules +**/node_modules +**/lib +**/package.json +!/package.json +step_counter_extension diff --git a/step_counter_extension/.yarnrc.yml b/step_counter_extension/.yarnrc.yml new file mode 100644 index 00000000..3186f3f0 --- /dev/null +++ b/step_counter_extension/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/step_counter_extension/README.md b/step_counter_extension/README.md new file mode 100644 index 00000000..b51604bd --- /dev/null +++ b/step_counter_extension/README.md @@ -0,0 +1,32 @@ +# 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 +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 (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). diff --git a/step_counter_extension/babel.config.js b/step_counter_extension/babel.config.js new file mode 100644 index 00000000..8b5c7642 --- /dev/null +++ b/step_counter_extension/babel.config.js @@ -0,0 +1 @@ +module.exports = require('@jupyterlab/testutils/lib/babel.config'); diff --git a/step_counter_extension/install.json b/step_counter_extension/install.json new file mode 100644 index 00000000..e092856a --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/jest.config.js b/step_counter_extension/jest.config.js new file mode 100644 index 00000000..b0471e66 --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/package.json b/step_counter_extension/package.json new file mode 100644 index 00000000..a086c996 --- /dev/null +++ b/step_counter_extension/package.json @@ -0,0 +1,203 @@ +{ + "name": "step_counter_extension", + "version": "0.1.0", + "description": "Adds a step 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 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:./../step_counter" + }, + "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/step_counter_extension/pyproject.toml b/step_counter_extension/pyproject.toml new file mode 100644 index 00000000..19638b25 --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/setup.py b/step_counter_extension/setup.py new file mode 100644 index 00000000..aefdf20d --- /dev/null +++ b/step_counter_extension/setup.py @@ -0,0 +1 @@ +__import__("setuptools").setup() diff --git a/step_counter_extension/src/__tests__/step_counter_extension.spec.ts b/step_counter_extension/src/__tests__/step_counter_extension.spec.ts new file mode 100644 index 00000000..cecdc9dd --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/src/index.ts b/step_counter_extension/src/index.ts new file mode 100644 index 00000000..a47f2063 --- /dev/null +++ b/step_counter_extension/src/index.ts @@ -0,0 +1,112 @@ +// 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). + +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 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; + this.counter.countChanged.connect(this.updateStepCountDisplay, this); + + // 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 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); + this.stepCountLabel = stepCountLabel; + + 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(); + } +} + +// 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 (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 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/step_counter_extension/step_counter_extension/__init__.py b/step_counter_extension/step_counter_extension/__init__.py new file mode 100644 index 00000000..a95ae863 --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/style/base.css b/step_counter_extension/style/base.css new file mode 100644 index 00000000..0669ebd5 --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/style/index.css b/step_counter_extension/style/index.css new file mode 100644 index 00000000..8a7ea29e --- /dev/null +++ b/step_counter_extension/style/index.css @@ -0,0 +1 @@ +@import url('base.css'); diff --git a/step_counter_extension/style/index.js b/step_counter_extension/style/index.js new file mode 100644 index 00000000..a028a764 --- /dev/null +++ b/step_counter_extension/style/index.js @@ -0,0 +1 @@ +import './base.css'; diff --git a/step_counter_extension/tsconfig.json b/step_counter_extension/tsconfig.json new file mode 100644 index 00000000..98979175 --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/tsconfig.test.json b/step_counter_extension/tsconfig.test.json new file mode 100644 index 00000000..1de37fd0 --- /dev/null +++ b/step_counter_extension/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "types": ["jest"] + } +} diff --git a/step_counter_extension/ui-tests/README.md b/step_counter_extension/ui-tests/README.md new file mode 100644 index 00000000..dbe6e8aa --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/ui-tests/jupyter_server_test_config.py b/step_counter_extension/ui-tests/jupyter_server_test_config.py new file mode 100644 index 00000000..f2a94782 --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/ui-tests/package.json b/step_counter_extension/ui-tests/package.json new file mode 100644 index 00000000..3ea36608 --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/ui-tests/playwright.config.js b/step_counter_extension/ui-tests/playwright.config.js new file mode 100644 index 00000000..9ece6fa1 --- /dev/null +++ b/step_counter_extension/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/step_counter_extension/ui-tests/tests/step_counter_extension.spec.ts b/step_counter_extension/ui-tests/tests/step_counter_extension.spec.ts new file mode 100644 index 00000000..525e5a35 --- /dev/null +++ b/step_counter_extension/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); +});