% Polygerrit plugins
To provide a robust, powerful, simple UI extension interface for PolyGerrit.
(inspired by chromium extension manifesto)
This design aims to provide plugin system, that is:
Gerrit has an extensive plugin development API that covers a myriad of plugin use-cases. This document focuses on the UI-focused extension points and how they differ from their GWT UI counterparts. No changes to server-side plugins are proposed.
The Gerrit GWT UI provides number of UI extension options with plugin developers who use GWT being the primary focus. Per the documentation above, a GWT UI extension must extend the abstract class PluginEntryPoint, but can also add dependencies on other classes like stock GWT components (e.g. DialogBox). The class can then be integrated into the Gerrit core UI via the RootPanel class. Once compiled, the jar file is placed into the site's plugins/ folder and the PluginLoader class handles loading the plugin.
Gerrit also provides JS API to be used for JS plugins that don't use GWT compilation. This API is used as an interface between a plugin compiled with use of gerrit-plugin-gwtui and GWT UI running in browser. This way a plugin can work with any other implementation of JS API. In practice, the documentation does not fully describe the existing implementation.
Also it's worth noting that historically non-GWT plugin authors in many cases chose to bypass the provided JS API and carry the burden of implementing and maintaining direct DOM/CSS manipulation of Gerrit UI knowing it may and will break without warning with the future version of Gerrit instead of using a proper route of contacting Gerrit team for upgrading or fixing existing JS API.
Since PolyGerrit UI is based on a completely different UI technology (Polymer instead of GWT), providing one-to-one feature parity to plugins built this way as well as seamless migration would require significant modification of existing gerrit-plugin-gwtui and/or Gerrit GWT UI. In addition, such modification would have to be platform, browser, and version agnostic in order to handle current and future versions of both GWT UI and PolyGerrit until GWT UI is deprecated and deleted.
As of now, PolyGerrit implements part of Gerrit JS API interface and has introduced a number of experimental private APIs in order to accommodate primary clients (Chrome, Android), to gather additional use-cases and provide feature parity with code review systems that are currently used by aforementioned teams.
This document describes changes to the existing Gerrit JavaScript API that allow plugin authors to modify and extend the behavior of PolyGerrit UI.
tl;dr: heavily based on Web Components
After analysis of existing Gerrit plugins, it became apparent that the plugins would benefit a larger API surface than declared in existing JS API. For example, plugins need ability to build UI using same elements and styles host UI uses, and extend such elements for custom appearance or behavior. This naturally calls for using DOM as a part of API surface. (TODO See corresponding section for more detail)
At the same time, it‘s important to provide plugin developers with API that is stable, well defined, and minimize breaking changes with future UI development. JS API is a perfect example of that - it’s easy to support and test such methods, and makes possible to swap implementation keeping same interface. However, the cost for implementing JS API as expressive as DOM is likely will be prohibitive. So it make sense to take advantage of another well-defined, stable, and robust API that already exists - Gerrit REST API. This API is already used in UI, so for simple cases (e.g. hide an action button from change actions), plugin author may chose to take advantage of decorating REST API response before it's consumed by UI. (TODO See corresponding section for more detail)
Also there are number of API changes and additions facilitating plugin development and deployment for PolyGerrit.
Entry point for the plugin and the loading method is changed to follow HTML Imports spec. index.html to be loaded from following URL by PolyGerrit:
gerrit-site-root/plugins/plugin-name/static/index.html
It expected to contain a JS script tag (inline or referenced), which in turn would follow current Gerrit JS API entry point requirements, i.e. wrapped in Gerrit.install().
PolyGerrit to provide API for simple plugin configuration via pg-plugin-name.config
file, all properties of which will be inherited and may be overwritten on project and site level via refs/meta/config
branch and gerrit.config
.
self.panel is deprecated in the new API, and an alternative is offered.
As a better long-term alternative, PolyGerrit will provide number of UI insertion/decoration points, aiming to keep feature parity with the already defined panels API.
Also PolyGerrit to provide tools, samples, and guidelines on best practices for plugin development.
Currently Gerrit supports two types of plugins: .jar and .js based. Following changes to file structure are proposed:
The entry point instead of .js file to be index.html:
gerrit-site-root/plugins/plugin-name/static/index.html
In order to simplify migration, already existing js files may be referenced from the index.html.
For .jar-based plugins, multiple options are available:
Final file structure to look like this:
+--- gerrit-site-root/plugins +--- my-plugin/ +--- my-plugin.config (optional) +--- static/ +--- index.html +--- my-plugin.js +--- my-submit-button.html (optional) +--- my-submit-button.js (optional)
When needed, index.html should be used for preloading fonts, images, libraries, etc.
Minimal form for index.html to look like this:
<dom-module id="my-plugin"> <script src="my-plugin.js"></script> </dom-module>
Here my-plugin
is a plugin name. By custom elements specification, the custom element's name must contain a dash (-).
my-plugin.js to follow supported part of Gerrit JS plugin API (see migration section for specifics)
Additionally, a plugin may contain any number of html files describing individual UI elements to be inserted into PolyGerrit UI or for decorating insertion points.
Polygerrit to import top-level index.html on startup. Due to the nature of HTML Imports spec its content is never added to the DOM and thus never rendered. Plugin authors should use index.html in order to preload resources if needed and to provide an entry point for top-level script.
Polygerrit to load plugin-specific configuration and provide it to plugin via top-level API. Plugin to initialize itself on Gerrit.install().
Polygerrit to query and insert plugin-provided UI elements into corresponding extension points.
Individual plugin-provided elements are imported directly into endpoint elements.
Recommended approach for production is use Polymer + vulcanize same way PolyGerrit uses, Cookbook plugin to be updated as an example.
The existing spec is build on assumption that all plugins are loaded and executed after PolyGerrit start, in other words plugins don't block UI.
Generally, the cons side is severe enough to justify adding a way for plugin to block either part or whole PolyGerrit UI, however this probably could come after rest of the doc is stable.
tl;dr: Promote to API level what chumpdetector and landingwidget do.
PolyGerrit to fetch default configuration for each plugin and provide it as a new method as a top-level API:
// in gr-public-api-interface.js: Plugin.prototype.getConfig = function() { // returns combined config object. }
Plugin configuration a key-value hash, created by overlapping plugin-specific configuration from following sources, in that order:
All following sources are to be loaded and combined by existing API endpoint.
Note that all configuration names and values are public, so if the plugin needs to hide some portion on configuration, it should implement it as a Java server-side plugin.
Here are samples, provided in lower to higher override levels:
# gerrit-testsite/plugins/myplugin/pg-myplugin.config [foo] bar = some-initial-value
# gerrit.config [pgplugin "myplugin foo"] bar = site-wide-emergency-override
# refs/meta/config / pg-myplugin.config [foo] bar = project-specific-override
And the plugin code may look following:
Gerrit.install(function(plugin) { var config = plugin.getConfig(); // config is {foo: {bar: 'project-specific-override'}} var bar = config.foo.bar; });
TODO: Consider exposing plugin variables in admin panel (see cookbook or git-numberer)
Plugin developer may declare minimal and target API version required in pg-plugin.config
Version format for PolyGerrit API is similar to npm's semver and has following format:
MAJOR.MINOR.PATCH
Any part of version may be replaced with x as a wildcard, and rest of version is ignored, e.g. 1.2.x matches all patch versions for version 1.2, and 1.x means any minor/patch versions for major version 1.
[version] minApiVersion = 1.2.x targetApiVersion = 1.3.2
PolyGerrit will read version numbers from plugin and take following action in case of mismatch:
This mechanism uses regular plugin configuration inheritance and may be used by admin as an escape hatch in case new PolyGerrit API is deployed but plugin author has not updated required API version yet.
Plugin author may choose to avoid modifying DOM and take advantage of stability of REST API and modify incoming REST API responses before they are used in PolyGerrit. New plugin method to be added, which invokes plugin-provided callback to modify REST request body or JSON server response. Modified object is passed further and is handled in the same way regular payload/response is.
Gerrit.install(function(plugin) { plugin.modifyRestResponse(method, url, function(result) { // Modify response here. result.foo = 'bar'; return result; }); });
PolyGerrit to use part of DOM as an API available for extension.
PolyGerrit to provide extension points for insertion/decoration via new gr-plugin-endpoint element.
Plugin to provide UI elements (as custom elements) for insertion into extension points.
New method to be added to public JS API for registering element with an endpoint.
Appropriate instance of gr-plugin-endpoint to instantiate and setup plugin-provided UI elements.
<!-- in gr-change-view.html --> <div class="changeInfo-column changeMetadata"> <!-- ... --> <gr-plugin-endpoint name="change/info"></gr-endpoint> <!-- ... --> </div> <!-- my-submit-button.html, Polymer-style --> <dom-element id="my-submit-button"> <div>Plugin-provided button</div> <script src="my-submit-button.js"></script> </dom-element>
PolyGerrit to provide description for endpoints, including elements that are instantiated and key properties for those elements.
gr-endpoint-decorator to instantiate plugin-provided UI decorator web component. Plugin-provided decorator receives a reference to element for decoration, and an event when the element was updated (created/modified/etc).
UI decorator web component can provide styles that will be imported into decorator endpoint. The intent here is to use Polymer style modules and whatever web standard comes in to replace it.
<!-- in gr-change-actions.html --> <template is="dom-repeat" items="[[_actions]]"> <gr-endpoint-decorator name="[[_computeEndpointName(action.__key)]]"> <gr-button title$="[[action.title]]" on-tap="_handleActionTap"></gr-button> </gr-endpoint-decorator> </templates> // in gr-change-actions.js Polymer({ is: 'gr-change-actions', // ... _computeEndpointName: function(actionKey) { return 'change/actions/' + actionKey; }, });
The self.panel() will be marked as @deprecated
and will be removed in one of the following releases, with strong recommendation of using recommended approach (i.e. Web Components).
PolyGerrit to provide implementation of self.panel(), that has feature parity with existing GWT implementation.
PolyGerrit UI to have an alternative implementation of self.panel, which will:
This is essentially a syntactic sugar for creating custom elements. It is mostly compatible with existing self.panel(), and hides creating and registering custom elements for simple cases. This approach can also be used to make migration from GWT UI simpler.
This would enable plugin developer to do following:
// In my-plugin.js Gerrit.install(function(plugin) { plugin.registerInlineElement('change/info', function() { // this is an Element var element = this; element.innerHTML = '<p>Lorem ipsum</p>'; }); });
This should be equivalent to creating two files as per recommended approach:
<!-- in my-plugin/index.html --> <link rel="import" href="polymer/polymer.html"> <link rel="import" href="my-lorem-ipsum.html"> <script src="my-plugin.js"></script> // in my-plugin.js Gerrit.install(function(plugin) { plugin.registerElement('change/info', 'my-lorem-ipsum'); }); <!-- in my-lorem-ipsum.html --> <dom-module id="my-lorem-ipsum"> <p>Lorem ipsum</p> </dom-module>
Sample code for PolyGerrit interface:
// In PolyGerrit interface, gr-public-js-api.js: Plugin.prototype.registerInlineElement = function(name, callback) { var pluginName = this.getPluginName(); var hash = Math.random().toString().substr(2, 5); var componentName = [pluginName, name, hash].join('-'); class PanelComponent extends Polymer.Element { static get is() { return componentName; } connectedCallback() { // Called every time the element is inserted into the DOM callback({body: this}); } } customElements.define(PanelComponent.is, PanelComponent); // Register the component with standard API on plugin's behalf. this.registerElement(componentName, PanelComponent.is); }; // @deprecated, provided to simplify migration Plugin.prototype.panel = function(name, callback) { this.registerInlineElement(name, function() { var element = this; callback({ body: element, change: element.change, revision: element.revision, plugin: element.plugin, }); }); };
Also, the code creating a custom component wrapper for a callback can be exposed to plugin developers to provide simple way for creating basic one-shot components, which could be reused or extended later if needed.
(see related change)
PolyGerrit loads shared styles using style modules approach and applies them to the document level This makes possible for plugins to provide custom global CSS variables in form of CSS mixins. gr-change-metadata applies plugin-provided mixins to specific sections of DOM to hide them.
Related Polymer API:
https://www.polymer-project.org/1.0/docs/devguide/styling#custom-style
Sample code for using proposed API, in plugin.js:
Gerrit.install(function(plugin) { var stylesUrl = document.currentScript.src.replace( /\/[^\/]+\.js/, '/plugin-style.html'); plugin.registerStyleModule('change-metadata', stylesUrl); });
Please note that plugin-style has to be unique, so plugin name should be used.
Sample code for using proposed API, in plugin-style.html:
<dom-module id="plugin-style"> <template> <style> :root { --change-metadata-assignee: { display: none; } --change-metadata-label-status: { display: none; } --change-metadata-strategy: { display: none; } --change-metadata-topic: { display: none; } } </style> </template> </dom-module>
Each plugin-provided UI element is a Custom Element. Each UI element is defined in a separate html file. Upon creation, each UI element get following attributes set:
<!-- sample code for the purpose of illustrating structure --> <gr-plugin-endpoint name="change/info"> <my-submit-button plugin-context="[[plugin]]"> <gr-button primary data-label="Submit"></gr-button> </my-submit-button> </gr-endpoint>
Since plugin is expected to consist of number of separate UI elements, it's important to provide a way for any part of plugin to react to PolyGerrit event or action.
gr-plugin-endpoint sends DOM events for meta-level actions (comment created, change description updated, etc) into all plugin-provided UI elements, so any element can react to any/all events if needed.
Project polygerrit-ui-toolkit to be created and to contain as many of independent PolyGerrit UI elements as possible (e.g. gr-button).
UI Toolkit to be minified independently from main PolyGerrit app, and its development to use same tools and technologies as main PolyGerrit app (eg, Polymer, vulcanize, etc).
PolyGerrit app to import minified toolkit via common url and use elements from it.
Plugin author may choose to import the toolkit as well to use or extend elements provided.
UI Toolkit to provide a minimal skeleton plugin that could be copied and filled in for a quick start.
For recommended approach on using UI toolkit, see Cookbook plugin as an example.
UI Toolkit to contain sample plugin skeleton for quick start.
run-server.go to be modified to take params and overwrite available plugins, also to serve plugin files locally.
There are no hard requirements for plugins to have any kinds of test suite. However, it's strongly encouraged to have a test suite for each custom component and main plugin script itself.
Recommended approach is use same methods and practices PolyGerrit uses, ie Polymer + WCT.
For recommended way for writing tests, see Cookbook plugin as an example.
Build an alternative Cookbook plugin to show what's different from previous version and how to do same things in the new one.
Topics to cover:
gerrit-plugin-gwtui is not supported. PolyGerrit aims to provide equal or better feature parity eventually, but existing GWT plugins wouldn't work directly.
Existing Gerrit JS API will be mostly supported. Here's the list of notable changes (not final):
The plugin should just include index.html of minimal form:
<dom-module id="my-plugin"> <script src="my-plugin.js"></script> </dom-module>
If plugin uses document.querySelector(), the author should consider using webcomponents or self.panel(). If the existing support is insufficient for author‘s needs, the author should file a bug against PolyGerrit team to implement what’s missing or provide guidance on best approach.
If plugin uses panels, two options are available:
If plugin relies on URL structure, the author should test and update it if needed.
If plugin uses popup(), plugin author should ensure new UI looks appropriate.