Skip to content

Commit add7afb

Browse files
authored
Merge pull request #676 from phillip-kruger/dev-ui-and-mcp
Add Dev UI pages and Dev MCP tools for the HTTP Problem extension
2 parents 8b4557f + db3a53b commit add7afb

10 files changed

Lines changed: 444 additions & 1 deletion

File tree

deployment/pom.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
<dependency>
1616
<groupId>io.quarkiverse.httpproblem</groupId>
1717
<artifactId>quarkus-http-problem</artifactId>
18-
<version>${project.version}</version>
18+
</dependency>
19+
<dependency>
20+
<groupId>io.quarkiverse.httpproblem</groupId>
21+
<artifactId>quarkus-http-problem-dev</artifactId>
1922
</dependency>
2023
<dependency>
2124
<groupId>io.quarkus</groupId>
@@ -45,6 +48,10 @@
4548
<groupId>io.quarkus</groupId>
4649
<artifactId>quarkus-smallrye-openapi-spi</artifactId>
4750
</dependency>
51+
<dependency>
52+
<groupId>io.quarkus</groupId>
53+
<artifactId>quarkus-devui-deployment-spi</artifactId>
54+
</dependency>
4855

4956
<dependency>
5057
<groupId>io.quarkus</groupId>

deployment/src/main/java/io/quarkiverse/httpproblem/deployment/ProblemProcessor.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import io.quarkiverse.httpproblem.DetailSanitizer;
1818
import io.quarkiverse.httpproblem.ProblemRuntimeFixedConfig;
19+
import io.quarkiverse.httpproblem.deployment.devui.ExceptionMapperInfo;
20+
import io.quarkiverse.httpproblem.devui.ProblemJsonRpcService;
1921
import io.quarkiverse.httpproblem.postprocessing.MdcPropertiesInjector;
2022
import io.quarkiverse.httpproblem.postprocessing.PostProcessorsRegistry;
2123
import io.quarkiverse.httpproblem.postprocessing.ProblemDefaultsProvider;
@@ -29,12 +31,16 @@
2931
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
3032
import io.quarkus.deployment.Capabilities;
3133
import io.quarkus.deployment.Capability;
34+
import io.quarkus.deployment.IsDevelopment;
3235
import io.quarkus.deployment.annotations.BuildProducer;
3336
import io.quarkus.deployment.annotations.BuildStep;
3437
import io.quarkus.deployment.annotations.Record;
3538
import io.quarkus.deployment.builditem.FeatureBuildItem;
3639
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
3740
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
41+
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
42+
import io.quarkus.devui.spi.page.CardPageBuildItem;
43+
import io.quarkus.devui.spi.page.Page;
3844
import io.quarkus.jsonb.spi.JsonbDeserializerBuildItem;
3945
import io.quarkus.jsonb.spi.JsonbSerializerBuildItem;
4046
import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem;
@@ -279,4 +285,48 @@ void setupLogging(ProblemRecorder recorder, ProblemBuildConfig config,
279285
UnremovableBeanBuildItem markPostProcessorsUnremovable() {
280286
return UnremovableBeanBuildItem.beanTypes(ProblemPostProcessor.class);
281287
}
288+
289+
@BuildStep(onlyIf = IsDevelopment.class)
290+
CardPageBuildItem createDevUIPages(ProblemBuildConfig config, Capabilities capabilities) {
291+
CardPageBuildItem card = new CardPageBuildItem();
292+
293+
List<ExceptionMapperInfo> mapperInfos = neededExceptionMappers(config, capabilities).stream()
294+
.map(m -> new ExceptionMapperInfo(
295+
shortName(m.exceptionClassName),
296+
shortName(m.mapperClassName),
297+
"Active"))
298+
.collect(Collectors.toList());
299+
300+
card.addBuildTimeData("mappers", mapperInfos,
301+
"List of active HTTP Problem exception mappers with their exception type and status");
302+
303+
card.addPage(Page.tableDataPageBuilder("Exception Mappers")
304+
.icon("font-awesome-solid:map")
305+
.showColumn("exception")
306+
.showColumn("mapper")
307+
.showColumn("status")
308+
.buildTimeDataKey("mappers"));
309+
310+
card.addPage(Page.webComponentPageBuilder()
311+
.title("Post Processors")
312+
.icon("font-awesome-solid:layer-group")
313+
.componentLink("qwc-http-problem-processors.js"));
314+
315+
card.addPage(Page.webComponentPageBuilder()
316+
.title("Test")
317+
.icon("font-awesome-solid:flask")
318+
.componentLink("qwc-http-problem-test.js"));
319+
320+
return card;
321+
}
322+
323+
@BuildStep
324+
JsonRPCProvidersBuildItem registerJsonRpcService() {
325+
return new JsonRPCProvidersBuildItem(ProblemJsonRpcService.class);
326+
}
327+
328+
private static String shortName(String fqcn) {
329+
int lastDot = fqcn.lastIndexOf('.');
330+
return lastDot >= 0 ? fqcn.substring(lastDot + 1) : fqcn;
331+
}
282332
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.quarkiverse.httpproblem.deployment.devui;
2+
3+
public final class ExceptionMapperInfo {
4+
5+
public final String exception;
6+
public final String mapper;
7+
public final String status;
8+
9+
public ExceptionMapperInfo(String exception, String mapper, String status) {
10+
this.exception = exception;
11+
this.mapper = mapper;
12+
this.status = status;
13+
}
14+
15+
public String getException() {
16+
return exception;
17+
}
18+
19+
public String getMapper() {
20+
return mapper;
21+
}
22+
23+
public String getStatus() {
24+
return status;
25+
}
26+
27+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { LitElement, html, css } from 'lit';
2+
import { JsonRpc } from 'jsonrpc';
3+
4+
export class QwcHttpProblemProcessors extends LitElement {
5+
6+
static styles = css`
7+
.processors-table {
8+
width: 100%;
9+
border-collapse: collapse;
10+
}
11+
.processors-table th, .processors-table td {
12+
padding: 8px 12px;
13+
text-align: left;
14+
border-bottom: 1px solid var(--lumo-contrast-10pct);
15+
}
16+
.processors-table th {
17+
color: var(--lumo-secondary-text-color);
18+
font-weight: 600;
19+
}
20+
.order-badge {
21+
display: inline-block;
22+
background: var(--lumo-primary-color-10pct);
23+
color: var(--lumo-primary-text-color);
24+
padding: 2px 8px;
25+
border-radius: 4px;
26+
font-size: 0.85em;
27+
}
28+
`;
29+
30+
static properties = {
31+
_processors: { state: true },
32+
_loading: { state: true }
33+
};
34+
35+
constructor() {
36+
super();
37+
this._processors = [];
38+
this._loading = true;
39+
this.jsonRpc = new JsonRpc(this);
40+
}
41+
42+
connectedCallback() {
43+
super.connectedCallback();
44+
this.jsonRpc.getPostProcessors().then(response => {
45+
this._processors = response.result;
46+
this._loading = false;
47+
});
48+
}
49+
50+
render() {
51+
if (this._loading) {
52+
return html`<p>Loading post-processors...</p>`;
53+
}
54+
55+
if (this._processors.length === 0) {
56+
return html`<p>No post-processors registered.</p>`;
57+
}
58+
59+
return html`
60+
<table class="processors-table">
61+
<thead>
62+
<tr>
63+
<th>Order</th>
64+
<th>Processor</th>
65+
<th>Class</th>
66+
<th>Priority</th>
67+
</tr>
68+
</thead>
69+
<tbody>
70+
${this._processors.map((p, i) => html`
71+
<tr>
72+
<td><span class="order-badge">${i + 1}</span></td>
73+
<td>${p.name}</td>
74+
<td><code>${p.className}</code></td>
75+
<td>${p.priority}</td>
76+
</tr>
77+
`)}
78+
</tbody>
79+
</table>
80+
`;
81+
}
82+
}
83+
84+
customElements.define('qwc-http-problem-processors', QwcHttpProblemProcessors);
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { LitElement, html, css } from 'lit';
2+
import { JsonRpc } from 'jsonrpc';
3+
4+
export class QwcHttpProblemTest extends LitElement {
5+
6+
static styles = css`
7+
.test-form {
8+
display: flex;
9+
gap: 12px;
10+
align-items: flex-end;
11+
margin-bottom: 16px;
12+
}
13+
.field {
14+
display: flex;
15+
flex-direction: column;
16+
gap: 4px;
17+
}
18+
.field label {
19+
font-size: 0.85em;
20+
color: var(--lumo-secondary-text-color);
21+
}
22+
.field input {
23+
padding: 6px 10px;
24+
border: 1px solid var(--lumo-contrast-20pct);
25+
border-radius: 4px;
26+
background: var(--lumo-base-color);
27+
color: var(--lumo-body-text-color);
28+
}
29+
.field input:focus {
30+
outline: 2px solid var(--lumo-primary-color);
31+
border-color: transparent;
32+
}
33+
button {
34+
padding: 6px 16px;
35+
background: var(--lumo-primary-color);
36+
color: var(--lumo-primary-contrast-color);
37+
border: none;
38+
border-radius: 4px;
39+
cursor: pointer;
40+
}
41+
button:hover {
42+
opacity: 0.9;
43+
}
44+
button:disabled {
45+
opacity: 0.5;
46+
cursor: not-allowed;
47+
}
48+
.result {
49+
background: var(--lumo-contrast-5pct);
50+
border-radius: 6px;
51+
padding: 16px;
52+
}
53+
.result pre {
54+
margin: 0;
55+
white-space: pre-wrap;
56+
word-break: break-word;
57+
color: var(--lumo-body-text-color);
58+
font-family: monospace;
59+
}
60+
.result-label {
61+
font-size: 0.85em;
62+
color: var(--lumo-secondary-text-color);
63+
margin-bottom: 8px;
64+
}
65+
`;
66+
67+
static properties = {
68+
_statusCode: { state: true },
69+
_detail: { state: true },
70+
_result: { state: true },
71+
_loading: { state: true }
72+
};
73+
74+
constructor() {
75+
super();
76+
this._statusCode = 500;
77+
this._detail = '';
78+
this._result = null;
79+
this._loading = false;
80+
this.jsonRpc = new JsonRpc(this);
81+
}
82+
83+
_test() {
84+
this._loading = true;
85+
this.jsonRpc.testProblem({
86+
statusCode: parseInt(this._statusCode),
87+
detail: this._detail || null
88+
}).then(response => {
89+
this._result = response.result;
90+
this._loading = false;
91+
});
92+
}
93+
94+
render() {
95+
return html`
96+
<div class="test-form">
97+
<div class="field">
98+
<label>Status Code</label>
99+
<input type="number" min="100" max="599"
100+
.value="${this._statusCode}"
101+
@input="${e => this._statusCode = e.target.value}">
102+
</div>
103+
<div class="field">
104+
<label>Detail (optional)</label>
105+
<input type="text" placeholder="e.g. Resource not found"
106+
.value="${this._detail}"
107+
@input="${e => this._detail = e.target.value}">
108+
</div>
109+
<button @click="${this._test}" ?disabled="${this._loading}">
110+
${this._loading ? 'Testing...' : 'Test'}
111+
</button>
112+
</div>
113+
114+
${this._result ? html`
115+
<div class="result">
116+
<div class="result-label">Response (application/problem+json)</div>
117+
<pre>${JSON.stringify(this._result, null, 2)}</pre>
118+
</div>
119+
` : ''}
120+
`;
121+
}
122+
}
123+
124+
customElements.define('qwc-http-problem-test', QwcHttpProblemTest);

pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
<modules>
4545
<module>runtime</module>
46+
<module>runtime-dev</module>
4647
<module>deployment</module>
4748
<module>integration-test</module>
4849
</modules>
@@ -63,6 +64,17 @@
6364
<version>${zalando-problem.version}</version>
6465
</dependency>
6566

67+
<dependency>
68+
<groupId>io.quarkiverse.httpproblem</groupId>
69+
<artifactId>quarkus-http-problem</artifactId>
70+
<version>${project.version}</version>
71+
</dependency>
72+
<dependency>
73+
<groupId>io.quarkiverse.httpproblem</groupId>
74+
<artifactId>quarkus-http-problem-dev</artifactId>
75+
<version>${project.version}</version>
76+
</dependency>
77+
6678
<dependency>
6779
<groupId>org.assertj</groupId>
6880
<artifactId>assertj-core</artifactId>

0 commit comments

Comments
 (0)