@@ -49,13 +49,21 @@ This tutorial is inspired by many prior works.
4949
5050## 🛠️ Setup
5151
52+ Before we get started, we need to set up:
53+
54+ * Development dependencies on our local machine
55+ * Git configuration on our local machine
56+ * A repository on GitHub
57+
58+
5259### Dependency environment
5360
61+ Create an environment named ` jupytercon2025 ` .
5462We'll use this environment for the rest of this workshop:
5563
5664``` bash
5765# Create an environment named "jupytercon2025"
58- micromamba create -n jupytercon2025
66+ micromamba create --name jupytercon2025
5967
6068# Activate it
6169# IMPORTANT: Run this every time you open a new terminal!
@@ -74,15 +82,17 @@ micromamba install python pip nodejs gh "copier~=9.2" jinja2-time
7482### Important Git settings
7583
76841 . Git needs to know who you are.
85+
7786 Configure identity information Git will use when we commit:
7887
7988 ``` bash
8089 git config --global user.email
" [email protected] " 8190 git config --global user.name " Your Name Here"
8291 ```
8392
84- 2 . The modern conventional branch name is "main", and this tutorial will assume that's
85- your default branch.
93+ 2 . The modern conventional branch name is ` main ` , and this tutorial will assume
94+ you're working on the ` main ` branch.
95+
8696 Ensure your default branch is set to ` main ` :
8797
8898 ``` bash
@@ -222,7 +232,7 @@ Our extension will:
222232
223233## 🏋️ Exercise A (15 minutes): Extension creation and development loop
224234
225- ### Create a new extension from the [ official template] ( https://github.com/jupyterlab/extension-template )
235+ ### 🔧 Create a new extension from the [ official template] ( https://github.com/jupyterlab/extension-template )
226236
2272371 . Instantiate the template to get started on our new extension!
228238
@@ -312,7 +322,7 @@ Our extension will:
312322 :::
313323
314324
315- # ### 🧪 Test
325+ # ## 🧪 Test
316326
3173271. Start JupyterLab in a ** separate terminal** .
318328
@@ -376,7 +386,7 @@ git push -u origin main
376386:::
377387
378388
379- ### Do a complete development loop
389+ ### 🔧 Do a complete development loop
380390
3813910. Close the JupyterLab server with `CTRL+C`.
382392
@@ -389,7 +399,7 @@ git push -u origin main
3893992. Rebuild the extension with ` jlpm build` .
390400
391401
392- # ### 🧪 Test
402+ # ## 🧪 Test
393403
394404Follow the same testing steps as last time.
395405
@@ -417,7 +427,7 @@ git push -u origin main
417427:::
418428
419429
420- # ## What just happened ?
430+ # ## 🧠 What do we know now ?
421431
422432We know how to get started: we learned how to instantiate a new extension from the
423433official template and set it up for development.
@@ -433,12 +443,12 @@ Now we have all the knowledge we need to keep iterating on our extension!
433443🎓 Well done!
434444
435445
436- # # Creating a widget
446+ # # 😖 "Hello, world" is boring!
437447
438448Our working extension is a basic " hello, world" application.
439449All it does is log a string to the console, then make a request to the back-end
440450for another string, which is also logged to the console.
441- This all happens once, when the extension is activated when the user opens JupyterLab .
451+ This all happens ** once** when the user opens JupyterLab and the extension is activated .
442452
443453Our goal is to display a viewer for a random photo and caption, with a refresh button to
444454instantly display a new image.
@@ -448,7 +458,7 @@ will eventually house that content.
448458
449459## 🏋️ Exercise B (20 minutes): Launching a "hello, world" {term}`widget <widget>`
450460
451- ### Create a "hello, world" widget
461+ ### 🔧 Create a "hello, world" widget
452462
453463To display this widget in the {term}`main area <main area>`, we need to
454464implement a {term}`widget <widget>` which displays our content (for now, just
@@ -518,7 +528,7 @@ widget in JupyterLab yet.
518528Let's fix that now.
519529
520530
521- ### Create a {term}` command <command> ` to display the {term}` widget <widget> ` in the {term}` main area <main area> `
531+ ### 🔧 Create a {term}` command <command> ` to display the {term}` widget <widget> ` in the {term}` main area <main area> `
522532
523533In ` src/index.ts ` , we need to update our plugin to define a command in our
524534{term}` plugin's <plugin> ` ` activate ` method:
@@ -573,7 +583,7 @@ But right now, this command is not being used by anything!
573583Next, we'll add it to the {term}` command palette <command palette> ` .
574584
575585
576- ### Register our {term}` command <command> ` with the {term}` command palette <command palette> `
586+ ### 🔧 Register our {term}` command <command> ` with the {term}` command palette <command palette> `
577587
578588First, import the command palette interface at the top of ` src/index.ts ` :
579589
@@ -668,7 +678,7 @@ git push -u origin main
668678:::
669679
670680
671- ### Optional: Register with the {term}` launcher <launcher> `
681+ ### 🔧 Optional: Register with the {term}` launcher <launcher> `
672682
673683Unlike the command palette, this functionality needs to be installed as a dependency.
674684First, install ` @jupyterlab/launcher ` with ` jlpm add @jupyterlab/launcher ` to make
@@ -718,7 +728,7 @@ git push -u origin main
718728:::
719729
720730
721- #### My launcher button works, but it has no icon!
731+ #### Troubleshooting: My launcher button works, but it has no icon!
722732
723733Adding an icon is one extra step.
724734We can import the icon in ` src/index.ts ` like so:
@@ -760,7 +770,19 @@ git push -u origin main
760770:::
761771
762772
763- ## What's next?
773+ ### 🧠 What do we know now?
774+
775+ We know that a {term}` widget <widget> ` by itself isn't very useful, and we need some way to display it.
776+
777+ We decided we want to display our widget in the {term}` main area <main area> ` , and we
778+ know that we need a {term}` command <command> ` to achieve that.
779+
780+ Finally, we know that we can register our command with the
781+ {term}` command palette <command palette> ` and/or the {term}` launcher <launcher> `
782+ to give the user an easy way to execute the command to open the widget.
783+
784+
785+ ## 😖 It's still just "hello, world"!
764786
765787We've graduated from "Hello, world" in the console to "Hello, world" in a
766788{term}` main area widget <main area widget> ` .
@@ -774,7 +796,7 @@ Now we need to implement the logic and glue the pieces together.
774796
775797## 🏋️ Exercise C (20 minutes): Serve images and captions from the server extension
776798
777- ### Set up images and captions
799+ ### 🔧 Define images and captions
778800
779801Create a new directory at ` jupytercon2025_extension_workshop/images ` :
780802
@@ -813,7 +835,7 @@ IMAGES_AND_CAPTIONS = [
813835```
814836
815837
816- ### Update the server to serve images and captions
838+ ### 🔧 Update the server to serve images and captions
817839
818840Our server behaviors are defined in
819841` jupytercon2025_extension_workshop/routes.py ` , so that module will need to know
@@ -897,7 +919,7 @@ def setup_route_handlers(web_app):
897919```
898920
899921
900- #### 🧪 Test
922+ ### 🧪 Test
901923
902924Now's the best time for us to stop and test before moving on to consuming this
903925data with our widget.
@@ -933,7 +955,7 @@ git push -u origin main
933955:::
934956
935957
936- ### Connect the {term}` widget ` to the {term}` server extension `
958+ ### 🔧 Connect the {term}` widget ` to the {term}` server extension `
937959
938960Now that our backend is working, we need to glue our widget to it.
939961
@@ -962,7 +984,7 @@ that nothing is calling that method yet:
962984``` {code} typescript
963985:linenos:
964986:emphasize-lines: 7-19, 21-23
965- :file : src/widget.ts
987+ :filename : src/widget.ts
966988
967989class ImageCaptionWidget extends Widget {
968990 // Initialization
@@ -996,7 +1018,7 @@ Now, we're calling `load_image()` when we initialize the widget:
9961018``` {code} typescript
9971019:linenos:
9981020:emphasize-lines: 12-25
999- :file : src/widget.ts
1021+ :filename : src/widget.ts
10001022
10011023class ImageCaptionWidget extends Widget {
10021024 // Initialization
@@ -1037,7 +1059,7 @@ class ImageCaptionWidget extends Widget {
10371059}
10381060```
10391061
1040- #### 🧪 Test
1062+ ### 🧪 Test
10411063
10421064Now that we have our widget user interface hooked up to the data coming from the server, let's test again.
10431065Because we changed the JavaScript, we need to use ` jlpm run build ` , but we _ don't_ need to restart the JupyterLab server.
@@ -1077,14 +1099,32 @@ git push -u origin main
10771099:::
10781100
10791101
1080- ## 🏋️ Exercise D (15 minutes): Add user interactivity to the widget
1102+ ### 🧠 What do we know now?
1103+
1104+ We know that a {term}` server extension <server extension> ` can access the hardware
1105+ resources of the JupyterLab server, for example to read data from disk.
1106+
1107+ We know that server extensions provide HTTP endpoints that can be consumed by
1108+ {term}` frontend extensions <frontend extension> ` .
1109+
1110+ We know how to provide JSON data from the server and consume it in a {term}`widget
1111+ <widget >`.
1112+
1113+ We know how to dynamically update {term}` widget ` HTML elements.
1114+
1115+
1116+ ## 😖 The widget isn't interactive!
10811117
10821118Right now, you only get a random image when you first open the widget.
10831119It's much more interesting if the widget can respond to user actions!
1120+
1121+
1122+ ## 🏋️ Exercise D (10 minutes): Add interactivity to the widget
1123+
10841124Let's add a toolbar and refresh button which triggers the image to change immediately.
10851125
10861126
1087- ### Import a toolbar UI component and icon
1127+ ### 🔧 Import a toolbar UI component and icon
10881128
10891129For all of this to work, we need the ` ToolbarButton ` to use in our
10901130widget.
@@ -1108,7 +1148,7 @@ import {
11081148```
11091149
11101150
1111- ### Add the button to the widget and connect the logic
1151+ ### 🔧 Add the button to the widget and connect the logic
11121152
11131153Now we can use the ` ToolbarButton ` class to instantiate a new button with an icon,
11141154tooltip, and behavior (` onClick ` ).
@@ -1169,7 +1209,16 @@ git push -u origin main
11691209:::
11701210
11711211
1172- ## Problem: The widget disappears when we refresh the page
1212+ ### 🧠 What do we know now?
1213+
1214+ We know that {term}` main area widgets <main area widget> ` can offer a toolbar for
1215+ interacting with the widget.
1216+
1217+ We know how to add a button to the toolbar with an icon, tooltip, and ` onClick `
1218+ behavior.
1219+
1220+
1221+ ## 😖 The widget disappears when we refresh the page
11731222
11741223:::{important} 👀 You should notice...
11751224:class: simple
@@ -1183,9 +1232,12 @@ disappears.
11831232## 🏋️ Exercise E (15 minutes): Preserve layout
11841233
11851234JupyterLab can save and restore layouts, but we need to define how our widget restores
1186- its state.
1235+ its state for that to work.
1236+
1237+ The layout restorer ** loads** layouts, and the widget tracker ** saves** layouts.
1238+
11871239
1188- First, let's import the layout restorer:
1240+ ### 🔧 Import the layout restorer plugin and widget tracker
11891241
11901242``` {code} typescript
11911243:linenos:
@@ -1204,6 +1256,9 @@ import {
12041256
12051257```
12061258
1259+
1260+ ### 🔧 Use the layout restorer plugin as a dependency
1261+
12071262Now, we'll define the layout restorer {term}` token <token> ` as an _ optional_
12081263dependency, as it may not be available in all JupyterLab deployments.
12091264When we pass an optional dependency to the ` activate ` function, we follow two
@@ -1234,6 +1289,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
12341289 ) => {
12351290```
12361291
1292+
1293+ ### 🔧 Track our widget and conditionally restore it
1294+
12371295Now that we have the dependency, we need to define _ how_ the widget's layout
12381296will be saved and restored.
12391297First, we need to define a tracker object:
@@ -1314,5 +1372,21 @@ widget is initialized.
13141372:::
13151373
13161374
1375+ ### 🧠 What do we know now?
1376+
1377+ We know that by default, JupyterLab doesn't track our open widget or restore its state
1378+ when we refresh the page.
1379+
1380+ We know that we can use the ` WidgetTracker ` to remember the state of our widget and the
1381+ ` ILayoutRestorer ` plugin to restore that state.
1382+
1383+
1384+ ## 🎉 Great job!
1385+
1386+ The extension we built together today is just a small example of what can be done with
1387+ the Jupyter extension system.
1388+ What other things can you imagine building?
1389+
1390+
13171391[ ^ rebuild-not-always-required ] : We don't actually _ always_ need to rebuild -- only when
13181392we change the JavaScript. If we only changed Python, we only need to restart JupyterLab.
0 commit comments