Skip to content

Commit 0676bb9

Browse files
authored
Merge pull request #31571 from mkouba/scheduler-new-devui-executions
Scheduler - new Dev UI improvements
2 parents f2b586c + d94f011 commit 0676bb9

File tree

7 files changed

+360
-97
lines changed

7 files changed

+360
-97
lines changed

extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzSchedulerImpl.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) {
257257
}
258258
}, invoker,
259259
SchedulerUtils.parseOverdueGracePeriod(scheduled, defaultOverdueGracePeriod),
260-
quartzSupport.getRuntimeConfig().runBlockingScheduledMethodOnQuartzThread, false));
260+
quartzSupport.getRuntimeConfig().runBlockingScheduledMethodOnQuartzThread, false,
261+
method.getMethodDescription()));
261262
}
262263
}
263264
}
@@ -818,7 +819,7 @@ public org.quartz.Trigger apply(TriggerKey triggerKey) {
818819
}
819820
}, invoker,
820821
SchedulerUtils.parseOverdueGracePeriod(scheduled, defaultOverdueGracePeriod),
821-
runtimeConfig.runBlockingScheduledMethodOnQuartzThread, true));
822+
runtimeConfig.runBlockingScheduledMethodOnQuartzThread, true, null));
822823

823824
if (existing != null) {
824825
throw new IllegalStateException("A job with this identity is already scheduled: " + identity);
@@ -907,18 +908,20 @@ static class QuartzTrigger implements Trigger {
907908
final ScheduledInvoker invoker;
908909
final Duration gracePeriod;
909910
final boolean isProgrammatic;
911+
final String methodDescription;
910912

911913
final boolean runBlockingMethodOnQuartzThread;
912914

913915
QuartzTrigger(org.quartz.TriggerKey triggerKey, Function<TriggerKey, org.quartz.Trigger> triggerFunction,
914916
ScheduledInvoker invoker, Duration gracePeriod, boolean runBlockingMethodOnQuartzThread,
915-
boolean isProgrammatic) {
917+
boolean isProgrammatic, String methodDescription) {
916918
this.triggerKey = triggerKey;
917919
this.triggerFunction = triggerFunction;
918920
this.invoker = invoker;
919921
this.gracePeriod = gracePeriod;
920922
this.runBlockingMethodOnQuartzThread = runBlockingMethodOnQuartzThread;
921923
this.isProgrammatic = isProgrammatic;
924+
this.methodDescription = methodDescription;
922925
}
923926

924927
@Override
@@ -952,6 +955,11 @@ private org.quartz.Trigger getTrigger() {
952955
return triggerFunction.apply(triggerKey);
953956
}
954957

958+
@Override
959+
public String getMethodDescription() {
960+
return methodDescription;
961+
}
962+
955963
}
956964

957965
static class QuartzScheduledExecution implements ScheduledExecution {

extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,12 @@ public interface Trigger {
3939
*/
4040
boolean isOverdue();
4141

42+
/**
43+
*
44+
* @return the method description or {@code null} for a trigger of a programmatically added job
45+
*/
46+
default String getMethodDescription() {
47+
return null;
48+
}
49+
4250
}

extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/devui/SchedulerDevUIProcessor.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,36 @@
33
import java.util.List;
44

55
import io.quarkus.deployment.IsDevelopment;
6+
import io.quarkus.deployment.annotations.BuildProducer;
67
import io.quarkus.deployment.annotations.BuildStep;
78
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
89
import io.quarkus.devui.spi.page.CardPageBuildItem;
10+
import io.quarkus.devui.spi.page.FooterPageBuildItem;
911
import io.quarkus.devui.spi.page.Page;
12+
import io.quarkus.devui.spi.page.WebComponentPageBuilder;
1013
import io.quarkus.scheduler.deployment.ScheduledBusinessMethodItem;
1114
import io.quarkus.scheduler.runtime.devui.SchedulerJsonRPCService;
1215

1316
public class SchedulerDevUIProcessor {
1417

1518
@BuildStep(onlyIf = IsDevelopment.class)
16-
CardPageBuildItem page(List<ScheduledBusinessMethodItem> scheduledMethods) {
19+
void page(List<ScheduledBusinessMethodItem> scheduledMethods,
20+
BuildProducer<CardPageBuildItem> cardPages,
21+
BuildProducer<FooterPageBuildItem> footerPages) {
1722

1823
CardPageBuildItem pageBuildItem = new CardPageBuildItem("Scheduler");
1924

2025
pageBuildItem.addPage(Page.webComponentPageBuilder()
2126
.icon("font-awesome-solid:clock")
2227
.componentLink("qwc-scheduler-scheduled-methods.js")
2328
.staticLabel(String.valueOf(scheduledMethods.size())));
29+
cardPages.produce(pageBuildItem);
2430

25-
return pageBuildItem;
31+
WebComponentPageBuilder logPageBuilder = Page.webComponentPageBuilder()
32+
.icon("font-awesome-solid:clock")
33+
.title("Scheduler")
34+
.componentLink("qwc-scheduler-log.js");
35+
footerPages.produce(new FooterPageBuildItem("Scheduler", logPageBuilder));
2636
}
2737

2838
@BuildStep(onlyIf = IsDevelopment.class)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { LitElement, html, css} from 'lit';
2+
import { JsonRpc } from 'jsonrpc';
3+
import { LogController } from 'log-controller';
4+
5+
/**
6+
* This component shows the log of scheduled executions.
7+
*/
8+
export class QwcSchedulerLog extends LitElement {
9+
10+
jsonRpc = new JsonRpc("Scheduler");
11+
logControl = new LogController(this, "qwc-scheduler-log");
12+
13+
static styles = css`
14+
.text-error {
15+
color: var(--lumo-error-text-color);
16+
}
17+
.text-normal {
18+
color: var(--lumo-success-color-50pct);
19+
}
20+
.text-info {
21+
color: var(--lumo-primary-text-color);
22+
}
23+
`;
24+
25+
static properties = {
26+
_logEntries: {state:true},
27+
_observer: {state:false},
28+
_zoom: {state:true},
29+
_increment: {state: false},
30+
_followLog: {state: false},
31+
};
32+
33+
constructor() {
34+
super();
35+
this.logControl
36+
.addToggle("On/off switch", true, (e) => {
37+
this._toggleOnOffClicked(e);
38+
}).addItem("Zoom out", "font-awesome-solid:magnifying-glass-minus", "grey", (e) => {
39+
this._zoomOut();
40+
}).addItem("Zoom in", "font-awesome-solid:magnifying-glass-plus", "grey", (e) => {
41+
this._zoomIn();
42+
}).addItem("Clear", "font-awesome-solid:trash-can", "#FF004A", (e) => {
43+
this._clearLog();
44+
}).addFollow("Follow log", true , (e) => {
45+
this._toggleFollowLog(e);
46+
});
47+
this._logEntries = [];
48+
this._zoom = parseFloat(1.0);
49+
this._increment = parseFloat(0.05);
50+
this._followLog = true;
51+
}
52+
53+
connectedCallback() {
54+
super.connectedCallback();
55+
this._connect();
56+
}
57+
58+
disconnectedCallback() {
59+
this._disconnect();
60+
super.disconnectedCallback();
61+
}
62+
63+
_connect(){
64+
if(!this._observer){
65+
this._observer = this.jsonRpc.streamLog().onNext(jsonRpcResponse => {
66+
this._addLogEntry(jsonRpcResponse.result);
67+
});
68+
}
69+
}
70+
71+
_disconnect() {
72+
if(this._observer){
73+
this._observer.cancel();
74+
}
75+
}
76+
77+
render() {
78+
return html`${this._logEntries.map((le) =>
79+
html`<code class="log" style="font-size: ${this._zoom}em;">
80+
<div class="logEntry">
81+
${this._renderLogEntry(le)}
82+
</div>
83+
</code>`
84+
)}`;
85+
}
86+
87+
_renderLogEntry(logEntry){
88+
return html`
89+
${this._renderTime(logEntry)}
90+
${this._renderIcon(logEntry)}
91+
${this._renderMessage(logEntry)}
92+
`;
93+
}
94+
95+
_renderTime(logEntry){
96+
return html`<span title='Time'>${logEntry.timestamp}</span>`;
97+
}
98+
99+
_renderMessage(logEntry){
100+
return logEntry.success ? html`Job executed ${this._renderInfo(logEntry)}` : html`Job failed ${this._renderInfo(logEntry)}: <span class="text-error">${logEntry.message}<span>` ;
101+
}
102+
103+
_renderInfo(logEntry){
104+
return html`<span title='Info' class='text-info'>[${this._renderIdentity(logEntry)}${this._renderMethodDescription(logEntry)}]</span>`;
105+
}
106+
107+
_renderIdentity(logEntry){
108+
return logEntry.triggerIdentity ? html`identity: ${logEntry.triggerIdentity}${logEntry.triggerMethodDescription ? ', ' : ''}` : '';
109+
}
110+
111+
_renderMethodDescription(logEntry){
112+
return logEntry.triggerMethodDescription ? html`method: ${logEntry.triggerMethodDescription}` : '';
113+
}
114+
115+
_renderIcon(logEntry){
116+
if (logEntry.success){
117+
return html`<vaadin-icon icon="font-awesome-solid:circle-info" class="icon-info text-normal"></vaadin-icon>`;
118+
} else {
119+
return html`<vaadin-icon icon="font-awesome-solid:radiation" class="text-error"></vaadin-icon>`;
120+
}
121+
}
122+
123+
_addLogEntry(logEntry){
124+
this._logEntries = [
125+
...this._logEntries,
126+
logEntry
127+
];
128+
this._scrollToBottom();
129+
}
130+
131+
_toggleOnOffClicked(e){
132+
if(e){
133+
this._connect();
134+
}else{
135+
this._disconnect();
136+
}
137+
}
138+
139+
_zoomOut(){
140+
this._zoom = parseFloat(parseFloat(this._zoom) - parseFloat(this._increment)).toFixed(2);
141+
}
142+
143+
_zoomIn(){
144+
this._zoom = parseFloat(parseFloat(this._zoom) + parseFloat(this._increment)).toFixed(2);
145+
}
146+
147+
_clearLog(){
148+
this._logEntries = [];
149+
}
150+
151+
_toggleFollowLog(e){
152+
this._followLog = e;
153+
this._scrollToBottom();
154+
}
155+
156+
async _scrollToBottom(){
157+
if(this._followLog){
158+
await this.updateComplete;
159+
160+
const last = Array.from(
161+
this.shadowRoot.querySelectorAll('.logEntry')
162+
).pop();
163+
164+
if(last){
165+
last.scrollIntoView({
166+
behavior: "smooth",
167+
block: "end"
168+
});
169+
}
170+
}
171+
}
172+
}
173+
customElements.define('qwc-scheduler-log', QwcSchedulerLog);

extensions/scheduler/deployment/src/main/resources/dev-ui/scheduler/qwc-scheduler-scheduled-methods.js

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,28 @@ export class QwcSchedulerScheduledMethods extends LitElement {
5858

5959
connectedCallback() {
6060
super.connectedCallback();
61-
this.jsonRpc.getScheduledMethods()
61+
this.jsonRpc.getData()
6262
.then(jsonResponse => {
63-
this._scheduledMethods = jsonResponse.result;
63+
this._scheduledMethods = jsonResponse.result.methods;
64+
this._schedulerRunning = jsonResponse.result.schedulerRunning;
6465
this._filteredScheduledMethods = this._scheduledMethods;
6566
})
6667
.then(() => {
6768
this._runningStatusStream = this.jsonRpc.streamRunningStatus().onNext(jsonResponse => {
68-
this._schedulerRunning = jsonResponse.result["q_scheduler"];
69-
this._scheduledMethods = this._scheduledMethods.map(scheduledMethod => {
70-
scheduledMethod.schedules.forEach(schedule => {
71-
if (schedule.identity) {
72-
schedule.running = jsonResponse.result[schedule.identity];
73-
}
74-
});
75-
return scheduledMethod;
76-
});
77-
this._filteredScheduledMethods = this._scheduledMethods;
69+
const identity = jsonResponse.result.id;
70+
if (identity === "quarkus_scheduler") {
71+
this._schedulerRunning = jsonResponse.result["running"];
72+
} else {
73+
this._scheduledMethods = this._scheduledMethods.map(sm => {
74+
sm.schedules.forEach(schedule => {
75+
if (schedule.identity === identity) {
76+
schedule.running = jsonResponse.result["running"];
77+
}
78+
});
79+
return sm;
80+
});
81+
this._filteredScheduledMethods = this._scheduledMethods;
82+
}
7883
}
7984
)});
8085
}
@@ -188,7 +193,6 @@ export class QwcSchedulerScheduledMethods extends LitElement {
188193
_pauseJob(identity) {
189194
this.jsonRpc.pauseJob({"identity": identity}).then(jsonResponse => {
190195
if (jsonResponse.result.success) {
191-
this._updateIdentityStatus(identity, false);
192196
notifier.showSuccessMessage(jsonResponse.result.message);
193197
} else {
194198
notifier.showErrorMessage(jsonResponse.result.message);
@@ -199,7 +203,6 @@ export class QwcSchedulerScheduledMethods extends LitElement {
199203
_resumeJob(identity) {
200204
this.jsonRpc.resumeJob({"identity": identity}).then(jsonResponse => {
201205
if (jsonResponse.result.success) {
202-
this._updateIdentityStatus(identity, true);
203206
notifier.showSuccessMessage(jsonResponse.result.message);
204207
} else {
205208
notifier.showErrorMessage(jsonResponse.result.message, "bottom-stretch");
@@ -210,7 +213,6 @@ export class QwcSchedulerScheduledMethods extends LitElement {
210213
_pauseScheduler() {
211214
this.jsonRpc.pauseScheduler().then(jsonResponse => {
212215
if (jsonResponse.result.success) {
213-
this._schedulerRunning = false;
214216
notifier.showSuccessMessage(jsonResponse.result.message);
215217
} else {
216218
notifier.showErrorMessage(jsonResponse.result.message);
@@ -221,7 +223,6 @@ export class QwcSchedulerScheduledMethods extends LitElement {
221223
_resumeScheduler() {
222224
this.jsonRpc.resumeScheduler().then(jsonResponse => {
223225
if (jsonResponse.result.success) {
224-
this._schedulerRunning = true;
225226
notifier.showSuccessMessage(jsonResponse.result.message);
226227
} else {
227228
notifier.showErrorMessage(jsonResponse.result.message);
@@ -239,17 +240,6 @@ export class QwcSchedulerScheduledMethods extends LitElement {
239240
});
240241
}
241242

242-
_updateIdentityStatus(identity, running) {
243-
this._filteredScheduledMethods = this._filteredScheduledMethods.map(scheduledMethod => {
244-
scheduledMethod.schedules.forEach(schedule => {
245-
if (schedule.identity === identity) {
246-
schedule.running = running;
247-
}
248-
});
249-
return scheduledMethod;
250-
});
251-
}
252-
253243
_matchesTerm(scheduledMethod, searchTerm) {
254244
if (!searchTerm
255245
|| scheduledMethod.declaringClassName.toLowerCase().includes(searchTerm)

0 commit comments

Comments
 (0)