JupyterLab Notice プラグイン導入メモ(jnotice)

> 目的: ユーザ HOME の .jupyterhub_notice.txt をポーリングし、JupyterLab 画面にトースト表示する。イメージ焼き直しで Lab4 federated extension を最小構成で組み込み。


1) 成果物の配置

/opt/conda/share/jupyter/labextensions/jnotice/
  ├─ package.json
  └─ static/
       └─ remoteEntry.jnotice.js

/opt/conda/etc/jupyter/jupyter_server_config.d/
  ├─ 90-allow-hidden.json
  └─ 90-jnotice.json

90-allow-hidden.json

{
  "ContentsManager": { "allow_hidden": true },
  "ServerApp":       { "allow_hidden": true }
}

90-jnotice.json(Server 側の設定)

{
  "ServerApp": {
    "tornado_settings": {
      "page_config_data": {
        "federated_extensions": [
          {
            "name": "jnotice",
            "extension": "./extension",
            "load": "extensions/jnotice/static/remoteEntry.jnotice.js"
          }
        ]
      }
    }
  }
}

> 備考: Lab4 環境では jupyter_lab_config.d ではなく jupyter_server_config.d を参照する。

package.json(labextension 側)

{
  "name": "jnotice",
  "version": "0.0.1",
  "private": true,
  "jupyterlab": {
    "extension": true,
    "outputDir": "static",
    "_build": {
      "name": "jnotice",
      "load": "static/remoteEntry.jnotice.js",
      "extension": "./extension"
    }
  },
  "dependencies": {
    "@jupyterlab/application": "^4.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "ts-loader": "^9.0.0",
    "webpack": "^5.0.0",
    "webpack-cli": "^5.0.0"
  }
}

webpack.config.cjs(assign + Banner で _JUPYTERLAB を保証)

const path = require('path');
const webpack = require('webpack');
const { container } = webpack;
const { ModuleFederationPlugin } = container;

module.exports = {
  output: {
    path: path.resolve(__dirname, 'static'),
    filename: 'remoteEntry.jnotice.js',
    publicPath: 'auto'
  },
  resolve: { extensions: ['.ts', '.js'] },
  module: { rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }] },
  plugins: [
    new webpack.BannerPlugin({ banner: 'window._JUPYTERLAB = window._JUPYTERLAB || {};', raw: true }),
    new ModuleFederationPlugin({
      name: 'jnotice',
      filename: 'remoteEntry.jnotice.js',
      shareScope: 'jupyterlab',
      library: { type: 'assign', name: 'window._JUPYTERLAB["jnotice"]' },
      exposes: { './extension': './src/index.ts' },
      shared: {
        '@jupyterlab/application': { singleton: true, requiredVersion: '^4.0.0', import: false, shareKey: '@jupyterlab/application', shareScope: 'jupyterlab' },
        '@jupyterlab/ui-components': { singleton: true, requiredVersion: '^4.0.0', import: false, shareKey: '@jupyterlab/ui-components', shareScope: 'jupyterlab' },
        '@lumino/widgets': { singleton: true, requiredVersion: '^2.0.0', import: false, shareKey: '@lumino/widgets', shareScope: 'jupyterlab' }
      }
    })
  ],
  mode: 'production',
  optimization: { minimize: true }
};

src/index.ts(ポーリング & トースト)

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

const plugin: JupyterFrontEndPlugin<void> = {
  id: 'jnotice:plugin',
  autoStart: true,
  activate: async (_app: JupyterFrontEnd) => {
    const cfgEl = document.getElementById('jupyter-config-data');
    const cfg = cfgEl && cfgEl.textContent ? JSON.parse(cfgEl.textContent) : {};
    const base: string = cfg.baseUrl || (location.pathname.match(/\/user\/[^/]+\/?/)?.[0] || '/');
    const xsrf = (document.cookie.match(/(?:^|; )_xsrf=([^;]+)/)?.[1] || '');
    const url  = base + 'files/.jupyterhub_notice.txt';

    let last = '';
    async function poll() {
      try {
        const r = await fetch(url + '?_=' + Date.now(), {
          credentials: 'same-origin',
          headers: xsrf ? { 'X-XSRFToken': xsrf } : {}
        });
        if (!r.ok) return;
        const t = (await r.text()).trim();
        if (t && t !== last) { last = t; toast(t); }
      } catch {}
    }

    function toast(msg: string) {
      const el = document.createElement('div');
      el.textContent = msg;
      el.style.cssText = 'position:fixed;right:20px;bottom:20px;background:#ffefc6;color:#222;padding:10px 16px;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.2);z-index:9999;max-width:360px;';
      document.body.appendChild(el);
      setTimeout(() => el.remove(), 15000);
    }

    setTimeout(() => { poll(); setInterval(poll, 30000); }, 1000);
    console.log('[jnotice] plugin loaded');
  }
};

export default plugin;

2) ビルド(イメージ内)

# 例: /tmp/jnotice にソース一式を展開
npm install --no-audit --no-fund
npx webpack --config webpack.config.cjs --mode=production

# 生成物コピー
LABEXT=/opt/conda/share/jupyter/labextensions/jnotice
mkdir -p "$LABEXT/static"
cp -a static/remoteEntry.jnotice.js "$LABEXT/static/"
cp -a package.json "$LABEXT/"
chmod -R a+rX "$LABEXT"

3) 動作確認の手順

  1. ブラウザで Lab を開く → 開発者ツール を開く。

  2. コンソールで以下を実行:

    const cfg = JSON.parse(document.getElementById('jupyter-config-data')?.textContent||'{}');
    cfg.federated_extensions?.find(x=>x.name==='jnotice');   // ← 存在すれば OK
    window._JUPYTERLAB?.jnotice;                              // ← {get, init} が見えれば OK
  3. ~/.jupyterhub_notice.txt にメッセージを書き込み → 1分以内(初回1秒後・以降30秒間隔)にトースト。


4) よくあるハマりどころ


5) health_check と通知ファイル

NOTICE_FILE="$NB_DIR/.jupyterhub_notice.txt"
mkdir -p "$(dirname "$NOTICE_FILE")" 2>/dev/null || true
printf '%s\n' '⚠️ あと5分で自動停止します' > "$NOTICE_FILE"

6) 今後の改善


最終確認フロー(ワンライナー)

(()=>{
 const cfg=JSON.parse(document.getElementById('jupyter-config-data')?.textContent||'{}');
 console.log(!!cfg.federated_extensions?.find(x=>x.name==='jnotice'), window._JUPYTERLAB?.jnotice);
})();

トップ   編集 凍結 差分 履歴 添付 複製 名前変更 リロード   新規 ページ一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2025-10-28 (火) 18:48:46