Dev UI for extension developers
Dev UI v2
This guide covers the Dev UI v2, which is the default from Quarkus 3 onwards. |
This guide covers the Quarkus Dev UI for extension authors.
Quarkus ships with a Developer UI, which is available in dev mode (when you start
quarkus with mvn quarkus:dev
) at /q/dev-ui by default. It will show you something like
this:
It allows you to:
-
quickly visualize all the extensions currently loaded
-
view extension statuses and go directly to extension documentation
-
view and change
Configuration
-
manage and visualize
Continuous Testing
-
view
Dev Services
information -
view the Build information
-
view and stream various logs
Each extension used in the application will be listed. There, you can find the guides, additional information, and the applicable configuration for each extension:
Make my extension extend the Dev UI
To make your extension listed in the Dev UI, you don’t need to do anything!
So you can always start with that :)
Extensions can:
Add links to an extension card
External links
These are links that reference other (external from Dev UI) data. This data can be HTML pages, text, or other data.
A good example of this is the SmallRye OpenAPI extension that contains links to the generated OpenAPI schema in both JSON and YAML format, and a link to Swagger UI:
The links to these external references are known at build time. So to get links like this on your card, you add the following Build Step in your extension:
@BuildStep(onlyIf = IsDevelopment.class)(1)
public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); (2)
cardPageBuildItem.addPage(Page.externalPageBuilder("Schema yaml") (3)
.url(nonApplicationRootPathBuildItem.resolvePath("openapi")) (4)
.isYamlContent() (5)
.icon("font-awesome-solid:file-lines")); (6)
cardPageBuildItem.addPage(Page.externalPageBuilder("Schema json")
.url(nonApplicationRootPathBuildItem.resolvePath("openapi") + "?format=json")
.isJsonContent()
.icon("font-awesome-solid:file-code"));
cardPageBuildItem.addPage(Page.externalPageBuilder("Swagger UI")
.url(nonApplicationRootPathBuildItem.resolvePath("swagger-ui"))
.isHtmlContent()
.icon("font-awesome-solid:signs-post"));
return cardPageBuildItem;
}
1 | Always make sure that this build step is only run when in dev mode |
2 | To add anything on the card, you must return/produce a CardPageBuildItem . |
3 | To add a link, you can use the addPage method, as all links go to a "page". Page has some builders to assist with building a page. For external links, use the externalPageBuilder . |
4 | Adding the url of the external link (in this case, we use NonApplicationRootPathBuildItem to create this link, as this link is under the configurable non-application path, default /q ). Always use NonApplicationRootPathBuildItem if your link is available under /q . |
5 | You can (optionally) hint the content type of the content you are navigating to. If there is no hint, a header call will be made to determine the MediaType ; |
6 | You can add an icon. All free font-awesome icons are available. |
Note about icons
If you find your icon at Font awesome, you can map as follow: Example <i class="fa-solid fa-house"></i> will map to font-awesome-solid:house , so fa becomes font-awesome and for the icon name, remove the fa- .
|
Embedding external content
By default, even external links will render inside (embedded) in Dev UI. In the case of HTML, the page will be rendered, and any other content will be shown using code-mirror to markup the media type. For example, the OpenAPI schema document in YAML format:
If you do not want to embed the content, you can use the .doNotEmbed()
on the Page Builder, this will then open the link in a new tab.
Runtime external links
The example above assumes you know the link to use at build time. There might be cases where you only know this at runtime. In that case, you can use a Communicating to the backend Method that returns the link to add, and use that when creating the link. Rather than using the .url
method on the page builder, use the .dynamicUrlJsonRPCMethodName("yourJsonRPCMethodName")
.
Adding labels
You can add an option label to the link in the card using one of the builder methods on the page builder. These labels can be
-
static (known at build time)
.staticLabel("staticLabelValue")
-
dynamic (loaded at runtime)
.dynamicLabelJsonRPCMethodName("yourJsonRPCMethodName")
-
streaming (continuously streaming updated values at runtime)
.streamingLabelJsonRPCMethodName("yourJsonRPCMethodName")
For dynamic and streaming labels, see the Communicating to the backend Section.
Add full pages
You can also link to an "internal" page (as opposed to the above "external" page). This means that you can build the page and add data and actions for rendering in Dev UI.
Build time data
To make build time data available on your full page, you can add any data to your CardPageBuildItem
with a key and a value:
CardPageBuildItem pageBuildItem = new CardPageBuildItem();
pageBuildItem.addBuildTimeData("someKey", getSomeValueObject());
You can add multiple of these key-value pairs for all the data you know at build time that you need on the page.
There are a few options to add full page content in Dev UI. Starting from the most basic (good start) to a full blown web-component (preferred).
Display some build time data on a screen (without having to do frontend coding):
If you have some data that is known at build time that you want to display, you can use one of the following builders in Page
:
Raw data
This will display your data in its raw (serialised) JSON value:
cardPageBuildItem.addPage(Page.rawDataPageBuilder("Raw data") (1)
.icon("font-awesome-brands:js")
.buildTimeDataKey("someKey")); (2)
1 | Use the rawDataPageBuilder . |
2 | Link back to the key used when you added the build time data in addBuildTimeData on the Page BuildItem. |
That will create a link to a page that renders the raw data in JSON:
Table data
You can also display your Build time data in a table if the structure allows it:
cardPageBuildItem.addPage(Page.tableDataPageBuilder("Table data") (1)
.icon("font-awesome-solid:table")
.showColumn("timestamp") (2)
.showColumn("user") (2)
.showColumn("fullJoke") (2)
.buildTimeDataKey("someKey")); (3)
1 | Use the tableDataPageBuilder . |
2 | Optionally only show certain fields. |
3 | Link back to the key used when you added the build time data in addBuildTimeData on the Page BuildItem. |
That will create a link to a page that renders the data in a table:
Qute data
You can also display your build time data using a qute template. All build time data keys are available to use in the template:
cardPageBuildItem.addPage(Page.quteDataPageBuilder("Qute data") (1)
.icon("font-awesome-solid:q")
.templateLink("qute-jokes-template.html")); (2)
1 | Use the quteDataPageBuilder . |
2 | Link to the Qute template in /deployment/src/main/resources/dev-ui/ . |
Use any Qute template to display the data, for example, qute-jokes-template.html
:
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>User</th>
<th>Joke</th>
</tr>
</thead>
<tbody>
{#for joke in jokes} (1)
<tr>
<td>{joke.timestamp}</td>
<td><span><img src="{joke.profilePic}" height="30px"></img> {joke.user}</span></td>
<td>{joke.fullJoke}</td>
</tr>
{/for}
</tbody>
</table>
1 | jokes added as a build time data key on the Page Build Item. |
Web component page
To build an interactive page with actions and runtime (or build time) data, you must use the web component page:
cardPageBuildItem.addPage(Page.webComponentPageBuilder() (1)
.icon("font-awesome-solid:egg")
.componentLink("qwc-arc-beans.js") (2)
.staticLabel(String.valueOf(beans.size())));
1 | Use the webComponentPageBuilder . |
2 | Link to the Web Component in /deployment/src/main/resources/dev-ui/ . The title can also be defined (using .title("My title") in the builder), but if not, the title will be assumed from the componentLink, which should always have the format qwc (stands for Quarkus Web Component) dash extensionName (example, arc in this case ) dash page title ("Beans" in this case) |
Dev UI uses Lit to make building these web components easier. You can read more about Web Components and Lit:
Basic structure of a Web component page
A Web component page is just a JavaScript Class that creates a new HTML Element:
import { LitElement, html, css} from 'lit'; (1)
import { beans } from 'build-time-data'; (2)
/**
* This component shows the Arc Beans
*/
export class QwcArcBeans extends LitElement { (3)
static styles = css` (4)
.annotation {
color: var(--lumo-contrast-50pct); (5)
}
.producer {
color: var(--lumo-primary-text-color);
}
`;
static properties = {
_beans: {state: true}, (6)
};
constructor() { (7)
super();
this._beans = beans;
}
render() { (8)
if (this._beans) {
return html`<ul>
${this._beans.map((bean) => (9)
html`<li>${bean.providerType.name}</li>`
)}</ul>`;
} else {
return html`No beans found`;
}
}
}
customElements.define('qwc-arc-beans', QwcArcBeans); (10)
1 | You can import Classes and/or functions from other libraries.
In this case, we use the LitElement class and html & css functions from Lit |
2 | Build time data as defined in the Build step and can be imported using the key and always from build-time-data . All keys added in your Build step will be available. |
3 | The component should be named in the following format: Qwc (stands for Quarkus Web Component), then Extension Name, then Page Title, all concatenated with Camel Case. This will also match the file name format as described earlier. The component should also extend LitComponent . |
4 | CSS styles can be added using the css function, and these styles only apply to your component. |
5 | Styles can reference globally defined CSS variables to make sure your page renders correctly, especially when switching between light and dark mode. You can find all CSS variables in the Vaadin documentation (Color, Sizing and Spacing, etc.) |
6 | Properties can be added. Use _ in front of a property if that property is private. Properties are usually injected in the HTML template, and can be defined as having state, meaning that if that property changes, the component should re-render. In this case, the beans are Build time data and only change on hot-reload, which will be covered later. |
7 | Constructors (optional) should always call super first and then set the default values for the properties. |
8 | The render method (from LitElement ) will be called to render the page. In this method, you return the markup of the page you want. You can use the html function from Lit , which gives you a template language to output the HTML you want. Once the template is created, you must only set or change the properties to re-render the page content. Read more about Lit html |
9 | You can use the built-in template functions to do conditional, list, etc. Read more about Lit Templates |
10 | You must always register your Web component as a custom element, with a unique tag. Here, the tag will follow the same format as the filename (qwc dash extension name dash page title ); |
Using Vaadin UI components for rendering
Dev UI makes extensive usage of Vaadin web components as UI Building blocks.
As an example, the Arc Beans are rendered using a Vaadin Grid:
import { LitElement, html, css} from 'lit';
import { beans } from 'build-time-data';
import '@vaadin/grid'; (1)
import { columnBodyRenderer } from '@vaadin/grid/lit.js'; (2)
import '@vaadin/vertical-layout';
import '@qomponent/qui-badge'; (3)
/**
* This component shows the Arc Beans
*/
export class QwcArcBeans extends LitElement {
static styles = css`
.arctable {
height: 100%;
padding-bottom: 10px;
}
code {
font-size: 85%;
}
.annotation {
color: var(--lumo-contrast-50pct);
}
.producer {
color: var(--lumo-primary-text-color);
}
`;
static properties = {
_beans: {state: true},
};
constructor() {
super();
this._beans = beans;
}
render() {
if (this._beans) {
return html`
<vaadin-grid .items="${this._beans}" class="arctable" theme="no-border">
<vaadin-grid-column auto-width
header="Bean"
${columnBodyRenderer(this._beanRenderer, [])}
resizable>
</vaadin-grid-column>
<vaadin-grid-column auto-width
header="Kind"
${columnBodyRenderer(this._kindRenderer, [])}
resizable>
</vaadin-grid-column>
<vaadin-grid-column auto-width
header="Associated Interceptors"
${columnBodyRenderer(this._interceptorsRenderer, [])}
resizable>
</vaadin-grid-column>
</vaadin-grid>`;
} else {
return html`No beans found`;
}
}
_beanRenderer(bean) {
return html`<vaadin-vertical-layout>
<code class="annotation">@${bean.scope.simpleName}</code>
${bean.nonDefaultQualifiers.map(qualifier =>
html`${this._qualifierRenderer(qualifier)}`
)}
<code>${bean.providerType.name}</code>
</vaadin-vertical-layout>`;
}
_kindRenderer(bean) {
return html`
<vaadin-vertical-layout>
${this._kindBadgeRenderer(bean)}
${this._kindClassRenderer(bean)}
</vaadin-vertical-layout>
`;
}
_kindBadgeRenderer(bean){
let kind = this._camelize(bean.kind);
let level = null;
if(bean.kind.toLowerCase() === "field"){
kind = "Producer field";
level = "success";
}else if(bean.kind.toLowerCase() === "method"){
kind = "Producer method";
level = "success";
}else if(bean.kind.toLowerCase() === "synthetic"){
level = "contrast";
}
return html`
${level
? html`<qui-badge level='${level}' small><span>${kind}</span></qui-badge>`
: html`<qui-badge small><span>${kind}</span></qui-badge>`
}`;
}
_kindClassRenderer(bean){
return html`
${bean.declaringClass
? html`<code class="producer">${bean.declaringClass.simpleName}.${bean.memberName}()</code>`
: html`<code class="producer">${bean.memberName}</code>`
}
`;
}
_interceptorsRenderer(bean) {
if (bean.interceptors && bean.interceptors.length > 0) {
return html`<vaadin-vertical-layout>
${bean.interceptorInfos.map(interceptor =>
html`<div>
<code>${interceptor.interceptorClass.name}</code>
<qui-badge class="${bean.kind.toLowerCase()}" small pill><span>${interceptor.priority}</span></qui-badge>
</div>`
)}
</vaadin-vertical-layout>`;
}
}
_qualifierRenderer(qualifier) {
return html`<code class="annotation">${qualifier.simpleName}</code>`;
}
_camelize(str) {
return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) {
if (+match === 0)
return "";
return index === 0 ? match.toUpperCase() : match.toLowerCase();
});
}
}
customElements.define('qwc-arc-beans', QwcArcBeans);
1 | Import the Vaadin component you want to use |
2 | You can also import other functions if needed |
3 | You can also use any component in the Qomponent library, described below |
Qomponent
We also include all components from the Qomponent library
Card
Card component to display contents in a card
import '@qomponent/qui-card';
<qui-card header="Small">
<div slot="content">
<div class="cardcontents">
<span>Hello</span>
</div>
</div>
</qui-card>
Badge
Badge UI Component based on the vaadin themed badge
import '@qomponent/qui-badge';
You can use any combination of small, primary, pill, with icon and clickable with any level of default
, success
, warning
, error
, contrast
, or set your own colors.
<div class="cards">
<h3>Tiny</h3>
<div class="card">
<qui-badge tiny><span>Default</span></qui-badge>
<qui-badge level="success" tiny><span>Success</span></qui-badge>
<qui-badge level="warning" tiny><span>Warning</span></qui-badge>
<qui-badge level="error" tiny><span>Error</span></qui-badge>
<qui-badge level="contrast" tiny><span>Contrast</span></qui-badge>
<qui-badge background="pink" color="purple" tiny><span>Custom colors</span></qui-badge>
</div>
<h3>Small</h3>
<div class="card">
<qui-badge small><span>Default</span></qui-badge>
<qui-badge level="success" small><span>Success</span></qui-badge>
<qui-badge level="warning" small><span>Warning</span></qui-badge>
<qui-badge level="error" small><span>Error</span></qui-badge>
<qui-badge level="contrast" small><span>Contrast</span></qui-badge>
<qui-badge background="pink" color="purple" small><span>Custom colors</span></qui-badge>
</div>
<h3>Primary</h3>
<div class="card">
<qui-badge primary><span>Default primary</span></qui-badge>
<qui-badge level="success" primary><span>Success primary</span></qui-badge>
<qui-badge level="warning" primary><span>Warning primary</span></qui-badge>
<qui-badge level="error" primary><span>Error primary</span></qui-badge>
<qui-badge level="contrast" primary><span>Contrast primary</span></qui-badge>
<qui-badge background="pink" color="purple" primary><span>Custom colors</span></qui-badge>
</div>
<h3>Pill</h3>
<div class="card">
<qui-badge pill><span>Default pill</span></qui-badge>
<qui-badge level="success" pill><span>Success pill</span></qui-badge>
<qui-badge level="warning" pill><span>Warning pill</span></qui-badge>
<qui-badge level="error" pill><span>Error pill</span></qui-badge>
<qui-badge level="contrast" pill><span>Contrast pill</span></qui-badge>
<qui-badge background="pink" color="purple" pill><span>Custom colors</span></qui-badge>
</div>
<h3>With Icon</h3>
<div class="card">
<qui-badge text="Default" icon="circle-info">
<span>Default icon</span>
</qui-badge>
<qui-badge text="Success" level="success" icon="circle-check">
<span>Success icon</span>
</qui-badge>
<qui-badge text="Warning" level="warning" icon="warning">
<span>Warning icon</span>
</qui-badge>
<qui-badge text="Error" level="error" icon="circle-exclamation">
<span>Error icon</span>
</qui-badge>
<qui-badge text="Contrast" level="contrast" icon="adjust">
<span>Contrast icon</span>
</qui-badge>
<qui-badge text="Custom" background="pink" color="purple" icon="flag-checkered">
<span>Custom colors</span>
</qui-badge>
</div>
<h3>Icon only</h3>
<div class="card">
<qui-badge icon="circle-info"></qui-badge>
<qui-badge level="success" icon="circle-check"></qui-badge>
<qui-badge level="warning" icon="warning"></qui-badge>
<qui-badge level="error" icon="circle-exclamation"></qui-badge>
<qui-badge level="contrast" icon="adjust"></qui-badge>
<qui-badge level="contrast" background="pink" color="purple" icon="flag-checkered"></qui-badge>
</div>
<h3>Clickable</h3>
<div class="card">
<qui-badge clickable><span>Default</span></qui-badge>
<qui-badge clickable level="success"><span>Success</span></qui-badge>
<qui-badge clickable level="warning"><span>Warning</span></qui-badge>
<qui-badge clickable level="error"><span>Error</span></qui-badge>
<qui-badge clickable level="contrast"><span>Contrast</span></qui-badge>
<qui-badge clickable background="pink" color="purple"><span>Custom colors</span></qui-badge>
</div>
</div>
Alert
Alerts are modeled around the Bootstrap alerts. Click here for more info.
Also, see the Notification controller below as an alternative.
import '@qomponent/qui-alert';
<qui-alert><span>Info alert</span></qui-alert>
<qui-alert level="success"><span>Success alert</span></qui-alert>
<qui-alert level="warning"><span>Warning alert</span></qui-alert>
<qui-alert level="error"><span>Error alert</span></qui-alert>
<qui-alert permanent><span>Permanent Info alert</span></qui-alert>
<qui-alert level="success" permanent><span>Permanent Success alert</span></qui-alert>
<qui-alert level="warning" permanent><span>Permanent Warning alert</span></qui-alert>
<qui-alert level="error" permanent><span>Permanent Error alert</span></qui-alert>
<qui-alert center><span>Center Info alert</span></qui-alert>
<qui-alert level="success" center><span>Center Success alert</span></qui-alert>
<qui-alert level="warning" center><span>Center Warning alert</span></qui-alert>
<qui-alert level="error" center><span>Center Error alert</span></qui-alert>
<qui-alert showIcon><span>Info alert with icon</span></qui-alert>
<qui-alert level="success" showIcon><span>Success alert with icon</span></qui-alert>
<qui-alert level="warning" showIcon><span>Warning alert with icon</span></qui-alert>
<qui-alert level="error" showIcon><span>Error alert with icon</span></qui-alert>
<qui-alert icon="vaadin:flag-checkered"><span>Info alert with custom icon</span></qui-alert>
<qui-alert level="success" icon="vaadin:flag-checkered"><span>Success alert with custom icon</span></qui-alert>
<qui-alert level="warning" icon="vaadin:flag-checkered"><span>Warning alert with custom icon</span></qui-alert>
<qui-alert level="error" icon="vaadin:flag-checkered"><span>Error alert with custom icon</span></qui-alert>
<qui-alert size="small" showIcon><span>Small Info alert with icon</span></qui-alert>
<qui-alert level="success" size="small" showIcon><span>Small Success alert with icon</span></qui-alert>
<qui-alert level="warning" size="small" showIcon><span>Small Warning alert with icon</span></qui-alert>
<qui-alert level="error" size="small" showIcon><span>Small Error alert with icon</span></qui-alert>
<qui-alert showIcon><span>Info <code>alert</code> with markup <br><a href="https://quarkus.io/" target="_blank">quarkus.io</a></span></qui-alert>
<qui-alert level="success" showIcon><span>Success <code>alert</code> with markup <br><a href="https://quarkus.io/" target="_blank">quarkus.io</a></span></qui-alert>
<qui-alert level="warning" showIcon><span>Warning <code>alert</code> with markup <br><a href="https://quarkus.io/" target="_blank">quarkus.io</a></span></qui-alert>
<qui-alert level="error" showIcon><span>Error <code>alert</code> with markup <br><a href="https://quarkus.io/" target="_blank">quarkus.io</a></span></qui-alert>
<qui-alert showIcon primary><span>Primary Info alert with icon</span></qui-alert>
<qui-alert level="success" showIcon primary><span>Primary Success alert with icon</span></qui-alert>
<qui-alert level="warning" showIcon primary><span>Primary Warning alert with icon</span></qui-alert>
<qui-alert level="error" showIcon primary><span>Primary Error alert with icon</span></qui-alert>
<qui-alert title="Information"><span>Info alert with title</span></qui-alert>
<qui-alert title="Well done" level="success"><span>Success alert with title</span></qui-alert>
<qui-alert title="Beware" level="warning"><span>Warning alert with title</span></qui-alert>
<qui-alert title="Ka-boom" level="error"><span>Error alert with title</span></qui-alert>
<qui-alert title="Information" showIcon><span>Info alert with title and icon</span></qui-alert>
<qui-alert title="Well done" level="success" showIcon><span>Success alert with title and icon</span></qui-alert>
<qui-alert title="Beware" level="warning" showIcon><span>Warning alert with title and icon</span></qui-alert>
<qui-alert title="Ka-boom" level="error" showIcon><span>Error alert with title and icon</span></qui-alert>
Code block
Display a code block. This component is aware of the theme and will use the correct codemirror theme to match the light/dark mode.
import '@qomponent/qui-code-block';
<qui-code-block mode="properties">
<slot>
foo = bar
</slot>
</qui-code-block>
Or fetching the contents from a URL:
<div class="codeBlock">
<qui-code-block
mode='${this._mode}'
src='${this._externalUrl}'>
</qui-code-block>
</div>
To make sure that the code block adopt the correct code-mirror theme (based on the current one in Dev UI), you can do the following:
import { observeState } from 'lit-element-state';
import { themeState } from 'theme-state';
Then change the extends
to observe state:
extends observeState(LitElement) {
Now you can get the current theme, so add the theme
property to your code block, example:
<div class="codeBlock">
<qui-code-block
mode='${this._mode}'
src='${this._externalUrl}'
theme='${themeState.theme.name}'>
</qui-code-block>
</div>
Modes: - properties - js - java - xml - json - yaml - sql - html - css - sass - markdown
Internal components
IDE link
Creates a link to a resource (like a Java source file) that can be opened in the user’s IDE (if we could detect the IDE).
import 'qui-ide-link';
<qui-ide-link title='Source full class name'
class='text-source'
fileName='${sourceClassNameFull}'
lineNumber=${sourceLineNumber}>[${sourceClassNameFull}]</qui-ide-link>;
Using internal controllers
Some internal controllers are available to make certain things easier:
-
Notifier
-
Storage
-
Log
-
Router
Notifier
This is an easy way to show a toast message. The toast can be placed on the screen (default left bottom) and can have a level (Info, Success, Warning, Error). Any of the levels can also be primary, which will create a more prominent toast message.
See the source of this controller here.
Example usage:
import { notifier } from 'notifier';
<a @click=${() => this._info()}>Info</a>;
_info(position = null){
notifier.showInfoMessage("This is an information message", position);
}
You can find all the valid positions here.
Storage
An easy way to access the local storage in a safe way. This will store values in the local storage, which is scoped for your extension. This way, you do not have to worry that you might clash with another extension.
Local storage is useful to remember user preferences or state. For example, the footer remembers the state (open/close) and the size when open of the bottom drawer.
import { StorageController } from 'storage-controller';
// ...
storageControl = new StorageController(this); // Passing in this will scope the storage to your extension
// ...
const storedHeight = this.storageControl.get("height"); // Get some value
// ...
this.storageControl.set('height', 123); // Set some val
Log
The log controller is used to add control buttons to a (footer) log. See Add a footer tab.
import { LogController } from 'log-controller';
// ...
logControl = new LogController(this); // Passing in this will scope the control to your extension
// ...
this.logControl
.addToggle("On/off switch", true, (e) => {
this._toggleOnOffClicked(e);
}).addItem("Log levels", "font-awesome-solid:layer-group", "var(--lumo-tertiary-text-color)", (e) => {
this._logLevels();
}).addItem("Columns", "font-awesome-solid:table-columns", "var(--lumo-tertiary-text-color)", (e) => {
this._columns();
}).addItem("Zoom out", "font-awesome-solid:magnifying-glass-minus", "var(--lumo-tertiary-text-color)", (e) => {
this._zoomOut();
}).addItem("Zoom in", "font-awesome-solid:magnifying-glass-plus", "var(--lumo-tertiary-text-color)", (e) => {
this._zoomIn();
}).addItem("Clear", "font-awesome-solid:trash-can", "var(--lumo-error-color)", (e) => {
this._clearLog();
}).addFollow("Follow log", true , (e) => {
this._toggleFollowLog(e);
}).done();
Router
The router is mostly used internally. This uses Vaadin Router under the covers to route URLs to the correct page/section within the SPA. It will update the navigation and allow history (back button). This also creates the sub-menu available on extensions that have multiple pages.
See the controller for some methods that might be useful.
Communicating to the backend
JsonRPC against the Runtime classpath
You can fetch or stream runtime data (rather than Build time data discussed earlier) or execute methods against the runtime classpath (as opposed to deployment classpath). There are two parts to getting data during runtime. The Java side in the runtime module, and then the usage in the web component.
Java part
This code is responsible for making data available to display on the UI.
You must register the JsonPRCService
in your processor in the deployment module:
@BuildStep(onlyIf = IsDevelopment.class)(1)
JsonRPCProvidersBuildItem createJsonRPCServiceForCache() {(2)
return new JsonRPCProvidersBuildItem(CacheJsonRPCService.class);(3)
}
1 | Always only do this in Dev Mode |
2 | Produce or return a JsonRPCProvidersBuildItem |
3 | Define the class in your runtime module that will contain methods that make data available in the UI |
Now, in your Runtime module, create the JsonRPC Service. This class will default to an application-scoped bean, except if you explicitly scope the bean. All public methods that return something will be made available to call from the Web component Javascript.
The return object in these methods can be:
-
primitives or
String
, -
io.vertx.core.json.JsonArray
-
io.vertx.core.json.JsonObject
-
any other POJO that can be serializable to JSON
All of the above can be blocking (POJO) or nonblocking (@NonBlocking
or Uni
). Alternatively, data can be streamed using Multi
.
@NonBlocking (1)
public JsonArray getAll() { (2)
Collection<String> names = manager.getCacheNames();
List<CaffeineCache> allCaches = new ArrayList<>(names.size());
for (String name : names) {
Optional<Cache> cache = manager.getCache(name);
if (cache.isPresent() && cache.get() instanceof CaffeineCache) {
allCaches.add((CaffeineCache) cache.get());
}
}
allCaches.sort(Comparator.comparing(CaffeineCache::getName));
var array = new JsonArray();
for (CaffeineCache cc : allCaches) {
array.add(getJsonRepresentationForCache(cc));
}
return array;
}
1 | This example runs nonblocking. We could also return Uni<JsonArray> |
2 | The method name getAll will be available in the Javascript |
Webcomponent (Javascript) part
Now you can use the JsonRPC controller to access the getAll
method (and any other methods in your JsonRPC Service)
import { JsonRpc } from 'jsonrpc';
// ...
jsonRpc = new JsonRpc(this); // Passing in this will scope the RPC calls to your extension
// ...
/**
* Called when displayed
*/
connectedCallback() {
super.connectedCallback();
this.jsonRpc.getAll().then(jsonRpcResponse => { (1)
this._caches = new Map();
jsonRpcResponse.result.forEach(c => { (2)
this._caches.set(c.name, c);
});
});
}
1 | Note the method getAll corresponds to the method in your Java Service. This method returns a Promise with the JsonRPC result. |
2 | In this case, the result is an array, so we can loop over it. |
JsonArray (or any Java collection), either blocking or nonblocking, will return an array; otherwise, a JsonObject will be returned.
You can also pass in parameters in the method being called, for example: (In the Runtime Java code)
public Uni<JsonObject> clear(String name) { (1)
Optional<Cache> cache = manager.getCache(name);
if (cache.isPresent()) {
return cache.get().invalidateAll().map((t) -> getJsonRepresentationForCache(cache.get()));
} else {
return Uni.createFrom().item(new JsonObject().put("name", name).put("size", -1));
}
}
1 | The clear method takes one parameter called name |
In the Webcomponent (Javascript):
_clear(name) {
this.jsonRpc.clear({name: name}).then(jsonRpcResponse => { (1)
this._updateCache(jsonRpcResponse.result)
});
}
1 | The name parameter is passed in. |
Streaming data
You can keep a UI screen updated with the latest data by continuously streaming data to the screen. This can be done with Multi
(Java side) and Observer
(Javascript side)
Java side of streaming data:
public class JokesJsonRPCService {
private final BroadcastProcessor<Joke> jokeStream = BroadcastProcessor.create();
@PostConstruct
void init() {
Multi.createFrom().ticks().every(Duration.ofHours(4)).subscribe().with((item) -> {
jokeStream.onNext(getJoke());
});
}
public Multi<Joke> streamJokes() { (1)
return jokeStream;
}
// ...
}
1 | Return the Multi that will stream jokes |
Javascript side of streaming data:
this._observer = this.jsonRpc.streamJokes().onNext(jsonRpcResponse => { (1)
this._addToJokes(jsonRpcResponse.result);
this._numberOfJokes = this._numberOfJokes++;
});
// ...
this._observer.cancel(); (2)
1 | You can call the method (optionally passing in parameters) and then provide the code that will be called on the next event. |
2 | Make sure to keep an instance of the observer to cancel later if needed. |
JsonRpc against the deployment classpath
In certain cases you might need to execute methods and/or get data against the deployment classpath. This also happens over JsonRPC communication, but in this case you do not create a JsonRPC Service in the runtime module,
you can just supply the code to be run in a supplier in the deployment module. To do this you will produce a BuildTimeActionBuildItem
, example:
@BuildStep(onlyIf = IsDevelopment.class)
BuildTimeActionBuildItem createBuildTimeActions() { (1)
BuildTimeActionBuildItem generateManifestActions = new BuildTimeActionBuildItem();(2)
generateManifestActions.addAction("generateManifests", ignored -> { (3)
try {
List<Manifest> manifests = holder.getManifests();
// Avoid relying on databind.
Map<String, String> map = new LinkedHashMap<>();
for (Manifest manifest : manifests) {
map.put(manifest.getName(), manifest.getContent());
}
return map;
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return generateManifestActions;
}
1 | Return or use a BuildProducer to create a BuildTimeActionBuildItem |
2 | BuildTimeActionBuildItem is automatically scoped with your extension namespace |
3 | Here we add an action, that is the same as a request-response method. The method name (that can be called from js in the same way as any json-rpc service) is generateManifests . |
You can also return a CompletableFuture
/CompletionStage
as an action, and if you want to stream data you need to use addSubscription
(rather than addAction
) and return a Flow.Publisher
.
Hot reload
You can update a screen automatically when a hot reload happens. To do this, replace the LitElement
that your Webcomponent extends with QwcHotReloadElement
.
QwcHotReloadElement
extends LitElement
, so your component is still a Lit Element.
When extending a QwcHotReloadElement
, you have to use the hotReload
method. (You must also still provide the render
method from Lit)
import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';
// ...
export class QwcMyExtensionPage extends QwcHotReloadElement {
render(){
// ...
}
hotReload(){
// ..
}
}
Custom cards
You can customize the card that is being displayed on the extension page if you do not want to use the default built-in card.
To do this, you must provide a Webcomponent that will be loaded in the place of the provided card and register this in the Java Processor:
cardPageBuildItem.setCustomCard("qwc-mycustom-card.js");
On the Javascript side, you have access to all the pages (in case you want to create links)
import { pages } from 'build-time-data';
And the following properties will be passed in:
-
extensionName
-
description
-
guide
-
namespace
static properties = {
extensionName: {type: String},
description: {type: String},
guide: {type: String},
namespace: {type: String}
}
State (advance)
State allows properties to contain state and can be reused globally. An example of state properties is the theme, the connection state (if we are connected to the backend), etc.
See the current built-in state objects.
The state in Dev UI uses LitState. You can read more about it in their documentation.
Add a footer tab
Apart from adding a card and a page, extensions can add a tab to the footer. This is useful for things that are happening continuously. A page will lose connection to the backend when navigating away from that page, and a log in the footer will be permanently connected.
Adding something to the footer works exactly like adding a Card, except you use a FooterPageBuildItem
rather than a CardPageBuildItem
.
FooterPageBuildItem footerPageBuildItem = new FooterPageBuildItem();
footerPageBuildItem.addPage(Page.webComponentPageBuilder()
.icon("font-awesome-regular:face-grin-tongue-wink")
.title("Joke Log")
.componentLink("qwc-jokes-log.js"));
footerProducer.produce(footerPageBuildItem);
In your Webcomponent, you can then stream the log to the UI:
export class QwcJokesLog extends LitElement {
jsonRpc = new JsonRpc(this);
logControl = new LogController(this);
// ....
}
Add a log to the footer
There is an easy way to add a log stream to the footer, without having to create the above mentioned footer.
If you just want to stream a log to a tab you can just produce a FooterLogBuildItem
. This way you only provide a name and a Flow.Publisher<String>
for your log.
Here is an example from the Dev Services deployment module:
@BuildStep(onlyIf = { IsDevelopment.class })
public List<DevServiceDescriptionBuildItem> config(
// ...
BuildProducer<FooterLogBuildItem> footerLogProducer){
// ...
// Dev UI Log stream
for (DevServiceDescriptionBuildItem service : serviceDescriptions) {
if (service.getContainerInfo() != null) {
footerLogProducer.produce(new FooterLogBuildItem(service.getName(), () -> {
return createLogPublisher(service.getContainerInfo().getId());
}));
}
}
}
// ...
private Flow.Publisher<String> createLogPublisher(String containerId) {
try (FrameConsumerResultCallback resultCallback = new FrameConsumerResultCallback()) {
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
resultCallback.addConsumer(OutputFrame.OutputType.STDERR,
frame -> publisher.submit(frame.getUtf8String()));
resultCallback.addConsumer(OutputFrame.OutputType.STDOUT,
frame -> publisher.submit(frame.getUtf8String()));
LogContainerCmd logCmd = DockerClientFactory.lazyClient()
.logContainerCmd(containerId)
.withFollowStream(true)
.withTailAll()
.withStdErr(true)
.withStdOut(true);
logCmd.exec(resultCallback);
return publisher;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Add a section menu
This allows an extension to link a page directly in the section Menu.
Adding something to the section menu works exactly like adding a Card, except you use a MenuPageBuildItem
rather than a CardPageBuildItem
.
MenuPageBuildItem menuPageBuildItem = new MenuPageBuildItem();
menuPageBuildItem.addPage(Page.webComponentPageBuilder()
.icon("font-awesome-regular:face-grin-tongue-wink")
.title("One Joke")
.componentLink("qwc-jokes-menu.js"));
menuProducer.produce(menuPageBuildItem);
Your page can be any Page similar to Cards.
Testing
You can add tests to your extension that test:
-
Build time data
-
Runtime data via JsonRPC
You must add this to your pom:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-dev-ui-tests</artifactId>
<scope>test</scope>
</dependency>
This will give you access to two base classes for creating these tests.
Testing Build time data
If you added Build time data, for example:
cardPageBuildItem.addBuildTimeData("somekey", somevalue);
To test that your build time data is generated correctly, you can add a test that extends DevUIBuildTimeDataTest
.
public class SomeTest extends DevUIBuildTimeDataTest {
@RegisterExtension
static final QuarkusDevModeTest config = new QuarkusDevModeTest().withEmptyApplication();
public SomeTest() {
super("io.quarkus.my-extension");
}
@Test
public void testSomekey() throws Exception {
JsonNode somekeyResponse = super.getBuildTimeData("somekey");
Assertions.assertNotNull(somekeyResponse);
// Check more values on somekeyResponse
}
}
Testing Runtime data
If you added a JsonRPC Service with runtime data responses, for example:
public boolean updateProperties(String content, String type) {
// ...
}
To test that updateProperties
executes correctly via JsonRPC, you can add a test that extends DevUIJsonRPCTest
.
public class SomeTest extends DevUIJsonRPCTest {
@RegisterExtension
static final QuarkusDevModeTest config = new QuarkusDevModeTest().withEmptyApplication();
public SomeTest() {
super("io.quarkus.my-extension");
}
@Test
public void testUpdateProperties() throws Exception {
JsonNode updatePropertyResponse = super.executeJsonRPCMethod("updateProperty",
Map.of(
"name", "quarkus.application.name",
"value", "changedByTest"));
Assertions.assertTrue(updatePropertyResponse.asBoolean());
// Get the properties to make sure it is changed
JsonNode allPropertiesResponse = super.executeJsonRPCMethod("getAllValues");
String applicationName = allPropertiesResponse.get("quarkus.application.name").asText();
Assertions.assertEquals("changedByTest", applicationName);
}
}