Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(UI): forward Zeebe client exceptions to the web UI #353

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 33 additions & 18 deletions src/main/java/io/zeebe/monitor/rest/ExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
package io.zeebe.monitor.rest;

import io.camunda.zeebe.client.api.command.ClientException;
import io.zeebe.monitor.rest.ui.ErrorMessage;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class ExceptionHandler {

private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class);
private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class);

private final WhitelabelProperties whitelabelProperties;
private final WhitelabelProperties whitelabelProperties;

public ExceptionHandler(WhitelabelProperties whitelabelProperties) {
this.whitelabelProperties = whitelabelProperties;
}
public ExceptionHandler(WhitelabelProperties whitelabelProperties) {
this.whitelabelProperties = whitelabelProperties;
}

@org.springframework.web.bind.annotation.ExceptionHandler(RuntimeException.class)
public String handleRuntimeException(RuntimeException exc, final Model model) {
LOG.error(exc.getMessage(), exc);
@org.springframework.web.bind.annotation.ExceptionHandler(value = {ClientException.class})
protected ResponseEntity<Object> handleZeebeClientException(final RuntimeException ex, final WebRequest request) {
LOG.debug("Zeebe Client Exception caught and forwarding to UI.", ex);
return ResponseEntity
.status(HttpStatus.FAILED_DEPENDENCY)
.contentType(MediaType.APPLICATION_JSON)
.body(new ErrorMessage(ex.getMessage()));
}

model.addAttribute("error", exc.getClass().getSimpleName());
model.addAttribute("message", exc.getMessage());
model.addAttribute("trace", ExceptionUtils.getStackTrace(exc));
@org.springframework.web.bind.annotation.ExceptionHandler(RuntimeException.class)
public String handleRuntimeException(final RuntimeException exc, final Model model) {
LOG.error(exc.getMessage(), exc);

model.addAttribute("custom-title", whitelabelProperties.getCustomTitle());
model.addAttribute("context-path", whitelabelProperties.getBasePath());
model.addAttribute("logo-path", whitelabelProperties.getLogoPath());
model.addAttribute("custom-css-path", whitelabelProperties.getCustomCssPath());
model.addAttribute("custom-js-path", whitelabelProperties.getCustomCssPath());
model.addAttribute("error", exc.getClass().getSimpleName());
model.addAttribute("message", exc.getMessage());
model.addAttribute("trace", ExceptionUtils.getStackTrace(exc));

return "error";
}
model.addAttribute("custom-title", whitelabelProperties.getCustomTitle());
model.addAttribute("context-path", whitelabelProperties.getBasePath());
model.addAttribute("logo-path", whitelabelProperties.getLogoPath());
model.addAttribute("custom-css-path", whitelabelProperties.getCustomCssPath());
model.addAttribute("custom-js-path", whitelabelProperties.getCustomCssPath());

return "error";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.zeebe.monitor.rest;

import io.camunda.zeebe.client.ZeebeClient;
import io.zeebe.monitor.zeebe.ZeebeNotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -28,6 +29,7 @@
public class ProcessInstanceResource {

@Autowired private ZeebeClient zeebeClient;
@Autowired private ZeebeNotificationService zeebeNotificationService;

@RequestMapping(path = "/{key}", method = RequestMethod.DELETE)
public void cancelProcessInstance(@PathVariable("key") final long key) throws Exception {
Expand Down Expand Up @@ -56,6 +58,13 @@ public void resolveIncident(
.newUpdateRetriesCommand(dto.getJobKey())
.retries(dto.getRemainingRetries())
.send()
.exceptionally(e -> {
// catch this exception and forward to the user
zeebeNotificationService.sendZeebeClusterError(e.getMessage());
// AND continue with second Zeebe command below
return null;
})
.toCompletableFuture()
.join();
}

Expand Down
18 changes: 18 additions & 0 deletions src/main/java/io/zeebe/monitor/rest/ui/ErrorMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.zeebe.monitor.rest.ui;

public class ErrorMessage {

private String message;

public ErrorMessage(String message) {
this.message = message;
}

public String getMessage() {
return message;
}

public void setMessage(final String message) {
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.zeebe.monitor.rest;
package io.zeebe.monitor.rest.ui;

public class ProcessInstanceNotification {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.zeebe.monitor.rest.ui;

public class ZeebeClusterNotification {

private Type type;
private String message;

public enum Type {
INFORMATION,
SUCCESS,
ERROR
}

public Type getType() {
return type;
}

public void setType(final Type type) {
this.type = type;
}

public String getMessage() {
return message;
}

public void setMessage(final String message) {
this.message = message;
}
}
17 changes: 15 additions & 2 deletions src/main/java/io/zeebe/monitor/zeebe/ZeebeNotificationService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package io.zeebe.monitor.zeebe;

import io.zeebe.monitor.rest.ProcessInstanceNotification;
import io.zeebe.monitor.rest.ProcessInstanceNotification.Type;
import io.zeebe.monitor.rest.ui.ProcessInstanceNotification;
import io.zeebe.monitor.rest.ui.ProcessInstanceNotification.Type;
import io.zeebe.monitor.rest.ui.ZeebeClusterNotification;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.simp.SimpMessagingTemplate;
Expand Down Expand Up @@ -48,8 +49,20 @@ public void sendEndedProcessInstance(
sendNotification(notification);
}

public void sendZeebeClusterError(final String message) {
final ZeebeClusterNotification notification = new ZeebeClusterNotification();
notification.setMessage(message);
notification.setType(ZeebeClusterNotification.Type.ERROR);
sendNotification(notification);
}

private void sendNotification(final ProcessInstanceNotification notification) {
final var destination = basePath + "notifications/process-instance";
webSocket.convertAndSend(destination, notification);
}

private void sendNotification(final ZeebeClusterNotification notification) {
final var destination = basePath + "notifications/zeebe-cluster";
webSocket.convertAndSend(destination, notification);
}
}
95 changes: 86 additions & 9 deletions src/main/resources/public/js/app.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,85 @@
/**
* @typedef ErrorMessage
* @type {object}
* @property {string} message
*/

/**
* @typedef ProcessInstanceNotification
* @type {object}
* @property {number} processInstanceKey
* @property {number} processDefinitionKey
* @property {string} type
*/

/**
* @typedef ZeebeClusterNotification
* @type {object}
* @property {string} message
* @property {string} type
*/

/**
* @param {string} elementId - the HTML element ID to attach the text to
* @param {string} title - the title for the message
* @param {string} message - the actual text message
*/
function appendAndSowMessageToElement(elementId, title, message) {
var dataTexts = jQuery("#" + elementId + " [data-text]");
var length = dataTexts.length;
// skip duplicates
if (length > 0) {
if (jQuery(dataTexts[length - 1]).text().endsWith(message)) {
return;
}
}
// drop the top of the stack == the oldest entries
for (var i = 0; i < Math.max(0, length - 3); i++) {
jQuery(dataTexts[i]).fadeOut(dataTexts[i].remove);
}
var newTextElement = jQuery("<div/>");
newTextElement.hide();
newTextElement.attr("data-text", "");
newTextElement.append(jQuery("<strong>" + title + "</strong>"));
var textSpanElement = jQuery("<span/>");
textSpanElement.text(message);
newTextElement.append(textSpanElement);
var panelElement = jQuery("#" + elementId);
if (length === 0) {
panelElement.prepend(newTextElement);
} else {
jQuery(dataTexts[dataTexts.length - 1]).after(newTextElement);
}
panelElement.show();
newTextElement.fadeIn();
}

/**
* @param {string} message - the actual text message
*/
function showError(message) {
document.getElementById("errorText").innerHTML = message;
$('#errorPanel').show();
appendAndSowMessageToElement("errorPanel", "Error: ", message);
}

/**
* @param {string} message - the actual text message
*/
function showSuccess(message) {
document.getElementById("successText").innerHTML = message;
$('#successPanel').show();
appendAndSowMessageToElement("successPanel", "Success: ", message);
}

/**
* @param {string} message - the actual text message
*/
function showInfo(message) {
document.getElementById("infoText").innerHTML = message;
$('#infoPanel').show();
appendAndSowMessageToElement("infoPanel", "Info: ", message);
}

function showErrorResonse(xhr, ajaxOptions, thrownError) {
if (xhr.responseJSON) {
showError(xhr.responseJSON.message);
/** @type {ErrorMessage} */
let errorMessage = xhr.responseJSON;
showError(errorMessage.message);
} else {
showError(thrownError);
}
Expand Down Expand Up @@ -43,7 +107,10 @@ function connect() {
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
stompClient.subscribe(buildPath('notifications/process-instance'), function (message) {
handleMessage(JSON.parse(message.body));
handleProcessInstanceNotification(JSON.parse(message.body));
nitram509 marked this conversation as resolved.
Show resolved Hide resolved
});
stompClient.subscribe(buildPath('notifications/zeebe-cluster'), function (message) {
handleZeebeClusterNotification(JSON.parse(message.body));
});
});
}
Expand All @@ -59,7 +126,10 @@ function sendMessage(msg) {
JSON.stringify(msg));
}

function handleMessage(notification) {
/**
* @param notification {ProcessInstanceNotification}
*/
function handleProcessInstanceNotification(notification) {

if (subscribedProcessInstanceKeys.includes(notification.processInstanceKey)) {
showInfo('Process instance has changed.');
Expand All @@ -70,6 +140,13 @@ function handleMessage(notification) {
}
}

/**
* @param notification {ZeebeClusterNotification}
*/
function handleZeebeClusterNotification(notification) {
showError(notification.message)
}

function subscribeForProcessInstance(key) {
subscribedProcessInstanceKeys.push(key);
}
Expand Down
12 changes: 6 additions & 6 deletions src/main/resources/templates/layout/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,24 @@
<div class="container-fluid">

<div id="errorPanel" class="alert alert-danger alert-dismissible fade show" style="display:none;" role="alert">
<strong>Error:</strong> <span id="errorText"></span>
<button type="button" class="close" aria-label="Close" onclick="$('.alert').hide()">
<!-- text will be inserted via jQuery at runtime -->
nitram509 marked this conversation as resolved.
Show resolved Hide resolved
<button type="button" class="close" aria-label="Close" onclick="(function(){jQuery('#errorPanel [data-text]').remove(); jQuery('#errorPanel').hide()}())">
<span aria-hidden="true">&times;</span>
</button>
</div>

<div id="successPanel" class="alert alert-success alert-dismissible fade show" style="display:none;" role="alert">
<strong>Success:</strong> <span id="successText"></span>
<!-- text will be inserted via jQuery at runtime -->
(<a href="#" onClick="reload()">Refresh</a>)
<button type="button" class="close" aria-label="Close" onclick="$('.alert').hide()">
<button type="button" class="close" aria-label="Close" onclick="(function(){jQuery('#successPanel [data-text]').remove(); jQuery('#successPanel').hide()}())">
<span aria-hidden="true">&times;</span>
</button>
</div>

<div id="infoPanel" class="alert alert-primary alert-dismissible fade show" style="display:none;" role="alert">
<strong>Info:</strong> <span id="infoText"></span>
<!-- text will be inserted via jQuery at runtime -->
(<a href="#" onClick="reload()">Refresh</a>)
<button type="button" class="close" aria-label="Close" onclick="$('.alert').hide()">
<button type="button" class="close" aria-label="Close" onclick="(function(){jQuery('#infoPanel [data-text]').remove(); jQuery('#infoPanel').hide()}())">
<span aria-hidden="true">&times;</span>
</button>
</div>
Loading