Controlled UI Plugins
This guide explains how to create a controlled UI plugin based on the new front-end extensions paradigm.
Source branch with the example project: example/controlled-plugin.
The name controlled explains the main advantage of these plugins – a developer controls the plugin behavior. A controlled plugin knows how to react on the lifecycle events. It uses the Plugin API to update its content, to subscribe and unsubscribe to events, and to abort requests. In other words, controlled plugins allow creating rich applications within the TeamCity UI. Moreover, when the Plugin Wrapper knows that a plugin is controlled by a developer, it stops requesting the plugin content every time and reduce lifecycle events.
Composing controlled UI plugin
Plugin Wrapper considers the plugin as a controlled one, if it has an
ON_CONTEXT_UPDATE handler or if it's a React Component.
Let's start with the Java Controller:
The code is pretty close to the basic plugin v.1. We've only changed
PlaceId.ALL_PAGES_FOOTER_PLUGIN_CONTAINER and explicitly added the JS file
controlled-plugin-core.js; the JSP now contains the next code:
addJSFile in the JavaController, the second one is to use
<bs:linkScript> in JSP. The
<bs:linkScript> helper generates a resolved path to the script file (including
There are no reasons to use one loader prior to other, except that .addJsFile() files are loaded and invoked before the content of a plugin is rendered. Apart from that, we at JetBrains consider using
<bs:linkScript> as a clearer frontend-centric way of adding script files.
We prepare the ES6 template.
We subscribe the plugin to the context update event. The subscription handler provides the last context as the first argument. Whenever the context changes, we ask the plugin to update the content with the new string generated at point 3.
Let’s build a plugin and look at this behavior:
First, the console now looks a little different:
ON_CREATE phase, Plugin Wrapper starts loading all attached scripts and styles. Script loading is an asynchronous function, so all scripts getting loaded after the synchronous
After scripts are initialized, we call a subscription. That's why we see subscription lines after
Plugin debugging. SakuraUI-Plugin / SAKURA_BUILD_OVERVIEW. Subscribe to Lifecycle. ON_CONTEXT_UPDATE
Now, if you try to select another build, you'll trigger the context update. There is a major difference between the basic and controlled plugins here: Plugin Wrapper never recreates controlled plugins from a scratch (skipping
ON_DELETE ). Instead, it updates the content accordingly to the new context using the
ON_CONTEXT_UPDATE handlers. We used the Plugin API method
replaceContent, so we also received a notification that the content has been updated.
Every time we navigate, we also face the
ON_UNMOUNT lifecycle event. Unmount happens every time the plugin should disappear from the screen. Or, in more specific terms of frontend, Unmount happens whenever you remove a node from the DOM.
You can subscribe to any lifecycle hooks with the Plugin API:
onMount- element is mounted to the DOM.
onContentUpdate- plugin content has been updated via the
onContextUpdate- PluginUI context has been updated.
onUnmount- element is unmounted.
onDelete- plugin is completely removed from the memory.
The newly added handler for
ON_MOUNT event fires at least one time, even if
ON_MOUNT has been triggered before.
This hook is a good place to add event listeners because it fires right after the moment the content has been applied to the DOM tree. Try to add this code to the plugin's JS:
Using the code above, we call the Plugin API method
registerEventHandler to add the click handler. We recommend using this method because it guarantees that the handler will be detached before
ON_UNMOUNT and attached again next time the component is
ON_MOUNT. Hence, you can avoid memory leaks and dead callbacks.
That is what you get:
As you see, right before the component is unmounted, Plugin Wrapper removes all handlers. Next time the Plugin will be mounted, all specified
ON_MOUNT callbacks will be fired and the event listeners will be attached again.
Every lifecycle event subscription takes a handler as an argument. This handler receives the current context. Every subscription returns an unsubscribe function, so if you do not want to keep receiving updates, just call the unsubscriber:
We've learned how the lifecycle hooks work and how to consume them. Using this API, you can control async operations and event handling. This API is sufficient to build any complicated plugins for TeamCity. This is a path you can follow to build Plugin using your favorite UI Framework / Library.
But if you do prefer React, we have a little more for you. In this section you can read about React-based plugins, learn more about internal plugin mechanisms and how to reuse our components in your plugin.