diff --git a/src/components/datagrid_new/db_management.py b/src/components/datagrid_new/db_management.py index ee8f685..a49e31e 100644 --- a/src/components/datagrid_new/db_management.py +++ b/src/components/datagrid_new/db_management.py @@ -29,7 +29,7 @@ class DataGridDbManager: def __init__(self, session: dict, settings_manager: SettingsManager, key: tuple): self._session = session 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 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): 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): if self._settings_manager is None: return diff --git a/src/components/workflows/WorkflowsApp.py b/src/components/workflows/WorkflowsApp.py index 6cb9a9e..518e0f3 100644 --- a/src/components/workflows/WorkflowsApp.py +++ b/src/components/workflows/WorkflowsApp.py @@ -65,6 +65,7 @@ def post(session, _id: str, from_id: str, to_id: str): instance = InstanceManager.get(session, _id) return instance.add_connection(from_id, to_id) + @rt(Routes.DeleteConnection) def post(session, _id: str, from_id: str, to_id: str): logger.debug( @@ -72,6 +73,7 @@ def post(session, _id: str, from_id: str, to_id: str): instance = InstanceManager.get(session, _id) return instance.delete_connection(from_id, to_id) + @rt(Routes.ResizeDesigner) def post(session, _id: str, designer_height: int): logger.debug( @@ -122,4 +124,12 @@ def post(session, _id: str, component_id: str, event_name: str, details: dict): details.pop("_id") details.pop("component_id") details.pop("event_name") - return instance.on_processor_details_event(component_id, event_name, details) \ No newline at end of file + 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)) diff --git a/src/components/workflows/assets/icons.py b/src/components/workflows/assets/icons.py new file mode 100644 index 0000000..da6157d --- /dev/null +++ b/src/components/workflows/assets/icons.py @@ -0,0 +1,25 @@ +from fastcore.basics import NotStr + +# Fluent Play20Filled +icon_play = NotStr( + """""") + +# Fluent Pause20Filled +icon_pause = NotStr( + """""") + +# Fluent Stop20Filled +icon_stop = NotStr( + """""") + +# fluent PlayCircle20Regular +icon_play_circle = NotStr( + """""") + +# fluent PauseCircle20Regular +icon_pause_circle = NotStr( + """""") + +# fluent RecordStop20Regular +icon_stop_circle = NotStr( + """""") diff --git a/src/components/workflows/commands.py b/src/components/workflows/commands.py index 8b815c4..4500b38 100644 --- a/src/components/workflows/commands.py +++ b/src/components/workflows/commands.py @@ -34,7 +34,7 @@ class WorkflowDesignerCommandManager(BaseCommandManager): def __init__(self, owner): super().__init__(owner) - def select_processor(self, component_id :str): + def select_processor(self, component_id: str): return { "hx_post": f"{ROUTE_ROOT}{Routes.SelectProcessor}", "hx-target": f"#p_{self._id}", @@ -67,3 +67,32 @@ class WorkflowDesignerCommandManager(BaseCommandManager): "hx-swap": "outerHTML", "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) diff --git a/src/components/workflows/components/WorkflowDesigner.py b/src/components/workflows/components/WorkflowDesigner.py index 50b61a1..a4dc46a 100644 --- a/src/components/workflows/components/WorkflowDesigner.py +++ b/src/components/workflows/components/WorkflowDesigner.py @@ -5,11 +5,14 @@ from fasthtml.components import * from fasthtml.xtend import Script 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.components.WorkflowPlayer import WorkflowPlayer from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID, ProcessorTypes from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowComponent, \ - Connection, WorkflowsDesignerDbManager -from components_helpers import apply_boundaries, mk_tooltip, mk_dialog_buttons + Connection, WorkflowsDesignerDbManager, WorkflowsPlayerSettings +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 utils.DbManagementHelper import DbManagementHelper @@ -47,11 +50,13 @@ class WorkflowDesigner(BaseComponent): def __init__(self, session, _id=None, settings_manager=None, + tabs_manager=None, key: str = None, designer_settings: WorkflowsDesignerSettings = None, boundaries: dict = None): super().__init__(session, _id) self._settings_manager = settings_manager + self.tabs_manager = tabs_manager self._key = key self._designer_settings = designer_settings self._db = WorkflowsDesignerDbManager(session, settings_manager) @@ -169,6 +174,23 @@ class WorkflowDesigner(BaseComponent): self._db.save_state(self._key, self._state) 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): if component_id in self._state.components: component = self._state.components[component_id] @@ -183,6 +205,7 @@ class WorkflowDesigner(BaseComponent): return Div( 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"), + self._mk_media(), self._mk_designer(), Div(cls="wkf-splitter", id=f"s_{self._id}"), self._mk_properties(), @@ -259,6 +282,14 @@ class WorkflowDesigner(BaseComponent): 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): if processor_name == "Jira": return self._mk_jira_processor_details(component) diff --git a/src/components/workflows/components/WorkflowPlayer.py b/src/components/workflows/components/WorkflowPlayer.py new file mode 100644 index 0000000..0694575 --- /dev/null +++ b/src/components/workflows/components/WorkflowPlayer.py @@ -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}") diff --git a/src/components/workflows/components/Workflows.py b/src/components/workflows/components/Workflows.py index 95c4dc1..3cd863f 100644 --- a/src/components/workflows/components/Workflows.py +++ b/src/components/workflows/components/Workflows.py @@ -101,6 +101,7 @@ class Workflows(BaseComponentSingleton): WorkflowDesigner.create_component_id(self._session, workflow_name), WorkflowDesigner, settings_manager=self._settings_manager, + tabs_manager=self.tabs_manager, key=workflow_name, designer_settings=WorkflowsDesignerSettings(workflow_name=workflow_name), boundaries=tab_boundaries) diff --git a/src/components/workflows/constants.py b/src/components/workflows/constants.py index 4688621..e8c56f7 100644 --- a/src/components/workflows/constants.py +++ b/src/components/workflows/constants.py @@ -1,5 +1,6 @@ WORKFLOWS_INSTANCE_ID = "__Workflows__" WORKFLOW_DESIGNER_INSTANCE_ID = "__WorkflowDesigner__" +WORKFLOW_PLAYER_INSTANCE_ID = "__WorkflowPlayer__" WORKFLOWS_DB_ENTRY = "Workflows" WORKFLOW_DESIGNER_DB_ENTRY = "WorkflowDesigner" WORKFLOW_DESIGNER_DB_SETTINGS_ENTRY = "Settings" @@ -27,4 +28,8 @@ class Routes: SaveProperties = "/save-properties" CancelProperties = "/cancel-properties" SelectProcessor = "/select-processor" - OnProcessorDetailsEvent = "/on-processor-details-event" \ No newline at end of file + OnProcessorDetailsEvent = "/on-processor-details-event" + PlayWorkflow = "/play-workflow" + PauseWorkflow = "/pause-workflow" + StopWorkflow = "/stop-workflow" + \ No newline at end of file diff --git a/src/components/workflows/db_management.py b/src/components/workflows/db_management.py index e1cc9c4..a77c2cc 100644 --- a/src/components/workflows/db_management.py +++ b/src/components/workflows/db_management.py @@ -41,6 +41,11 @@ class WorkflowsDesignerState: selected_component_id = None +@dataclass +class WorkflowsPlayerSettings: + workflow_name: str = "No Name" + + @dataclass class WorkflowsSettings: workflows: list[str] = field(default_factory=list)