SPA UI Plugins
This guide explains how to create a Single-page Application (SPA) React plugin based on the new front-end extensions paradigm.
Source branch with an example project (FlowJS): example/react-plugin.
Source branch with an example project (TypeScript): example/react-plugin-typescript.
In TeamCity, we use React. Every component in the Sakura UI is a React component. Moreover, many components in the classic UI are the same React components but aged visually for UI/UX consistency.
Similarly to a building process, we build the UI using small panels and bricks called components. Our components are based on the Ring UI library. We compose library components to build our UI.
Starting from TeamCity 2020.2 we share our "bricks". We expose some of our internal components and give an opportunity to use the Ring UI library in plugins, as well, as other React UI kits. Therefore, if you decide to develop your UI plugin with React, there is no needing to stylize your own buttons or dialogs – you can reuse the components we use ourselves.
Composing SPA UI plugin
Before we start, please prepare your environment. Usually, modern Web development requires you to install Node JS and Node Package Manager (NPM). It might be not very convenient if you are a Java Developer and make the very first step with the modern Web. For this case, we prepared the
Docker-compose.yml which will take care of compiling/transpiling and bundling in the JS.
To build your plugin within the Docker container, use
To run a plugin React application in the Webpack Development server mode, use
If you prefer to run scripts locally using your environment, simply use the Intellij Idea Run Configurations or use CLI:
/demoPlugin-server/src/main/resources/buildServerResources directory. Then you have to launch the Maven Package command:
Let's build the Demo plugin and apply it to the TeamCity. You will see the addition in the sidebar.
As in the previous sections, we start with
How Plugins are loaded
What is the difference between
To understand it, let us explain the Plugin Wrapper workflow. In TeamCity 2020, at the time of
DOMContentLoaded, we send a request to
[TEAMCITY_BASE_URL]/app/placeId/__ALL__. The server responds with the mapping
Array<Plugins>. Each entry in this map contains metadata about the plugin: name, controller URL, list of attached CSS, and JS files.
Using this mapping, Plugin Wrappers understand that there are some plugins attached to theirs
During the constructing, Plugin Constructor checks some minimal requirements (for example, that
PlaceID is specified and the plugin with the same ID doesn't exist) and, if all tests are passed, invokes
onCreate. As you see, the
In other words, the plugin creation consists of multiple steps: request list of Plugins, request plugin HTML, parse HTML, create a plugin with parsed HTML, add subscription to the
ON_CREATE event, where the styles and scripts are loaded asynchronously.
And the JSP File:
We use internal property
From now, we are ready to the pure frontend. Here's a brief description of React basics.
In some cases, there could be multiple VDOMs on one page. It could happen if you use different React instances or if two parts have no common ancestor. Those VDOMs have no connection to each other. If you write your React plugin using the separate React instance, this plugin will have no access to some features like a common vDOM or common React contexts. The React application crashes when it faces two different React instances.
For plugin compatibility, we expose our internal React and ReactDOM instances via the Teamcity React API. The right usage of the TeamCity React instance is the key to writing a performant and safe plugin.
As you can see, there is a new folder named
Frontend. In this folder, we have a React application which uses Flow JS. If you prefer static typed way of programming, you will definitely like Flow JS, but you are free to use any language (we use Flow JS, our Space team loves KotlinJS, most people all over the globe like TypeScript).
Unless you've decided to use Docker container, there are two mandatory actions you have to do:
Install the npm module
package.jsonand in NPM).
Configure the Webpack (
We tried to make the Webpack config as simple as we can do, so it simply extends the predefined config:
As you see in
webpack.config.js, there is an "entry" item which points to a certain file from where we start our React journey.
Import the Ring UI components. It could be buttons, dialogs, whatever you would find suitable your goal. For this tutorial, we selected the heading.
Import type definitions from
@jetbrains/teamcity-apito add typings to the App component.
Example of using the Ring UI H2 component.
Our application receives only the
PluginContextas a parameter. Whenever the location updates, Plugin Wrapper lets the plugin know about the new PluginUI context. React starts checking what DOM nodes should be changed and then applies changes to the DOM. You can click on the name in the sidebar to expand the container and make sure that the React plugin has subscribed to the plugin context updates.
We also should highlight, that there is an opportunity to reuse not a Public Library Components, but the internal TeamCity components. For now, we expose only the All Builds component. In the next releases we will add Contexts and a few more components. To reuse internal TeamCity components, add the following code to
After the initialization, this component renders a Ring UI loader which will be shown for the next 3 second. After 3 seconds, it starts rendering the Sakura UI AllBuilds components which we imported from
This was an example of how to compose Ring UI components with TeamCity Sakura UI components and add your own code. At the moment, we have one exposed component (AllBuilds) but the list of exposed components will grow soon.