Assure we save after every chunk of tasks get completed on the interstital endpoint.

Fix a merge screwup in the workflow execution service
Add an instructions for end user component so we can reuse the logic in multiple places.
Split up the intersitital page so that it clears out the interval timer.
This commit is contained in:
Dan 2023-04-17 14:43:30 -04:00
parent 19d4fb9981
commit 47a3332ed3
5 changed files with 101 additions and 29 deletions

View File

@ -404,6 +404,9 @@ def interstitial(process_instance_id: int):
last_task = spiff_task last_task = spiff_task
processor.do_engine_steps(execution_strategy_name="one_at_a_time") processor.do_engine_steps(execution_strategy_name="one_at_a_time")
spiff_task = processor.next_task() spiff_task = processor.next_task()
# Note, this has to be done in case someone leaves the page,
# which can cancel this function before saving.
processor.save() # Fixme - maybe find a way not to do this on every method?
return return
# return Response(get_data(), mimetype='text/event-stream') # return Response(get_data(), mimetype='text/event-stream')

View File

@ -331,6 +331,15 @@ class WorkflowExecutionService:
# execution_strategy.spiff_run # execution_strategy.spiff_run
# spiff.[some_run_task_method] # spiff.[some_run_task_method]
def run_and_save(self, exit_at: None = None, save: bool = False) -> None: def run_and_save(self, exit_at: None = None, save: bool = False) -> None:
"""Do_engine_steps."""
with safe_assertion(ProcessInstanceLockService.has_lock(self.process_instance_model.id)) as tripped:
if tripped:
raise AssertionError(
"The current thread has not obtained a lock for this process"
f" instance ({self.process_instance_model.id})."
)
try:
self.bpmn_process_instance.refresh_waiting_tasks() self.bpmn_process_instance.refresh_waiting_tasks()
# TODO: implicit re-entrant locks here `with_dequeued` # TODO: implicit re-entrant locks here `with_dequeued`
@ -339,9 +348,17 @@ class WorkflowExecutionService:
if self.bpmn_process_instance.is_completed(): if self.bpmn_process_instance.is_completed():
self.process_instance_completer(self.bpmn_process_instance) self.process_instance_completer(self.bpmn_process_instance)
self.execution_strategy.spiff_run(self.bpmn_process_instance, exit_at) self.process_bpmn_messages()
if self.bpmn_process_instance.is_completed(): self.queue_waiting_receive_messages()
self.process_instance_completer(self.bpmn_process_instance) except SpiffWorkflowException as swe:
raise ApiError.from_workflow_exception("task_error", str(swe), swe) from swe
finally:
self.execution_strategy.save(self.bpmn_process_instance)
db.session.commit()
if save:
self.process_instance_saver()
def process_bpmn_messages(self) -> None: def process_bpmn_messages(self) -> None:
"""Process_bpmn_messages.""" """Process_bpmn_messages."""

View File

@ -0,0 +1,27 @@
import React from 'react';
// @ts-ignore
import MDEditor from '@uiw/react-md-editor';
export default function InstructionsForEndUser({ task }: any) {
if (!task) {
return null;
}
let instructions = '';
console.log("I was passed a task: ", task);
const { properties } = task;
const { instructionsForEndUser } = properties;
if (instructionsForEndUser) {
instructions = instructionsForEndUser;
}
return (
<div className="markdown">
{/*
https://www.npmjs.com/package/@uiw/react-md-editor switches to dark mode by default by respecting @media (prefers-color-scheme: dark)
This makes it look like our site is broken, so until the rest of the site supports dark mode, turn off dark mode for this component.
*/}
<div data-color-mode="light">
<MDEditor.Markdown source={instructions} />
</div>
</div>
);
}

View File

@ -1,12 +1,16 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import {useNavigate, useParams} from 'react-router-dom';
import { fetchEventSource } from '@microsoft/fetch-event-source'; import { fetchEventSource } from '@microsoft/fetch-event-source';
import { BACKEND_BASE_URL } from '../config'; import { BACKEND_BASE_URL } from '../config';
import { getBasicHeaders } from '../services/HttpService'; import { getBasicHeaders } from '../services/HttpService';
import {Task} from '../interfaces';
import InstructionsForEndUser from '../components/InstructionsForEndUser';
export default function ProcessInterstitial() { export default function ProcessInterstitial() {
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [lastTask, setLastTask] = useState<any>(null);
const params = useParams(); const params = useParams();
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
fetchEventSource( fetchEventSource(
@ -15,12 +19,53 @@ export default function ProcessInterstitial() {
headers: getBasicHeaders(), headers: getBasicHeaders(),
onmessage(ev) { onmessage(ev) {
console.log(data, ev.data); console.log(data, ev.data);
const parsedData = JSON.parse(ev.data); const task = JSON.parse(ev.data);
setData((data) => [...data, parsedData]); setData((prevData) => [...prevData, task]);
setLastTask(task);
},
onclose() {
console.log("Connection Closed by the Server");
}, },
} }
); );
}, []); }, []);
return <div className="App">The last streamed item was: {data}</div>; useEffect(() => {
// Added this seperate use effect so that the timer interval will be cleared if
// we end up redirecting back to the TaskShow page.
if (lastTask && ['User Task', 'Manual Task'].includes(lastTask.type)) {
const timerId = setInterval(() => {
navigate(`/tasks/${lastTask.process_instance_id}/${lastTask.id}`);
}, 1000);
return () => clearInterval(timerId);
}
return undefined;
}, [lastTask]);
return (
<div className="container">
<h3 className="p-3 text-center">React - Display a list of items</h3>
<table className="table table-striped table-bordered">
<thead>
<tr>
<th>Task Title</th>
</tr>
</thead>
<tbody>
{data &&
data.map((d) => (
<tr key={d.id}>
<td>{d.title}</td>
<td>{d.state}</td>
<td>{d.type}</td>
<td>
<InstructionsForEndUser task={d} />
</td>
</tr>
))}
</tbody>
</table>
</div>
);
} }

View File

@ -23,6 +23,7 @@ import useAPIError from '../hooks/UseApiError';
import { modifyProcessIdentifierForPathParam } from '../helpers'; import { modifyProcessIdentifierForPathParam } from '../helpers';
import { ProcessInstanceTask } from '../interfaces'; import { ProcessInstanceTask } from '../interfaces';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import InstructionsForEndUser from '../components/InstructionsForEndUser';
// TODO: move this somewhere else // TODO: move this somewhere else
function TypeAheadWidget({ function TypeAheadWidget({
@ -402,27 +403,6 @@ export default function TaskShow() {
); );
}; };
const instructionsElement = () => {
if (!task) {
return null;
}
let instructions = '';
if (task.properties.instructionsForEndUser) {
instructions = task.properties.instructionsForEndUser;
}
return (
<div className="markdown">
{/*
https://www.npmjs.com/package/@uiw/react-md-editor switches to dark mode by default by respecting @media (prefers-color-scheme: dark)
This makes it look like our site is broken, so until the rest of the site supports dark mode, turn off dark mode for this component.
*/}
<div data-color-mode="light">
<MDEditor.Markdown source={instructions} />
</div>
</div>
);
};
if (task) { if (task) {
let statusString = ''; let statusString = '';
if (task.state !== 'READY') { if (task.state !== 'READY') {
@ -446,7 +426,7 @@ export default function TaskShow() {
<h3> <h3>
Task: {task.title} ({task.process_model_display_name}){statusString} Task: {task.title} ({task.process_model_display_name}){statusString}
</h3> </h3>
{instructionsElement()} <InstructionsForEndUser task={task} />
{formElement()} {formElement()}
</main> </main>
); );