Improving error management

This commit is contained in:
2025-07-12 17:45:30 +02:00
parent 2754312141
commit bdd954b243
5 changed files with 81 additions and 30 deletions

View File

@@ -1,6 +1,11 @@
const tooltipElementId = "mmt-app"
function bindTooltipsWithDelegation() {
// To display the tooltip, the attribute 'data-tooltip' is mandatory => it contains the text to tooltip
// Then
// the 'truncate' to show only when the text is truncated
// the class 'mmt-tooltip' for force the display
const elementId = tooltipElementId
console.debug("bindTooltips on element " + elementId);

View File

@@ -43,7 +43,7 @@ def post(session, _id: str, component_type: str, x: int, y: int):
@rt(Routes.MoveComponent)
def post(session, _id: str, component_id: str, x: int, y: int):
def post(session, _id: str, component_id: str, x: float, y: float):
logger.debug(
f"Entering {Routes.MoveComponent} with args {debug_session(session)}, {_id=}, {component_id=}, {x=}, {y=}")
instance = InstanceManager.get(session, _id)
@@ -133,3 +133,10 @@ def post(session, _id: str, tab_boundaries: str):
f"Entering {Routes.PlayWorkflow} with args {debug_session(session)}, {_id=}")
instance = InstanceManager.get(session, _id)
return instance.play_workflow(json.loads(tab_boundaries))
@rt(Routes.StopWorkflow)
def post(session, _id: str, tab_boundaries: str):
logger.debug(
f"Entering {Routes.StopWorkflow} with args {debug_session(session)}, {_id=}")
instance = InstanceManager.get(session, _id)
return instance.stop_workflow(json.loads(tab_boundaries))

View File

@@ -114,7 +114,6 @@
}
.wkf-component-content.not-run {
background: var(--color-neutral);
}
.wkf-connection-line {

View File

@@ -198,12 +198,18 @@ class WorkflowDesigner(BaseComponent):
self._error_message = self._player.global_error
else:
# change the tab and display the results
self._player.set_boundaries(boundaries)
self.tabs_manager.add_tab(f"Workflow {self._designer_settings.workflow_name}", self._player, self._player.key)
return self.tabs_manager.refresh()
def stop_workflow(self, boundaries):
self._error_message = None
self._player.run()
return self.tabs_manager.refresh()
def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
if component_id in self._state.components:
component = self._state.components[component_id]
@@ -271,6 +277,12 @@ class WorkflowDesigner(BaseComponent):
state_class = 'not-run' # To be styled as greyed-out
else:
state_class = ''
if runtime_state.state == ComponentState.FAILURE:
tooltip_content = runtime_state.error_message
tooltip_class = "mmt-tooltip"
else:
tooltip_content = None
tooltip_class = ""
return Div(
# Input connection point
@@ -290,9 +302,10 @@ class WorkflowDesigner(BaseComponent):
data_component_id=component.id,
data_point_type="output"),
cls=f"wkf-workflow-component w-32 {'selected' if is_selected else ''}",
cls=f"wkf-workflow-component w-32 {'selected' if is_selected else ''} {tooltip_class}",
style=f"left: {component.x}px; top: {component.y}px;",
data_component_id=component.id,
data_tooltip=tooltip_content,
draggable="true"
)
@@ -303,7 +316,7 @@ class WorkflowDesigner(BaseComponent):
# Render components
*[self._mk_component(comp, state) for comp, state in zip(self._state.components.values(),
self._player.runtime_states)],
self._player.runtime_states.values())],
)
def _mk_canvas(self, oob=False):
@@ -337,8 +350,8 @@ class WorkflowDesigner(BaseComponent):
def _mk_media(self):
return Div(
mk_icon(icon_play, cls="mr-1", **self.commands.play_workflow()),
mk_icon(icon_pause, cls="mr-1", **self.commands.play_workflow()),
mk_icon(icon_stop, cls="mr-1", **self.commands.play_workflow()),
mk_icon(icon_pause, cls="mr-1", **self.commands.pause_workflow()),
mk_icon(icon_stop, cls="mr-1", **self.commands.stop_workflow()),
cls=f"media-controls flex m-2"
)

View File

@@ -1,4 +1,5 @@
from collections import deque
from dataclasses import dataclass
import pandas as pd
from fasthtml.components import *
@@ -22,6 +23,12 @@ grid_settings = DataGridSettings(
open_settings_visible=False)
@dataclass
class WorkflowsPlayerError(Exception):
component_id: str
error: Exception
class WorkflowPlayer(BaseComponent):
def __init__(self, session,
_id=None,
@@ -42,16 +49,16 @@ class WorkflowPlayer(BaseComponent):
key=self.key,
grid_settings=grid_settings,
boundaries=boundaries)
self.runtime_states = [WorkflowComponentRuntimeState(component.id) for component in player_settings.components]
self.runtime_states = {component.id: WorkflowComponentRuntimeState(component.id)
for component in player_settings.components}
self.global_error = None
self.has_error = False
def set_boundaries(self, boundaries: dict):
self._datagrid.set_boundaries(boundaries)
def run(self):
# Reset all component states to NOT_RUN before execution
for state in self.runtime_states:
state.state = ComponentState.NOT_RUN
state.error_message = None
self.global_error = None
self._reset_state(state=ComponentState.NOT_RUN)
components_by_id = {c.id: c for c in self._player_settings.components}
@@ -63,10 +70,16 @@ class WorkflowPlayer(BaseComponent):
self._datagrid.init_from_dataframe(pd.DataFrame([]))
return
engine = self._get_engine()
res = engine.run_to_list()
try:
engine = self._get_engine()
except WorkflowsPlayerError as ex:
if ex.component_id in self.runtime_states:
self.runtime_states[ex.component_id].state = ComponentState.FAILURE
self.runtime_states[ex.component_id].error_message = str(ex.error)
self.global_error = f"Failed to init component '{ex.component_id}': {ex.error}"
return
runtime_states_by_id = {rs.id: rs for rs in self.runtime_states}
res = engine.run_to_list()
if engine.has_error:
self.has_error = True
@@ -78,7 +91,7 @@ class WorkflowPlayer(BaseComponent):
# Determine component states by simulating a "stop-on-fail" execution
first_failure_found = False
for component in sorted_components:
runtime_state = runtime_states_by_id.get(component.id)
runtime_state = self.runtime_states.get(component.id)
if not runtime_state:
continue
@@ -108,6 +121,9 @@ class WorkflowPlayer(BaseComponent):
df = pd.DataFrame(data)
self._datagrid.init_from_dataframe(df)
def stop(self):
self._reset_state()
def __ft__(self):
return Div(
self._datagrid,
@@ -172,21 +188,32 @@ class WorkflowPlayer(BaseComponent):
sorted_components = self._get_sorted_components()
engine = WorkflowEngine()
for component in sorted_components:
if component.type == ProcessorTypes.Producer and component.properties["processor_name"] == "Repository":
engine.add_processor(
TableDataProducer(self._session,
self._settings_manager,
component.id,
component.properties["repository"],
component.properties["table"]))
elif component.type == ProcessorTypes.Filter and component.properties["processor_name"] == "Default":
engine.add_processor(DefaultDataFilter(component.id, component.properties["filter"]))
elif component.type == ProcessorTypes.Presenter and component.properties["processor_name"] == "Default":
engine.add_processor(DefaultDataPresenter(component.id, component.properties["columns"]))
try:
if component.type == ProcessorTypes.Producer and component.properties["processor_name"] == "Repository":
engine.add_processor(
TableDataProducer(self._session,
self._settings_manager,
component.id,
component.properties["repository"],
component.properties["table"]))
elif component.type == ProcessorTypes.Filter and component.properties["processor_name"] == "Default":
engine.add_processor(DefaultDataFilter(component.id, component.properties["filter"]))
elif component.type == ProcessorTypes.Presenter and component.properties["processor_name"] == "Default":
engine.add_processor(DefaultDataPresenter(component.id, component.properties["columns"]))
except Exception as e:
raise WorkflowsPlayerError(component.id, e)
return engine
def _reset_state(self, state: ComponentState = ComponentState.SUCCESS):
self.global_error = None
self.has_error = False
for runtime_state in self.runtime_states.values():
runtime_state.state = state
runtime_state.error_message = None
@staticmethod
def create_component_id(session, suffix=None):
prefix = f"{WORKFLOW_PLAYER_INSTANCE_ID}{session['user_id']}"