4

I'm working on creating a server-side extension in JupyterLab and have been searching for quite a while for a way to get the currently active notebook name inside my index.ts file. I found this existing extension that gets the currently active tab name with ILabShell and sets the browser tab to have the same name. In my case I'll be triggering the process with a button on the notebook toolbar so the active tab will always be a notebook. However, when I try to use the code in the activate section of my extension, I get TypeError: labShell.currentChanged is undefined where labShell is an instance of ILabShell. That extension doesn't support JupyterLab 3.0+ so I believe that's part of the problem. However, currentChanged for ILabShell is clearly defined here. What if anything can I change to make it work? Is there another way to accomplish what I'm trying to do? I'm aware of things like this to get the notebook name inside the notebook but that's not quite what I'm trying to do.

Windows 10,
Node v14.17.0,
npm 6.14.13,
jlpm 1.21.1,
jupyter lab 3.0.14

I'm using this example server extension as a template: https://github.com/jupyterlab/extension-examples/tree/master/server-extension

index.ts file from the existing extension to get the current tab name:

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

import { Title, Widget } from '@lumino/widgets';

/**
 * Initialization data for the jupyterlab-active-as-tab-name extension.
 */
const extension: JupyterFrontEndPlugin<void> = {
  id: 'jupyterlab-active-as-tab-name',
  autoStart: true,
  requires: [ILabShell],
  activate: (app: JupyterFrontEnd, labShell: ILabShell) => {
    const onTitleChanged = (title: Title<Widget>) => {
      console.log('the JupyterLab main application:', title);
      document.title = title.label;
    };

    // Keep the session object on the status item up-to-date.
    labShell.currentChanged.connect((_, change) => {
      const { oldValue, newValue } = change;

      // Clean up after the old value if it exists,
      // listen for changes to the title of the activity
      if (oldValue) {
        oldValue.title.changed.disconnect(onTitleChanged);
      }
      if (newValue) {
        newValue.title.changed.connect(onTitleChanged);
      }
    });
  }
};

export default extension;

My index.ts file:

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

import { ICommandPalette } from '@jupyterlab/apputils';

import { ILauncher } from '@jupyterlab/launcher';

import { requestAPI } from './handler';

import { ToolbarButton } from '@jupyterlab/apputils';

import { DocumentRegistry } from '@jupyterlab/docregistry';

import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook';

import { IDisposable } from '@lumino/disposable';

import { Title, Widget } from '@lumino/widgets';

export class ButtonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
    
  constructor(app: JupyterFrontEnd) { 
      this.app = app;
  }
  
  readonly app: JupyterFrontEnd
    
  createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {

    // dummy json data to test post requests
    const data2 = {"test message" : "message"}
    const options = {
      method: 'POST',
      body: JSON.stringify(data2),
      headers: {'Content-Type': 'application/json'
      }
    };
 
    // Create the toolbar button
    let mybutton = new ToolbarButton({ 
      label: 'Measure Energy Usage',
      onClick: async () => {

        // POST request to Jupyter server
        const dataToSend = { file: 'nbtest.ipynb' };
        try {
          const reply = await requestAPI<any>('hello', {
            body: JSON.stringify(dataToSend),
            method: 'POST'
          });
          console.log(reply);
        } catch (reason) {
          console.error(
            `Error on POST /jlab-ext-example/hello ${dataToSend}.\n${reason}`
          );
        }
        
        // sample POST request to svr.js
        fetch('http://localhost:9898/api', options);
      }
    });
    // Add the toolbar button to the notebook toolbar
    panel.toolbar.insertItem(10, 'MeasureEnergyUsage', mybutton);
    console.log("MeasEnerUsage activated");

    // The ToolbarButton class implements `IDisposable`, so the
    // button *is* the extension for the purposes of this method.
    return mybutton;
  }
}

/**
 * Initialization data for the server-extension-example extension.
 */
const extension: JupyterFrontEndPlugin<void> = {
  id: 'server-extension-example',
  autoStart: true,
  optional: [ILauncher],
  requires: [ICommandPalette, ILabShell],
  activate: async (
    app: JupyterFrontEnd,
    palette: ICommandPalette,
    launcher: ILauncher | null,
    labShell: ILabShell
  ) => {
    console.log('JupyterLab extension server-extension-example is activated!');
    const your_button = new ButtonExtension(app);
    app.docRegistry.addWidgetExtension('Notebook', your_button);
    
    // sample GET request to jupyter server
    try {
      const data = await requestAPI<any>('hello');
      console.log(data);
    } catch (reason) {
      console.error(`Error on GET /jlab-ext-example/hello.\n${reason}`);
    }

    // get name of active tab

    const onTitleChanged = (title: Title<Widget>) => {
      console.log('the JupyterLab main application:', title);
      document.title = title.label;
    };
    
    // Keep the session object on the status item up-to-date.
    labShell.currentChanged.connect((_, change) => {
      const { oldValue, newValue } = change;

      // Clean up after the old value if it exists,
      // listen for changes to the title of the activity
      if (oldValue) {
        oldValue.title.changed.disconnect(onTitleChanged);
      }
      if (newValue) {
        newValue.title.changed.connect(onTitleChanged);
      }
    });
  }
};

export default extension;

1 Answers1

3

There are two ways to fix it and one way to improve it. I recommend using (2) and (3).

  1. Your order of arguments in activate is wrong. There is no magic matching of argument types to signature function; instead arguments are passed in the order given in requires and then optional. This means that you will receive:

    ...[JupyterFrontEnd, ICommandPalette, ILabShell, ILauncher]
    

    but what you are expecting is:

    ...[JupyterFrontEnd, ICommandPalette, ILauncher, ILabShell]
    

    In other words, optionals are always at the end. There is no static type check so this is a common source of mistakes - just make sure you double check the order next time (or debug/console.log to see what you are getting).

  2. Actually, don't require ILabShell as a token. Use the ILabShell that comes in app.shell instead. This way your extension will be also compatible with other frontends built using JupyterLab components.

    shell = app.shell as ILabShell
    
  3. (optional improvement) install RetroLab as a development-only requirement and use import type (this way it is not a runtime requirement) to ensure compatibility with RetroLab:

    import type { IRetroShell } from '@retrolab/application';
    // ... and then in `activate()`:
    shell = app.shell as ILabShell | IRetroShell
    

    to be clear: not doing so would not make your extension incompatible; what it does is ensures you do not make it incompatible by depending on lab-specific behaviour of the ILabShell in the future.

So in total it would look like:

import {
  ILabShell,
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';
import type { IRetroShell } from '@retrolab/application';

// ...

const extension: JupyterFrontEndPlugin<void> = {
  id: 'server-extension-example',
  autoStart: true,
  optional: [ILauncher],
  requires: [ICommandPalette],
  activate: async (
    app: JupyterFrontEnd,
    palette: ICommandPalette,
    launcher: ILauncher | null
  ) => {
    let shell = app.shell as ILabShell | IRetroShell ;
    shell.currentChanged.connect((_, change) => {
        console.log(change);
        // ...
    });
  }
};

export default extension;
Dima
  • 35
  • 5
krassowski
  • 13,598
  • 4
  • 60
  • 92