Skip to content

Latest commit

 

History

History
366 lines (299 loc) · 10.7 KB

README.md

File metadata and controls

366 lines (299 loc) · 10.7 KB

Settings

Create and use new Settings for your extension.

This example shows how to create and use settings in a JupyterLab extension.

settings example

The core token required for handling the settings is ISettingRegistry (documentation). To use it, you first need to install its npm package:

jlpm add @jupyterlab/settingregistry

Once this is done, you can import the interface in your code.

// src/index.ts#L6-L6

import { ISettingRegistry } from '@jupyterlab/settingregistry';

To see how you can access the settings, let's have a look at src/index.ts.

// src/index.ts#L15-L21

const extension: JupyterFrontEndPlugin<void> = {
  id: PLUGIN_ID,
  description: 'A JupyterLab minimal example using settings.',
  autoStart: true,
  requires: [ISettingRegistry],
  activate: (app: JupyterFrontEnd, settings: ISettingRegistry) => {
    const { commands } = app;

The ISettingRegistry is passed to the activate function as an argument (variable settings) in addition to the JupyterLab application (variable app). You request that dependency with the property requires: [ISettingRegistry],. It lists the additional arguments you want to inject into the activate function in the JupyterFontEndPlugin.

But before going further, you need to define the settings of your extension. This is done through a JSON Schema. In the example it is called schema/settings-example.json.

// schema/settings-example.json

{
  "title": "Settings Example",
  "description": "Settings of the settings example.",
  "jupyter.lab.menus": {
    "main": [
      {
        "id": "jp-mainmenu-example-settings",
        "label": "Settings Example",
        "rank": 80,
        "items": [
          {
            "command": "@jupyterlab-examples/settings: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"
}

The title is the entry shown in the JupyterLab Advanced Settings. The description entry is a more detailed explanation of the extension using those settings. The type is a mandatory key required by JSON Schema. For all extensions, this will be an object as the settings are defined by a dictionary. Then the most important entry is properties describing a mapping of setting id and the associated properties.

JupyterLab is using special keys jupyter.lab.xyz to allow extension to add some features to the UI for a data description. In the above example, the jupyter.lab.menus key allow to add menu entry for your extension in the main menu bar. Have a look at the menu example for more information.

The naming of the file needs to follow a strict convention. When using settings, your extension name must be structured as package name:settings name. The settings file must be named settings name. In the example, the package name is:

// package.json#L2-L2

"name": "@jupyterlab-examples/settings",

and the extension id is:

// src/index.ts#L8-L8

const PLUGIN_ID = '@jupyterlab-examples/settings:settings-example';

Therefore the settings file must be named settings-example.json.

The folder containing the settings definition needs to be specified in the package.json file in the jupyterlab section (here schema):

// package.json#L94-L98

"jupyterlab": {
    "extension": true,
    "outputDir": "jupyterlab_examples_settings/labextension",
    "schemaDir": "schema"
},

And you should not forget to add it to the files of the package:

// package.json#L16-L20

"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}",
    "schema/*.json"
],

Now that the settings are defined and included in the package, you can use them inside your extension. Let's look at this example:

// src/index.ts#L15-L80

const extension: JupyterFrontEndPlugin<void> = {
  id: PLUGIN_ID,
  description: 'A JupyterLab minimal example using settings.',
  autoStart: true,
  requires: [ISettingRegistry],
  activate: (app: JupyterFrontEnd, settings: ISettingRegistry) => {
    const { commands } = app;
    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}'`
      );
    }

    // Wait for the application to be restored and
    // for the settings for this plugin to be loaded
    Promise.all([app.restored, settings.load(PLUGIN_ID)])
      .then(([, setting]) => {
        // Read the settings
        loadSetting(setting);

        // Listen for your plugin setting changes using Signal
        setting.changed.connect(loadSetting);

        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}`
        );
      });
  }
};

First, the extension waits for the application and for your plugin settings to be loaded :

// src/index.ts#L42-L42

Promise.all([app.restored, settings.load(PLUGIN_ID)])

Then once the settings are loaded, each setting can be read using the get method and the setting id (the key defined in the settings JSON file).

After getting the setting, you need to access the composite attribute to get its value and specify the type explicitly.

// src/index.ts#L30-L38

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}'`
  );
}

composite means the setting value is the composition of the default and the user values.

To react at a setting change by the user, you should use the signal changed. In this case, when that signal is emitted the function loadSetting is called with the new settings.

// src/index.ts#L48-L48

setting.changed.connect(loadSetting);

Finally, to demonstrate the programmatic change of a setting, a command to toggle the flag and increment the limit settings is implemented.

// src/index.ts#L53-L71

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}`
      );
    });
}

The set method of setting is the one storing the new value.

// src/index.ts#L56-L57

setting.set('flag', !flag),
setting.set('limit', limit + 1)

That command can be executed by clicking on the item menu defined in the settings file:

// schema/settings-example.json#L4-L17

"jupyter.lab.menus": {
  "main": [
    {
      "id": "jp-mainmenu-example-settings",
      "label": "Settings Example",
      "rank": 80,
      "items": [
        {
          "command": "@jupyterlab-examples/settings:toggle-flag"
        }
      ]
    }
  ]
},

Have a look at the menu example for more information.

Note

This example uses Promises. This is a technology to handle asynchronous action like reading the settings in this example. So have a look at that tutorial if you want to know more about the then/catch used here.

Where to Go Next

This example makes use of various concepts of JupyterLab. To get more information about them, have a look at the corresponding examples:

You may be interested to save state variables instead of settings; i.e. save variables that the user is not aware of (e.g. the current opened widgets). For that, you will need another core token IStateDB (see that example for more information).