---
name: writing-extension-devui
description: >
  How to add a Dev UI page to a Quarkus extension: deployment processors,
  runtime-dev JSON-RPC services, and Lit web components.
---

# Writing a Dev UI for a Quarkus Extension

Dev UI is the interactive dashboard at `/q/dev-ui` during `quarkus:dev`.
Extensions add pages via build items, runtime JSON-RPC services, and Lit web
components. See the [Dev UI guide](https://quarkus.io/guides/dev-ui) for full
documentation.

## Directory Layout

```
my-extension/
  deployment/src/main/java/.../deployment/devui/
    MyFeatureDevUIProcessor.java          # Build steps
  deployment/src/main/resources/dev-ui/
    qwc-myfeature-dashboard.js            # Lit web components
  runtime-dev/src/main/java/.../runtime/dev/ui/
    MyFeatureJsonRpcService.java          # JSON-RPC service
```

- **JS naming:** `qwc-<extensionname>-<pagename>.js`
- **JSON-RPC services go in `runtime-dev/`**, not `runtime/`. Register as a
  conditional dev dependency — see the `classloading-and-runtime-dev` skill.

## Deployment Processor

Gate all Dev UI build steps with `@BuildStep(onlyIf = IsDevelopment.class)` or
use `@BuildSteps(onlyIf = IsLocalDevelopment.class)` at the class level.

```java
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.devui.spi.page.Page;

@BuildStep(onlyIf = IsDevelopment.class)
CardPageBuildItem devUI() {
    CardPageBuildItem card = new CardPageBuildItem();

    card.addPage(Page.webComponentPageBuilder()
            .title("Dashboard")
            .componentLink("qwc-myfeature-dashboard.js")
            .icon("font-awesome-solid:robot"));

    // Build-time data — available in JS via: import { items } from 'build-time-data';
    card.addBuildTimeData("items", someList);

    return card;
}
```

Register the JSON-RPC provider in a separate build step. This one must **not**
be gated: it is also used to discover valid usages of execution model affecting
annotations, which happens outside dev mode.

```java
import io.quarkus.devjsonrpc.spi.JsonRPCProvidersBuildItem;

@BuildStep
JsonRPCProvidersBuildItem jsonRpcProvider() {
    return new JsonRPCProvidersBuildItem(MyFeatureJsonRpcService.class);
}
```

**Maven dependency** for the deployment module:

```xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-devui-deployment-spi</artifactId>
</dependency>
```

That brings in `quarkus-devjsonrpc-deployment-spi` transitively, which is where
`JsonRPCProvidersBuildItem` lives.

## Runtime JSON-RPC Service

A plain class in `runtime-dev`. Every public method becomes a JSON-RPC endpoint
automatically — registration happens via `JsonRPCProvidersBuildItem`.

```java
public class MyFeatureJsonRpcService {
    @Inject
    SomeBean bean;

    public List<Item> getItems() { return bean.listAll(); }
    public boolean doAction(String id) { return bean.execute(id); }
}
```

- Use `@Inject` for CDI; `@PostConstruct` for initialization.
- Return JSON-serializable data, **not** HTML.
- For streaming, return `Multi<JsonObject>` (Smallrye Mutiny).

## Frontend Web Components

Components extend `QwcHotReloadElement` (not `LitElement` directly) and use
Vaadin Web Components for consistent styling.

```javascript
import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element';
import { JsonRpc } from 'jsonrpc';
import { items } from 'build-time-data';

export class QwcMyfeatureDashboard extends QwcHotReloadElement {
    jsonRpc = new JsonRpc(this);
    static properties = { _items: { state: true } };

    constructor() {
        super();
        this._items = items;
    }

    connectedCallback() {
        super.connectedCallback();
        this.hotReload();
    }

    hotReload() {
        this.jsonRpc.getItems().then(r => { this._items = r.result; });
    }

    render() {
        if (!this._items)
            return html`<vaadin-progress-bar indeterminate></vaadin-progress-bar>`;
        return html`<vaadin-grid .items="${this._items}" theme="row-stripes">
            <vaadin-grid-column path="name" header="Name"></vaadin-grid-column>
        </vaadin-grid>`;
    }
}
customElements.define('qwc-myfeature-dashboard', QwcMyfeatureDashboard);
```

- `build-time-data` keys must match what was passed to `card.addBuildTimeData(key, value)`.
- `JsonRpc` method names must match the Java service method names exactly.
- Access results via `response.result`.
- For state updates, use spread: `this._items = [...this._items, newItem]`.
- Unsubscribe streaming observers in `disconnectedCallback()`.

## Observability Dashboard

An extension that captures a telemetry signal (traces, logs, events) can offer
its page as a card on the core **Observability** dashboard, on top of its own
extension card. Produce an `ObservabilitySignalBuildItem`
(`io.quarkus.devui.spi.observability`, in `quarkus-devui-deployment-spi`)
next to the page it refers to:

```java
signals.produce(new ObservabilitySignalBuildItem(
        "traces",                                  // unique key, identifies the stored card
        "OpenTelemetry Traces",                    // title (name the backend, not just the signal)
        "font-awesome-solid:diagram-project",      // icon
        "quarkus-opentelemetry/traces",            // page id: <namespace>/<dashed-title>, or null
        "spanCount"));                             // JSON-RPC live count, or null
```

The dashboard imports that page's web component and renders it inline in a card,
so size the component against its host (`height: 100%` or a flex column), not
against the viewport. A null page id advertises the signal without contributing
a card, which is what metrics does - meters are picked individually instead.

Meters need no build item: everything registered with Micrometer or the
OpenTelemetry SDK is offered in the dashboard's picker automatically. Only a new
metrics *backend* (one that samples into `MetricsTimeSeriesStore`) produces a
`MetricsBackendBuildItem`.

Full documentation: `docs/src/main/asciidoc/dev-ui.adoc`, "Observability dashboard".

## Testing

Extend `DevUIJsonRPCTest` (`io.quarkus.devui.tests`). Pass the extension
namespace to the super constructor, then call `executeJsonRPCMethod()`:

```java
public class MyFeatureDevUITest extends DevUIJsonRPCTest {
    @RegisterExtension
    static final QuarkusDevModeTest config = new QuarkusDevModeTest()
            .withApplicationRoot((jar) -> jar.addClass(MyBean.class));

    public MyFeatureDevUITest() { super("quarkus-myfeature"); }

    @Test
    public void testGetItems() throws Exception {
        JsonNode result = super.executeJsonRPCMethod("getItems");
        assertNotNull(result);
    }
}
```

## Key Rules

- **Correct imports:** `CardPageBuildItem` is in `io.quarkus.devui.spi.page`
  (`quarkus-devui-deployment-spi`), `JsonRPCProvidersBuildItem` is in
  `io.quarkus.devjsonrpc.spi` (`quarkus-devjsonrpc-deployment-spi`).
- **JSON-RPC services belong in `runtime-dev/`**, never in `runtime/`.
- **JS files go in `deployment/src/main/resources/dev-ui/`**.
- **Extend `QwcHotReloadElement`**, not `LitElement` — it provides the
  `hotReload()` hook that re-runs on dev-mode restarts.
- **Return JSON from services**, not HTML. Use `Page.externalPageBuilder()` for
  external content like Swagger UI.