From a378444d5dddaa946b915743865f830b087c7ca9 Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Wed, 5 Apr 2023 05:43:46 +0200 Subject: [PATCH] feat: Add support for signal start events --- .../zeebe/play/rest/ProcessesResource.kt | 65 +++++++++++++++--- .../zeebe/play/rest/SignalResource.kt | 28 ++++++++ src/main/resources/public/js/rest-client.js | 7 ++ src/main/resources/public/js/view-bpmn.js | 26 ++++++++ src/main/resources/public/js/view-common.js | 30 +++++++++ .../public/js/view-process-instance.js | 19 ++++++ src/main/resources/public/js/view-process.js | 63 ++++++++++++++++++ src/main/resources/public/js/zeeqs-client.js | 24 +++++++ .../process-details-signal-subscriptions.html | 29 ++++++++ .../deployment/process/process-details.html | 26 ++++++++ .../views/deployment/process/process.html | 3 + .../views/modals/broadcast-signal-modal.html | 66 +++++++++++++++++++ 12 files changed, 378 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/org/camunda/community/zeebe/play/rest/SignalResource.kt create mode 100644 src/main/resources/templates/views/deployment/process/process-details-signal-subscriptions.html create mode 100644 src/main/resources/templates/views/modals/broadcast-signal-modal.html diff --git a/src/main/kotlin/org/camunda/community/zeebe/play/rest/ProcessesResource.kt b/src/main/kotlin/org/camunda/community/zeebe/play/rest/ProcessesResource.kt index 181c5c6c..bceb2d43 100644 --- a/src/main/kotlin/org/camunda/community/zeebe/play/rest/ProcessesResource.kt +++ b/src/main/kotlin/org/camunda/community/zeebe/play/rest/ProcessesResource.kt @@ -2,17 +2,13 @@ package org.camunda.community.zeebe.play.rest import io.camunda.zeebe.client.ZeebeClient import io.camunda.zeebe.model.bpmn.Bpmn -import io.camunda.zeebe.model.bpmn.instance.MessageEventDefinition -import io.camunda.zeebe.model.bpmn.instance.Process -import io.camunda.zeebe.model.bpmn.instance.StartEvent -import io.camunda.zeebe.model.bpmn.instance.TimerEventDefinition +import io.camunda.zeebe.model.bpmn.instance.* +import io.zeebe.zeeqs.data.entity.ProcessInstanceState import io.zeebe.zeeqs.data.entity.TimerState -import io.zeebe.zeeqs.data.repository.MessageCorrelationRepository -import io.zeebe.zeeqs.data.repository.MessageSubscriptionRepository -import io.zeebe.zeeqs.data.repository.ProcessRepository -import io.zeebe.zeeqs.data.repository.TimerRepository +import io.zeebe.zeeqs.data.repository.* import org.camunda.community.zeebe.play.connectors.ConnectorService import org.camunda.community.zeebe.play.services.ZeebeClockService +import org.springframework.data.domain.PageRequest import org.springframework.data.repository.findByIdOrNull import org.springframework.web.bind.annotation.* import java.io.ByteArrayInputStream @@ -31,6 +27,8 @@ class ProcessesResource( private val processRepository: ProcessRepository, private val messageSubscriptionRepository: MessageSubscriptionRepository, private val messageCorrelationRepository: MessageCorrelationRepository, + private val signalSubscriptionRepository: SignalSubscriptionRepository, + private val processInstanceRepository: ProcessInstanceRepository, private val timerRepository: TimerRepository, private val clockService: ZeebeClockService ) { @@ -88,6 +86,8 @@ class ProcessesResource( return createProcessInstanceWithMessageStartEvent(processKey, variables) } else if (startEvent.eventDefinitions.any { it is TimerEventDefinition }) { return createProcessInstanceWithTimerStartEvent(processKey) + } else if (startEvent.eventDefinitions.any { it is SignalEventDefinition }) { + return createProcessInstanceWithSignalStartEvent(processKey, variables) } else { val type = startEvent.eventDefinitions.first().elementType.typeName throw RuntimeException("Can't start process instance with start event of type '$type'") @@ -169,6 +169,55 @@ class ProcessesResource( return processInstanceKey } + private fun createProcessInstanceWithSignalStartEvent( + processKey: Long, + variables: String + ): Long { + val signalSubscription = signalSubscriptionRepository + .findByProcessDefinitionKey(processKey) + .firstOrNull() + ?: throw RuntimeException("No signal subscription found for process '$processKey'") + + val signalKey = zeebeClient.newBroadcastSignalCommand() + .signalName(signalSubscription.signalName) + .variables(variables) + .send() + .join() + .key + + return executor.submit(Callable { + getProcessInstanceKeyForSignal( + processKey = processKey, + signalKey = signalKey + ) + }).get() + } + + private fun getProcessInstanceKeyForSignal(processKey: Long, signalKey: Long): Long { + var processInstanceKey = -1L + while (processInstanceKey < 0) { + processInstanceKey = + processInstanceRepository.findByProcessDefinitionKeyAndStateIn( + processDefinitionKey = processKey, + stateIn = listOf( + ProcessInstanceState.ACTIVATED, + ProcessInstanceState.COMPLETED, + ProcessInstanceState.TERMINATED + ), + pageable = PageRequest.of(0, 1000) + ) + // since the signal was broadcast first, the signal key should be higher + .firstOrNull { it.key > signalKey } + ?.key + ?: run { + // wait and retry + Thread.sleep(RETRY_INTERVAL.toMillis()) + -1L + } + } + return processInstanceKey + } + @RequestMapping( path = ["/{processKey}/missing-connector-secrets"], method = [RequestMethod.GET] diff --git a/src/main/kotlin/org/camunda/community/zeebe/play/rest/SignalResource.kt b/src/main/kotlin/org/camunda/community/zeebe/play/rest/SignalResource.kt new file mode 100644 index 00000000..802d0faa --- /dev/null +++ b/src/main/kotlin/org/camunda/community/zeebe/play/rest/SignalResource.kt @@ -0,0 +1,28 @@ +package org.camunda.community.zeebe.play.rest + +import io.camunda.zeebe.client.ZeebeClient +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/rest/signals") +class SignalResource(private val zeebeClient: ZeebeClient) { + + @RequestMapping(method = [RequestMethod.POST]) + fun broadcastSignal(@RequestBody command: BroadcastSignalCommand): Long { + return zeebeClient.newBroadcastSignalCommand() + .signalName(command.signalName) + .variables(command.variables) + .send() + .join() + .key + } + + data class BroadcastSignalCommand( + val signalName: String, + val variables: String? + ) + +} \ No newline at end of file diff --git a/src/main/resources/public/js/rest-client.js b/src/main/resources/public/js/rest-client.js index 8678b1e5..8482b894 100644 --- a/src/main/resources/public/js/rest-client.js +++ b/src/main/resources/public/js/rest-client.js @@ -150,3 +150,10 @@ function sendEvaluateDecisionRequest(decisionKey, variables) { function sendGetDecisionInputsRequest(decisionKey) { return sendGetRequest(`decisions/${decisionKey}/inputs`); } + +function sendBroadcastSignalRequest(signalName, variables) { + return sendPostRequest("signals", { + signalName: signalName, + variables: variables, + }); +} diff --git a/src/main/resources/public/js/view-bpmn.js b/src/main/resources/public/js/view-bpmn.js index 2871e381..4b2e26c3 100644 --- a/src/main/resources/public/js/view-bpmn.js +++ b/src/main/resources/public/js/view-bpmn.js @@ -116,6 +116,28 @@ function removePublishMessageButton(elementId) { overlays.remove({ element: elementId, type: "publish-message" }); } +function addBroadcastSignalButton(elementId, clickAction) { + const buttonId = "broadcast-signal-diagram-action-" + elementId; + const content = ` + `; + + overlays.add(elementId, "broadcast-signal", { + position: { + top: -20, + left: -20, + }, + html: content, + }); + + $("#" + buttonId).click(clickAction); +} + +function removeAllBroadcastSignalButtons() { + overlays.remove({ type: "broadcast-signal" }); +} + function highlightElement(elementId) { if (highlightedElementId && highlightedElementId !== elementId) { canvas.removeMarker(highlightedElementId, "bpmn-element-selected"); @@ -579,9 +601,11 @@ function toggleDetailsCollapse() { function zoomIn() { bpmnViewer.get("zoomScroll").stepZoom(0.1); } + function zoomOut() { bpmnViewer.get("zoomScroll").stepZoom(-0.1); } + function resetViewport() { const outerViewbox = canvas.viewbox().outer; canvas.viewbox({ @@ -591,6 +615,7 @@ function resetViewport() { height: outerViewbox.height, }); } + function enterFullscreen() { const button = document.querySelector("#toggleFullscreenButton"); @@ -607,6 +632,7 @@ function enterFullscreen() { document.documentElement.requestFullscreen(); } + function exitFullscreen() { const button = document.querySelector("#toggleFullscreenButton"); diff --git a/src/main/resources/public/js/view-common.js b/src/main/resources/public/js/view-common.js index dfd1e286..149aafde 100644 --- a/src/main/resources/public/js/view-common.js +++ b/src/main/resources/public/js/view-common.js @@ -949,3 +949,33 @@ if (resizeHandle) { }); }); } + +function showBroadcastSignalModal(signalName) { + $("#broadcast-signal-name").val(signalName); + $("#broadcast-signal-variables").val(""); + + $("#broadcast-signal-modal").modal("show"); +} + +function broadcastSignalFromModal() { + const signalName = $("#broadcast-signal-name").val(); + const variables = $("#broadcast-signal-variables").val(); + + history.push({ + action: "broadcastSignal", + signalName, + variables, + }); + refreshHistory(); + + sendBroadcastSignalRequest(signalName, variables) + .done((signalKey) => { + const toastId = "signal-broadcasted-" + signalKey; + showNotificationSuccess( + toastId, + "New signal broadcasted", + "" + signalName + "" + ); + }) + .fail(showFailure("broadcast-signal-failed", "Failed to broadcast signal")); +} diff --git a/src/main/resources/public/js/view-process-instance.js b/src/main/resources/public/js/view-process-instance.js index 1145c2dc..949074f3 100644 --- a/src/main/resources/public/js/view-process-instance.js +++ b/src/main/resources/public/js/view-process-instance.js @@ -200,6 +200,11 @@ async function rewind(task) { ) { // timer start event newId = await createNewInstanceFromTimerStartEvent(startEvent); + } else if ( + startEvent.eventDefinitions[0].$type === "bpmn:SignalEventDefinition" + ) { + // signal start event + newId = await createNewInstanceFromSignalStartEvent(startEvent); } track?.("zeebePlay:bpmnelement:completed", { @@ -415,6 +420,20 @@ function waitForMessageSubscription(id, messageName, correlationKey) { }); } +async function createNewInstanceFromSignalStartEvent(startEvent) { + const signalName = startEvent.eventDefinitions[0].signalRef.name; + + const numberOfCurrentInstances = await getNumberOfCurrentInstancesFor( + currentProcessKey + ); + await sendBroadcastSignalRequest(signalName); + + return await waitForNewInstanceFor( + currentProcessKey, + numberOfCurrentInstances + ); +} + function fetchTimerForElement(id, elementId) { return new Promise((resolve, reject) => { let remainingTries = 6; diff --git a/src/main/resources/public/js/view-process.js b/src/main/resources/public/js/view-process.js index 427056f0..23866aea 100644 --- a/src/main/resources/public/js/view-process.js +++ b/src/main/resources/public/js/view-process.js @@ -47,6 +47,7 @@ function loadProcessView() { loadInstancesOfProcess(instancesOfProcessCurrentPage); loadMessageSubscriptionsOfProcess(); + loadSignalSubscriptionsOfProcess(); loadTimersOfProcess(); } @@ -337,6 +338,68 @@ function loadTimersOfProcess() { }); } +function loadSignalSubscriptionsOfProcess() { + const processKey = getProcessKey(); + + querySignalSubscriptionsByProcess(processKey).done(function (response) { + let process = response.data.process; + + let signalSubscriptions = process.signalSubscriptions; + let totalCount = signalSubscriptions.length; + + $("#signal-subscriptions-total-count").text(totalCount); + + $("#signal-subscriptions-of-process-table tbody").empty(); + + removeAllBroadcastSignalButtons(); + + const indexOffset = 1; + + signalSubscriptions.forEach((signalSubscription, index) => { + const isActive = signalSubscription.state === "CREATED"; + + const buttonId = "broadcast-signal-action-" + signalSubscription.key; + + let action = ""; + if (isActive) { + action = ` + `; + } + + $("#signal-subscriptions-of-process-table > tbody:last-child").append(` + + ${indexOffset + index} + ${signalSubscription.key} + ${signalSubscription.signalName} + ${formatBpmnElementInstance(signalSubscription.element)} + ${action} + `); + + if (isActive) { + $("#" + buttonId).click(function () { + showBroadcastSignalModal(signalSubscription.signalName); + }); + + addBroadcastSignalButton( + signalSubscription.element.elementId, + function () { + track?.("zeebePlay:bpmnelement:completed", { + element_type: "START_EVENT", + From: "processPage", + process_id: getProcessId(), + }); + + showBroadcastSignalModal(signalSubscription.signalName); + } + ); + } + }); + }); +} + function loadProcessElementOverview() { const processKey = getProcessKey(); diff --git a/src/main/resources/public/js/zeeqs-client.js b/src/main/resources/public/js/zeeqs-client.js index ea7fd8e5..dd50e822 100644 --- a/src/main/resources/public/js/zeeqs-client.js +++ b/src/main/resources/public/js/zeeqs-client.js @@ -72,6 +72,23 @@ const messageSubscriptionsByProcessQuery = `query MessageSubscriptionsOfProcess( } }`; +const signalSubscriptionsByProcessQuery = `query SignalSubscriptionsOfProcess($key: ID!, $zoneId: String!) { + process(key: $key) { + + signalSubscriptions { + key + signalName + state + timestamp(zoneId: $zoneId) + element { + elementId + elementName + bpmnElementType + } + } + } + }`; + const timersByProcessQuery = `query TimersOfProcess($key: ID!, $zoneId: String!) { process(key: $key) { @@ -760,6 +777,13 @@ function queryMessageSubscriptionsByProcess(processKey) { }); } +function querySignalSubscriptionsByProcess(processKey) { + return fetchData(signalSubscriptionsByProcessQuery, { + key: processKey, + zoneId: getTimeZone(), + }); +} + function queryTimersByProcess(processKey) { return fetchData(timersByProcessQuery, { key: processKey, diff --git a/src/main/resources/templates/views/deployment/process/process-details-signal-subscriptions.html b/src/main/resources/templates/views/deployment/process/process-details-signal-subscriptions.html new file mode 100644 index 00000000..09115260 --- /dev/null +++ b/src/main/resources/templates/views/deployment/process/process-details-signal-subscriptions.html @@ -0,0 +1,29 @@ +
+
+
+ + + + + + + + + + + + + + + + + + + +
#Signal Subscription KeySignal NameElement
1---
+
+
+
diff --git a/src/main/resources/templates/views/deployment/process/process-details.html b/src/main/resources/templates/views/deployment/process/process-details.html index 39757be1..dc71f84c 100644 --- a/src/main/resources/templates/views/deployment/process/process-details.html +++ b/src/main/resources/templates/views/deployment/process/process-details.html @@ -33,6 +33,21 @@ 0 +