Notes on the JupyterLab Notebook HTML DOM Model, Part 7: Extension User Settings

In the previous posts in this series, I have reviewed the JupyterLab notebook DOM model and described an extension that can be used to set class tags on cell DOM elements based on cell tag metadata. In this post, we will review how JupyterLab extension user settings can be used to enable and disable cell toolbar buttons and identify which tags should be considered for “tag to class” processing.

JupyterLab extension user settings can be used to provide a persistent way of storing user settings. For an installed extension, settings are accessed from the JupyterLab Settings > Advanced Settings Editor menu:

The settings for a particular extension can be modified from the settings editor for that extension. Settings include options for setting boolean values via checkboxes, strings and numeric values. (I’m not sure if you can also raise dialogues for things like colour pickers, as you used to be able to do in the classic notebook extensions configurator?)

The defualt settings are defined in the schema/plugin.json JSON file (note this can also be edited manually via the JSON Settings Editor).

Each user setting is defined in a structured way via the properties element in the schema/plugin.json file. For example, here is a string definition and a boolean defintion:

"properties": {
    "tagprefix": {
      "type": "string",
      "title": "Tag prefix",
      "description": "Prefix to identify tag2class tags",
      "default": "iou-"
    },
    "activity_button": {
      "type": "boolean",
      "title": "Activity button",
      "description": "Whether to display the \"Activity [A]\" button",
      "default": false
    }
}

Numerics can be defined as a “number” type, as for example this setting taken from the jupyterlab-cell-flash extension:

"duration": {
      "type": "number",
      "title": "Duration (seconds)",
      "description": "The duration of the flash effect animation (in seconds)",
      "default": 0.5
    }

The extension-cookiecutter-ts gives an example of how to access and read settings data as part of an extension’s activation, but is not overly helpful when trying to make sense of how to use that data the scope of another widget (it might be obvious to someone who speaks Typescript; I’m at the level of riffing from examples provided with zero knowledge other than inspection of the example generated file). However, the ocordes/jupyterlab_notebookbuttons extension did provide a useful crib, and which forms the basis of the following example.

To start with, we make sure that the ISettingRegistry is available. We can then pass it to one or more registered extensions widgets of our ou creation that might want to make use of the settings:

import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

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

const plugin: JupyterFrontEndPlugin<void> = {
  activate,
  id: 'jupyterlab-empinken:plugin',
  autoStart: true,
  optional: [ISettingRegistry]
};

/**
 * Activate the extension.
 *
 * @param app Main application object
 */
 function activate(
   app: JupyterFrontEnd,
   settingRegistry: ISettingRegistry | null): void 

  // Pass the settingRegistry as a parameter to the new widget extensions
  // app.docRegistry.addWidgetExtension('Notebook', new ClassDemoExtension(settingRegistry));
  // app.docRegistry.addWidgetExtension('Notebook', new ButtonExtension(settingRegistry));
}

export default plugin;

So how do we then need to define our widgets? To begin with, we need to make sure we accept the settings parameter; a constructor then calls a function that actually reads in the settings. The settings themselves are read in from a settings file addressed behind the scenes using the plugin.id from the plugin named object we created in the previous step :

export class ButtonExtension
  implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel>
{
  //----  
  // Settings crib via:
  // https://github.com/ocordes/jupyterlab_notebookbuttons/blob/main/src/index.ts

  settings: ISettingRegistry.ISettings;

  constructor(protected settingRegistry: ISettingRegistry) {
    console.log('constructor');
    // read the settings
    this.setup_settings();
  }

  setup_settings(): void {
    Promise.all([this.settingRegistry.load(plugin.id)])
      .then(([settings]) => {
        console.log('reading settings');
        this.settings = settings;
        // update of settings is done automatically
        //settings.changed.connect(() => {
        //  this.update_settings(settings);
        //});
      })
      .catch((reason: Error) => {
        console.error(reason.message);
      });
  }
}

We can now access values from the settings, for example:

    // Create a tag prefix
    // Retrieve the tagprefix value from the extension settings file
    const tag_prefix =  this.settings.get('tagprefix').composite.toString();

    //Suppose we have four tag types 
    // that we want to toggle the behaviour of
    let tag_types: string[] = ['activity', 'solution', 'learner', 'tutor'];

    // Create an array to hold button definitions
    var tagButtonSpec = new Array();

    // Iterate over the tag_types
    // Note the use of *of* rather than *in*
    for (let typ of tag_types) {
      console.log("Setup on "+typ);
      tagButtonSpec[typ] = new Array();

      // Set array values based on settings retrieved from settings file
      tagButtonSpec[typ]['enabled'] = this.settings.get(typ.toString() +'_button').composite;
      // We can then test on: 
      //if (tagButtonSpec[typ]['enabled']) {}
    }

The setting of the tag_prefix value provides us with a way to limit which families of tags we might want to use as the basis for cell DOM classing.

If we already have the keys of an array defined, we can iterate over them in the following way:

// Get the key values from the array
let typ: keyof typeof tagButtonSpec;

// We can then iterate through them
for (typ in tagButtonSpec) {
  // The typ is actually a symbol object of some sort;
  // if we need the string value, use: typ.toString()
  tagButtonSpec[typ]['render'] = this.settings.get(typ.toString() +'_render').composite;
}

We can use the setting state to decide whether or not we want to render a toggle button on the UI, or render the coloured background for a particular tag.

User environments can have custom settings applied via entries in an overrides.json settings file (eg <sys.prefix>/local/share/jupyter/lab/settings/overrides.json; see the docs).

In the next post in this series, we’ll look at how we might be able to use settings to when setting the colours used to define the backgrounds for each empinken style tag.

Author: Tony Hirst

I'm a Senior Lecturer at The Open University, with an interest in #opendata policy and practice, as well as general web tinkering...

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: