working but barely functional UI for manually sending events

This commit is contained in:
Elizabeth Esswein 2022-12-14 15:21:39 -05:00
parent c821b2ad17
commit 232ff1c6f7
6 changed files with 171 additions and 12 deletions

View File

@ -586,6 +586,27 @@ paths:
schema: schema:
$ref: "#/components/schemas/Workflow" $ref: "#/components/schemas/Workflow"
/process-instances/{process_instance_id}/event:
parameters:
- name: process_instance_id
in: path
required: true
description: The unique id of the process instance
schema:
type: string
post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.send_bpmn_event
summary: Send a BPMN event to the process
tags:
- Process Instances
responses:
"200":
description: Event Sent Successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/process-models/{process_group_id}/{process_model_id}/script-unit-tests: /process-models/{process_group_id}/{process_model_id}/script-unit-tests:
parameters: parameters:
- name: process_group_id - name: process_group_id

View File

@ -118,6 +118,7 @@ class Task:
form_schema: Union[str, None] = None, form_schema: Union[str, None] = None,
form_ui_schema: Union[str, None] = None, form_ui_schema: Union[str, None] = None,
parent: Optional[str] = None, parent: Optional[str] = None,
event_definition: Union[dict[str, Any], None] = None
): ):
"""__init__.""" """__init__."""
self.id = id self.id = id
@ -129,6 +130,7 @@ class Task:
self.documentation = documentation self.documentation = documentation
self.lane = lane self.lane = lane
self.parent = parent self.parent = parent
self.event_definition = event_definition
self.data = data self.data = data
if self.data is None: if self.data is None:
@ -187,6 +189,7 @@ class Task:
"form_schema": self.form_schema, "form_schema": self.form_schema,
"form_ui_schema": self.form_ui_schema, "form_ui_schema": self.form_ui_schema,
"parent": self.parent, "parent": self.parent,
"event_definition": self.event_definition,
} }
@classmethod @classmethod
@ -287,6 +290,7 @@ class TaskSchema(Schema):
"process_instance_id", "process_instance_id",
"form_schema", "form_schema",
"form_ui_schema", "form_ui_schema",
"event_definition",
] ]
multi_instance_type = EnumField(MultiInstanceType) multi_instance_type = EnumField(MultiInstanceType)

View File

@ -1311,7 +1311,7 @@ def process_instance_task_list(
tasks = [] tasks = []
for spiff_task in spiff_tasks: for spiff_task in spiff_tasks:
task = ProcessInstanceService.spiff_task_to_api_task(spiff_task) task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
task.data = spiff_task.data task.data = spiff_task.data
tasks.append(task) tasks.append(task)
@ -1344,7 +1344,9 @@ def task_show(process_instance_id: int, task_id: str) -> flask.wrappers.Response
form_schema_file_name = properties["formJsonSchemaFilename"] form_schema_file_name = properties["formJsonSchemaFilename"]
if "formUiSchemaFilename" in properties: if "formUiSchemaFilename" in properties:
form_ui_schema_file_name = properties["formUiSchemaFilename"] form_ui_schema_file_name = properties["formUiSchemaFilename"]
task = ProcessInstanceService.spiff_task_to_api_task(spiff_task)
processor = ProcessInstanceProcessor(process_instance)
task = ProcessInstanceService.spiff_task_to_api_task(processor, spiff_task)
task.data = spiff_task.data task.data = spiff_task.data
task.process_model_display_name = process_model.display_name task.process_model_display_name = process_model.display_name
task.process_model_identifier = process_model.id task.process_model_identifier = process_model.id
@ -1882,3 +1884,21 @@ def update_task_data(process_instance_id: str, task_id: str, body: Dict) -> Resp
status=200, status=200,
mimetype="application/json", mimetype="application/json",
) )
def send_bpmn_event(process_instance_id: str, body: Dict) -> Response:
process_instance = ProcessInstanceModel.query.filter(
ProcessInstanceModel.id == int(process_instance_id)
).first()
if process_instance:
processor = ProcessInstanceProcessor(process_instance)
processor.send_bpmn_event(body)
else:
raise ApiError(
error_code="send_bpmn_event_error",
message=f"Could not send event to Instance: {process_instance_id}",
)
return Response(
json.dumps(ProcessInstanceModelSchema().dump(process_instance)),
status=200,
mimetype="application/json",
)

View File

@ -45,9 +45,8 @@ from SpiffWorkflow.spiff.serializer.task_spec_converters import (
from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import EndEventConverter
from SpiffWorkflow.spiff.serializer.task_spec_converters import ( from SpiffWorkflow.spiff.serializer.task_spec_converters import (
IntermediateCatchEventConverter, IntermediateCatchEventConverter,
)
from SpiffWorkflow.spiff.serializer.task_spec_converters import (
IntermediateThrowEventConverter, IntermediateThrowEventConverter,
EventBasedGatewayConverter,
) )
from SpiffWorkflow.spiff.serializer.task_spec_converters import ManualTaskConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import ManualTaskConverter
from SpiffWorkflow.spiff.serializer.task_spec_converters import NoneTaskConverter from SpiffWorkflow.spiff.serializer.task_spec_converters import NoneTaskConverter
@ -254,6 +253,7 @@ class ProcessInstanceProcessor:
EndEventConverter, EndEventConverter,
IntermediateCatchEventConverter, IntermediateCatchEventConverter,
IntermediateThrowEventConverter, IntermediateThrowEventConverter,
EventBasedGatewayConverter,
ManualTaskConverter, ManualTaskConverter,
NoneTaskConverter, NoneTaskConverter,
ReceiveTaskConverter, ReceiveTaskConverter,
@ -267,6 +267,7 @@ class ProcessInstanceProcessor:
] ]
) )
_serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION) _serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION)
_event_serializer = EventBasedGatewayConverter()
PROCESS_INSTANCE_ID_KEY = "process_instance_id" PROCESS_INSTANCE_ID_KEY = "process_instance_id"
VALIDATION_PROCESS_KEY = "validate_only" VALIDATION_PROCESS_KEY = "validate_only"
@ -658,6 +659,18 @@ class ProcessInstanceProcessor:
db.session.delete(at) db.session.delete(at)
db.session.commit() db.session.commit()
def serialize_task_spec(self, task_spec: SpiffTask) -> dict[str, Any]:
return self._serializer.spec_converter.convert(task_spec)
def send_bpmn_event(self, event_data: dict[str,Any]) -> None:
payload = event_data.pop("payload", None)
event_definition = self._event_serializer.restore(event_data)
if payload is not None:
event_definition.payload = payload
current_app.logger.info(f"Event of type {event_definition.event_type} sent to process instance {self.process_instance_model.id}")
self.bpmn_process_instance.catch(event_definition)
self.do_engine_steps(save=True)
@staticmethod @staticmethod
def get_parser() -> MyCustomParser: def get_parser() -> MyCustomParser:
"""Get_parser.""" """Get_parser."""

View File

@ -121,7 +121,7 @@ class ProcessInstanceService:
if next_task_trying_again is not None: if next_task_trying_again is not None:
process_instance_api.next_task = ( process_instance_api.next_task = (
ProcessInstanceService.spiff_task_to_api_task( ProcessInstanceService.spiff_task_to_api_task(
next_task_trying_again, add_docs_and_forms=True processor, next_task_trying_again, add_docs_and_forms=True
) )
) )
@ -277,7 +277,9 @@ class ProcessInstanceService:
@staticmethod @staticmethod
def spiff_task_to_api_task( def spiff_task_to_api_task(
spiff_task: SpiffTask, add_docs_and_forms: bool = False processor: ProcessInstanceProcessor,
spiff_task: SpiffTask,
add_docs_and_forms: bool = False
) -> Task: ) -> Task:
"""Spiff_task_to_api_task.""" """Spiff_task_to_api_task."""
task_type = spiff_task.task_spec.spec_type task_type = spiff_task.task_spec.spec_type
@ -306,6 +308,8 @@ class ProcessInstanceService:
if spiff_task.parent: if spiff_task.parent:
parent_id = spiff_task.parent.id parent_id = spiff_task.parent.id
serialized_task_spec = processor.serialize_task_spec(spiff_task.task_spec)
task = Task( task = Task(
spiff_task.id, spiff_task.id,
spiff_task.task_spec.name, spiff_task.task_spec.name,
@ -319,6 +323,7 @@ class ProcessInstanceService:
process_name=spiff_task.task_spec._wf_spec.description, process_name=spiff_task.task_spec._wf_spec.description,
properties=props, properties=props,
parent=parent_id, parent=parent_id,
event_definition=serialized_task_spec.get("event_definition"),
) )
return task return task

View File

@ -20,6 +20,7 @@ import {
ButtonSet, ButtonSet,
Tag, Tag,
Modal, Modal,
Dropdown,
Stack, Stack,
// @ts-ignore // @ts-ignore
} from '@carbon/react'; } from '@carbon/react';
@ -47,6 +48,9 @@ export default function ProcessInstanceShow() {
const [taskToDisplay, setTaskToDisplay] = useState<object | null>(null); const [taskToDisplay, setTaskToDisplay] = useState<object | null>(null);
const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>(''); const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>('');
const [editingTaskData, setEditingTaskData] = useState<boolean>(false); const [editingTaskData, setEditingTaskData] = useState<boolean>(false);
const [selectingEvent, setSelectingEvent] = useState<boolean>(false);
const [eventToSend, setEventToSend] = useState<any>({});
const [eventPayload, setEventPayload] = useState<string>('{}');
const setErrorMessage = (useContext as any)(ErrorContext)[1]; const setErrorMessage = (useContext as any)(ErrorContext)[1];
@ -404,8 +408,34 @@ export default function ProcessInstanceShow() {
); );
}; };
const cancelEditingTaskData = () => { const canSendEvent = (task: any) => {
// We actually could allow this for any waiting events
const taskTypes = ['Event Based Gateway'];
return (
taskTypes.filter((t) => t === task.type).length > 0 &&
task.state === 'WAITING' &&
showingLastSpiffStep(processInstance as any)
);
};
const getEvents = (task: any) => {
const handleMessage = (eventDefinition: any) => {
if (eventDefinition.typename === 'MessageEventDefinition') {
delete eventDefinition.message_var;
eventDefinition.payload = {};
}
return eventDefinition;
};
if (task.event_definition && task.event_definition.event_definitions)
return task.event_definition.event_definitions.map((e: any) => handleMessage(e));
if (task.event_definition)
return [handleMessage(task.event_definition)];
return [];
};
const cancelUpdatingTask = () => {
setEditingTaskData(false); setEditingTaskData(false);
setSelectingEvent(false);
initializeTaskDataToDisplay(taskToDisplay); initializeTaskDataToDisplay(taskToDisplay);
setErrorMessage(null); setErrorMessage(null);
}; };
@ -446,6 +476,18 @@ export default function ProcessInstanceShow() {
}); });
}; };
const sendEvent = () => {
if ('payload' in eventToSend)
eventToSend.payload = JSON.parse(eventPayload);
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/event`,
httpMethod: 'POST',
successCallback: saveTaskDataResult,
failureCallback: saveTaskDataFailure,
postBody: eventToSend,
});
};
const taskDataButtons = (task: any) => { const taskDataButtons = (task: any) => {
const buttons = []; const buttons = [];
@ -460,7 +502,7 @@ export default function ProcessInstanceShow() {
); );
} }
if (canEditTaskData(task)) { if (canEditTaskData(task) || canSendEvent(task)) {
if (editingTaskData) { if (editingTaskData) {
buttons.push( buttons.push(
<Button <Button
@ -473,7 +515,24 @@ export default function ProcessInstanceShow() {
buttons.push( buttons.push(
<Button <Button
data-qa="create-script-unit-test-button" data-qa="create-script-unit-test-button"
onClick={cancelEditingTaskData} onClick={cancelUpdatingTask}
>
Cancel
</Button>
);
} else if (selectingEvent) {
buttons.push(
<Button
data-qa="create-script-unit-test-button"
onClick={sendEvent}
>
Send
</Button>
);
buttons.push(
<Button
data-qa="create-script-unit-test-button"
onClick={cancelUpdatingTask}
> >
Cancel Cancel
</Button> </Button>
@ -487,6 +546,16 @@ export default function ProcessInstanceShow() {
Edit Edit
</Button> </Button>
); );
if (canSendEvent(task)) {
buttons.push(
<Button
data-qa="create-script-unit-test-button"
onClick={() => setSelectingEvent(true)}
>
Send Event
</Button>
);
}
} }
} }
@ -507,8 +576,35 @@ export default function ProcessInstanceShow() {
); );
}; };
const taskDataDisplayArea = () => { const eventSelector = (candidateEvents: any) => {
const editor = (
<Editor
height={300}
width="auto"
defaultLanguage="json"
onChange={(value: any) => setEventPayload(value || '{}')}
/>
)
return selectingEvent ? (
<Stack orientation="vertical">
<Dropdown
id="process-instance-select-event"
titleText="Event"
label="Select Event"
items={candidateEvents}
itemToString={(item: any) => item.name || item.label || item.typename}
onChange={(value: any) => setEventToSend(value.selectedItem)}
/>
{'payload' in eventToSend ? editor : ''}
</Stack>
) : (
taskDataContainer()
);
};
const taskUpdateDisplayArea = () => {
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay }; const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
const candidateEvents: any = getEvents(taskToUse);
if (taskToDisplay) { if (taskToDisplay) {
return ( return (
<Modal <Modal
@ -520,7 +616,7 @@ export default function ProcessInstanceShow() {
{taskToUse.name} ({taskToUse.type}): {taskToUse.state} {taskToUse.name} ({taskToUse.type}): {taskToUse.state}
{taskDataButtons(taskToUse)} {taskDataButtons(taskToUse)}
</Stack> </Stack>
{taskDataContainer()} {selectingEvent ? eventSelector(candidateEvents) : taskDataContainer()}
</Modal> </Modal>
); );
} }
@ -592,7 +688,7 @@ export default function ProcessInstanceShow() {
<br /> <br />
{getInfoTag(processInstanceToUse)} {getInfoTag(processInstanceToUse)}
<br /> <br />
{taskDataDisplayArea()} {taskUpdateDisplayArea()}
{stepsElement(processInstanceToUse)} {stepsElement(processInstanceToUse)}
<br /> <br />
<ReactDiagramEditor <ReactDiagramEditor