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

:::
```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**

對照 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]

- 原因是因為 command 指令在系統中找不到
- 需要在 code 裡面像系統註冊指令,子選項才生成
- **Settings > Advanced Settings Editor > AIMaker Settings**

對照 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)
>
> 點選 ==[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)
> 當 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` 物件**

<br>
### Step 4.c - 點了 menu item 後,去變更系統設定值,並接收變更事件
> [](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
```