I can show WorkflowPlayer tab

This commit is contained in:
2025-07-06 12:17:20 +02:00
parent 60872a0aec
commit e183584f52
9 changed files with 173 additions and 6 deletions

View File

@@ -29,7 +29,7 @@ class DataGridDbManager:
def __init__(self, session: dict, settings_manager: SettingsManager, key: tuple): def __init__(self, session: dict, settings_manager: SettingsManager, key: tuple):
self._session = session self._session = session
self._settings_manager = settings_manager self._settings_manager = settings_manager
self._key = "#".join(make_safe_id(item) for item in key) if key else "" self._key = self._key_as_string(key)
# init the db if needed # init the db if needed
if self._settings_manager and not self._settings_manager.exists(self._session, self._get_db_entry()): if self._settings_manager and not self._settings_manager.exists(self._session, self._get_db_entry()):
@@ -38,6 +38,16 @@ class DataGridDbManager:
def _get_db_entry(self): def _get_db_entry(self):
return f"{DATAGRID_DB_ENTRY}_{self._key}" return f"{DATAGRID_DB_ENTRY}_{self._key}"
@staticmethod
def _key_as_string(key):
if not key:
return ""
if isinstance(key, tuple):
return "#".join(make_safe_id(item) for item in key)
return make_safe_id(key)
def save_settings(self, settings: DataGridSettings): def save_settings(self, settings: DataGridSettings):
if self._settings_manager is None: if self._settings_manager is None:
return return

View File

@@ -65,6 +65,7 @@ def post(session, _id: str, from_id: str, to_id: str):
instance = InstanceManager.get(session, _id) instance = InstanceManager.get(session, _id)
return instance.add_connection(from_id, to_id) return instance.add_connection(from_id, to_id)
@rt(Routes.DeleteConnection) @rt(Routes.DeleteConnection)
def post(session, _id: str, from_id: str, to_id: str): def post(session, _id: str, from_id: str, to_id: str):
logger.debug( logger.debug(
@@ -72,6 +73,7 @@ def post(session, _id: str, from_id: str, to_id: str):
instance = InstanceManager.get(session, _id) instance = InstanceManager.get(session, _id)
return instance.delete_connection(from_id, to_id) return instance.delete_connection(from_id, to_id)
@rt(Routes.ResizeDesigner) @rt(Routes.ResizeDesigner)
def post(session, _id: str, designer_height: int): def post(session, _id: str, designer_height: int):
logger.debug( logger.debug(
@@ -122,4 +124,12 @@ def post(session, _id: str, component_id: str, event_name: str, details: dict):
details.pop("_id") details.pop("_id")
details.pop("component_id") details.pop("component_id")
details.pop("event_name") details.pop("event_name")
return instance.on_processor_details_event(component_id, event_name, details) return instance.on_processor_details_event(component_id, event_name, details)
@rt(Routes.PlayWorkflow)
def post(session, _id: str, tab_boundaries: str):
logger.debug(
f"Entering {Routes.PlayWorkflow} with args {debug_session(session)}, {_id=}")
instance = InstanceManager.get(session, _id)
return instance.play_workflow(json.loads(tab_boundaries))

View File

@@ -0,0 +1,25 @@
from fastcore.basics import NotStr
# Fluent Play20Filled
icon_play = NotStr(
"""<svg name="play" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M17.222 8.685a1.5 1.5 0 0 1 0 2.628l-10 5.498A1.5 1.5 0 0 1 5 15.496V4.502a1.5 1.5 0 0 1 2.223-1.314l10 5.497z" fill="currentColor"></path></g></svg>""")
# Fluent Pause20Filled
icon_pause = NotStr(
"""<svg name="pause" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M12 3.5A1.5 1.5 0 0 1 13.5 2h2A1.5 1.5 0 0 1 17 3.5v13a1.5 1.5 0 0 1-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5v-13zm-9 0A1.5 1.5 0 0 1 4.5 2h2A1.5 1.5 0 0 1 8 3.5v13A1.5 1.5 0 0 1 6.5 18h-2A1.5 1.5 0 0 1 3 16.5v-13z" fill="currentColor"></path></g></svg>""")
# Fluent Stop20Filled
icon_stop = NotStr(
"""<svg name="stop" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M4.5 3A1.5 1.5 0 0 0 3 4.5v11A1.5 1.5 0 0 0 4.5 17h11a1.5 1.5 0 0 0 1.5-1.5v-11A1.5 1.5 0 0 0 15.5 3h-11z" fill="currentColor"></path></g></svg>""")
# fluent PlayCircle20Regular
icon_play_circle = NotStr(
"""<svg name="play" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M9.125 7.184A.75.75 0 0 0 8 7.834v4.333a.75.75 0 0 0 1.125.65l4.125-2.384a.5.5 0 0 0 0-.866L9.125 7.184zM2 10a8 8 0 1 1 16 0a8 8 0 0 1-16 0zm8-7a7 7 0 1 0 0 14a7 7 0 0 0 0-14z" fill="currentColor"></path></g></svg>""")
# fluent PauseCircle20Regular
icon_pause_circle = NotStr(
"""<svg name="pause" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M9 7.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5zm3 0a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5zM10 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16zm-7 8a7 7 0 1 1 14 0a7 7 0 0 1-14 0z" fill="currentColor"></path></g></svg>""")
# fluent RecordStop20Regular
icon_stop_circle = NotStr(
"""<svg name="stop" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M10 3a7 7 0 1 0 0 14a7 7 0 0 0 0-14zm-8 7a8 8 0 1 1 16 0a8 8 0 0 1-16 0zm5-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V8z" fill="currentColor"></path></g></svg>""")

View File

@@ -34,7 +34,7 @@ class WorkflowDesignerCommandManager(BaseCommandManager):
def __init__(self, owner): def __init__(self, owner):
super().__init__(owner) super().__init__(owner)
def select_processor(self, component_id :str): def select_processor(self, component_id: str):
return { return {
"hx_post": f"{ROUTE_ROOT}{Routes.SelectProcessor}", "hx_post": f"{ROUTE_ROOT}{Routes.SelectProcessor}",
"hx-target": f"#p_{self._id}", "hx-target": f"#p_{self._id}",
@@ -67,3 +67,32 @@ class WorkflowDesignerCommandManager(BaseCommandManager):
"hx-swap": "outerHTML", "hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}", "component_id": "{component_id}", "event_name": "{event_name}"}}', "hx-vals": f'js:{{"_id": "{self._id}", "component_id": "{component_id}", "event_name": "{event_name}"}}',
} }
def play_workflow(self):
return {
"hx_post": f"{ROUTE_ROOT}{Routes.PlayWorkflow}",
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}", "tab_boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
}
def pause_workflow(self):
return {
"hx_post": f"{ROUTE_ROOT}{Routes.PauseWorkflow}",
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}", "tab_boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
}
def stop_workflow(self):
return {
"hx_post": f"{ROUTE_ROOT}{Routes.StopWorkflow}",
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
"hx-swap": "outerHTML",
"hx-vals": f'js:{{"_id": "{self._id}", "tab_boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
}
class WorkflowPlayerCommandManager(BaseCommandManager):
def __init__(self, owner):
super().__init__(owner)

View File

@@ -5,11 +5,14 @@ from fasthtml.components import *
from fasthtml.xtend import Script from fasthtml.xtend import Script
from components.BaseComponent import BaseComponent from components.BaseComponent import BaseComponent
from components.workflows.assets.icons import icon_play, icon_pause, icon_stop
from components.workflows.commands import WorkflowDesignerCommandManager from components.workflows.commands import WorkflowDesignerCommandManager
from components.workflows.components.WorkflowPlayer import WorkflowPlayer
from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID, ProcessorTypes from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID, ProcessorTypes
from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowComponent, \ from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowComponent, \
Connection, WorkflowsDesignerDbManager Connection, WorkflowsDesignerDbManager, WorkflowsPlayerSettings
from components_helpers import apply_boundaries, mk_tooltip, mk_dialog_buttons from components_helpers import apply_boundaries, mk_tooltip, mk_dialog_buttons, mk_icon
from core.instance_manager import InstanceManager
from core.utils import get_unique_id, make_safe_id from core.utils import get_unique_id, make_safe_id
from utils.DbManagementHelper import DbManagementHelper from utils.DbManagementHelper import DbManagementHelper
@@ -47,11 +50,13 @@ class WorkflowDesigner(BaseComponent):
def __init__(self, session, def __init__(self, session,
_id=None, _id=None,
settings_manager=None, settings_manager=None,
tabs_manager=None,
key: str = None, key: str = None,
designer_settings: WorkflowsDesignerSettings = None, designer_settings: WorkflowsDesignerSettings = None,
boundaries: dict = None): boundaries: dict = None):
super().__init__(session, _id) super().__init__(session, _id)
self._settings_manager = settings_manager self._settings_manager = settings_manager
self.tabs_manager = tabs_manager
self._key = key self._key = key
self._designer_settings = designer_settings self._designer_settings = designer_settings
self._db = WorkflowsDesignerDbManager(session, settings_manager) self._db = WorkflowsDesignerDbManager(session, settings_manager)
@@ -169,6 +174,23 @@ class WorkflowDesigner(BaseComponent):
self._db.save_state(self._key, self._state) self._db.save_state(self._key, self._state)
return self.refresh_properties() return self.refresh_properties()
def play_workflow(self, boundaries: dict):
if self._state.selected_component_id is None:
return self.error_message("No component selected")
workflow_name = self._designer_settings.workflow_name
player = InstanceManager.get(self._session,
WorkflowPlayer.create_component_id(self._session, workflow_name),
WorkflowPlayer,
settings_manager=self._settings_manager,
tabs_manager=self.tabs_manager,
player_settings=WorkflowsPlayerSettings(workflow_name),
boundaries=boundaries)
self.tabs_manager.add_tab(f"Workflow {workflow_name}", player, player.key)
# player.start(self._state.selected_component_id)
return self.tabs_manager.refresh()
def on_processor_details_event(self, component_id: str, event_name: str, details: dict): def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
if component_id in self._state.components: if component_id in self._state.components:
component = self._state.components[component_id] component = self._state.components[component_id]
@@ -183,6 +205,7 @@ class WorkflowDesigner(BaseComponent):
return Div( return Div(
H1(f"{self._designer_settings.workflow_name}", cls="text-xl font-bold"), H1(f"{self._designer_settings.workflow_name}", cls="text-xl font-bold"),
P("Drag components from the toolbox to the canvas to create your workflow.", cls="text-sm mb-6"), P("Drag components from the toolbox to the canvas to create your workflow.", cls="text-sm mb-6"),
self._mk_media(),
self._mk_designer(), self._mk_designer(),
Div(cls="wkf-splitter", id=f"s_{self._id}"), Div(cls="wkf-splitter", id=f"s_{self._id}"),
self._mk_properties(), self._mk_properties(),
@@ -259,6 +282,14 @@ class WorkflowDesigner(BaseComponent):
style=f"height:{self._state.designer_height}px;" style=f"height:{self._state.designer_height}px;"
) )
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()),
cls=f"media-controls flex m-2"
)
def _mk_processor_properties(self, component, processor_name): def _mk_processor_properties(self, component, processor_name):
if processor_name == "Jira": if processor_name == "Jira":
return self._mk_jira_processor_details(component) return self._mk_jira_processor_details(component)

View File

@@ -0,0 +1,51 @@
from fasthtml.components import *
from components.BaseComponent import BaseComponent
from components.datagrid_new.components.DataGrid import DataGrid
from components.datagrid_new.settings import DataGridSettings
from components.workflows.commands import WorkflowPlayerCommandManager
from components.workflows.constants import WORKFLOW_PLAYER_INSTANCE_ID
from components.workflows.db_management import WorkflowsPlayerSettings
from core.utils import get_unique_id, make_safe_id
grid_settings = DataGridSettings(
header_visible=True,
filter_all_visible=True,
views_visible=False,
open_file_visible=False,
open_settings_visible=False)
class WorkflowPlayer(BaseComponent):
def __init__(self, session,
_id=None,
settings_manager=None,
tabs_manager=None,
player_settings: WorkflowsPlayerSettings = None,
boundaries: dict = None):
super().__init__(session, _id)
self._settings_manager = settings_manager
self.tabs_manager = tabs_manager
self.key = f"__WorkflowPlayer_{player_settings.workflow_name}"
self._player_settings = player_settings
self._boundaries = boundaries
self.commands = WorkflowPlayerCommandManager(self)
self._datagrid = DataGrid(self._session,
DataGrid.create_component_id(session),
self.key,
grid_settings=grid_settings,
boundaries=boundaries)
def __ft__(self):
return Div(
self._datagrid,
id=self._id,
)
@staticmethod
def create_component_id(session, suffix=None):
prefix = f"{WORKFLOW_PLAYER_INSTANCE_ID}{session['user_id']}"
if suffix is None:
suffix = get_unique_id()
return make_safe_id(f"{prefix}{suffix}")

View File

@@ -101,6 +101,7 @@ class Workflows(BaseComponentSingleton):
WorkflowDesigner.create_component_id(self._session, workflow_name), WorkflowDesigner.create_component_id(self._session, workflow_name),
WorkflowDesigner, WorkflowDesigner,
settings_manager=self._settings_manager, settings_manager=self._settings_manager,
tabs_manager=self.tabs_manager,
key=workflow_name, key=workflow_name,
designer_settings=WorkflowsDesignerSettings(workflow_name=workflow_name), designer_settings=WorkflowsDesignerSettings(workflow_name=workflow_name),
boundaries=tab_boundaries) boundaries=tab_boundaries)

View File

@@ -1,5 +1,6 @@
WORKFLOWS_INSTANCE_ID = "__Workflows__" WORKFLOWS_INSTANCE_ID = "__Workflows__"
WORKFLOW_DESIGNER_INSTANCE_ID = "__WorkflowDesigner__" WORKFLOW_DESIGNER_INSTANCE_ID = "__WorkflowDesigner__"
WORKFLOW_PLAYER_INSTANCE_ID = "__WorkflowPlayer__"
WORKFLOWS_DB_ENTRY = "Workflows" WORKFLOWS_DB_ENTRY = "Workflows"
WORKFLOW_DESIGNER_DB_ENTRY = "WorkflowDesigner" WORKFLOW_DESIGNER_DB_ENTRY = "WorkflowDesigner"
WORKFLOW_DESIGNER_DB_SETTINGS_ENTRY = "Settings" WORKFLOW_DESIGNER_DB_SETTINGS_ENTRY = "Settings"
@@ -27,4 +28,8 @@ class Routes:
SaveProperties = "/save-properties" SaveProperties = "/save-properties"
CancelProperties = "/cancel-properties" CancelProperties = "/cancel-properties"
SelectProcessor = "/select-processor" SelectProcessor = "/select-processor"
OnProcessorDetailsEvent = "/on-processor-details-event" OnProcessorDetailsEvent = "/on-processor-details-event"
PlayWorkflow = "/play-workflow"
PauseWorkflow = "/pause-workflow"
StopWorkflow = "/stop-workflow"

View File

@@ -41,6 +41,11 @@ class WorkflowsDesignerState:
selected_component_id = None selected_component_id = None
@dataclass
class WorkflowsPlayerSettings:
workflow_name: str = "No Name"
@dataclass @dataclass
class WorkflowsSettings: class WorkflowsSettings:
workflows: list[str] = field(default_factory=list) workflows: list[str] = field(default_factory=list)