Notebook / Extensions / Main-Menu === > #ipython, jupyter notebook, python notebook, jupyterlab, elyra ###### tags: `Jupyter` ###### tags: `Jupyter`, `JupyterLab`, `Notebook`, `Elyra`, `Extension` :::info :bulb: **主題重點** 1. 如何添加 menu 選項 2. 點了 menu item 後,跳出 hello 對話框 3. 跟系統要設定值 4. 點了 menu item 後,去變更系統設定值,並接收變更事件 ::: <br> [TOC] <br> <hr> ## 官方範例 - ### [jupyterlab / extension-examples / main-menu](https://github.com/jupyterlab/extension-examples/tree/master/main-menu) - ### [jupyterlab / extension-examples / settings](https://github.com/jupyterlab/extension-examples/tree/master/settings) <br> ## 如何從最基本範例改起 ### Step 1 - 下載範本 ```bash= $ cookiecutter https://github.com/jupyterlab/extension-cookiecutter-ts author_name []: tj_tsai author_email []: labextension_name [myextension]: menu_aimaker <--- python_name [menu_aimaker]: project_short_description [A JupyterLab extension.]: has_settings [n]: n has_server_extension [n]: n has_binder [n]: y repository [https://github.com/github_username/menu_aimaker]: $ cd menu_aimaker ``` - 之後可啟用 has_settings,就可以不用設定 package.json <br> ### Step 2 - 安裝相依套件 + 編譯 + 安裝 ```bash= # 安裝相依套件 $ jlpm add @jupyterlab/apputils ... [1/4] Resolving packages... ... [2/4] Fetching packages... ... [3/4] Linking dependencies... ... [4/4] Building fresh packages... ... # 才能使用 tsc 指令,才能 build #$ jlpm run build # 持續編譯與安裝 $ jlpm run watch ``` - 安裝相依套件後,`package.json` 檔案變化 安裝前 ```json ... "dependencies": { "@jupyterlab/application": "^3.1.0" }, ... ``` 安裝後 ``` ... "dependencies": { "@jupyterlab/application": "^3.1.0", "@jupyterlab/apputils": "^3.2.4" }, ... ``` <br> ### Step 3 - 添加 menu 選項 1. **修改 `package.json`** :::warning :warning: 2022/02/21 1. jlpm watch 需要重啟,才能重新讀取 `package.json` 2. 必要屬性 ```json "schemaDir": "schema" ``` ::: ```diff ... "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}" + "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}", + "schema/*.json" ], ... "jupyterlab": { "extension": true, - "outputDir": "menu_aimaker/labextension" + "outputDir": "menu_aimaker/labextension", + "schemaDir": "schema" }, ... ``` 2. **新增 `schema/plugin.json` 檔案** :::warning :warning: 2022/02/21 1. jlpm watch 需要重啟,才能重新讀取 `plugin.json` 2. 必要屬性 ```json { "type": "object" } ``` Advanced Settings Editor 的基本 UI ![](https://i.imgur.com/9eA3X2I.png) ::: ```json= { "title": "AIMaker Settings", "description": "Settings for AIMaker", "jupyter.lab.menus": { "main": [ { "id": "jp-mainmenu-aimaker-settings", "label": "AIMaker", "rank": 80, "items": [ { "command": "aimaker-menu:toggle-flag" } ] } ] }, "properties": { "limit": { "type": "integer", "title": "Limit", "description": "This is an example of an integer setting.", "default": 25 }, "flag": { "type": "boolean", "title": "Simple flag", "description": "This is an example of a boolean setting.", "default": false } }, "additionalProperties": false, "type": "object" } ``` - `"jupyter.lab.menus"` 會在 main menu 長出來 - 其餘選項,會出現在 `Settings > Advanced Settings Editor > AIMaker Settings` - properties 存取方式 http://localhost:8888/lab/api/settings/my_online_examples:plugin 3. **重啟 build 指令** ```bash= # Ctrl+C 中斷,再重啟 $ jlpm run watch ``` 4. **觀看 JupyterLab** - **main menu** ![](https://i.imgur.com/2KptR48.png) 對照 menu 設定: ```json=4 "jupyter.lab.menus": { "main": [ { "id": "jp-mainmenu-aimaker-settings", "label": "AIMaker", "rank": 80, "items": [ { "command": "aimaker-menu:toggle-flag" } ] } ] }, ``` - 目前 [AIMaker] 長不出子選項 [Toggle Flag and Increment Limit] ![](https://i.imgur.com/vPhfhmT.png) - 原因是因為 command 指令在系統中找不到 - 需要在 code 裡面像系統註冊指令,子選項才生成 - **Settings > Advanced Settings Editor > AIMaker Settings** ![](https://i.imgur.com/mEyqzre.png) 對照 limit 屬性: ```json=18 "properties": { "limit": { "type": "integer", "title": "Limit", "description": "This is an example of an integer setting.", "default": 25 }, ``` <br> ### Step 4.a - 點了 menu item 後,跳出 hello 對話框 > [![](https://i.imgur.com/kcVlfFH.png)](https://i.imgur.com/kcVlfFH.png) > > 點選 ==[AIMaker]== > ==[Say Hello]==,會跳出一個訊息。 ```typescript= import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; const PLUGIN_ID = 'aimaker-menu:plugin'; const COMMAND_ID = 'aimaker-menu:toggle-flag'; /** * Initialization data for the aimaker-menu extension. */ const plugin: JupyterFrontEndPlugin<void> = { id: PLUGIN_ID, autoStart: true, activate: (app: JupyterFrontEnd) => { console.log('aimaker-menu is activated!'); app.commands.addCommand(COMMAND_ID, { label: 'Say Hello', execute: () => { alert('hello'); } });; } }; export default plugin; ``` - **`PLUGIN_ID = 'aimaker-menu:plugin'`** - `aimaker-menu` 是當前 NPM 套件名稱 (擴充套件名稱) - `plugin` 目前沒有用到,可以隨意命名,下個範例會講解用途 - **`app.commands.addCommand(...)`** - 向 JupyterFrontEnd 物件(i.e. `app`)的 command 物件,註冊新的指令 - 添加:key 和 function - 之後就可以呼叫 key,執行對應的 function <br> ### Step 4.b - 跟系統要設定值 > [![](https://i.imgur.com/WH59vKc.png)](https://i.imgur.com/WH59vKc.png) > 當 JupyterLab 起來的時候,且在 plugin 載入時,讀取當前的 AIMaker 設定值 ```javascript= import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ISettingRegistry } from '@jupyterlab/settingregistry' const PLUGIN_ID = 'aimaker-menu:plugin'; /** * Initialization data for the aimaker-menu extension. */ const plugin: JupyterFrontEndPlugin<void> = { id: PLUGIN_ID, autoStart: true, requires: [ISettingRegistry], activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => { console.log('aimaker-menu is activated!'); console.log('settingsRegistry:', settingRegistry); Promise.all([settingRegistry.load(PLUGIN_ID)]) .then(([settings]) => { // settings type: ISettingRegistry.ISettings const limit = settings.get('limit').composite; const flag = settings.get('flag').composite; alert('AIMaker Settings:\n\n' + ' - limit: ' + limit + '(type: ' + typeof(limit) + ')\n' + ' - flag: ' + flag + '(type: ' + typeof(flag) + ')\n' ); }); } }; export default plugin; ``` - **要求互動的元件:[ISettingRegistry](https://jupyterlab.github.io/jupyterlab/modules/_settingregistry_src_index_.isettingregistry.html)** 請求 JupyterLab 傳入 ISettingRegistry 元件 - **透過 promise (非同步程序)** 等待 `settingRegistry.load(...)` 作業完成 隨後在 `.then(...)` 中進行讀值,再跳出訊息顯示 - **屬性`composite`** > `composite` means the setting value is the composition of the default and the user values. - **印出 `settings`, `limit`, `flag` 物件** ![](https://i.imgur.com/5MLIaz4.png) <br> ### Step 4.c - 點了 menu item 後,去變更系統設定值,並接收變更事件 > [![](https://i.imgur.com/Kb3hgIw.png)](https://i.imgur.com/Kb3hgIw.png) > > 點選 ==[AIMaker]== > ==[Toggle flag and increase limit]==, > 會將 flag 值反轉並遞增 limit 值,隨後會觸發設定值變更事件 ```typescript= import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ISettingRegistry } from '@jupyterlab/settingregistry' const PLUGIN_ID = 'aimaker-menu:plugin'; const COMMAND_ID = 'aimaker-menu:toggle-flag'; /** * Initialization data for the aimaker-menu extension. */ const plugin: JupyterFrontEndPlugin<void> = { id: PLUGIN_ID, autoStart: true, requires: [ISettingRegistry], activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => { console.log('aimaker-menu is activated!'); console.log('settingsRegistry:', settingRegistry); let limit = 25; let flag = false; function loadSettings(settings: ISettingRegistry.ISettings): void { limit = settings.get('limit').composite as number; flag = settings.get('flag').composite as boolean; console.log('AIMaker Settings:\n\n' + ' - limit: ' + limit + ' (type: ' + typeof(limit) + ')\n' + ' - flag: ' + flag + ' (type: ' + typeof(flag) + ')\n' ); } function onSettingsChanged(settings: ISettingRegistry.ISettings): void { loadSettings(settings); } Promise.all([app.restored, settingRegistry.load(PLUGIN_ID)]) .then(([_, settings]) => { // shows the current settings loadSettings(settings); // listen to the changes settings.changed.connect(onSettingsChanged); // command: toggle flag and increase limit app.commands.addCommand(COMMAND_ID, { label: 'Toggle flag and increase limit', execute: () => { console.log('executing:', COMMAND_ID); settings.set('limit', limit + 1); settings.set('flag', !flag); } }); }); } }; export default plugin; ``` <br> ### Step 4.d - 官方範例 > 將套件名稱修改為 `aimaker-menu` ```typescript= import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; const PLUGIN_ID = 'aimaker-menu:plugin'; const COMMAND_ID = 'aimaker-menu:toggle-flag'; /** * Initialization data for the aimaker-menu extension. */ const plugin: JupyterFrontEndPlugin<void> = { id: PLUGIN_ID, autoStart: true, requires: [ISettingRegistry], activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry) => { console.log('JupyterLab extension aimaker-menu is activated! x4'); console.log('settingRegistry', settingRegistry); let limit = 25; let flag = false; /** * Load the settings for this extension * * @param setting Extension settings */ function loadSetting(setting: ISettingRegistry.ISettings): void { // Read the settings and convert to the correct type limit = setting.get('limit').composite as number; flag = setting.get('flag').composite as boolean; console.log( `Settings Example extension: Limit is set to '${limit}' and flag to '${flag}'` ); } Promise.all([app.restored, settingRegistry.load(PLUGIN_ID)]) .then(([x, setting]) => { console.log('x:', x); console.log('setting:', setting); loadSetting(setting); // Listen for your plugin setting changes using Signal //setting.changed.connect(loadSetting); app.commands.addCommand(COMMAND_ID, { label: 'Toggle Flag and Increment Limit', isToggled: () => flag, execute: () => { // Programmatically change a setting Promise.all([ setting.set('flag', !flag), setting.set('limit', limit + 1), ]) .then(() => { const newLimit = setting.get('limit').composite as number; const newFlag = setting.get('flag').composite as boolean; window.alert( `Settings Example extension: Limit is set to '${newLimit}' and flag to '${newFlag}'` ); }) .catch((reason) => { console.error( `Something went wrong when changing the settings.\n${reason}` ); }); }, }); }) .catch((reason) => { console.error( `Something went wrong when reading the settings.\n${reason}` ); }); } }; export default plugin; ``` <br> ### Step 99 - 打包你的套件 - ### [使用 python 套件打包](https://jupyterlab.readthedocs.io/en/stable/extension/extension_tutorial.html#packaging-your-extension) 1. 安裝 build 套件 ``` $ pip install build ``` 2. a. 打包成原始碼套件 (source_package.tar.gz) ``` $ python -m build -s ``` b. 打包成 wheel 套件 (.whl) ``` $ python -m build ```