From be848533cd004588705ba47d183bb6c0b83baa27 Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Mon, 23 Dec 2019 12:00:05 +0100 Subject: [PATCH] Launcher first version --- .github/workflows/main.yml | 1 + launcher/.gitignore | 6 ++ launcher/LICENSE | 30 +++++++ launcher/README.md | 110 ++++++++++++++++++++++++ launcher/package.json | 56 ++++++++++++ launcher/src/index.ts | 85 +++++++++++++++++++ launcher/style/Python-logo-notext.svg | 31 +++++++ launcher/style/index.css | 3 + launcher/tsconfig.json | 24 ++++++ launcher/tslint.json | 117 ++++++++++++++++++++++++++ lerna.json | 2 +- package.json | 2 +- 12 files changed, 465 insertions(+), 2 deletions(-) create mode 100644 launcher/.gitignore create mode 100644 launcher/LICENSE create mode 100644 launcher/package.json create mode 100644 launcher/src/index.ts create mode 100644 launcher/style/Python-logo-notext.svg create mode 100644 launcher/style/index.css create mode 100644 launcher/tsconfig.json create mode 100644 launcher/tslint.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ecb4eb2e..90e3749b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,6 +21,7 @@ jobs: - basics/signals - command-palette - commands + - launcher - main-menu - react/react-widget - settings diff --git a/launcher/.gitignore b/launcher/.gitignore new file mode 100644 index 00000000..18cace13 --- /dev/null +++ b/launcher/.gitignore @@ -0,0 +1,6 @@ +*.bundle.* +lib/ +node_modules/ +*.egg-info/ +.ipynb_checkpoints +*.tsbuildinfo diff --git a/launcher/LICENSE b/launcher/LICENSE new file mode 100644 index 00000000..2649ea39 --- /dev/null +++ b/launcher/LICENSE @@ -0,0 +1,30 @@ +BSD 3-Clause License + +Copyright (c) 2019, Jeremy Tuloup +Copyright (c) 2019, Jupyter Development Team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/launcher/README.md b/launcher/README.md index e69de29b..b6ab7ae0 100644 --- a/launcher/README.md +++ b/launcher/README.md @@ -0,0 +1,110 @@ +# Start your extension from the launcher + +In this example, you will learn how to start your extension from the launcher and how to have optional +dependencies to JupyterLab features. + +![Launcher example](preview.png) + +> Acknowledgement: This example is copied from Jeremy Tuloup [Python file extension](https://github.com/jtpio/jupyterlab-python-file). + +In this example, you will add the ability to create an empty Python file. To do so, +your extension will use two commands defined by the [documents manager](https://github.com/jupyterlab/jupyterlab/blob/master/packages/docmanager-extension/src/index.ts#L47-L75) of JupyterLab: + +- `'docmanager:new-untitled'`: Create new untitled document +- `'docmanager:open'`: Open a document + +The command will create a new Python file and then open it: + +```ts +// src/index.ts#L36-L58 + +commands.addCommand(CommandIDs.createNew, { + label: args => (args['isPalette'] ? 'New Python File' : 'Python File'), + caption: 'Create a new Python file', + iconClass: args => (args['isPalette'] ? '' : ICON_CLASS), + execute: async args => { + // Get the directory in which the Python file must be created; + // otherwise take the current filebrowser directory + let cwd = args['cwd'] || browserFactory.defaultBrowser.model.path; + + // Create a new untitled python file + const model = await commands.execute('docmanager:new-untitled', { + path: cwd, + type: 'file', + ext: 'py' + }); + + // Open the newly created file with the 'Editor' + return commands.execute('docmanager:open', { + path: model.path, + factory: FACTORY + }); + } +}); +``` + +To link that command to the JupyterLab launcher, the `ILauncher` interface needs to be passed to the `activate` +extension function. As that interface is provided by the `@jupyterlab/launcher` package, it needs first to be installed: + +```bash +jlpm add @jupyterlab/launcher +``` + +Then you can use it in the extension by importing it: + +```ts +// src/index.ts#L10-L10 + +import { ILauncher } from '@jupyterlab/launcher'; +``` + +And finally you can request it as extension dependency: + +```ts +// src/index.ts#L22-L33 + +const extension: JupyterFrontEndPlugin = { + id: '@jupyterlab-examples/launcher', + autoStart: true, + optional: [ILauncher, IMainMenu, ICommandPalette], + requires: [IFileBrowserFactory], + activate: ( + app: JupyterFrontEnd, + browserFactory: IFileBrowserFactory, + launcher: ILauncher | null, + menu: IMainMenu | null, + palette: ICommandPalette | null + ) => { +``` + +In this example, the `ILauncher` interface is requested as optional dependency and not as classical dependency. This allow other application without launcher to be able +to use your extension. +If the application is unable to provide an optional interface, it will take a `null` +value. +Therefore before adding the command to the launcher, you need to check if the `launcher` +variable is not `null`: + +```ts +// src/index.ts#L60-L67 + +// Add the command to the launcher +if (launcher) { + launcher.add({ + command: CommandIDs.createNew, + category: 'Other', + rank: 1 + }); +} +``` + +## Where to Go Next + +This example uses a _command_. This is an essential concept of JupyterLab. To know more about it +have a look at the [command example](../commands/README.md). + +As seen in this example too, an user can execute a command from other UI elements than the launcher. To +know more about those other possible, you could look at the following examples: + +- Add the command to the [command palette](../command-palette/README.md) +- Add the command to a [menu](../main-menu/README.md) +- Add the command to a [context menu](../context-menu/README.md) diff --git a/launcher/package.json b/launcher/package.json new file mode 100644 index 00000000..1d05a72e --- /dev/null +++ b/launcher/package.json @@ -0,0 +1,56 @@ +{ + "name": "@jupyterlab-examples/launcher", + "version": "0.1.0", + "description": "A minimal JupyterLab example using the launcher.", + "keywords": [ + "jupyter", + "jupyterlab", + "jupyterlab-extension" + ], + "homepage": "https://github.com/my_name/jupyterlab_myextension", + "bugs": { + "url": "https://github.com/my_name/jupyterlab_myextension/issues" + }, + "license": "BSD-3-Clause", + "author": "my_name", + "files": [ + "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "style/**/*.{css,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": "https://github.com/my_name/jupyterlab_myextension.git" + }, + "scripts": { + "build": "tsc", + "clean": "rimraf lib tsconfig.tsbuildinfo", + "link": "jupyter labextension link . --no-build", + "prepare": "jlpm run clean && jlpm run build", + "tslint": "tslint --fix -c tslint.json --project tsconfig.json \"**/*{.ts,.tsx}\"", + "tslint:check": "tslint -c tslint.json --project tsconfig.json \"**/*{.ts,.tsx}\"", + "watch": "tsc -w" + }, + "dependencies": { + "@jupyterlab/application": "^1.0.0", + "@jupyterlab/filebrowser": "^1.2.1", + "@jupyterlab/launcher": "^1.2.1", + "@jupyterlab/mainmenu": "^1.2.1" + }, + "devDependencies": { + "rimraf": "^2.6.1", + "tslint": "^5.20.1", + "tslint-config-prettier": "^1.18.0", + "tslint-plugin-prettier": "^2.0.1", + "tslint-react": "^4.1.0", + "typescript": "~3.5.2" + }, + "sideEffects": [ + "style/*.css" + ], + "jupyterlab": { + "extension": true + } +} diff --git a/launcher/src/index.ts b/launcher/src/index.ts new file mode 100644 index 00000000..cad376c1 --- /dev/null +++ b/launcher/src/index.ts @@ -0,0 +1,85 @@ +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { ICommandPalette } from '@jupyterlab/apputils'; + +import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; + +import { ILauncher } from '@jupyterlab/launcher'; + +import { IMainMenu } from '@jupyterlab/mainmenu'; + +const FACTORY = 'Editor'; +const ICON_CLASS = 'jp-example-PythonIcon'; +const PALETTE_CATEGORY = 'Text Editor'; + +namespace CommandIDs { + export const createNew = 'launcher:create-new-python-file'; +} + +const extension: JupyterFrontEndPlugin = { + id: '@jupyterlab-examples/launcher', + autoStart: true, + optional: [ILauncher, IMainMenu, ICommandPalette], + requires: [IFileBrowserFactory], + activate: ( + app: JupyterFrontEnd, + browserFactory: IFileBrowserFactory, + launcher: ILauncher | null, + menu: IMainMenu | null, + palette: ICommandPalette | null + ) => { + const { commands } = app; + + commands.addCommand(CommandIDs.createNew, { + label: args => (args['isPalette'] ? 'New Python File' : 'Python File'), + caption: 'Create a new Python file', + iconClass: args => (args['isPalette'] ? '' : ICON_CLASS), + execute: async args => { + // Get the directory in which the Python file must be created; + // otherwise take the current filebrowser directory + let cwd = args['cwd'] || browserFactory.defaultBrowser.model.path; + + // Create a new untitled python file + const model = await commands.execute('docmanager:new-untitled', { + path: cwd, + type: 'file', + ext: 'py' + }); + + // Open the newly created file with the 'Editor' + return commands.execute('docmanager:open', { + path: model.path, + factory: FACTORY + }); + } + }); + + // Add the command to the launcher + if (launcher) { + launcher.add({ + command: CommandIDs.createNew, + category: 'Other', + rank: 1 + }); + } + + // Add the command to the palette + if (palette) { + palette.addItem({ + command: CommandIDs.createNew, + args: { isPalette: true }, + category: PALETTE_CATEGORY + }); + } + + // Add the command to the menu + if (menu) { + menu.fileMenu.newMenu.addGroup([{ command: CommandIDs.createNew }], 30); + } + } +}; + +export default extension; diff --git a/launcher/style/Python-logo-notext.svg b/launcher/style/Python-logo-notext.svg new file mode 100644 index 00000000..23bd5a23 --- /dev/null +++ b/launcher/style/Python-logo-notext.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + \ No newline at end of file diff --git a/launcher/style/index.css b/launcher/style/index.css new file mode 100644 index 00000000..cc663229 --- /dev/null +++ b/launcher/style/index.css @@ -0,0 +1,3 @@ +.jp-example-PythonIcon { + background-image: url('Python-logo-notext.svg'); +} diff --git a/launcher/tsconfig.json b/launcher/tsconfig.json new file mode 100644 index 00000000..81139f54 --- /dev/null +++ b/launcher/tsconfig.json @@ -0,0 +1,24 @@ +{ + "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": false, + "target": "es2017", + "types": [] + }, + "include": ["src/*"] +} diff --git a/launcher/tslint.json b/launcher/tslint.json new file mode 100644 index 00000000..201ebcd5 --- /dev/null +++ b/launcher/tslint.json @@ -0,0 +1,117 @@ +{ + "rulesDirectory": ["tslint-plugin-prettier"], + "rules": { + "prettier": [true, { "singleQuote": true }], + "align": [true, "parameters", "statements"], + "await-promise": true, + "ban": [ + true, + ["_", "forEach"], + ["_", "each"], + ["$", "each"], + ["angular", "forEach"] + ], + "class-name": true, + "comment-format": [true, "check-space"], + "curly": true, + "eofline": true, + "forin": false, + "indent": [true, "spaces", 2], + "interface-name": [true, "always-prefix"], + "jsdoc-format": true, + "label-position": true, + "max-line-length": [false], + "member-access": false, + "member-ordering": [false], + "new-parens": true, + "no-angle-bracket-type-assertion": true, + "no-any": false, + "no-arg": true, + "no-bitwise": true, + "no-conditional-assignment": true, + "no-consecutive-blank-lines": false, + "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], + "no-construct": true, + "no-debugger": true, + "no-default-export": false, + "no-duplicate-variable": true, + "no-empty": true, + "no-eval": true, + "no-floating-promises": true, + "no-inferrable-types": false, + "no-internal-module": true, + "no-invalid-this": [true, "check-function-in-method"], + "no-null-keyword": false, + "no-reference": true, + "no-require-imports": false, + "no-shadowed-variable": false, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-use-before-declare": false, + "no-var-keyword": true, + "no-var-requires": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-finally", + "check-whitespace" + ], + "one-variable-per-declaration": [true, "ignore-for-loop"], + "quotemark": { + "options": [true, "single", "avoid-escape"], + "severity": "off" + }, + "radix": true, + "semicolon": [true, "always", "ignore-bound-class-methods"], + "switch-default": true, + "trailing-comma": [ + false, + { + "multiline": "never", + "singleline": "never" + } + ], + "triple-equals": [true, "allow-null-check", "allow-undefined-check"], + "typedef": [false], + "typedef-whitespace": [ + false, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "space", + "index-signature": "space", + "parameter": "space", + "property-declaration": "space", + "variable-declaration": "space" + } + ], + "use-isnan": true, + "use-strict": [false], + "variable-name": [ + true, + "check-format", + "allow-leading-underscore", + "ban-keywords", + "allow-pascal-case" + ], + "whitespace": [ + true, + "check-branch", + "check-operator", + "check-separator", + "check-type" + ] + }, + "linterOptions": { + "exclude": ["node_modules/**/*.ts", "node_modules/**/*.tsx", "**/*.d.ts"] + } +} diff --git a/lerna.json b/lerna.json index 7a9d2b1f..7cc250fa 100644 --- a/lerna.json +++ b/lerna.json @@ -6,7 +6,7 @@ "command-palette", "context-menu/*", "keyboard-shortcuts/*", - "launcher/*", + "launcher", "left-right-areas/*", "main-menu", "react/*", diff --git a/package.json b/package.json index 7458c2eb..53f51af5 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "command-palette", "context-menu/*", "keyboard-shortcuts/*", - "launcher/*", + "launcher", "left-right-areas/*", "main-menu", "react/*",