Edit this Page

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:

Dev UI overview

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:

Dev UI extension card

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:

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:

Dev UI extension card

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:

Dev UI embedded page

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.

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.

Dev UI card labels

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:

Dev UI raw page

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:

Dev UI table page

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

Dev UI 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.

Dev UI Alert
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.

Dev UI Code Block
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

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

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:

Dev UI Notifier
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.

Dev UI Log control
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.

Dev UI Log

When running a local application using the 999-SNAPSHOT version, the Dev UI will show a Dev UI Log in the footer. This is useful for debugging all JSON RPC messages flowing between the browser and the Quarkus app.

Dev UI Json RPC Log

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.

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);

    // ....
}

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);
    }
}

Related content