 Hello, everyone. Welcome to the Horizon deep dive customization session. My name is Ty. This is Matt and Cindy. And I'll be covering the first third of the presentation. So a bit of history before we get started on all of this. The initial plug-in support was we actually added that in Icehouse release. But a lot of the Angular plug-in work didn't actually start until the Kilo release, where we started introducing things like localization, basically Karma and Jasmine for our unit testing, and the initial launch instance work. And in Liberty, we introduced the user's panel. We had auto discovery. We had linting for our code, for code quality. And we had a lot of low-level widgets that we created to help us get along. And in Mitaka, we introduced two new panels, the Angular Swift panels and the Angular Images panel. Along with that, we have two new concepts, which are basically the action registry and the extensibility service. And we'll talk a little bit more about that. And so what's introduced in Newton? So the features that we introduced in Newton were, one, higher directives that basically replace a lot of the boiler plate templates that we used to have. So because we had a lot of lower-level widgets, we had a lot of templating code that also accompanied them. With higher directives, we basically got rid of all of that stuff. And some of the other features introduced in Newton were resource-centric widgets. And later on, Matt will talk a little bit more about that. Essentially, it allows us to define a heat resource type and allow us to use higher directives to then auto-generate a lot of the content that we need. We'll talk a little bit more about that later. Three, we had a lot more extension points. So now you can customize things like columns, search facets, details of you. If you wanted to add more stuff to your details, you can do that easily now. And four, lastly, we introduced the schema forms, which is a third-party library that allows you to essentially generate forms very, very quickly, just using, basically, JSON schema. So what are some of the takeaways we would like you to walk away from this session with? If you can walk away with just one thing out of this four, we would be very, very happy. So four of the things we're going to cover is Angular plug-in architecture, which I'll cover, and then resource action registry, which Matt will cover. And then lastly, three and four, how to write a plug-in and customize different things like actions, columns, details, facets, and so on. Okay, so how do we get here? Angular plug-in architecture, what led us down this road and why do we use it? So this slide will answer the why, and the next two slides will answer the what and how we got to it. So the reason why we use the Angular plug-in architecture is, one, it's extremely modular. So you'll see in this diagram that a lot of the actions are broken up into pieces. A lot of the steps, which you'll see below in the green region, is also broken up into modular little pieces. And because it's modular, we can easily extend and customize these actions and these steps. And as a result of that, we can also make them very flexible. So you can either define them using the schema, such as the schema form, or you can declaratively declare them and say, you know, I want more control. I want to customize my things more. I don't want to use the framework. I want to write out all the HTML and the JavaScript. You can do that too. And because of one, two, and three, these modular pieces make great building blocks, which then allows us to write higher directives that take advantage of these lower directives. And again, Matt will talk a little bit more about some of what some of these higher directives are. So how did we arrive at this Angular plug-in architecture? A couple of mid-cycles ago, we had a problem that was posed to us that we had to solve. Essentially, the problem is you have two independent teams. They both want to work, and they want to introduce their own little step into this workflow, right? In the past, there wasn't actually a way for you to do this. You would actually have to hack code, monkey patch it, do a whole bunch of things. You have to manually, you know, merge your code. So there's a lot of work just to get two plugins to work together. So we came up with the idea of the extensibility service, and this solves the problem that was proposed. The reason why it works is because it uses a plug-in play model, essentially allowing you to write many plugins, and they can all extend from the same workflow. In this example here, we have a run block that essentially you can then inject an existing workflow from Horizon, and you say, here's what I want to do with it. I can prepend this workflow with another step, or I can append this workflow with another step. I can even inject my own custom step into any arbitrary position, and I can give it even better, I can give it a priority so that I don't run into conflicts. So what's changed since L and N? Well, in L, when you wanted to create custom content, you basically start at the very top, you have a table controller, and then you have some table tabulating code that reads from the controller, and then you have a bunch of actions that the table knows about, and when you click on one of these actions, it launches a wizard model, which then walks you through a number of steps, and once you complete these steps, you then complete the action. In N, we sort of changed that a little bit. If you notice, at the very top, you don't have a table controller anymore. What you have in this place are the images module. So instead of writing your own table code here and your own logic, you essentially now says, hey, I have a resource type, a heat resource type. In this example, we're using images, and we're saying, hey, I have an images resource type. Here are the columns that I would like to see, and here are the properties that are set for this particular resource type. And then you have an actions module, which then is basically an aggregation of all the actions that exist for that particular resource type. And the reason why we have this actions module is that you can now use this across panel. It's no longer restricted to just a single panel. So if you wanted to show, let's say, users along with images, you can now do that, and you can take actions that are specific to that resource type. I'm going to introduce Matt next, and he's going to talk about the action registry a lot more in a lot more detail. Great. Thank you, Ty. All right. A little introduction first. I'm Matt Boylan from HPE. I'm one of the contributors to Horizon, and I also contribute to the Searchlight UI project. Some basic facts about me. I went to space camp. I fly planes, I raise chickens, and you have to trust me, this actually happened. I ran into Tim Allen in a deli. It's true. You could look it up. I'm going to talk about the resource type registry that he was referring to, and then I'm also going to go through and sort of examine and make the anatomy of a web page that's using the registry. So let's take a look at just an overview of Horizon and sort of how it's structured. It can get a little confusing because the word Horizon means several different things, and so I wanted to sort of de-conflict that. Over on the left, you see the Horizon repo contains a Horizon framework and also OpenStack dashboard, which is a directory that contains sort of these core dashboards. The Horizon framework contains widgets and the registry that I'm going to be talking about, and then obviously in a deployment, you might have multiple plugins that play into your installation of Horizon. So I just wanted to sort of paint that picture very easily for you. Developers, when they're looking to develop these applications, they want to be able to write features quickly. They want to be able to develop without having to generate a lot of scaffolding and boiler plate, as Ty was mentioning. They want consistency of display. They want things to sort of look consistent from one page to another. And sort of a new thing, at least from my perspective, is the ability to display similar controls and content in different contexts. So for example, right now, using the Images example, you can go into Horizon, drill down into the Images page, and you can see the content there. Well, I would also like to see some of that content in the Searchlight plugin that I'm working on. I want to be able to find images there, click on it, and then see the same content. So we just want to make sure that all these sort of questions are going together. The registry, it's an AngularJS service that allows AngularJS modules to provide or retrieve descriptions of resource types. So volumes, images, those sorts of things. And this is probably the most important aspect, is that the widgets that we're providing in the Horizon framework make use of these descriptions to present information and workflows. So that's what I'm going to try to get across at the end of this talk. As Ty said, it uses heat naming conventions. It's basically a map that just says, you know, OS Nova Server, what's the descriptor object related to that? That's all that it is. And any modules, as Ty also said, can register these things. So just as an overview, if I look at the Images page that exists now in Newton, all of these different aspects are coming from a configuration in the registry. So all the columns and actions and things that I've highlighted here. And similarly, within the details views, all the tabs and headers and actions that you see there are coming from that. So we're going to take a closer look at sort of how this actually comes about. So at runtime, the registry, this is actually an AngularJS thing. When you have a module that's initializing the two important phases it goes through. The first is config and the second is run. And just as a background, the registry initializes itself during config. And the main distinction here is that that makes it available for dependency injection, which is sort of a core feature of AngularJS. So that anything, any module that you write that I write can just receive that as a dependency injection. So that I'll get into exactly when you use that, but it's an important concept. So place your registrations in the run method of your AngularJS module. I'll show you more of that. And just as a highlight as to what the resource types look like, they'll contain some metadata. So like names, things like that, how to load the data. They're going to talk about the actions that you can perform on the data. And also, how can you view the data? I'll go quickly through this. But for example, like OS Nova server would say, well, we're going to call, I don't know why we call instance in Horizon, but instance and instances, well, we're going to keep that in the registry. What API do we use to actually access the data? That's the registry. And then what are all the different views and things. And the other thing I really wanted to point out on this slide is the fact that you don't have to register everything for every resource that you're developing for. You may just want to put names and you may not want details view. It's completely flexible as to how you execute that. So when you use this structure, the things you get for free, you get these tabular list pages. So if you're sort of familiar with the current scheme within Horizon, you go to a page and you see a table list. This also gives you client side results searching as part of that. So it just comes free with using this. All the action drop downs that you're familiar with are provided there. And then the basic structure for details page. And this is actually an collaboration on what's already been done in the sort of the Django side of Horizon. So we'll look at that a little bit more closely. You still have to understand Angular enough to do several things. You have to be able to write actions, which are essentially workflows. And we have some tools to help you with that. You have to write your business logic. And then you have to write your basic views, which are essentially templates. And I'll show you some examples of those. So I work on a plugin. I work on Searchlight UI. And I used the registry, again, not necessarily in the core prize. I'm using it in a completely different context. But I'm going to sort of look through the different places where this registry values show up. So here I'm just highlighting the names of the different types of things in my result set. Those come from the registry, the actions. And there are actually three different types of actions that we register. Their row actions or item actions, rather, that show up at the row level. So on a single item. Then you have batch actions, which are things which can be executed on multiple items. And then you have global actions, which can only be executed at any time without any action. So like launching an instance, you could certainly do without having selected a resource. We've introduced drawer views into many of the displays. So the images page is an example of this. And Searchlight is an example of this as well, where you can just click on a row and get expanded information about it. And then on the details pages that we've introduced, all this is generated from the registry as well. So let's just take a quick look at the details page a little bit more. So the names, the actions and the views, multiple views that you can tab through those are coming from the registry. So let's take a look at the code a little bit. So in your module, I said this is all going to be run through your run method. I'm just sort of pseudo code here. The run method here is having the registry injected into it. And then we are going to call the get resource type method, which is sort of the most critical method in the registry. And it just looks up the string that you're passing in. It says, do I have this in the registry? If it does, great, it's going to pass you back the information. If not, actually initialize the registry so that you could either add more information onto it. And that's really how this sort of plug in method works, is that any module can just call this get resource type and either access or provide information to it. And since it's done at run time, that is the run time for the module, it sort of happens before the application has fully started. So how do I make actions, for example? Actions are expressed in AngularJS as JS services that are registered with a resource type. And I don't want to get into the complexity of that because it's difficult to construct workflows. But basically what this means is that if you look at the very top, it might be hard to read. We call get resource type on the registry for instances, I suppose, is what we're using in this example. And you'll notice it says instance resource type.globalActions. So this means we're accessing the extensibility service for this list called global actions. We're going to append onto our global actions a new action, and this is called create instance. And it's using the service of launch instance service, which is this service that we've created, this workflow we've created. And it has an ID only for the purposes of, for the extensibility service, if someone wanted to remove this or prepend something in front of it, in their deployment, they could certainly do that. But either way, the whole point is that you're basically putting this code into your run method and you're modifying these lists as appropriate. So a couple of notes about actions very quickly. Actions have three major methods that you should be aware of. One is called scope, it's optional, and it's basically an init for whatever your feature is. And as a reminder, an action is like launch instance. So this just means that at the beginning of that cycle, is it going, what, is there anything that needs to be initialized? Dot allowed is sort of familiar for those of you that have done Python development or the Django development before, it's the same thing. It's basically saying, is we pass in some user in context information, is this user allowed to perform this action? If they're not, then the action is not shown for them. And then finally, of course, perform is actually performing the action itself, starting the workflow. One sort of advanced note on all of this is that any action that you fire off, it provides a promise back, a JavaScript promise, which is fairly important because this means that we can have fairly intelligent handling of completion of actions that you've performed, rather than just sort of saying, well, we came back from this action, so let's just reload the page. We don't know what to do. This provides mechanisms for us to more intelligently respond to what the user has done. I won't get into the details of that. How do I make details views? It's very similar. Now, let me show that details view. What you're looking at is there are four tabs with different content in them. That's represented by this kind of code where you say resource type dot details views dot append. We do this four times, we have four different views. Really, all you have to do is give it a name, give it an ID, and then point it to an AngularJS template that you're deploying somewhere. It's quite simple. Similarly, with drawer views, these expanded views within a table, you can say set a drawer template URL. It's just saying use this template to describe the content or to present the content that is here. At the very bottom, I don't know if you can read it terribly well, but this is an example of an entire drawer template. You can see that what we're doing is we're using this HZ resource property list. This is one of the widgets that has been developed that facilitates the use of the registry. In this case, it's saying let's use this widget. We're going to be using OS Notice Server, so this is an instance. Fetch some information from the registry about this so that we can display these properties which are then listed and grouped below. The result of that is you get four columns with properly formatted data in them. It's very simple. That's all you'd have to do to make a drawer. File-wise, you end up, this is a suggestion. You can structure your JavaScript however you want, but we found that it's useful to, of course, put most of your configuration in the module, file itself, so my resource.module, and then in actions and detail subdirectories, put your actions and your details in there. In addition to that, with the actions, we actually suggest you put all of your action registrations in the actions.js or module.js file as well, just so that it's sort of easy to reference. Then steps are more for like if you have workflow steps and you want to separate them from other work. I'm not going to do this right now, but how to make a basic panel, just go through steps I talked about. Register your actions, register columns in a list function, along with some filters for your columns, add a panel that routes to the common Angular ngView template, and then you're done, and then you just go to see that work. And then this is just an outline. I'm not going to get into great detail because there's great detail on the widgets and how they're related to one another. I think if you're curious about the widgets, I would look at this chart and start browsing around because for the most part, you can just take the highest level widget and apply whatever it is, the configuration it needs, and it will create all the sub-widgets within it. So best not to get too worried about that right now, and that concludes my portion. Cindy, you will have a demo for us? Great, thanks. Okay, so I'm going to show a demo to help reiterate everything that Ty and Matt have talked about. So let's first take a look at some source code before we start doing our customization. So we're going to take a look at the images panel. So let's look at the code for that. So it's stored under OpenStack dashboard, static folder, app, core, images. This is important to put all your Angular content under the static folder in order for it to be picked up by Django. So we have three files that we're going to look at for the images panel. These three are the key files. So let's take a look at the images module first. So in here, this is where we register all our information, and as Matt said, the registry is the backbone of all your Angular development. It's where you store all the information that you need to build your UIs, and the registry you can inject into any module and access it and make any modifications you want to it. It provides a single point of access, so when you write a plugin, you can grab it and then change whatever you want to it. So if we look at this code, in here we inject the registry on line 56, and then we also inject the resource type, which is the heat name, so it would be OS, glance images, and this is a constant up here. So we defined that up here. And like Matt said, we use the registry to retrieve the heat type and then we get all the information. So in this file, we set all the information that we can retrieve later. So in here, if you scroll down, you can set your name, you can set URL to the detail drawer, and if you scroll down, you can see we've set all our table columns. So here's a list. It makes it very easy to create a new table, and here we have a list of filter facets as well. So let's look at our next file. This is the actions file. And in here, we do the same thing. We inject the registry and the resource type, as well as all these action services, which we will use. These are all in the different files. If you look under images actions, all the service behavior is in here. So we won't look at that. So once again, registry.get resource type, we insert the heat name, and then we can access the item actions object and append all our actions to it. So we have these item actions are the row actions for each item, and we have five actions here. We have launch, create volume, edit image, update metadata. And for global actions, we have one item, create image, and for batch action, we have one item, delete image. So now let's take a look at the UI. So this is what it looks like. We have our columns that we defined that are generated, just based on our schema, as well as we have one global action, one batch action. We have some item actions here. We have our detailed drawer, if we open these up. And also we have details view. If you click on the image name, this is our detail view. We just have one, the overview page. So now let's take a look at our plugin. So take a mental snapshot of what the table looks like now, because we're going to customize it now. So let's stop our server right now and take a look at a script that I wrote to do all this for us. So I already get cloned this plugin repo, which you can find here. We have a link at the end of the presentation. You can download this and try it out in your own environment to see how it works. And it has all the instructions over here. So what we do is we get cloned the repo and then we tar up the plugin and we copy the enabled file into our OpenStack dashboard. This is the point of contact between the dashboard and the outside world. And it'll register the plugin, which is read up at startup. Then on this line, we pip install the package tar and then we start our server. So if things go well, the package tar file will be installed in the virtual environment. So let's open that up first to make sure that it's not already in there because we're doing a live demo. So we don't want that. So the package is called demo. And sure enough, it's not here. So let's fingers crossed everyone. So let's run that. Okay. So if anyone paid close attention, the package is now installed here. So let's open that up. So inside here, we follow the same file structure as we do in OpenStack dashboard. So we have static app core images. And these are new files. So let's open those up first. So we're going to take a look at demo.module. So let's first take a look. Let's step back and kind of cover the things that Matt and Ty talked about. So in here, we inject the things we need. So once again, we inject our registry service here as well as the resource type. And the run method block will execute after all these modules are loaded and the dependencies are resolved. So in the run module, you can think of it as a main method. So we passed in the registry and the image resource type. We access the registry and we print it to the console so we can take a look at it later. And then let's add a new column. So we're going to create a new column and then append it to the registry. So we made our own custom template, which is just a plain text in the color orange to honor Halloween, which is just around the corner. And so we add a new column. We also add a new detail view. So we access the registry's details view and we append another detail view. So we have a template to a new detail file. So let's open that up. So in here, we just have another text in orange, which just says happy Halloween. And we also override the summary drawer template. So let's take a look at that. So the drawer template is the little box that opens up if you click on a row. So in here, we just have peekaboo. Okay. Lastly, we add a new global action. So we call this one surprise service and we also injected this service up here. And we define the service. So I didn't quite follow the proper way, which you should actually put your services in an action folder, but this is just a small demo. So this is our service, which we defined. And we so each service you have to define a allowed function and a perform function. So allowed makes sure all the promises are filled before executing the perform, which was which would just be to open up the modal. So in allowed, you can also do policy and permission checks to make sure that the user is allowed to perform this action. And in perform, it'll just open the modal. And you can also use the schema form that I mentioned to create your form very easily. You just have to define it as a JSON and it'll generate the form for you. Okay. So we've tried this all out. So let's check out our newly applied plugin. Okay. So let's refresh my heart. So we're looking for anything in orange as a confirmation that this works. Yay. So it worked. We added a new table column called it's just a repeat of this column, but it's an orange. We also added new. We overrode the details drawer view. And we also added another detail view. So in addition to overview, we added another view called happy Halloween. And we added another global action. So if we click on surprise, we just have some blank boilerplate code. I was going to put some something crazy in there, but I don't want to scare people. So that is about it for the demo. So let's go back to the presentation. So this is just a small list of a recap and best practices. So the Angular images table is default since this release and it's set to true in the Angular features dictionary object in settings up P Y. And if you want to revert it back to the previous Django images panel, you can, but there's you should have no reason to do that. You should also from from this release on, you should use a registry for defining your panel. It gives you a lot of extension points. However, you can still define it as an HTML template if you want complete control of all the like little things in your table or you want to make it crazy or you don't even want to use a table. You can you can still use the HTML template, but registry makes things very easy to use. And also don't forget to put all your Angular content under the static folder. So looking forward, we would like to add more extension points, including adding it to the schema form so that you can create a form or you can extend an existing form very easily. We'd also like to be able to change the, what was the second? Okay, sorry. Okay, I forgot we were, we also wanted to be able to modify the behavior after we've already never mind. Okay, so this is a list of resources that we covered. So our, the demo for this, this session is here. So if you want to take a look at that downloaded, try it out yourself. This is a good starting point. We also have a lot of plug-in documentation written by Ty. So check that out. And it's very easy to use now. And this is our contact information. You can find us on IRC channel via these IRC handles. And we also have a meeting every week at this. Okay, so thank you for coming, everyone. This concludes our session. If you have any questions, please go to the microphone and ask us.