Skip to content

Commit 81c7019

Browse files
authored
feat: support new Dev UI (#559)
* feat: initial new Dev UI support using a Vaadin table * feat: add dynamic label * refactor: rename component to prevent name collisions * feat: consistent ordering of event sources & dependents * feat: use list instead of table * feat: provide controller class name to UI * fix: use the reconciler's class name rather * fix: use the condition's class name rather * fix: do not use Optional * feat: restore equivalent functionality to old dev UI * refactor: unify renderer names * feat: support hot reload * feat: allow displaying configured namespaces * feat: re-add missing namespaces, tweak appearance
1 parent 56abc11 commit 81c7019

File tree

9 files changed

+297
-15
lines changed

9 files changed

+297
-15
lines changed

bundle-generator/runtime/src/main/resources/META-INF/quarkus-extension.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Quarkus - Operator SDK - Bundle Generator
1+
name: Operator SDK - Bundle Generator
22
description: Bundle Generation for Java Operator SDK operators
33
metadata:
44
keywords:
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.quarkiverse.operatorsdk.deployment.devui;
2+
3+
import io.quarkiverse.operatorsdk.runtime.devui.JSONRPCService;
4+
import io.quarkus.deployment.IsDevelopment;
5+
import io.quarkus.deployment.annotations.BuildStep;
6+
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
7+
import io.quarkus.devui.spi.page.CardPageBuildItem;
8+
import io.quarkus.devui.spi.page.Page;
9+
10+
public class DevUIProcessor {
11+
12+
@SuppressWarnings("unused")
13+
@BuildStep(onlyIf = IsDevelopment.class)
14+
CardPageBuildItem create() {
15+
final var card = new CardPageBuildItem();
16+
card.addPage(Page.webComponentPageBuilder()
17+
.title("Controllers")
18+
.componentLink("qwc-qosdk-controllers.js")
19+
.dynamicLabelJsonRPCMethodName("controllersCount")
20+
.icon("font-awesome-solid:brain"));
21+
return card;
22+
}
23+
24+
@SuppressWarnings("unused")
25+
@BuildStep(onlyIf = IsDevelopment.class)
26+
JsonRPCProvidersBuildItem createJsonRPCService() {
27+
return new JsonRPCProvidersBuildItem(JSONRPCService.class);
28+
}
29+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import {QwcHotReloadElement, css, html} from 'qwc-hot-reload-element';
2+
import {JsonRpc} from 'jsonrpc';
3+
import '@vaadin/details';
4+
import '@vaadin/list-box';
5+
import '@vaadin/item';
6+
import '@vaadin/horizontal-layout';
7+
import '@vaadin/vertical-layout';
8+
import '@vaadin/icon';
9+
import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'
10+
import '@vaadin/form-layout';
11+
import '@vaadin/text-field';
12+
import 'qui-badge';
13+
14+
export class QWCQOSDKControllers extends QwcHotReloadElement {
15+
16+
jsonRpc = new JsonRpc(this);
17+
18+
constructor() {
19+
super();
20+
}
21+
22+
connectedCallback() {
23+
super.connectedCallback();
24+
this.hotReload();
25+
}
26+
27+
hotReload() {
28+
this.jsonRpc.getControllers().then(
29+
jsonRpcResponse => this._controllers = jsonRpcResponse.result);
30+
}
31+
32+
static properties = {
33+
_controllers: {state: true},
34+
}
35+
36+
render() {
37+
if (this._controllers) {
38+
return html`
39+
<vaadin-vertical-layout>
40+
<qui-badge level="contrast">${this._controllers.length} configured controllers</qui-badge>
41+
<vaadin-vertical-layout style="align-items: stretch; theme="spacing-s padding-s">
42+
${this._controllers.map(this.controller, this)}
43+
</vaadin-vertical-layout>
44+
</vaadin-vertical-layout>
45+
`
46+
}
47+
}
48+
49+
controller(controller) {
50+
return html`
51+
<vaadin-details theme="filled">
52+
<vaadin-details-summary slot="summary">
53+
${nameImplAndResource(controller.name, controller.className, controller.resourceClass)}
54+
</vaadin-details-summary>
55+
<vaadin-vertical-layout theme="spacing-s">
56+
<vaadin-details summary="Namespaces" theme="filled">
57+
<vaadin-vertical-layout>
58+
<span>Configured:</span>
59+
${controller.configuredNamespaces.map(ns => html`${name(ns)}`)}
60+
<span>Effective:</span>
61+
${controller.effectiveNamespaces.map(ns => html`${name(ns)}`)}
62+
</vaadin-vertical-layout>
63+
</vaadin-details>
64+
<vaadin-horizontal-layout theme="spacing-s">
65+
${this.children(controller.dependents, "Dependents",
66+
this.dependent)}
67+
${this.children(controller.eventSources, "Event Sources",
68+
this.eventSource)}
69+
</vaadin-horizontal-layout>
70+
</vaadin-vertical-layout>
71+
</vaadin-details>`
72+
}
73+
74+
eventSource(eventSource) {
75+
return html`
76+
${nameImplAndResource(eventSource.name, eventSource.type, eventSource.resourceClass)}
77+
`
78+
}
79+
80+
dependent(dependent) {
81+
return html`
82+
<vaadin-details theme="filled">
83+
<vaadin-details-summary slot="summary">
84+
${nameImplAndResource(dependent.name, dependent.type,
85+
dependent.resourceClass)}
86+
</vaadin-details-summary>
87+
<vaadin-vertical-layout>
88+
${dependsOn(dependent)}
89+
${conditions(dependent)}
90+
</vaadin-vertical-layout>
91+
</vaadin-details>`
92+
}
93+
94+
children(children, childrenName, childRenderer) {
95+
if (children) {
96+
let count = children.length;
97+
return html`
98+
<vaadin-details theme="filled" summary="${count} ${childrenName}">
99+
<vaadin-vertical-layout>
100+
${children.map((child) => html`${childRenderer(child)}`)}
101+
</vaadin-vertical-layout>
102+
</vaadin-details>
103+
`
104+
}
105+
}
106+
107+
}
108+
109+
function name(name) {
110+
return html`
111+
<qui-badge>${name}</qui-badge>`
112+
}
113+
114+
function nameImplAndResource(n, impl, resource) {
115+
return html`
116+
<vaadin-vertical-layout>
117+
${name(n)}
118+
<vaadin-horizontal-layout theme="spacing-xs">
119+
<vaadin-icon icon="lumo:arrow-right"></vaadin-icon>
120+
${clazz(impl)}
121+
${resourceClass(resource)}
122+
</vaadin-horizontal-layout>
123+
</vaadin-vertical-layout>`
124+
}
125+
126+
const fabric8Prefix = 'io.fabric8.kubernetes.api.model.';
127+
const josdkProcessingPrefix = 'io.javaoperatorsdk.operator.processing.';
128+
const userProvided = 'success';
129+
const k8sProvided = 'warning';
130+
const josdkProvided = 'contrast';
131+
132+
function resourceClass(resourceClass) {
133+
return clazz(resourceClass, true)
134+
}
135+
136+
function clazz(impl, isPill) {
137+
if (impl) {
138+
let level = userProvided;
139+
if (impl.startsWith(fabric8Prefix)) {
140+
level = k8sProvided;
141+
impl = impl.substring(fabric8Prefix.length);
142+
} else if (impl.startsWith(josdkProcessingPrefix)) {
143+
level = josdkProvided;
144+
impl = impl.substring(josdkProcessingPrefix.length);
145+
}
146+
return isPill ?
147+
html`<qui-badge level="${level}" small pill>${impl}</qui-badge>` :
148+
html`<qui-badge level="${level}" small>${impl}</qui-badge>`
149+
}
150+
}
151+
152+
function conditions(dependent) {
153+
let hasConditions = dependent.hasConditions;
154+
if (hasConditions) {
155+
return html`
156+
<vaadin-details summary="Conditions">
157+
<vaadin-vertical-layout>
158+
${field(dependent.readyCondition, "Ready")}
159+
${field(dependent.reconcileCondition, "Reconcile")}
160+
${field(dependent.deleteCondition, "Delete")}
161+
</vaadin-vertical-layout>
162+
</vaadin-details>
163+
`
164+
}
165+
}
166+
167+
function dependsOn(dependent) {
168+
if (dependent.dependsOn && dependent.dependsOn.length > 0) {
169+
return html`
170+
<vaadin-details summary="Depends on">
171+
<vaading-vertical-layout>
172+
${dependent.dependsOn.map(dep => html`${name(dep)}`)}
173+
</vaading-vertical-layout>
174+
</vaadin-details>
175+
`
176+
}
177+
}
178+
179+
function field(value, label) {
180+
if (value) {
181+
return html`
182+
<vaadin-horizontal-layout>
183+
<span>${label}</span>: ${clazz(value)}
184+
</vaadin-horizontal-layout>`
185+
}
186+
}
187+
188+
customElements.define('qwc-qosdk-controllers', QWCQOSDKControllers);

core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/devconsole/ControllerInfo.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkiverse.operatorsdk.runtime.devconsole;
22

3+
import java.util.LinkedHashSet;
34
import java.util.List;
45
import java.util.Set;
56
import java.util.stream.Collectors;
@@ -19,16 +20,23 @@ public ControllerInfo(Controller<P> controller) {
1920
controller.getConfiguration(), controller.getClient());
2021
dependents = controller.getConfiguration().getDependentResources().stream()
2122
.map(spec -> new DependentInfo(spec, context))
22-
.collect(Collectors.toSet());
23+
.sorted()
24+
.collect(Collectors.toCollection(LinkedHashSet::new));
2325
eventSources = controller.getEventSourceManager().getNamedEventSourcesStream()
2426
.map(EventSourceInfo::new)
25-
.collect(Collectors.toSet());
27+
.sorted()
28+
.collect(Collectors.toCollection(LinkedHashSet::new));
2629
}
2730

2831
public String getName() {
2932
return controller.getConfiguration().getName();
3033
}
3134

35+
@SuppressWarnings("unused")
36+
public String getClassName() {
37+
return controller.getConfiguration().getAssociatedReconcilerClassName();
38+
}
39+
3240
@SuppressWarnings("unused")
3341
public Class<P> getResourceClass() {
3442
return controller.getConfiguration().getResourceClass();
@@ -39,6 +47,11 @@ public Set<String> getEffectiveNamespaces() {
3947
return controller.getConfiguration().getEffectiveNamespaces();
4048
}
4149

50+
@SuppressWarnings("unused")
51+
public Set<String> getConfiguredNamespaces() {
52+
return controller.getConfiguration().getNamespaces();
53+
}
54+
4255
@SuppressWarnings("unused")
4356
public Set<EventSourceInfo> getEventSources() {
4457
return eventSources;

core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/devconsole/ControllersSupplier.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,16 @@ public Collection<ControllerInfo> get() {
3232
return Collections.emptyList();
3333
}
3434
}
35+
36+
public int count() {
37+
try (final var operatorHandle = Arc.container().instance(Operator.class)) {
38+
return operatorHandle.get()
39+
.getRegisteredControllersNumber();
40+
} catch (Exception e) {
41+
log.warn("Couldn't retrieve controllers information because "
42+
+ QuarkusConfigurationService.class.getSimpleName()
43+
+ " is not available", e);
44+
return 0;
45+
}
46+
}
3547
}

core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/devconsole/DependentInfo.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import io.quarkus.arc.Arc;
1616

1717
@SuppressWarnings({ "unused", "rawtypes" })
18-
public class DependentInfo<R, P extends HasMetadata> {
18+
public class DependentInfo<R, P extends HasMetadata> implements Comparable<DependentInfo> {
1919
private final DependentResourceSpec<R, P> spec;
2020
private final EventSourceContext<P> context;
2121

@@ -79,23 +79,36 @@ public Set<String> getDependsOn() {
7979
return spec.getDependsOn();
8080
}
8181

82-
public Condition getReadyCondition() {
83-
return spec.getReadyCondition();
82+
public boolean getHasConditions() {
83+
return getReadyCondition() != null || getReconcileCondition() != null || getDeletePostCondition() != null;
8484
}
8585

86-
public Condition getReconcileCondition() {
87-
return spec.getReconcileCondition();
86+
public String getReadyCondition() {
87+
return getConditionClassName(spec.getReadyCondition());
8888
}
8989

90-
public Condition getDeletePostCondition() {
91-
return spec.getDeletePostCondition();
90+
public String getReconcileCondition() {
91+
return getConditionClassName(spec.getReconcileCondition());
9292
}
9393

94-
public Optional<String> getUseEventSourceWithName() {
95-
return spec.getUseEventSourceWithName();
94+
public String getDeletePostCondition() {
95+
return getConditionClassName(spec.getDeletePostCondition());
96+
}
97+
98+
private String getConditionClassName(Condition condition) {
99+
return condition != null ? condition.getClass().getName() : null;
100+
}
101+
102+
public String getUseEventSourceWithName() {
103+
return spec.getUseEventSourceWithName().orElse(null);
96104
}
97105

98106
public String getType() {
99107
return spec.getDependentResourceClass().getName();
100108
}
109+
110+
@Override
111+
public int compareTo(DependentInfo other) {
112+
return getName().compareTo(other.getName());
113+
}
101114
}

core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/devconsole/EventSourceInfo.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import io.javaoperatorsdk.operator.processing.event.EventSourceMetadata;
44

5-
public class EventSourceInfo {
5+
public class EventSourceInfo implements Comparable<EventSourceInfo> {
66
private final EventSourceMetadata metadata;
77

88
public EventSourceInfo(EventSourceMetadata metadata) {
@@ -21,4 +21,9 @@ public String getResourceClass() {
2121
public String getType() {
2222
return metadata.type().getName();
2323
}
24+
25+
@Override
26+
public int compareTo(EventSourceInfo other) {
27+
return getName().compareTo(other.getName());
28+
}
2429
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkiverse.operatorsdk.runtime.devui;
2+
3+
import java.util.Collection;
4+
5+
import jakarta.enterprise.context.ApplicationScoped;
6+
7+
import io.quarkiverse.operatorsdk.runtime.devconsole.ControllerInfo;
8+
import io.quarkiverse.operatorsdk.runtime.devconsole.ControllersSupplier;
9+
10+
@ApplicationScoped
11+
public class JSONRPCService {
12+
private final ControllersSupplier supplier = new ControllersSupplier();
13+
14+
@SuppressWarnings("rawtypes")
15+
public Collection<ControllerInfo> getControllers() {
16+
return supplier.get();
17+
}
18+
19+
@SuppressWarnings("unused")
20+
public int controllersCount() {
21+
return supplier.count();
22+
}
23+
}

core/runtime/src/main/resources/META-INF/quarkus-extension.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
---
2-
name: Quarkus - Operator SDK
1+
name: Operator SDK
32
description: Quarkus extension for the Java Operator SDK (https://javaoperatorsdk.io)
43
metadata:
54
keywords:

0 commit comments

Comments
 (0)