Repository: nebari-dev/jupyterlab-launchpad (language: python)

Issue:
State in the table for running kernels does not get updated
The second table in the dialog does not get updated when the kernel state changes

https://github.com/nebari-dev/jupyterlab-launchpad/blob/5ba3fee41f7cf757d565d5b1266c546087abf0aa/src/dialogs.tsx#L292-L313

Because it includes things like "state: starting" it needs to be dynamically updated. We need to trigger `forceUpdate` when item metadata changes, and for that we need to pass through another signal, like with `lastUsedChanged`:

https://github.com/nebari-dev/jupyterlab-launchpad/blob/5ba3fee41f7cf757d565d5b1266c546087abf0aa/src/components/table.tsx#L103-L117

We derive the items from the `sessions` object passed in options:

https://github.com/nebari-dev/jupyterlab-launchpad/blob/5ba3fee41f7cf757d565d5b1266c546087abf0aa/src/dialogs.tsx#L239

and we fetch the `sessions` when creating the dialog:

https://github.com/nebari-dev/jupyterlab-launchpad/blob/5ba3fee41f7cf757d565d5b1266c546087abf0aa/src/dialogs.tsx#L75-L80

The `SessionManager` emits [`runningChanged`](https://jupyterlab.readthedocs.io/en/stable/api/classes/services.SessionManager-1.html#runningChanged) signal which we could use.


Discussion / hints:
`SessionManager.runningChanged` [does not emit when minor details of the session (like kernel state) changed](https://github.com/jupyterlab/jupyterlab/blob/f8cdbba3b8ac64c80f2c7701f35db4c568716f3f/packages/services/src/session/manager.ts#L268-L299). We need [`KernelManager.runningChanged`](https://github.com/jupyterlab/jupyterlab/blob/f8cdbba3b8ac64c80f2c7701f35db4c568716f3f/packages/services/src/kernel/manager.ts#L271-L279).

Fix patch:
diff --git a/package.json b/package.json
index 37da91c..e62712c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "jupyterlab-launchpad",
-    "version": "1.0.2",
+    "version": "1.0.3",
     "description": "A redesigned JupyterLab launcher",
     "keywords": [
         "jupyter",
diff --git a/src/dialogs.tsx b/src/dialogs.tsx
index 4f882c3..402d351 100644
--- a/src/dialogs.tsx
+++ b/src/dialogs.tsx
@@ -4,6 +4,7 @@ import {
 } from '@jupyterlab/application';
 import type { CommandRegistry } from '@lumino/commands';
 import type { Message } from '@lumino/messaging';
+import { Signal, ISignal } from '@lumino/signaling';
 import {
   SessionContextDialogs,
   ISessionContextDialogs,
@@ -70,14 +71,24 @@ class CustomSessionContextDialogs extends SessionContextDialogs {
     const hasCheckbox = typeof autoStartDefault === 'boolean';
     const settings = await this.options.settingRegistry.load(MAIN_PLUGIN_ID);
 
+    const dataChanged = new Signal<CustomSessionContextDialogs, void>(this);
+    sessionContext.sessionManager.runningChanged.connect(() => {
+      dataChanged.emit();
+    });
+    this.options.kernelManager.runningChanged.connect(() => {
+      dataChanged.emit();
+    });
+
     const dialog = new Dialog<Partial<Kernel.IModel> | null>({
       title: trans.__('Select Kernel'),
       body: new KernelSelector({
-        data: {
+        data: () => ({
           specs: sessionContext.specsManager.specs,
           sessions: sessionContext.sessionManager.running(),
+          kernels: this.options.kernelManager.running(),
           preference: sessionContext.kernelPreference
-        },
+        }),
+        dataChanged,
         name: sessionContext.name,
         commands: this.options.commands,
         favoritesDatabase: this.options.database.favorites,
@@ -131,6 +142,7 @@ export namespace CustomSessionContextDialogs {
     database: ILauncherDatabase;
     commands: CommandRegistry;
     settingRegistry: ISettingRegistry;
+    kernelManager: Kernel.IManager;
   }
 }
 
@@ -154,7 +166,8 @@ export const sessionDialogsPlugin: JupyterFrontEndPlugin<ISessionContextDialogs>
         translator: translator,
         database: database,
         commands: app.commands,
-        settingRegistry: settingRegistry
+        settingRegistry: settingRegistry,
+        kernelManager: app.serviceManager.kernels
       });
     }
   };
@@ -190,6 +203,7 @@ export class KernelSelector extends ReactWidget {
 
   onAfterAttach(msg: Message) {
     super.onAfterAttach(msg);
+    this.options.dataChanged.connect(this.update.bind(this));
     requestAnimationFrame(() => {
       // Set minimum dimensions so that when user starts typing to filter
       // the kernels the dialog does not start jumping around.
@@ -199,12 +213,18 @@ export class KernelSelector extends ReactWidget {
     });
   }
 
+  onAfterDetach(msg: Message) {
+    super.onAfterDetach(msg);
+    this.options.dataChanged.disconnect(this.update);
+  }
+
   /**
    * Render the launcher to virtual DOM nodes.
    */
   protected render(): React.ReactElement<any> | null {
     const items: ILauncher.IItemOptions[] = [];
-    const specs = this.options.data.specs!.kernelspecs!;
+    const data = this.options.data();
+    const specs = data.specs!.kernelspecs!;
     // Note: this command is not executed, but it is only used to match favourite/last used metadata
     const command =
       this.options.type === 'console'
@@ -236,8 +256,14 @@ export class KernelSelector extends ReactWidget {
       });
     }
     const runningItems: ILauncher.IItemOptions[] = [];
-    for (const model of this.options.data.sessions!) {
-      const kernel = model.kernel;
+    const kernels = new Map([...data.kernels].map(k => [k.id, k]));
+    for (const model of data.sessions!) {
+      // session models may be outdated, use the more frequently
+      // updated instance from the kernels manager
+      if (!model.kernel) {
+        continue;
+      }
+      const kernel = kernels.get(model.kernel?.id);
       if (!kernel) {
         continue;
       }
@@ -344,7 +370,10 @@ export namespace KernelSelector {
     settings: ISettingRegistry.ISettings;
     commands: CommandRegistry;
     trans: TranslationBundle;
-    data: SessionContext.IKernelSearch;
+    data: () => SessionContext.IKernelSearch & {
+      kernels: IterableIterator<Kernel.IModel>;
+    };
+    dataChanged: ISignal<CustomSessionContextDialogs, void>;
     acceptDialog: () => void;
     name: string;
     // known values are "notebook" and "console"


Test patch:
diff --git a/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-linux.png b/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-linux.png
index a225e96..1da1c77 100644
Binary files a/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-linux.png and b/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-linux.png differ
diff --git a/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-open-quicksettings-linux.png b/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-open-quicksettings-linux.png
index 6fedadd..4424f62 100644
Binary files a/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-open-quicksettings-linux.png and b/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-open-quicksettings-linux.png differ
diff --git a/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-search-in-individual-linux.png b/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-search-in-individual-linux.png
index 35d8639..a3cf6f5 100644
Binary files a/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-search-in-individual-linux.png and b/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-search-in-individual-linux.png differ
diff --git a/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-with-starred-linux.png b/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-with-starred-linux.png
index 6aa2e10..5cd3c3a 100644
Binary files a/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-with-starred-linux.png and b/ui-tests/tests/jupyterlab_new_launcher.spec.ts-snapshots/launcher-with-starred-linux.png differ