--- 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--.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 io.quarkus quarkus-devui-deployment-spi ``` 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 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` (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``; return html` `; } } 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: /, 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.