Tutorial: JupyterLab Extension Development for Everyone - AM Session 2
Key Takeaways
JupyterLab extension development using Python, JavaScript, and JupyterLab APIs, with a focus on customizing and enhancing the Jupyter experience, and leveraging AI for rapid prototyping and production-ready extensions
Full Transcript
Hello, welcome back. Appreciate you all sticking with us. Uh, just something that I'd like to point out from some issues we've noticed. If you're using Windows and you're struggling, uh, we mentioned this in our prerequisites document, but we should have called it out verbally at the beginning. Uh, Windows Subsystem for Linux is going to be your best bet for evolv avoiding any of these kind of weird problems with Windows. So if you're using Windows, please use Windows Subsystem for Linux or WSL. You can get started with that by running uh in the Windows command prompt WSL-install. Choose Auntu and let it install and then open up Ubuntu from your start menu and start from there. Never start from the command prompt or from the mini forge prompt or the Anaconda prompt. uh use Auntu. Um anything else we should emphasize before you go? >> I think we're good. >> All right. Cool. >> Um there are people that develop extensions on Windows. So it's possible to do just we've we've designed this workshop with a Unixcentric workflow. And so we're assuming either Linux or Mac OS or if you use WSL that's that's Linux uh on Windows. Okay. Um, let's see. Where have we started? Where where have we left off? We have an extension. Very simple extension that just console logs something, but it does work inside of Jupyter Lab, right? It's got this uh it's a JavaScript file. It's got this plugin which is just this dictionary or uh JavaScript object. And we export it. And Jupyter Lab is successfully calling the activate function. The auto start true says tells Jupyter Lab, hey, you know, activate me as soon as Jupyter Lab starts up. And so this activate function is called and we've got a console log. Okay, just a quick another show of hands. How many people now have an an extension that is working in Jupyter Lab? Okay, excellent. Excellent. So now we're going to take it to the next level. We're gonna um we're going to actually do something with this extension besides just print something to the console log. So, what we're first going to do is create a widget that opens in the main area. Um, so this is going back to uh our tutorial uh information here. Um, what we're going to do is we're we want something that opens up in the main area. And when it opens up in the main area, we it's going to have some Java uh HTML that we're just going to write here. Um, one of the things we're going to So, so we're creating a widget. This goes back to what we talked about a while ago that often plugins include widgets that then can be displayed to the user. Um, we're going to create uh essentially uh these are the lines right here. Um, this widget is just going to represent some HTML. Hello world. And then we're going to wrap it inside of this thing called a main area widget. So a main area widget is something specific to Jupyter Lab. And what it does is it it's a convenience that wraps a bunch of conventional behaviors uh around Jupyter Lab. It gives you a space for a toolbar. It handles some focusing behaviors etc for you. So that your widget in the main area like a tab like a notebook tab or other tab in the main area. So your tab has some uh consistent conventions with all the other main area widgets that we have in Jupyter not. So we're going to start with uh just creating a new file source widget.ts. So over here I'm gonna I'm going to do this development just straight in Jupyter Lab here. But you can you're welcome to use a code editor uh to edit your files etc. Oh actually I've already created it. So you can create a new file here. Here let me delete this so that we can Yeah. Yeah. Yeah. Uh let's see. Remove to trash. All right. Is that big enough? Is that good? Okay. Um, so I'm going to create a new file here, widget.ts, and gonna delete thetxt extension and open it up. And you'll notice that uh Jupyter Lab recognizes that it's a TypeScript file from the extension.ts. I'm just going to paste in this this widget here. All right. So, what is this doing? uh it's importing the widget package from this at luminino widgets. You'll see as you do Jupyter Lab development a lot of references is to this luminino package. Um by the way in in JavaScript the way to sort of think through this uh the package naming conventions is the app and then there's like an org name slash and then a package name. So this is one of the luminino org packages. The widget package inside of the lumino or uh lumino is a package that was written uh basically for jupitter lab as the base application framework. It provides a lot of utilities for doing applications in the web. And so uh a lot of times we'll pull stuff from this Lumino framework uh that Jupyter Lab is built on top of. And then the main area widget is coming straight from Jupyter Lab, one of the app utils packages. And then an image icon. Okay. So what are we doing here? We're going to create a widget. And the widget has a constructor. We call this super. This is sort of standard object-oriented uh things. And uh this widget's going to create a paragraph element in HTML and set the inner HTML to hello world and then append that paragraph element to this node. So one thing about widgets, so notice this is the extends the Lumino widget package is it it's it's a it's a JavaScript object that represents some HTML on the page. And so this node in a widget is the top level HTML node for this user interface element. And so we create a new paragraph element and then append child. This is a standard browser DOM function. We're going to take this paragraph child and put it inside of the widget tople HTML element. So that's what this constructor is doing. And then we want to embed it inside of Jupyter Lab. We want this sort of consistent behavior with other tabs inside of Jupyter Lab. And that's what this main area widget is doing. So um we'll create a image caption main area widget and it's going to extend uh the main area widget. This is uh subclassing essentially in JavaScript. And the main area widget, it takes a type parameter of an image capture widget. This is the thing that's going to be inside of the main area widget. and its constructor creates a new image caption widget which is this class up here that we just created and it's going to add a little bit of metadata to this. It's so in a main area widget this title contains some meta metadata that's used for for example the tabs and other places in the interface if they want to expose your widget in some way to the user. So um the label raid random image with caption the caption and the icon um are all metadata associated with this main area widget that we're going to put inside the Jupyter Lab interface. All right. So let's save this and go back to our instructions. So there's there's our widget. Essentially the the HTML that we're constructing is uh hello world paragraph inside of a paragraph element inside of the lumino widget and the end result is our HTML inside of the web page is going to look something like this. All right. So now we have our we have our plug-in that has the capability of creating a main area widget, but we don't have any way from Jupyter Lab to actually invoke that main area widget, create that main area widget and add it to the add it to Jupyter Lab. And so that's what we're doing next. We're going to create a command. So if you remember in Jupyter Lab, we can view the command pallet or it's shift command C on Mac and and you'll see the shortcut on Windows. So shift command C and this is a command pallet. So, there's lots of different commands that are registered inside this global command system in Jupyter Lab. And we're going to add our extension is going to add a command in here to create a new main area widget. Um, okay. So, how do we do this? Um, we're going to go back to the index.ts here and we're going to uh this is this is the root entry point for our application for our extension. And we're going to add a few things in here to create a command. Um the first thing we need to do is of course import our main area widget. Well, yeah. So, um so let's import the main area widget up at the top. Okay. So, this is importing from that widget ts file that we just created. That's the dot slash widget import from that file that we just created. and and we're going to register this command. Okay, so this is sort of an overview of what we're doing here. Um, so in order to register the command, we need a new object. We need to get a hold of the command pallet to tell it, hey, we've got a new command and we want you to add it. So the way we're going to get a hold of the command pallet is we're also going to uh import one other thing and that is the I command pallet. Okay. So there's a lot of uh things in Jupyter Lab that start with an I. This is this is a shortcut convention for interface. So I command pallet tells us the interface for the command pallet and it's going to it's essentially what you can do with the command pallet. So, we're going to import that interface, import that token, and then we're going to do something that I think is again one of the things that makes a Jupyter Lab extension system so powerful is yes, we have plugins in Jupyterl, but here's the cool thing is your plugin can provide an object to the system to do things with you and your plug-in can require objects from other plugins to do things with them. So in this case, the I command pallet is what we call a token. It's a it's an object that the system is going to give us. And we're going to say in our metadata to the plugin that our plugin requires, get the indentation to line up the I command pallet token. What this is going to do is it's going to say, "Juper Lab, when you start our plugin, give us the I command pallet plugins result." So, so the command pallet is a plugin. It's going to create an object that lets people add commands and do all sorts of other stuff with it. And we're saying in our plugin, give us that command pallets output, the command pallet plugins output, which lets us interface with the command pallet. So the requires tells Jupyter Lab, hey I want you to pass the command pallet token to me and it's going to be given by this thing I just imported the I command pallet. That's how we tell Jupyter lab give us the command pallet stuff and then and then our activate function will be called with the command pallet. So we need to uh you know change our arguments here to include the command pallet that Jupyter Lab is now going to pass to us right here. In fact, I'll just go ahead and copy all three lines here. Maybe. So now we have the app is always passed to us and we now have got some extra lines from the copy paste. We now also are being passed in the command pallet token. This lets the app lets us do stuff with the Jupyter Lab app base app. The pallet lets us do stuff with the command pallet. And what do we want to do with it? Well, we're just going to add a little bit of code down at the bottom here. I'll paste it in and then we'll walk through it. Um, let's just put it down here at the bottom here. So, let's see. Inside the activate function, we'll paste this code right here. Okay. So, inside the activate function. So now the activate function, this is the function that is called right at the start. You know the Jupyter lab is going to say, okay, you got a plugin. I'm going to call one function, the activate function. Here you go. And because we said this requires, it's going to say, okay, I'll pass the Jupyter Lab app, which I always pass. But I'm also going to get the command pallet object and pass that to you. And then we're going to take that command pallet object, right, which we've called pallet in the arguments to our activate function. We've called it pallet. And we're going to do a couple of things. One, okay, we're just going to we need a a string, a unique identifying string about for this command. So, we'll create a variable with that unique identifying string. And then appcommands add command. Okay. So, what's this doing? App, remember, is the Jupyter Lab application object. It's passed in as the first argument to every plugins activate function. It's got a it's got a commands object, and that commands object has an add command. uh uh method and it takes in the uh the unique ident identifying string for this command and then a dictionary of some metadata and in particular here it's going to take in uh an execute function. So this execute function is what happens when you run this command. It'll just run the execute function and a label. This is how it shows what the name of the command anywhere that command happens uh happens to be displayed. All right. And then what is this command going to do? This command is going to create a new image caption widget and it's going to add the widget to the Jupyter Lab main area shell. So again, I'm using the Jupyter Lab app.shell shell gives me sort of an object that controls like this this user interface the whole user interface uh in a particular and then it's going to add our main area widget into the main area. Remember this is the main area of the Jupyter Lab interface. So we could add it to other places like a side panel or something like that by changing this main to some other uh some other area of Jupyter Lab but we're going to add our widget to the main area. All right, that registers a command with the system. Now, a command the command pallet and the commands in the system are are different things. This registers a command with the system. And one of the underlying architecture things about Jupyter Lab is the commands are just a way to tell the system, hey, I've got some executable way. I've got some some functionality that I want to provide a way to execute. The menu items are just triggering commands. The keyboard shortcuts are just triggering commands. The command pallet is just triggering commands. So in order to put something in the menu, you create a command. And in order to create something for a keyboard shortcut, you create a command and then add it to the keyboard shortcuts. In order to create something in the command pallet, you create a command which provides it to the system but doesn't expose it to the user. In order to expose it to the user, you talk to the command pallet. So remember, this is the argument that was passed into the activate function. We're going to add an item to the command pallet. And we're just going to pass in this command ID and a category for the command. So this chunk here provides some functionality to the system through this commands registry that we have in the system. And this line right here exposes that command to the command pallet. We could just as easily add it to a menu item or add it to a keyboard shortcut, etc. But we'll do a command pallet here. All right. I think that's it. All right. So, if you notice, this has been continuously building, right? We can run JLPM build or if you have JLPM watch running, it should have been updating as we go along. And just before I refresh, we'll see random. Okay, there's no random widget, you know, add random widget image widget or whatever in the command pallet here. So, I refresh. In fact, why don't I go up here and I'll change this with command pallet just to make sure that uh so I'll save it. You can see JLPM watch is rebuilding down here or you can run JLPM build uh manually. And now I'm going to refresh. And let's see. Pull up the command pallet. Random widget. Nope, it's not here. What's going on? Uh, let me inspect the console to see. Okay. Uh, let's see. Let me refresh one more time. All right. I'm going to just run JLPM build directly. Yeah, let me let me run JLPM build directly. Oh, it found an error. That's the problem. If I would have made this uh a little bit bigger, I probably would have seen that there was an error. Ah, copy paste error. Index line 22. So, let's look at index line 22. Uh, we're missing a comma right here. Right, these are parameters. Here's the activate function and here's the parameters to the function. And I forgot there's a comma between the parameters. Okay. So, let me save that. We'll build it or again if we had watch it would have built automatically. And now refresh. Ah, we see now okay the console.log log has been updated, right? And let's look shift command C pulls up the command pallet. View random image and caption. I press enter on this. What it's going to do is before I press enter, let's review what it's going to do. The command pallet has this command. When I press enter on that command pallet command, it's going to run this execute function. The command execute function. The execute function is going to create a new image caption main area widget. which itself is going to create a new image caption widget, right? Add this metadata to it. Create and the image caption widget is going to create this paragraph element, you know, hello world, append it to the root widget uh node, and then it's going to add that main area widget to the main area of Jupyter Lab and then be done with the command execution. So, let's try it. Random Voila, here we are. Okay, so we now have a widget that is displayed to the user from a command. Woohoo. We're now actually providing new capabilities to Jupyter Lab here. Um, if we really wanted to check the HTML here to verify, you can see here. Okay, there's this widget here, right? And there's our paragraph element with hello world in it that we created. Uh let's see. Okay. Um people I think have been working through as I' as I've been working through but pause and Okay, we'll pause and give you a chance to do this and then we'll come back and we'll add this command some other place in the system as well. Yeah, use the green sticker. Put the green sticker up uh if you're ready to move on. Uh, and if you don't have a sticker up, we'll assume that you're working through and put a red sticker up if you need to need to have somebody come help. If you're all caught up, um, there is an optional step you can work on um, here to register your command with the launcher in addition to the uh, command pallet. And we've left this as a little more of an exercise for you uh, with some hints. Um, feel free to go ahead and start tackling that. All right, I think we see more and more green. So, I'm just going to cover the optional part, the the launcher part, and then, uh, then we'll move on to the next step. So, um, what we want to do is we want to add it to the launcher. Let's see. Where' my Drupal Lab go? Let me refresh. Uh, let's see what happened. One second. My my laptop went to sleep and and uh let me just relaunch Jupyter Lab. Ah, it changed port for whatever reason. Okay. Ah, here it is. Yes, I had the wrong tab open. Um, okay. So, uh, what I'd like to do is when I create create a new tab. This is called the launcher. It's the default thing that comes up when you click a new tab. And the idea here is uh we a user wants to do something new. So they click plus and it comes up with a lot of different activities and then you do something uh you know click on one of these activities and it replaces this new tab interface with whatever the thing was that you had. Um, so we can see a lot of, you know, this is how you create a new notebook and blah blah blah all these other things. We want like our widget on this on this launcher here. So what do we need to do? First thing is we need to interface with a new part of Jupyter Lab. A new plug-in in Jupyter Lab has provided this you know interface that we can add things to the launcher. So we need to import this token from Jupyter Lab. Uh let's see let's go back to the index.ts TS and we'll import the I launcher from the Jupyter launcher plugin. And then we're going to add this. We're essentially the same thing as we had before. We're going to add before we said pallet.add item. Well, I guess it's a slightly different method, but the launcher has an add method and it again takes a command. So, you can kind of see the commands are a backbone of things things that can do stuff in Jupyter Lab and then we can add a command to the launcher, add a command to the pallet, add a command to the menu, and they all share some sort of consistent interface. They all labeled consistently, they all execute the action the same, etc. And so, uh, that adds it to the launcher. Let's uh let's try it out. Uh I need a new terminal since I restarted and yep. Okay. JLPM build. And I got an error. Ah right. Do you see what hap what I what did I do wrong? I added the import up here and I called launcher.add and it's complaining. What is launcher? Yeah, let's show the error again here. So the error was okay one this is declared but its value is never read. That's a clue. Um and here cannot find the lame name launcher. like I'm trying to do launcher, but it's like I don't know what you're talking about. I don't know what launcher is. You see the step I missed. I needed to add it to the activate parameters. That's right. So, what I need to do is where's index.ts? Right up here. I need to one make sure it's required. That tells Jupyter Lab that hey, pass this into my activate function. And I need to add it here. and with a comma launcher I launcher. Okay, so now Juper Lab knows to go grab the launcher object out of the system, the thing that was returned from the launcher plugin and hand it to me so that I can interact with the launcher. And that's that's what this is. It's handing it to me. It's giving it to me as a parameter to the activate function. And now I should be able to use it. So let's build And we have one other problem. What's this problem? Semicolon. Nope. Not semicolon. Ah, yes. Yes. Thank you. Okay. So, what's the problem here? When I build it, it's like I don't know what package you're talking about. And the problem here is if you see in this package JSON, let me open it with uh the editor. You see in this package JSON that this tells uh the build system all the packages my package depends on. And you can see here in the dependencies, right? I depend on application, I depend on core utils, I depend on services, but I don't depend on this Jupyter Lab launcher package. So I need to add the Jupyter uh launcher launcher package to here. So the way to do that is yarn add jpm add. And then I'm just going to copy that package name in. Where is it? At jupy launcher. Okay. What does this do? This tells the JavaScript build system that my package depends on the Jupyter Lab launcher package. So first but so what did it do essentially this package JSON now if I refresh it or just close it and open it back up again open with editor. Now the dependencies here. Uh let's see where is dependencies. I got it. Yeah. Here. Yeah. I think I think it didn't actually save it to the package. JSON. So, it did build successfully. Yeah, I think Oh, the problem here is it's not actually reloading. So, let me close down all versions of the package. JSON and let's go down to the dependencies. Yeah, you do see that it was added. It just wasn't showing up in the interface. So, it added the launcher and now I can compile successfully. And now if I refresh and let's try pulling up the launcher. Close down a few things here. Pull up the launcher. And look, view a random image and caption. Woohoo. And if I click it, you'll notice it behaves a little bit differently than the normal stuff. There's one more step that I need to do. And this is a little bit about how the launcher works. Um, typically when you have the launcher, you click on something and it it replaces the launcher. Um, so like text file, see it replaced that launcher. With ours, it doesn't quite do that. Notice it added it next to the launcher in order for the launcher to work. Basically, what we need to do is we need to we need to at the end of our command, we need to hand back the widget we created, and the launcher can recognize, oh, that widget's been added, so I can delete myself. And essentially, it's replacing the launcher. So there's one more step that we'll do and that is at the end of our command we're going to return the widget. So I save this JPM build and a few seconds it builds and then we'll refresh. Now the launcher has this view random widget and it's replaced with with our new widget. So over here, let's see. Here's our changes, right? So the index.ts uh diff this file, we can see our changes here. Let's pull the terminal up here so that we can see all of the changes. So we've added the I command pallet, the eyeluncher, and the image caption main area widget. We've now required the command pallet and the launcher and then those are passed in as arguments to our activate function. And can I scroll down on this diff? Ah, whoever maintains the jupyer lab getit plugin can't scroll down on the diff. The other thing we've done is added the command and the command essentially creates a main area caption a main area widget adds it to the shell and then returns that widget so the launcher knows that okay that's the thing that we added and then we added that command to the pallet and to the launcher. Okay. Uh next up is Matt with some more functionality. Okay, so going back to our website, we have a troubleshooting step here. If you've reached this point where everything works the way Jason just showed you, you may notice that your Hold on. I'm a little bit ahead. Okay. You may notice that your launcher has a button, but the button has no icon on it. Um, and this is a pretty quick fix. go back to our code and we can import an icon from Jupyter Lab UI components. So, I'm just copying this line. Again, we're under this troubleshooting header here. Um, copying this line to import the icon. Now, we've got our icon and then we can add that icon to our commands metadata. So right above where we're defining the label in the command that we're defining here. So we've got this object that contains a bunch of information about our command. One of the things is execute. The other thing is label. Let's add icon image icon. Save and quit. build and then go back to Jupiter Lab and then you should see this icon here on your button. All right, we got we got it on the launcher and it's pretty and also when you click the thing again, it should replace the launcher. Okay. So, I'm going to commit that. I'm just going to copy the message that I wrote for the tutorial. So, everything looks right. Push it. Clean up a little bit after myself. Don't run the git stash drop. That won't do anything for you. Okay. Uh, so we have a widget. It wasn't useful until we added it to the command pallet and the launcher so that we had a way to show our widget. Um, we wanted to display our widget in the main area. So we used a main area widget and we added it to the application shell in the main area. Uh, and then we registered our command with the command pallet registry and with the launcher. But we're still just showing hello world. We still haven't left kind of boring land. We've just done the boring thing in a little cooler way. Um, so this uh last or this next step is going to deal with actually showing some images. Um, so what I'd like everybody to do is first create a new directory under your Python source directory which is called Jupyter Con 2025 extension workshops workshop. Uh, and make a directory in there called images. So I'm just copying the command from the tutorial make dur Jupyter Con 2025 extension workshop images that should not show anything if it succeeds and then we can use ls to show that we have an empty directory and then let's place some images in this directory. So you can choose your own images if you've got like your favorite cat pictures you can do that. Um, but to be safe, we're using some uh royaltyfree public domain images from the Library of Congress, uh, which are also cat images and you can pull this from our repository as well. So here we've got those four cat images and you can click on the image. You can do this for each of the four images. And then you can click this download button here. Make this bigger. You can click this download button. Everything's a little bit slow, but these are small images, so they should download pretty quickly. I'm just doing this four times. I'm having a weird problem with my browser cutting off the bottom of my screen because I switched something at the last second here. Um, okay. I've got my four images. They're all downloaded. And now I'm just going to move them from my downloads directory. Uh just remembering their names, failing to remember their names. Just going to do cat star and the entanglement. Okay, so I'm I've told it which images I want to move and now I have to tell it where to move them into the images directory. Okay, so now that I've moved those files, I can do ls in my images directory and I can see my four cat pictures. Okay, so we're done with that step and we can do a commit. We've added the images but we're not using them. We're doing lots of little commits because that's a good practice. And that's a good practice because it enables us to go back and see where we went wrong if something does go wrong. Okay, so I've committed and pushed and now we need to figure out how we're going to show these images. So, what I'm going to start with is a list, and I'm going to create a new file for that list called images and captions.py in the Jupyter Con 2025 extension workshop directory. images and captions.py. And this is only going to have my list. And the list has four dictionaries. Each dictionary has a file name key which contains the file name for one image and a caption key which contains the caption for one image. And I'm just reusing the same captions from the Library of Congress, but of course you can make hilarious uh captions for your cat images. Um, okay. So, this is just a list of mappings from file names to captions. And I'm saving that. And now we need to update our server to serve these file names and captions. So I'm going to open up Jupyter Con 2025 extension workshop, the Python source directory, routes.py. And right now we've got one router here called Hello Route Handler. And then we've got one pattern hello route pattern which joins the base URL of our our server with the base URL of our extension with the path that we want to define our route behind. So that adds up to this path that we used earlier to directly view this hello world data in our browser. Okay. So, I'm going to start by importing that images and captions list of dictionaries that we defined from that file. From images and captions, import images and captions. And this is all caps because that's kind of a good practice for uh constants. And then I'm going to create a new variable here to define the images directory that we created. So now if we want to reference something in the images directory, we just use this constant images dur. And we're going to need a couple more imports in just a few minutes. So I'm going to take care of those now. We're going to need base 64. We're going to need random. And for that images dur line we need path path lib path libs path uh class. Okay. Now we're defining a new handler. So right below the hello handler, let's create a new one. I don't know what's up with this copy and paste problem. If you also experience these strange new lines when you copy and paste from the website, let me know because it's an open bug and as far as I can tell, I'm the only one affected with it. >> Linux. >> Yeah. >> Okay. Talk to me later. I need another data point for this bug because as far as I know, it's just it's just been me until now. So, thank you. Okay. Uh, so I just cleaned up my weird blank lines. We've got our new route handler. It's built almost exactly like this other route handler. We've got a get method which tells it to respond to HTTP get requests. And then inside that get method, instead of just immediately returning something, we're going to randomly select one of our images. So we use the Python standard library random module to choose one of the images from the list. Random.choice is just takes a list returns one random thing from that list. So random selection is going to have one of these one of these four dictionaries. So one file name and one caption. And then we're going to open that file. So we're opening from images dur slash that randomly selected file name. We're opening it in binary read mode. And then we're going to base 64 encode it. This is not probably the normally normal way you would want to do this, but this is just for demonstration purposes. We're reading the file and we're encoding the bytes inside the file as base 64. And then when we're done doing that, we just return this new object which contains the base 64 bytes of the image and the caption from the randomly selected item from our list. Okay, so now we've defined the behavior, but we need to connect the behavior to an actual endpoint in our server extension. So I'm just going to copy this line here. In addition to hello route pattern, now we have image route pattern. Everything is the same up until this last element random image caption. So, as you can imagine, instead of hitting Jupyter Con 2025 extension workshophello, we will now hit Jupyter Con 2025 extension workshop slash random image caption to actually look at this data. And then we need to add that handler to this list of handlers that gets registered here when we do add handlers. So we'll add our pattern image route pattern and image and caption route handler to bind our route pattern to our route handler behavior. Okay, I think that's everything I need to do. Um I'm going to save and quit. See if it builds. All right, first try it built. Awesome. Um, now we can test. This time, uh, we only changed Python. So, I actually didn't need to run the JLPM run build since that's just compiling JavaScript to Python. I just kind of did it automatically. What we do need to do is restart Jupyter Lab because Jupyter Lab is the thing running on the server side. The Python is the thing running on the server side. So we restart the server. Okay. And now to test, we're going to hit in the URL Jupyter Con 2025 extension workshop slash what was it? Uh image random image caption. Random image caption. And then you should see the raw data of our image. Um you you'll get an object. One of the keys is B 64 bytes. The other key is caption. And the base 64 bytes just contains that encoded binary image data. Okay, so our server side's looking great. Is everybody doing okay? Anybody having some struggles? I see a red flag over there. I'm sorry. Uh Constantine Jason, can you get this? Thank you. If you're uh if you're looking for help and we're not giving you the attention you need, please feel free to shout out at us. I really apologize for that. Okay. So, I'm going to do a get commit and push. Okay. All right. So, we're serving up some data from the server side, but we want to display this data in our image or in our widget which we've created. But right now, our widget just says hello world. So, we are going to open up our widget source code. And instead of just printing out hello world here or in addition to I suppose we could remove this but I'm not going to. We're going to import this request API uh method which came with the extension template. This is not something we have to write because it's came for free with the template. Okay. And then we're going to add a method to our widget class called load image. And that method is going to handle talking to our new server extension endpoint to request some data. And then when that data comes through, if it for some reason there's an error, it's just going to write to the console an error message. But if it does come through, we're going to take the data, we're going to log it, which we probably don't need to do, but this is nice for debugging. We're going to talk more about debugging soon. Um, just time checking time. We got half an hour until lunch. Uh, and then we're going to update a class member called image to set the source. So like think about an HTML tag that looks like this to set the source to have this kind of magic incantation to tell it that instead of the source being a URL, it's just some base 64 encoded JPEG data. and then there's a comma and then there's a space and then there's our data. Remember that the data the B 64 data is under the key B64 bytes. And then we're going to set the caption element um inner HTML. So inner HTML would be we've got a paragraph tag and then this stuff here is the inner HTML of the paragraph tag and we're going to set that to the caption key of the data that we get back from the server. Okay. And then we need to tell our type checker that these image and caption fields exist on the class or I guess compiler is the correct term. So we tell it that image exists. It's an HTML image element. We tell it that caption exists. It's an HTML paragraph element. And then we need to in in addition to the hello world elements that we add to the um widget, we need to also add these image and caption elements. So just below where we add hello world, I'm going to create an HTML center tag with document.createelement center. I'm going to append it to the widget and then I'm going to put an image tag inside of it. So this image um which we update with the load image method uh is going to be an image element and then we're going to append it to the center tag. So then we get something like this. And then we're going to do the same thing with the caption. And that's also going to be appended to the center tag. So then it'll look something like this. Okay. And then I'm going to delete all this garbage I added. And then finally, when we're done setting up those HTML elements, we're going to call the load image method we just defined to actually put some image data in this image tag and put a caption in this uh paragraph tag. Oops, butter fingers. Okay, hopefully I did all that right. Let's try building Okay, we got it. And then let's go back to Jupyter Lab. We don't need to restart it because we didn't update the Python. We just need to refresh. And we can click our launcher and hope we get a kitty cat. We got a kitty cat. And if we scroll down or let me go full screen here. There we go. We should see our caption right below. Okay. So, since everything works, I'm going to go ahead and do a get add, get commit, and get push. >> [snorts] >> Okay, we're pushed. Um, if you are running into trouble at this point, a couple of things might be happening. You might be seeing new errors in your browser console or you might be seeing new errors in your Jupyter Lab logs, your Jupyter Lab server logs. Um, so if you're seeing errors in your Jupyter Lab server logs, that's going to tell you something's wrong on the Python side. If you're seeing something going wrong in your browser console, that tells you there's something wrong on the JavaScript side. Although hopefully if there was anything wrong on the JavaScript side, your compiler catches it when you do JLPM run build. So you may also be seeing errors when you ran JLPM run build and those should point you in the right direction. Is everybody doing okay? I'm looking for red stickers. I see one blue shirt. Jason, you got it. Okay, so we learned a couple of new things. Uh, our server extension can access the hardware resources and the data on the disk on the server side. That's why we wrote that's why we opened that image from the server side and that server side extension provided an HTTP endpoint that we consumed with our front-end extension. Um, we know how to provide JSON data from the server and consume it on the widget and then use that JSON data to dynamically update the widgets HTML elements. But our widget still isn't interactive. So, we have our random image and we can open more random images. We can open as many of these widgets as we want. But what if we want some sort of interactivity in our widget? That's what we're going to cover in the next exercise. Okay. Now, we're going to do some interactivity. This is probably the exciting part. Uh so, our widget right now Again, we can open a new widget. We get one random image. That's all we get. But what if we want to be able to refresh this random image by doing something interactive with our widget? So, we're going to add a toolbar button. And this is all stuff that comes from Jupiter Lab and Lumino. This is all things that we can import. And we just kind of need to glue them together. So, we're going to open our widget.ts code and we're going to import toolbar button from the same place we imported main area widget Jupyter Lab app utils. So now we should have two imports. And then we're also going to import an icon that we can use for that button from the same place that we imported image icon. So now we've got image icon and refresh icon both being imported from Jupyter Lab UI components. And now we're going to add the button to the widget and connect the logic. So we are going to edit our main area widget. I'm just going to copy and paste this code below at the bottom of our constructor method. Okay, so this is the code that I just pasted in. We're defining a new toolbar button and we're calling it refresh button. We're giving it the refresh icon that we just imported. We're giving it giving it a tool tip of refresh image. And then we're giving it a behavior that occurs on click. And that behavior is just calling that load image method that we defined earlier on our widget. So we have load image here and this runs at activation time where we call this.load load image and then it occurs again when we click the toolbar button that we're adding. So, we've defined a new toolbar button, but we haven't added it to the toolbar at this point, which is this line of code, thistoolbar.add item. We give it a little name identifier, and then we give it the object, the toolbar button object that we just created. So, this was a quick one. This should work now. Let's build and hope we didn't make any mistakes. Looks like we didn't. The compiler seems happy. And then we can refresh. And notice that every time we refresh, our open widgets go away. We're going to deal with that soon. So, let's open a new one. We see our kitty cat and we also see a new little refresh button here. When we hover over it, it says refresh image just like we asked it to in the tool tip field. And then when we click on it, we get some new kitty cats. Uh you might notice sometimes you click the button and it looks like you like you maybe nothing happened. Um there's only four images, so randomly, of course, sometimes you're going to get the same image multiple times in a row. And perhaps you would add some functionality to this uh widget that maybe notifies you. Um pops up a little notification in the corner that says uh got the same image twice. Or maybe you could add some functionality that randomizes again anytime you get the same image twice. So this is where you should be. You should have a refresh button. You click it, you get a new kitty. All right, let's commit that. and push it. All right. Okay, so we just learned our main area widget has some kind of outofthe-box functionality that we can use a toolbar and we can add buttons to the toolbar and those buttons have um you know some metadata we can set to give them icons, tool tips and the onclick behaviors are the important part of the button is what connects the click of that button to some logic that we've defined and we just reused the logic that we um previously defined for initialization. to load a random image. Okay. And again, if we refresh this page, our widget goes away. We don't want that to happen. We want it to stay through refreshes. And we'll deal with that in exercise E after lunch. If you're still having trouble, we've got 10 minutes until lunch. Uh, put up a red sticky and we'll come help you out.
Original Description
Tutorial: JupyterLab Extension Development for Everyone - Rosio Reyes & Konstantin Taletskiy, Anaconda; Matt Fisher, Eric & Wendy Schmidt Center for Data Science and Environment; Jason Grout, Independent; Martha Cryan, Plotly
Speakers: Jason Grout, Konstantin Taletskiy, Martha Cryan, Matt Fisher, Rosio Reyes
JupyterLab extensions are the key to customizing, enhancing, and scaling the Jupyter experience, whether you’re integrating domain-specific tools, adding new UI components or bridging frontend and backend workflows.
In this hands-on workshop, we’ll walk through the complete lifecycle of building a JupyterLab extension, from scaffolding and plugin architecture to packaging and publishing.
Participants will be exposed to basic and advanced extension development concepts: creating custom UI components, managing state and data, interfacing with other frontend and server extensions, and publishing extensions on PyPI. You’ll come away with the building blocks needed to begin your journey in developing extensions.
Together, we will create a basic extension from scratch, overview the landscape of extensions, explore advanced extensions (for example, JupyterGIS, providing real-time collaborative geospatial analysis in JupyterLab), and work on our own new extensions or contribute to existing extensions. After lunch we'll show you how to use AI to rapidly prototype, iterate on complex features and ship production-ready extensions that solve real user problems.
By the end, you'll have created multiple working extensions and the knowledge to bring your own ideas to life, contributing to the vibrant Jupyter ecosystem.
If you plan to join this session, please review the course materials here.
Watch on YouTube ↗
(saves to browser)
Sign in to unlock AI tutor explanation · ⚡30
Playlist
Uploads from JupyterCon · JupyterCon · 0 of 60
← Previous
Next →
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Interview Joshua Patterson NVIDIA
JupyterCon
Dave Stuart - Jupyter as an Enterprise “Do It Yourself” (DIY) Analytic Platform | JupyterCon 2020
JupyterCon
Jeffrey Mew - Supercharge your Data Science workflow | JupyterCon 2020
JupyterCon
Michelle Ufford- Supercharging SQL Users with Jupyter Notebooks | JupyterCon 2020
JupyterCon
Alan Yu - What we learned from introducing Jupyter Notebooks to the SQL community | JupyterCon 2020
JupyterCon
Chris Holdgraf- 2i2c: sustaining open source through hosted Jupyter infrastructure | JupyterCon 2020
JupyterCon
Yiwen Li - Intro to Elyra - an AI centric extension for JupyterLab | JupyterCon 2020
JupyterCon
Luciano Resende - What's new on Elyra - A set of AI centric JupyterLab extensions | JupyterCon 2020
JupyterCon
Alan Chin - Explore and Extend AI Pipeline Runtimes with Elyra and JupyterLab | JupyterCon 2020
JupyterCon
Eduardo Blancas- Streamline your Data Science projects with Ploomber | JupyterCon 2020
JupyterCon
Thorin Tabor - Democratizing the accessibility of computational workflows | JupyterCon 2020
JupyterCon
Simon Willison- Using Datasette with Jupyter to publish your data | JupyterCon 2020
JupyterCon
Brendan O'Brien - Using Qri (“query”) to fetch, query, combine and publish datasets.|JupyterCon 2020
JupyterCon
Georgiana Dolocan - Putting the JupyterHub puzzle pieces together | JupyterCon 2020
JupyterCon
Yuvi Panda- Running nonjupyter applications on JupyterHub with jupyter-server-proxy| JupyterCon 2020
JupyterCon
Richard Wagner- The Streetwise Guide to JupyterHub Security | JupyterCon 2020
JupyterCon
TamNguyen- Handling Custom Jupyter Data Sources | JupyterCon 2020
JupyterCon
Immanuel Bayer- ipyannotator - the infinitely hackable annotation framework | JupyterCon 2020
JupyterCon
Rebecca Kelly- A shared Python, R and Q Jupyter Notebook - A Quant Sandbox Dream |JupyterCon 2020
JupyterCon
Itay Dafna - Leap of faith: Transitioning from Excel to Jupyter-based applications | JupyterCon 2020
JupyterCon
Damián Avila - Using the Jupyterverse to power MADS | JupyterCon 2020
JupyterCon
Chiin Rui Tan- From Zero to Hero | JupyterCon 2020
JupyterCon
Firas Moosvi- Teaching an Active Learning class with Jupyter Book| JupyterCon 2020
JupyterCon
Daniel Mietchen- Jupyter in the Wikimedia ecosystem | JupyterCon 2020
JupyterCon
Qiusheng Wu- How Jupyter and geemap enable interactive mapping and analysis | JupyterCon 2020
JupyterCon
Stephanie Juneau- Jupyterenabled astrophysical analysis for researchers and students|JupyterCon 2020
JupyterCon
Denton Gentry- The Care and Feeding of JupyterHub for Climate Solution Models| JupyterCon 2020
JupyterCon
Tingkai Liu- FlyBrainLab: Interactive Computing in the Connectomic/Synaptomic Era | JupyterCon 2020
JupyterCon
Kunal Bhalla- A Notebook Style Guide| JupyterCon 2020
JupyterCon
Julia Wagemann - How to avoid 'Death by Jupyter Notebooks' | JupyterCon 2020
JupyterCon
David Pugh - Best practices for managing Jupyter-based data science | JupyterCon 2020
JupyterCon
Karla Spuldaro - Debugging notebooks and python scripts in JupyterLab | JupyterCon 2020
JupyterCon
Shreyas Dalia - assert browserTest == True # Frontend Testing JupyterLab | JupyterCon 2020
JupyterCon
Chris Holdgraf - The new Jupyter Book stack | JupyterCon 2020
JupyterCon
Hamel Husain - Fastpages - A new, open source Jupyter notebook blogging system | JupyterCon 2020
JupyterCon
Marc Wouts - Jupytext: Jupyter Notebooks as Markdown Documents | JupyterCon 2020
JupyterCon
Sheeba Samuel- ProvBook |JupyterCon 2020
JupyterCon
Philipp Rudiger - To Jupyter and back again | JupyterCon 2020
JupyterCon
Jacob Tomlinson - What is my GPU doing? | JupyterCon 2020
JupyterCon
Afshin Darian - A visual debugger in Jupyter | JupyterCon 2020
JupyterCon
Eric Charles - Jupyter Real Time Collaboration| JupyterCon 2020
JupyterCon
Devin Robison - Optimizing model performance | JupyterCon 2020
JupyterCon
Junhua zhao - PayPal Notebooks: ML & Data Science experience | JupyterCon 2020
JupyterCon
April Wang - Redesigning Notebooks for Better Collaboration | JupyterCon 2020
JupyterCon
Bryan Weber - Distributing and Collecting Jupyter Notebooks for Manual Grading| JupyterCon 2020
JupyterCon
Georgiana Dolocan - The Littlest JupyterHub distribution | JupyterCon 2020
JupyterCon
Tim Metzler - Electronic Examination using Jupyter Notebook | JupyterCon 2020
JupyterCon
Blaine Mooers - Why develop a snippet library for Jupyter in your subject domain? | JupyterCon 2020
JupyterCon
Ryan Abernathey - Cloud Native Repositories for Big Scientific Data | JupyterCon 2020
JupyterCon
Tanya Rai - Introducing Bento: Jupyter Notebooks @ Facebook | JupyterCon 2020
JupyterCon
Kenton McHenry - From Papers to Notebooks | JupyterCon 2020
JupyterCon
Ryan Herr - After model.fit, before you deploy| JupyterCon 2020
JupyterCon
Ana Ruvalcaba - Community building is a sustainability strategy | JupyterCon 2020
JupyterCon
Martin Renou - Xeus: an ecosystem of Jupyter kernels | JupyterCon 2020
JupyterCon
Michael Wilson - Teaching teenagers to understand Dark Energy | JupyterCon 2020
JupyterCon
Davide De Marchi - Voilà dashboards for policy support | JupyterCon 2020
JupyterCon
Marcos Lopez Caniego - ESASky's JupyterLab widget| JupyterCon 2020
JupyterCon
Praveen Kanamarlapud - Kernel Life Cycle Management | JupyterCon 2020
JupyterCon
Aaron Bray - Pulse Physiology Engine | JupyterCon 2020
JupyterCon
Aaron Watters - Using WebGL2 transform/feedback in Jupyter widgets | JupyterCon 2020
JupyterCon
More on: AI Workflow Automation
View skill →Related Reads
📰
📰
📰
📰
Three Token-2022 Mints in One Week: Fees, Yield, and Soulbound
Dev.to · atharv shukla
Maximize Google Workspace AI Power: Safeguard Data and Boost Performance in 2026
Dev.to AI
What is Gemini Spark, and what can it actually do for you?
TechCabal
How I use python to save hours every week
Dev.to AI
🎓
Tutor Explanation
DeepCamp AI