TeamCity
Powerful CI/CD for DevOps-centric teams
TeamCity 2020.2: updated Plugin Development
Since its very beginning, TeamCity has provided extension points you can use to improve its functionality and your experience. Starting in 2020.2 EAP1, we are taking them a step further and offering you an improved way to write and integrate UI plugins, both in the Main and the Experimental UI (code-named Sakura).
In this post, we share our experience, concerns, and motivation behind revising the plugin development pathway in TeamCity. If you prefer to get straight to technical information, welcome to the updated plugin documentation.
We want the API to be as useful as possible by the 2020.2 release, so your feedback on this update is very important to us. Please let us know your thoughts and observations in YouTrack, the comments section under this post, or the TeamCity support center.
TL;DR:
- We have added a new section to the plugin documentation.
- There is a way to integrate plugins with the Sakura UI.
- All existing plugins continue to work as they worked before 2020.2.
- UI plugins can be written in a more frontend-centric way, which involves modern web techs.
- UI plugins are framework-agnostic, so you can use any library, framework, and bundler.
- For those who prefer React, we expose our internal components, so you can write a plugin composing existing components from the Ring UI and the Sakura UI – which means you don’t always have to write everything yourself.
State of plugin development before Sakura UI
If you have ever tried to write a TeamCity plugin, you probably used this guide.
Here was our main idea for the workflow: to create a plugin, you should create a Java controller where you register the PageExtension.The PageExtension should have a certain PlaceID and some resources (JSP, JS, CSS files) attached to it. Whenever a browser requests a TeamCity page and the core meets the PlaceID container, it tries to find plugins that should be attached to this container. Then, if TeamCity finds any plugins, it compiles the attached JSP and puts the output to the HTML-response. In short, that’s it.
Depending on your imagination, you could use the JSP as a template-container and compile it with the attached JavaScript, make requests, and update the DOM accordingly. That is how we built plugins previously.
Through the years, we figured out that this approach has some restrictions, and even we, the TeamCity developers, were writing some UI plugins in a less than ideal manner. For example, we used plain JavaScript to put an HTML element anywhere in the DOM (for example, in the header, using the class-selector .userPanel). To be fair, that was enough at the time, but the web has advanced beyond it.
Since we released the Sakura UI, we’ve found a new issue: those JSP / HTML plugins are not fully compatible with Single Page Applications (SPA). Every time we navigated through an SPA, we would be required to download the plugin content using XMLHttpRequest. We had no reliable way to tell the plugin about the current context. And worse, every time a Browser downloaded the plugin, it would also download JavaScript files and run them in the global scope without performing any cleaning. So, any plugin more complicated than just-an-HTML potentially led to memory leaks.
In an attempt to avoid these issues, we’ve implemented a solution using iframes. For example, we use iframes to show you the plugins for the Build Results page or when you open tabs from the classic UI in Sakura. We did not rewrite the content of the tabs (or plugins). Instead we created an Adapter, which loads the tab in a separate frame and removes excess elements (like the header or footer). It works, though sometimes with bugs. For example, the sad one – TW-64595 – is caused by frame / container height synchronization. When the frame’s content changes its height, we should update the height of the parent’s container accordingly (nobody likes double-scrollbars). Sometimes this doesn’t work well. Other minor issues include being required to download the full document every time we render an iframe and the frame content not having access to the current window object.
New header and Single Page Application challenges
While we were developing the new header, we encountered some new issues. As you know, the header is a natural place to put plugins: investigations, achievements, search bar, and much more. But, the new header is based on React. By default, the current Header is disabled. To enable it – add the internal property teamcity.ui.sakuraHeader = true, as it is described in TW-67375
During the experiment, we added two plugins to the New Header: Investigations and Search By Build Number
React is a UI library that helps developers write apps in a declarative way. In short, it tries to avoid updating the app parts that should not be updated. Say we have the TeamCity Sakura UI, which consists of the header, the sidebar, and content. Whenever you expand a sidebar item, you change the state of the application. This change affects only the sidebar – not the header or content. To put it simply, React tells the browser: “please, update the DIV in the sidebar and keep the header and content as they are.”
However, if you click on an item in the sidebar, you change the NavigationContext. This affects the entire app: the content panel should render a selected build configuration or a project; the header should update the Search plugin to use the latest selected item. At the beginning, we were obligated to update every plugin from scratch every time the React container received updates (NavigationContext, in our case). This means, if we have a simple JSP-based plugin, we should remove the previous plugin’s HTML content from the React container, then request a new HTML, and then add it to the DOM. As a result, the Browser will get tons of requests and the layout will shift. In some cases this is a required behavior and you have an opportunity to write in this manner. We call this type of a plugin a “Basic plugin”. Ideally though, it’s not how we want our plugins to work.
If you have an advanced plugin, which encapsulates elements in DOM via JavaScript and uses more sophisticated logic (like if it requests or updates the DOM according to the model updates), you write a JavaScript file with selectors, addEventListeners, and some subscriptions. From this point, it would be helpful to get an API that makes it possible to clean your callbacks, timeouts, and eventListeners. For sure, it would be nice to avoid Layout Shifting for the better UX.
And, finally, over the last 3 years we’ve written a lot of reusable React components. Why wouldn’t we allow Plugin Developers to reuse them like we do?
Those are the main reasons we’ve decided to improve plugin development from the frontend developer perspective. We’ve tried to solve these problems in the current update, and here is what we can offer:
- There is a way to integrate plugins to the Sakura UI.
- All existing plugins continue to work as they did before 2020.2.
- UI plugins can be written in a more frontend-centric way that involves modern web technologies.
- UI plugins are framework-agnostic, so you can use any library, framework, and bundler.
- For those who prefer React, we expose our internal components, so you can write a plugin composing existing components from the Ring UI and the Sakura UI, which means you don’t have to write everything yourself.
In the GIF below, you can see a screencast from the local environment. Here is an example of a React-based plugin in the Sidebar section:
- This is a React Plugin, it’s integrated in the React vDOM tree.
- It re-renders only when necessary.
- It has access to the current context.
- It works in the Sakura UI.
- It reuses components (The Ring UI heading in this case).
This post was meant to be an introduction, where we explain why we decided to improve plugin development and how it works for now. If you are ready to dive deep into the code, we invite you to take a look at the official documentation for the 2020.2 EAP.