writing-extension-devui
quarkusio/quarkus/.agents/skills/writing-extension-devui/SKILL.md
How to add a Dev UI page to a Quarkus extension: deployment processors, runtime-dev JSON-RPC services, and Lit web components.
Skill16k starsChanged yesterday
What's in it
- Writing a Dev UI for a Quarkus Extension
- Directory Layout
- Deployment Processor
- Runtime JSON-RPC Service
- Frontend Web Components
- Observability Dashboard
- Testing
- Key Rules
---
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.More agent context in quarkusio/quarkus
13 other files this repository gives its agents.
AGENTS.md
CLAUDE.md
Skill
- building-and-testing.agents/skills/building-and-testing/SKILL.md
- building-docs.agents/skills/building-docs/SKILL.md
- classloading-and-runtime-dev.agents/skills/classloading-and-runtime-dev/SKILL.md
- coding-style.agents/skills/coding-style/SKILL.md
- converting-recorders-to-services.agents/skills/converting-recorders-to-services/SKILL.md
- creating-extensions.agents/skills/creating-extensions/SKILL.md
- manage-deprecations.agents/skills/manage-deprecations/SKILL.md
- pull-requests.agents/skills/pull-requests/SKILL.md
- working-with-config.agents/skills/working-with-config/SKILL.md
- writing-build-steps.agents/skills/writing-build-steps/SKILL.md
- writing-tests.agents/skills/writing-tests/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

