Repository: jupyterlab/extension-examples (language: python)

Issue:
Update to JLab 1
Update all the examples so they are compatible with JupyterLab 1.0:

This is mostly focusing on getting them to compile for JupyterLab 1, but they would require another pass to check they are working properly.

### TODO

- [x] 1_hello_world
- [x] 2_commands_and_menus
- [x] 3a_widgets
- [x] 3b_datagrid
- [x] 4a_kernel_output
- [x] 4b_jupyterwidgets
- [x] 5_signals_and_buttons
- [x] 6_kernel_messaging
- [ ] 7_serving_files?


Discussion / hints:
Tried this PR linking examples 1 to 5. jlab can be built with latest jlab.

Only example 1 is an extension (which works as the expected message can be seen in the console`the JupyterLab main application:...)

@jtpio do you get the same behavior?

```
JupyterLab v2.0.0a1
Known labextensions:
   app dir: /opt/datalayer/opt/miniconda3/envs/datalayer/share/jupyter/lab
        1_hello_world v0.1.0  enabled  OK*

   local extensions:
        1_hello_world: /Users/echar4/datalayer/repos/jupyterlab-extension-examples/1_hello_world
   
   linked packages:
        2_commands_and_menus: /Users/echar4/datalayer/repos/jupyterlab-extension-examples/2_commands_and_menus
        3a_widgets: /Users/echar4/datalayer/repos/jupyterlab-extension-examples/3a_widgets
        3b_datagrid: /Users/echar4/datalayer/repos/jupyterlab-extension-examples/3b_datagrid
        4a_kernel_output: /Users/echar4/datalayer/repos/jupyterlab-extension-examples/4a_kernel_output
        4b_jupyterwidgets: /Users/echar4/datalayer/repos/jupyterlab-extension-examples/4b_jupyterwidgets
        5_signals_and_buttons: /Users/echar4/datalayer/repos/jupyterlab-extension-examples/5_signals_and_buttons
```
Versions in root README.md should also be updated.
For now I've been installing the extensions one by one with `jupyter labextension install <folder> --debug`, and this is what I have:

```
$ jupyter labextension list
JupyterLab v1.2.1
Known labextensions:
   app dir: /home/jtp/miniconda/envs/jupyterlab-extension-tutorial/share/jupyter/lab
        1_hello_world v0.1.0  enabled  OK*
        2_commands_and_menus v0.1.0  enabled  OK*
        3a_widgets v0.1.0  enabled  OK*
        3b_datagrid v0.1.0  enabled  OK*
        4a_kernel_output v0.1.0  enabled  OK*
        4b_jupyterwidgets v0.1.0  enabled  OK*
        5_signals_and_buttons v0.1.0  enabled  OK*
        6_kernel_messaging v0.1.0  enabled  OK*

   local extensions:
        1_hello_world: /home/jtp/git/jtpio/jupyterlab-extension-tutorial/1_hello_world
        2_commands_and_menus: /home/jtp/git/jtpio/jupyterlab-extension-tutorial/2_commands_and_menus
        3a_widgets: /home/jtp/git/jtpio/jupyterlab-extension-tutorial/3a_widgets
        3b_datagrid: /home/jtp/git/jtpio/jupyterlab-extension-tutorial/3b_datagrid
        4a_kernel_output: /home/jtp/git/jtpio/jupyterlab-extension-tutorial/4a_kernel_output
        4b_jupyterwidgets: /home/jtp/git/jtpio/jupyterlab-extension-tutorial/4b_jupyterwidgets
        5_signals_and_buttons: /home/jtp/git/jtpio/jupyterlab-extension-tutorial/5_signals_and_buttons
        6_kernel_messaging: /home/jtp/git/jtpio/jupyterlab-extension-tutorial/6_kernel_messaging

```
so far so good. I checked the log and it returned `a linked package because it does not have extension metadata:   Missing extension module "lib/index.js"`

Indeed I have not built the folder example 2 to 5. When I build, I have them listed. Tested extension 2 and it works fine.

LGTM

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 31746c65..e801d067 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -33,6 +33,7 @@ jobs:
           - toolbar-button
           - widgets
           - completer
+          - contentheader
         os: [ubuntu-latest, macos-latest, windows-latest]
 
     defaults:
diff --git a/README.md b/README.md
index b744ceb6..639f0107 100644
--- a/README.md
+++ b/README.md
@@ -7,22 +7,23 @@
 2. [Develop by Examples](#develop-by-examples)
    1. [Commands](#commands)
    2. [Command Palette](#command-palette)
-   3. [Context Menu](#context-menu)
-   4. [Custom Log Console](#custom-log-console)
-   5. [Datagrid](#datagrid)
-   6. _[Hello World](#hello-world)_
-   7. [Kernel Messaging](#kernel-messaging)
-   8. [Kernel Output](#kernel-output)
-   9. [Launcher](#launcher)
-   10. [Log Messages](#log-messages)
-   11. [Main Menu](#main-menu)
-   12. [React Widget](#react-widget)
-   13. _[Server Hello World](#server-hello-world)_
-   14. [Settings](#settings)
-   15. [Signals](#signals)
-   16. [State](#state)
-   17. [Toolbar Item](#toolbar-item)
-   18. [Widgets](#widgets)
+   3. Main Widget [Content Header](#main-widget-content-header)
+   4. [Context Menu](#context-menu)
+   5. [Custom Log Console](#custom-log-console)
+   6. [Datagrid](#datagrid)
+   7. _[Hello World](#hello-world)_
+   8. [Kernel Messaging](#kernel-messaging)
+   9. [Kernel Output](#kernel-output)
+   10. [Launcher](#launcher)
+   11. [Log Messages](#log-messages)
+   12. [Main Menu](#main-menu)
+   13. [React Widget](#react-widget)
+   14. _[Server Hello World](#server-hello-world)_
+   15. [Settings](#settings)
+   16. [Signals](#signals)
+   17. [State](#state)
+   18. [Toolbar Item](#toolbar-item)
+   19. [Widgets](#widgets)
 3. [Prerequisites](#prerequisites)
 4. [Develop and Use the Examples](#develop-and-use-the-examples)
 5. [Test the Examples](#test-the-examples)
@@ -83,6 +84,7 @@ Start with the [Hello World](hello-world) and then jump to the topic you are int
 - [Commands](commands)
 - [Command Palette](command-palette)
 - [Completer](completer)
+- Main Widget [Content Header](contentheader)
 - [Context Menu](context-menu)
 - [Custom Log Console](custom-log-console)
 - [Datagrid](datagrid)
@@ -130,6 +132,12 @@ Customize tab autocomplete data sources.
 
 [![Completer](completer/preview.png)](completer)
 
+### Main Widget [Content Header](contentheader)
+
+Put widgets at the top of a main JupyterLab area widget.
+
+[![contentHeader](contentheader/preview.gif)](contentheader)
+
 ### [Context Menu](context-menu)
 
 Add a new button to an existent context menu.
diff --git a/completer/README.md b/completer/README.md
index 05b14a94..22a7a9e1 100644
--- a/completer/README.md
+++ b/completer/README.md
@@ -27,7 +27,7 @@ The first part is contained in the `index.ts` file, the second is in `connector.
 
 ## Creating a custom DataConnector
 
-`src/customconnector.ts` defines a `CustomConnector` to generate mock autocomplete suggestions. Like the `ContextConnector` it is based on, `CustomConnector` extends _JupyterLab_'s abstract [`DataConnector`](https://jupyterlab.readthedocs.io/en/latest/api/classes/statedb.dataconnector.html) class.
+`src/customconnector.ts` defines a `CustomConnector` to generate mock autocomplete suggestions. Like the `ContextConnector` it is based on, `CustomConnector` extends _JupyterLab_'s abstract [`DataConnector`](https://jupyterlab.readthedocs.io/en/latest/api/classes/statedb.DataConnector.html) class.
 
 The only abstract method in `DataConnector` is `fetch`, which must be implemented in your `CustomConnector`.
 
diff --git a/contentheader/.eslintignore b/contentheader/.eslintignore
new file mode 100644
index 00000000..8bd31f70
--- /dev/null
+++ b/contentheader/.eslintignore
@@ -0,0 +1,5 @@
+node_modules
+dist
+coverage
+**/*.d.ts
+ui-tests
diff --git a/contentheader/.eslintrc.js b/contentheader/.eslintrc.js
new file mode 100644
index 00000000..c2375786
--- /dev/null
+++ b/contentheader/.eslintrc.js
@@ -0,0 +1,53 @@
+module.exports = {
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/eslint-recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:jsdoc/recommended',
+    'plugin:prettier/recommended',
+    'plugin:react/recommended',
+  ],
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    project: 'tsconfig.json',
+    sourceType: 'module',
+  },
+  plugins: ['@typescript-eslint', 'jsdoc'],
+  rules: {
+    '@typescript-eslint/naming-convention': [
+      'error',
+      {
+        selector: 'interface',
+        format: ['PascalCase'],
+        custom: {
+          regex: '^I[A-Z]',
+          match: true,
+        },
+      },
+    ],
+    '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }],
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-namespace': 'off',
+    '@typescript-eslint/no-use-before-define': 'off',
+    '@typescript-eslint/quotes': [
+      'error',
+      'single',
+      { avoidEscape: true, allowTemplateLiterals: false },
+    ],
+    curly: ['error', 'all'],
+    eqeqeq: 'error',
+    'jsdoc/require-param-type': 'off',
+    'jsdoc/require-property-type': 'off',
+    'jsdoc/require-returns-type': 'off',
+    'jsdoc/no-types': 'warn',
+    'prefer-arrow-callback': 'error',
+  },
+  settings: {
+    jsdoc: {
+      mode: 'typescript',
+    },
+    react: {
+      version: 'detect',
+    },
+  },
+};
diff --git a/contentheader/.gitignore b/contentheader/.gitignore
new file mode 100644
index 00000000..18cace13
--- /dev/null
+++ b/contentheader/.gitignore
@@ -0,0 +1,6 @@
+*.bundle.*
+lib/
+node_modules/
+*.egg-info/
+.ipynb_checkpoints
+*.tsbuildinfo
diff --git a/contentheader/MANIFEST.in b/contentheader/MANIFEST.in
new file mode 100644
index 00000000..fb4f2e0c
--- /dev/null
+++ b/contentheader/MANIFEST.in
@@ -0,0 +1,24 @@
+include LICENSE
+include *.md
+include pyproject.toml
+
+include package.json
+include install.json
+include ts*.json
+include yarn.lock
+
+graft jupyterlab_examples_contentheader/labextension
+
+# Javascript files
+graft src
+graft style
+prune **/node_modules
+prune lib
+prune binder
+
+# Patterns to exclude from any directory
+global-exclude *~
+global-exclude *.pyc
+global-exclude *.pyo
+global-exclude .git
+global-exclude .ipynb_checkpoints
diff --git a/contentheader/README.md b/contentheader/README.md
new file mode 100644
index 00000000..8b46d74b
--- /dev/null
+++ b/contentheader/README.md
@@ -0,0 +1,96 @@
+# Main Widget Content Header
+
+> Insert a widget into the `contentHeader` section of a `MainAreaWidget`—useful for toolbars, headers, etc.
+
+![Demo: activate the extension using Command Palette to populate the time in GMT in the content header](./preview.gif)
+
+This JupyterLab example extension is intended to demo one specific feature of `MainAreaWidget`, namely, its `contentHeader` section.
+
+> As background, `MainAreaWidget` is a high-level JupyterLab widget that conventionally is used to enclose the Launcher (shown above) or a notebook editor. The `contentHeader`, in turn, is a Lumino `BoxPanel` widget positioned at the very top of this main area. This makes the `contentHeader` potentially useful to extensions needing some place to put content that the user will _always see_.
+
+In code: after you get a `MainAreaWidget`, for example via
+
+```ts
+// src/index.ts#L37-L37
+
+const main = app.shell.currentWidget;
+```
+
+you can then create a widget of interest, for example as
+
+```ts
+// src/index.ts#L40-L40
+
+const widget = new Widget();
+```
+
+before finally adding it to the JupyterLab main area's `contentHeader` real estate at its very top:
+
+```ts
+// src/index.ts#L45-L45
+
+main.contentHeader.addWidget(widget);
+```
+
+## Install
+
+First, ensure you've followed the installation instructions in the [top-level README](../README.md), e.g.:
+
+```bash
+# clone the repository
+git clone https://github.com/jupyterlab/extension-examples.git jupyterlab-extension-examples
+
+# go to the extension examples folder
+cd jupyterlab-extension-examples
+
+# create a new environment
+conda env create
+
+# activate the environment
+conda activate jupyterlab-extension-examples
+```
+
+Then build this extension and launch it — this largely follows from the instructions in the top-level [README](../README.md), except using this example instead of the `hello-world` one:
+
+```bash
+# go to the contentheader example
+cd contentheader
+
+# install the extension in editable mode
+python -m pip install -e .
+
+# install your development version of the extension with JupyterLab
+jupyter labextension develop . --overwrite
+
+# build the TypeScript source after making changes
+jlpm run build
+
+# start JupyterLab
+jupyter lab
+```
+
+## Activate
+
+As in the demo animated GIF above, once in JupyterLab
+
+1. Open the [Command Palette](https://jupyterlab.readthedocs.io/en/stable/user/commands.html) via, e.g., _View_ ➜ _Activate Command Palette_.
+2. Type in `populate` to see the command created by this extension: _Populate content header (time example)_.
+3. This will create a small header bar at the top of the main JupyterLab window that shows the current time in GMT (Greenwich Mean Time, also called UTC, Coordinated Universal Time).
+
+> Nota bene, you can _toggle_ the `contentHeader` via the _Command Palette_ ➜ _Show Header Above Content_ if you decide you don't want to see it.
+
+> Nota bene 2, the `contentHeader` and the _Show Header Above Content_ toggle apply on a _per_ `MainAreaWidget`: if you run this extension, it will show the time in the `contentHeader` of the _active_ main area, and the time will not appear in another notebook tab. This may be useful for some extension workflows and not others.
+
+## Implementation notes
+
+For full details see the body of the `execute` function in [`index.ts`](./src/index.ts), but in prose, here's what to do to make use of the `contentHeader` widget.
+
+First, get a `MainAreaWidget`. The approach used here will likely be useful for many JupyterLab use cases: `JupyterFrontEnd.shell.currentWidget` will be a `MainAreaWidget` if your extension is being activated with a classic JupyterLab window, and you can ensure that by testing for `app.shell.currentWidget instanceof MainAreaWidget`.
+
+`MainAreaWidget.contentHeader` is a "top-to-bottom" vertical [Lumino BoxPanel](https://jupyterlab.github.io/lumino/widgets/classes/boxpanel.html). You can call its `addWidget` and `insertWidget` methods to populate this space with your own custom widgets.
+
+## Background
+
+The motivation for this extension example was a question on the Jupyter Discourse, ["How to add Widget to an arbitrary HTMLElement?"](https://discourse.jupyter.org/t/how-to-add-widget-to-an-arbitrary-htmlelement/11576), where Michał Krassowski kindly recommended to create this example to remind JupyterLab developers of this feature.
+
+The feature was added in [that PR](https://github.com/jupyterlab/jupyterlab/pull/9984) which has further discussion.
diff --git a/contentheader/install.json b/contentheader/install.json
new file mode 100644
index 00000000..c265e55e
--- /dev/null
+++ b/contentheader/install.json
@@ -0,0 +1,5 @@
+{
+  "packageManager": "python",
+  "packageName": "jupyterlab_examples_contentheader",
+  "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_examples_contentheader"
+}
diff --git a/contentheader/jupyterlab_examples_contentheader/__init__.py b/contentheader/jupyterlab_examples_contentheader/__init__.py
new file mode 100644
index 00000000..63040033
--- /dev/null
+++ b/contentheader/jupyterlab_examples_contentheader/__init__.py
@@ -0,0 +1,17 @@
+
+import json
+from pathlib import Path
+
+from ._version import __version__
+
+HERE = Path(__file__).parent.resolve()
+
+with (HERE / "labextension" / "package.json").open() as fid:
+    data = json.load(fid)
+
+def _jupyter_labextension_paths():
+    return [{
+        "src": "labextension",
+        "dest": data["name"]
+    }]
+
diff --git a/contentheader/jupyterlab_examples_contentheader/_version.py b/contentheader/jupyterlab_examples_contentheader/_version.py
new file mode 100644
index 00000000..89190b47
--- /dev/null
+++ b/contentheader/jupyterlab_examples_contentheader/_version.py
@@ -0,0 +1,23 @@
+import json
+from pathlib import Path
+
+__all__ = ["__version__"]
+
+def _fetchVersion():
+    HERE = Path(__file__).parent.resolve()
+
+    for settings in HERE.rglob("package.json"): 
+        try:
+            with settings.open() as f:
+                version = json.load(f)["version"]
+                return (
+                    version.replace("-alpha.", "a")
+                    .replace("-beta.", "b")
+                    .replace("-rc.", "rc")
+                )
+        except FileNotFoundError:
+            pass
+
+    raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}")
+
+__version__ = _fetchVersion()
diff --git a/contentheader/package.json b/contentheader/package.json
new file mode 100644
index 00000000..9a53fa24
--- /dev/null
+++ b/contentheader/package.json
@@ -0,0 +1,88 @@
+{
+  "name": "@jupyterlab-examples/contentheader",
+  "version": "0.1.0",
+  "description": "Minimal JupyterLab example using contentHeader.",
+  "keywords": [
+    "jupyter",
+    "jupyterlab",
+    "jupyterlab-extension"
+  ],
+  "homepage": "https://github.com/jupyterlab/extension-examples",
+  "bugs": {
+    "url": "https://github.com/jupyterlab/extension-examples/issues"
+  },
+  "license": "BSD-3-Clause",
+  "author": {
+    "name": "Project Jupyter Contributors",
+    "email": ""
+  },
+  "contributors": [
+    {
+      "name": "Ahmed Fasih",
+      "email": "afasih@bloomberg.net"
+    }
+  ],
+  "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}"
+  ],
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "style": "style/index.css",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyterlab/extension-examples.git"
+  },
+  "scripts": {
+    "build": "jlpm run build:lib && jlpm run build:labextension:dev",
+    "build:prod": "jlpm run clean && jlpm run build:lib && jlpm run build:labextension",
+    "build:labextension": "jupyter labextension build .",
+    "build:labextension:dev": "jupyter labextension build --development True .",
+    "build:lib": "tsc",
+    "clean": "jlpm run clean:lib",
+    "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
+    "clean:labextension": "rimraf jupyterlab_examples_contentheader/labextension",
+    "clean:all": "jlpm run clean:lib && jlpm run clean:labextension",
+    "eslint": "eslint . --ext .ts,.tsx --fix",
+    "eslint:check": "eslint . --ext .ts,.tsx",
+    "install:extension": "jlpm run build",
+    "watch": "run-p watch:src watch:labextension",
+    "watch:src": "tsc -w",
+    "watch:labextension": "jupyter labextension watch ."
+  },
+  "dependencies": {
+    "@jupyterlab/application": "^3.1.0"
+  },
+  "devDependencies": {
+    "@jupyterlab/builder": "^3.1.0",
+    "@typescript-eslint/eslint-plugin": "^4.8.1",
+    "@typescript-eslint/parser": "^4.8.1",
+    "eslint": "^7.14.0",
+    "eslint-config-prettier": "^6.15.0",
+    "eslint-plugin-prettier": "^3.1.4",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.1.1",
+    "rimraf": "^3.0.2",
+    "typescript": "~4.1.3"
+  },
+  "sideEffects": [
+    "style/*.css",
+    "style/index.js"
+  ],
+  "styleModule": "style/index.js",
+  "publishConfig": {
+    "access": "public"
+  },
+  "jupyterlab": {
+    "extension": true,
+    "outputDir": "jupyterlab_examples_contentheader/labextension"
+  },
+  "jupyter-releaser": {
+    "hooks": {
+      "before-build-npm": [
+        "python -m pip install jupyterlab~=3.1",
+        "jlpm"
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/contentheader/preview.gif b/contentheader/preview.gif
new file mode 100644
index 00000000..922f97eb
Binary files /dev/null and b/contentheader/preview.gif differ
diff --git a/contentheader/pyproject.toml b/contentheader/pyproject.toml
new file mode 100644
index 00000000..686e39d9
--- /dev/null
+++ b/contentheader/pyproject.toml
@@ -0,0 +1,17 @@
+[build-system]
+requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"]
+build-backend = "jupyter_packaging.build_api"
+
+[tool.jupyter-packaging.options]
+skip-if-exists = ["jupyterlab_examples_contentheader/labextension/static/style.js"]
+ensured-targets = ["jupyterlab_examples_contentheader/labextension/static/style.js", "jupyterlab_examples_contentheader/labextension/package.json"]
+
+[tool.jupyter-packaging.builder]
+factory = "jupyter_packaging.npm_builder"
+
+[tool.jupyter-packaging.build-args]
+build_cmd = "build:prod"
+npm = ["jlpm"]
+
+[tool.check-manifest]
+ignore = ["jupyterlab_examples_contentheader/labextension/**", "yarn.lock", ".*", "package-lock.json"]
diff --git a/contentheader/setup.py b/contentheader/setup.py
new file mode 100644
index 00000000..af64a3f0
--- /dev/null
+++ b/contentheader/setup.py
@@ -0,0 +1,95 @@
+"""
+jupyterlab_examples_contentheader setup
+"""
+import json
+import sys
+from pathlib import Path
+
+import setuptools
+
+HERE = Path(__file__).parent.resolve()
+
+# The name of the project
+name = "jupyterlab_examples_contentheader"
+
+lab_path = (HERE / name.replace("-", "_") / "labextension")
+
+# Representative files that should exist after a successful build
+ensured_targets = [
+    str(lab_path / "package.json"),
+    str(lab_path / "static/style.js")
+]
+
+labext_name = "@jupyterlab-examples/contentheader"
+
+data_files_spec = [
+    ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"),
+    ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"),
+]
+
+long_description = (HERE / "README.md").read_text()
+
+# Get the package info from package.json
+pkg_json = json.loads((HERE / "package.json").read_bytes())
+version = (
+    pkg_json["version"]
+    .replace("-alpha.", "a")
+    .replace("-beta.", "b")
+    .replace("-rc.", "rc")
+) 
+
+setup_args = dict(
+    name=name,
+    version=version,
+    url=pkg_json["homepage"],
+    author=pkg_json["author"]["name"],
+    author_email=pkg_json["author"]["email"],
+    description=pkg_json["description"],
+    license=pkg_json["license"],
+    license_file="LICENSE",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    packages=setuptools.find_packages(),
+    install_requires=[],
+    zip_safe=False,
+    include_package_data=True,
+    python_requires=">=3.6",
+    platforms="Linux, Mac OS X, Windows",
+    keywords=["Jupyter", "JupyterLab", "JupyterLab3"],
+    classifiers=[
+        "License :: OSI Approved :: BSD License",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Framework :: Jupyter",
+        "Framework :: Jupyter :: JupyterLab",
+        "Framework :: Jupyter :: JupyterLab :: 3",
+        "Framework :: Jupyter :: JupyterLab :: Extensions",
+        "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
+    ],
+)
+
+try:
+    from jupyter_packaging import (
+        wrap_installers,
+        npm_builder,
+        get_data_files
+    )
+    post_develop = npm_builder(
+        build_cmd="install:extension", source_dir="src", build_dir=lab_path
+    )
+    setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets)
+    setup_args["data_files"] = get_data_files(data_files_spec)
+except ImportError as e:
+    import logging
+    logging.basicConfig(format="%(levelname)s: %(message)s")
+    logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.")
+    if not ("--name" in sys.argv or "--version" in sys.argv):
+        raise e
+
+if __name__ == "__main__":
+    setuptools.setup(**setup_args)
diff --git a/contentheader/src/index.ts b/contentheader/src/index.ts
new file mode 100644
index 00000000..8b575003
--- /dev/null
+++ b/contentheader/src/index.ts
@@ -0,0 +1,62 @@
+import {
+  JupyterFrontEnd,
+  JupyterFrontEndPlugin,
+} from '@jupyterlab/application';
+import { ICommandPalette } from '@jupyterlab/apputils';
+import { Widget } from '@lumino/widgets';
+import { MainAreaWidget } from '@jupyterlab/apputils';
+
+/**
+ * Generate whatever content to render in `contentHeader`
+ *
+ * @returns string
+ */
+function generateContent(): string {
+  return 'Time in GMT is: ' + new Date().toUTCString();
+}
+
+/**
+ * Initialization data for the contentheader extension.
+ */
+const plugin: JupyterFrontEndPlugin<void> = {
+  id: 'contentheader:plugin',
+  autoStart: true,
+  optional: [ICommandPalette],
+  activate: (app: JupyterFrontEnd, pal?: ICommandPalette) => {
+    console.log('JupyterLab extension contentheader is activated!');
+    const { commands } = app;
+    const command = 'jlab-examples:contentheader';
+    // Create the command, which can be easily invoked by
+    // 1- opening the Command Palette, and
+    // 2- running "Populate content header...".
+    commands.addCommand(command, {
+      label: 'Populate content header (time example)',
+      caption: 'Populate content header (time example)',
+      execute: (args: any) => {
+        // Check to ensure this is a MainAreaWidget
+        const main = app.shell.currentWidget;
+        if (main instanceof MainAreaWidget) {
+          // Create a widget
+          const widget = new Widget();
+          widget.addClass('example-extension-contentheader-widget');
+          widget.node.textContent = generateContent();
+
+          // and insert it into the header
+          main.contentHeader.addWidget(widget);
+
+          // Every so often, update the widget's contents
+          setInterval(() => {
+            widget.node.textContent = generateContent();
+          }, 1000);
+        }
+      },
+    });
+    // Create a command palette entry for easy access
+    const category = 'Extension Examples';
+    if (pal) {
+      pal.addItem({ command, category, args: { origin: 'from palette' } });
+    }
+  },
+};
+
+export default plugin;
diff --git a/contentheader/style/base.css b/contentheader/style/base.css
new file mode 100644
index 00000000..9b9443b7
--- /dev/null
+++ b/contentheader/style/base.css
@@ -0,0 +1,6 @@
+.example-extension-contentheader-widget {
+  min-height: 1rem;
+  background-color: white;
+  color: black;
+  padding-left: 0.5rem;
+}
diff --git a/contentheader/style/index.css b/contentheader/style/index.css
new file mode 100644
index 00000000..8a7ea29e
--- /dev/null
+++ b/contentheader/style/index.css
@@ -0,0 +1,1 @@
+@import url('base.css');
diff --git a/contentheader/style/index.js b/contentheader/style/index.js
new file mode 100644
index 00000000..a028a764
--- /dev/null
+++ b/contentheader/style/index.js
@@ -0,0 +1,1 @@
+import './base.css';
diff --git a/contentheader/tsconfig.json b/contentheader/tsconfig.json
new file mode 100644
index 00000000..81139f54
--- /dev/null
+++ b/contentheader/tsconfig.json
@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "allowSyntheticDefaultImports": true,
+    "composite": true,
+    "declaration": true,
+    "esModuleInterop": true,
+    "incremental": true,
+    "jsx": "react",
+    "module": "esnext",
+    "moduleResolution": "node",
+    "noEmitOnError": true,
+    "noImplicitAny": true,
+    "noUnusedLocals": true,
+    "preserveWatchOutput": true,
+    "resolveJsonModule": true,
+    "outDir": "lib",
+    "rootDir": "src",
+    "strict": true,
+    "strictNullChecks": false,
+    "target": "es2017",
+    "types": []
+  },
+  "include": ["src/*"]
+}
diff --git a/toolbar-button/README.md b/toolbar-button/README.md
index c26b6da4..c26c3b21 100644
--- a/toolbar-button/README.md
+++ b/toolbar-button/README.md
@@ -41,7 +41,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
 };
 ```
 
-New widgets can be added to a document widget by implementing the interface [DocumentRegistry.IWidgetExtension](https://jupyterlab.readthedocs.io/en/latest/api/interfaces/docregistry.documentregistry.iwidgetextension.html). In particular, you need to add your widget in the `createNew` method that is called when creating a new
+New widgets can be added to a document widget by implementing the interface [DocumentRegistry.IWidgetExtension](https://jupyterlab.readthedocs.io/en/latest/api/interfaces/docregistry.DocumentRegistry.IWidgetExtension.html). In particular, you need to add your widget in the `createNew` method that is called when creating a new
 document widget; in this case a notebook panel.
 
 ```ts


Test patch:
diff --git a/contentheader/ui-tests/.env b/contentheader/ui-tests/.env
new file mode 100644
index 00000000..bb0ac972
--- /dev/null
+++ b/contentheader/ui-tests/.env
@@ -0,0 +1,2 @@
+EXT_FOLDER=contentheader/jupyterlab_examples_contentheader
+EXT_NAME=contentheader
\ No newline at end of file
diff --git a/contentheader/ui-tests/README.md b/contentheader/ui-tests/README.md
new file mode 100644
index 00000000..d2be7291
--- /dev/null
+++ b/contentheader/ui-tests/README.md
@@ -0,0 +1,117 @@
+# Test
+
+The test will produce a video to help debugging and check what happened.
+
+To execute integration tests, you have two options:
+
+- use docker-compose (cons: needs to know and use docker) - this is a more reliable solution.
+- run tests locally (cons: will interact with your JupyterLab user settings)
+
+## Test on docker
+
+1. Compile the extension:
+
+```
+jlpm install
+jlpm run build:prod
+```
+
+2. Execute the docker stack in the example folder:
+
+```
+docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env build --no-cache
+docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm e2e
+docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env down
+```
+
+## Test locally
+
+1. Compile the extension:
+
+```
+jlpm install
+jlpm run build:prod
+```
+
+2. Start JupyterLab _with the extension installed_ without any token or password
+
+```
+jupyter lab --ServerApp.token= --ServerApp.password=
+```
+
+3. Execute in another console the [Playwright](https://playwright.dev/docs/intro) tests:
+
+```
+cd ui-tests
+jlpm install
+npx playwright install
+npx playwright test
+```
+
+# Create tests
+
+To create tests, the easiest way is to use the code generator tool of playwright:
+
+1. Compile the extension:
+
+```
+jlpm install
+jlpm run build:prod
+```
+
+2. Start JupyterLab _with the extension installed_ without any token or password:
+
+**Using docker**
+
+```
+docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab
+```
+
+**Using local installation**
+
+```
+jupyter lab --ServerApp.token= --ServerApp.password=
+```
+
+3. Launch the code generator tool:
+
+```
+cd ui-tests
+jlpm install
+npx playwright install
+npx playwright codegen localhost:8888
+```
+
+# Debug tests
+
+To debug tests, a good way is to use the inspector tool of playwright:
+
+1. Compile the extension:
+
+```
+jlpm install
+jlpm run build:prod
+```
+
+2. Start JupyterLab _with the extension installed_ without any token or password:
+
+**Using docker**
+
+```
+docker-compose -f ../end-to-end-tests/docker-compose.yml --env-file ./ui-tests/.env run --rm -p 8888:8888 lab
+```
+
+**Using local installation**
+
+```
+jupyter lab --ServerApp.token= --ServerApp.password=
+```
+
+3. Launch the debug tool:
+
+```
+cd ui-tests
+jlpm install
+npx playwright install
+PWDEBUG=1 npx playwright test
+```
diff --git a/contentheader/ui-tests/package.json b/contentheader/ui-tests/package.json
new file mode 100644
index 00000000..b76f7fe1
--- /dev/null
+++ b/contentheader/ui-tests/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "@jupyterlab-examples/contentheader-tests",
+  "version": "0.1.0",
+  "description": "Integration test for widgets example",
+  "repository": "https://github.com/jupyterlab/extension-examples",
+  "author": "Project Jupyter Contributors",
+  "license": "BSD-3-Clause",
+  "private": true,
+  "devDependencies": {
+    "@playwright/test": "1.13.1",
+    "typescript": "~4.1.3"
+  }
+}
\ No newline at end of file
diff --git a/contentheader/ui-tests/playwright.config.ts b/contentheader/ui-tests/playwright.config.ts
new file mode 100644
index 00000000..223d8850
--- /dev/null
+++ b/contentheader/ui-tests/playwright.config.ts
@@ -0,0 +1,18 @@
+import { PlaywrightTestConfig } from '@playwright/test';
+
+const config: PlaywrightTestConfig = {
+  timeout: 60000,
+  use: {
+    // Browser options
+    // headless: false,
+    // slowMo: 500,
+
+    // Context options
+    viewport: { width: 1280, height: 720 },
+
+    // Artifacts
+    video: 'on',
+  },
+};
+
+export default config;
diff --git a/contentheader/ui-tests/tests/contentheader.spec.ts b/contentheader/ui-tests/tests/contentheader.spec.ts
new file mode 100644
index 00000000..63b427e2
--- /dev/null
+++ b/contentheader/ui-tests/tests/contentheader.spec.ts
@@ -0,0 +1,24 @@
+import { test, expect } from '@playwright/test';
+
+const TARGET_URL = process.env.TARGET_URL ?? 'http://localhost:8888';
+
+test('should populate the contentheader', async ({ page }) => {
+  await page.goto(`${TARGET_URL}/lab`);
+  await page.waitForSelector('#jupyterlab-splash', { state: 'detached' });
+  await page.waitForSelector('div[role="main"] >> text=Launcher');
+
+  // Click text=View
+  await page.click('text=View');
+  // Click text=Activate Command Palette
+  await page.click('text=Activate Command Palette');
+  // Fill [aria-label="Command Palette Section"] [placeholder="SEARCH"]
+  await page.fill(
+    '[aria-label="Command Palette Section"] [placeholder="SEARCH"]',
+    'populate content header'
+  );
+  // Click text=... (from index.ts, `command` -> `label`)
+  await page.click('text=Populate content header (time example)');
+
+  // Ensure we see text from the extension example
+  expect(await page.waitForSelector('text=Time in GMT is:')).toBeTruthy();
+});