From f86f4852c7413b9144eeeb800a712d91a534304b Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Fri, 4 Jul 2025 19:03:32 +0200 Subject: [PATCH] I can load and save Jira and Table Processor details --- src/components/workflows/WorkflowsApp.py | 50 +++- src/components/workflows/assets/Workflows.css | 2 - src/components/workflows/assets/Workflows.js | 6 +- src/components/workflows/commands.py | 39 +++ .../workflows/components/WorkflowDesigner.py | 279 +++++++++++++----- src/components/workflows/constants.py | 6 +- src/components/workflows/db_management.py | 3 + src/utils/DbManagementHelper.py | 14 + 8 files changed, 321 insertions(+), 78 deletions(-) create mode 100644 src/utils/DbManagementHelper.py diff --git a/src/components/workflows/WorkflowsApp.py b/src/components/workflows/WorkflowsApp.py index 53648c5..83ab679 100644 --- a/src/components/workflows/WorkflowsApp.py +++ b/src/components/workflows/WorkflowsApp.py @@ -41,6 +41,7 @@ def post(session, _id: str, component_type: str, x: int, y: int): instance = InstanceManager.get(session, _id) return instance.add_component(component_type, x, y) + @rt(Routes.MoveComponent) def post(session, _id: str, component_id: str, x: int, y: int): logger.debug( @@ -56,6 +57,7 @@ def post(session, _id: str, component_id: str): instance = InstanceManager.get(session, _id) return instance.delete_component(component_id) + @rt(Routes.AddConnection) def post(session, _id: str, from_id: str, to_id: str): logger.debug( @@ -63,9 +65,55 @@ 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.ResizeDesigner) def post(session, _id: str, designer_height: int): logger.debug( f"Entering {Routes.ResizeDesigner} with args {debug_session(session)}, {_id=}, {designer_height=}") instance = InstanceManager.get(session, _id) - return instance.set_designer_height(designer_height) \ No newline at end of file + return instance.set_designer_height(designer_height) + + +@rt(Routes.SelectComponent) +def post(session, _id: str, component_id: str): + logger.debug( + f"Entering {Routes.SelectComponent} with args {debug_session(session)}, {_id=}, {component_id=}") + instance = InstanceManager.get(session, _id) + return instance.select_component(component_id) + + +@rt(Routes.SaveProperties) +def post(session, _id: str, component_id: str, details: dict): + logger.debug( + f"Entering {Routes.SaveProperties} with args {debug_session(session)}, {_id=}, {component_id=}, {details=}") + instance = InstanceManager.get(session, _id) + details.pop("_id") + details.pop("component_id") + return instance.save_properties(component_id, details) + + +@rt(Routes.CancelProperties) +def post(session, _id: str, component_id: str): + logger.debug( + f"Entering {Routes.CancelProperties} with args {debug_session(session)}, {_id=}, {component_id=}") + instance = InstanceManager.get(session, _id) + return instance.cancel_properties(component_id) + + +@rt(Routes.SelectProcessor) +def post(session, _id: str, component_id: str, processor_name: str): + logger.debug( + f"Entering {Routes.SelectProcessor} with args {debug_session(session)}, {_id=}, {component_id=}, {processor_name=}") + instance = InstanceManager.get(session, _id) + return instance.set_selected_processor(component_id, processor_name) + + +@rt(Routes.OnProcessorDetailsEvent) +def post(session, _id: str, component_id: str, event_name: str, details: dict): + logger.debug( + f"Entering {Routes.OnProcessorDetailsEvent} with args {debug_session(session)}, {_id=}, {component_id=}, {event_name=}, {details=}") + instance = InstanceManager.get(session, _id) + 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 diff --git a/src/components/workflows/assets/Workflows.css b/src/components/workflows/assets/Workflows.css index bff75e4..06bf17a 100644 --- a/src/components/workflows/assets/Workflows.css +++ b/src/components/workflows/assets/Workflows.css @@ -1,6 +1,5 @@ .wkf-toolbox-item { cursor: grab; - color: var(--color-neutral); } .wkf-toolbox-item:active { @@ -74,7 +73,6 @@ border: 2px solid transparent; transition: all 0.2s; height: 64px; - color: var(--color-neutral); } .wkf-workflow-component:hover { diff --git a/src/components/workflows/assets/Workflows.js b/src/components/workflows/assets/Workflows.js index 550692c..4e21283 100644 --- a/src/components/workflows/assets/Workflows.js +++ b/src/components/workflows/assets/Workflows.js @@ -217,10 +217,10 @@ function bindWorkflowDesignerToolbox(elementId) { if (!component) return; componentId = component.dataset.componentId - htmx.ajax('POST', '/workflows/select-connection', { - target: `#c_${elementId}`, + htmx.ajax('POST', '/workflows/select-component', { + target: `#p_${elementId}`, headers: {"Content-Type": "application/x-www-form-urlencoded"}, - swap: "innerHTML", + swap: "outerHTML", values: { _id: elementId, component_id: componentId diff --git a/src/components/workflows/commands.py b/src/components/workflows/commands.py index 0148e92..8b815c4 100644 --- a/src/components/workflows/commands.py +++ b/src/components/workflows/commands.py @@ -28,3 +28,42 @@ class WorkflowsCommandManager(BaseCommandManager): "hx-swap": "outerHTML", "hx-vals": f'js:{{"_id": "{self._id}", "name": "{workflow_name}", "tab_boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}', } + + +class WorkflowDesignerCommandManager(BaseCommandManager): + def __init__(self, owner): + super().__init__(owner) + + def select_processor(self, component_id :str): + return { + "hx_post": f"{ROUTE_ROOT}{Routes.SelectProcessor}", + "hx-target": f"#p_{self._id}", + "hx-swap": "outerHTML", + "hx-trigger": "change", + "hx-vals": f'js:{{"_id": "{self._id}", "component_id": "{component_id}"}}', + } + + def save_properties(self, component_id: str): + return { + "hx_post": f"{ROUTE_ROOT}{Routes.SaveProperties}", + "hx-target": f"#p_{self._id}", + "hx-swap": "outerHTML", + "hx-vals": f'js:{{"_id": "{self._id}", "component_id": "{component_id}"}}', + } + + def cancel_properties(self, component_id: str): + return { + "hx_post": f"{ROUTE_ROOT}{Routes.CancelProperties}", + "hx-target": f"#p_{self._id}", + "hx-swap": "outerHTML", + "hx-vals": f'js:{{"_id": "{self._id}", "component_id": "{component_id}"}}', + } + + def on_processor_details_event(self, component_id: str, event_name: str): + return { + "hx_post": f"{ROUTE_ROOT}{Routes.OnProcessorDetailsEvent}", + "hx-target": f"#p_{self._id}", + "hx-trigger": "change", + "hx-swap": "outerHTML", + "hx-vals": f'js:{{"_id": "{self._id}", "component_id": "{component_id}", "event_name": "{event_name}"}}', + } diff --git a/src/components/workflows/components/WorkflowDesigner.py b/src/components/workflows/components/WorkflowDesigner.py index 7dedeb4..fcf1c7b 100644 --- a/src/components/workflows/components/WorkflowDesigner.py +++ b/src/components/workflows/components/WorkflowDesigner.py @@ -1,13 +1,19 @@ +import logging + from fastcore.basics import NotStr from fasthtml.components import * from fasthtml.xtend import Script from components.BaseComponent import BaseComponent +from components.workflows.commands import WorkflowDesignerCommandManager from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowComponent, \ Connection, WorkflowsDesignerDbManager -from components_helpers import apply_boundaries, mk_tooltip -from core.utils import get_unique_id +from components_helpers import apply_boundaries, mk_tooltip, mk_dialog_buttons +from core.utils import get_unique_id, make_safe_id +from utils.DbManagementHelper import DbManagementHelper + +logger = logging.getLogger("WorkflowDesigner") # Component templates COMPONENT_TYPES = { @@ -15,22 +21,27 @@ COMPONENT_TYPES = { "title": "Data Producer", "description": "Generates or loads data", "icon": "📊", - "color": "bg-green-100 border-green-300" + "color": "bg-green-100 border-green-300 text-neutral" }, "filter": { "title": "Data Filter", "description": "Filters and transforms data", "icon": "🔍", - "color": "bg-blue-100 border-blue-300" + "color": "bg-blue-100 border-blue-300 text-neutral" }, "presenter": { "title": "Data Presenter", "description": "Displays or exports data", "icon": "📋", - "color": "bg-purple-100 border-purple-300" + "color": "bg-purple-100 border-purple-300 text-neutral" } } +PROCESSOR_TYPES = { + "producer": ["Repository", "Jira"], + "filter": ["Default"], + "presenter": ["Default"]} + class WorkflowDesigner(BaseComponent): def __init__(self, session, @@ -46,13 +57,17 @@ class WorkflowDesigner(BaseComponent): self._db = WorkflowsDesignerDbManager(session, settings_manager) self._state = self._db.load_state(key) self._boundaries = boundaries + self.commands = WorkflowDesignerCommandManager(self) def set_boundaries(self, boundaries: dict): self._boundaries = boundaries - def refresh(self, oob=False): + def refresh_designer(self): return self._mk_elements() + def refresh_properties(self): + return self._mk_properties() + def add_component(self, component_type, x, y): self._state.component_counter += 1 @@ -70,7 +85,7 @@ class WorkflowDesigner(BaseComponent): self._state.components[component_id] = component self._db.save_state(self._key, self._state) # update db - return self.refresh() + return self.refresh_designer() def move_component(self, component_id, x, y): if component_id in self._state.components: @@ -78,7 +93,7 @@ class WorkflowDesigner(BaseComponent): self._state.components[component_id].y = int(y) self._db.save_state(self._key, self._state) # update db - return self.refresh() + return self.refresh_designer() def delete_component(self, component_id): # Remove component @@ -91,13 +106,13 @@ class WorkflowDesigner(BaseComponent): # update db self._db.save_state(self._key, self._state) - return self.refresh() + return self.refresh_designer() def add_connection(self, from_id, to_id): # Check if connection already exists for connection in self._state.connections: if connection.from_id == from_id and connection.to_id == to_id: - return self.refresh() # , self.error_message("Connection already exists") + return self.refresh_designer() # , self.error_message("Connection already exists") connection_id = f"conn_{len(self._state.connections) + 1}" connection = Connection(id=connection_id, from_id=from_id, to_id=to_id) @@ -106,12 +121,48 @@ class WorkflowDesigner(BaseComponent): # update db self._db.save_state(self._key, self._state) - return self.refresh() + return self.refresh_designer() def set_designer_height(self, height): self._state.designer_height = height self._db.save_state(self._key, self._state) - return self.__ft__() # refresh the whole component + return self.__ft__() # refresh the whole component + + def select_component(self, component_id): + if component_id in self._state.components: + self._state.selected_component_id = component_id + self._db.save_state(self._key, self._state) + + return self.refresh_properties() + + def save_properties(self, component_id: str, details: dict): + if component_id in self._state.components: + component = self._state.components[component_id] + component.properties = details + self._db.save_state(self._key, self._state) + logger.debug(f"Saved properties for component {component_id}: {details}") + + return self.refresh_properties() + + def cancel_properties(self, component_id: str): + if component_id in self._state.components: + logger.debug(f"Cancel saving properties for component {component_id}") + + return self.refresh_properties() + + def set_selected_processor(self, component_id: str, processor_name: str): + self._state.selected_processor_name[component_id] = processor_name + self._db.save_state(self._key, self._state) + return self.refresh_properties() + + 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] + if event_name == "OnRepositoryChanged": + component.properties["repository"] = details["repository"] + component.properties["table"] = None + + return self.refresh_properties() def __ft__(self): return Div( @@ -125,56 +176,6 @@ class WorkflowDesigner(BaseComponent): id=f"{self._id}", ) - @staticmethod - def create_component_id(session, suffix=None): - prefix = f"{WORKFLOW_DESIGNER_INSTANCE_ID}{session['user_id']}" - if suffix is None: - suffix = get_unique_id() - - return f"{prefix}{suffix}" - - @staticmethod - def _mk_toolbox_item(component_type: str, info: dict): - return Div( - mk_tooltip( - Div( - Span(info["icon"], cls="mb-2"), - H4(info["title"], cls="font-semibold text-xs"), - cls=f"p-2 rounded-lg border-2 {info['color']} flex text-center" - ), - tooltip=info["description"]), - cls="wkf-toolbox-item p-2", - draggable="true", - data_type=component_type - ) - - @staticmethod - def _mk_workflow_component(component: WorkflowComponent): - info = COMPONENT_TYPES[component.type] - return Div( - # Input connection point - Div(cls="wkf-connection-point wkf-input-point", - data_component_id=component.id, - data_point_type="input"), - - # Component content - Div( - Span(info["icon"], cls="text-xl mb-1"), - H4(component.title, cls="font-semibold text-xs"), - cls=f"p-3 rounded-lg border-2 {info['color']} bg-white shadow-lg flex items-center" - ), - - # Output connection point - Div(cls="wkf-connection-point wkf-output-point", - data_component_id=component.id, - data_point_type="output"), - - cls="wkf-workflow-component w-32", - style=f"left: {component.x}px; top: {component.y}px;", - data_component_id=component.id, - draggable="true" - ) - def _mk_connection_svg(self, conn: Connection): if conn.from_id not in self._state.components or conn.to_id not in self._state.components: return "" @@ -241,22 +242,158 @@ class WorkflowDesigner(BaseComponent): style=f"height:{self._state.designer_height}px;" ) + def _mk_processor_properties(self, component, processor_name): + if processor_name == "Jira": + return self._mk_jira_processor_details(component) + elif processor_name == "Repository": + return self._mk_repository_processor_details(component) + + return Div('Not defined yet !') + + def _mk_properties_details(self): + def _mk_header(): + return Div( + Div( + Span(icon), + H4(component.title, cls="font-semibold text-xs"), + cls=f"rounded-lg border-2 {color} flex text-center px-2" + ), + H1(component_id, cls="ml-4"), + cls="flex mb-2" + ) + + def _mk_select(): + return Select( + *[Option(processor_name, selected="selected" if processor_name == selected_processor_name else None) + for processor_name in PROCESSOR_TYPES[component.type]], + cls="select select-sm w-64 mb-2", + id="processor_name", + name="processor_name", + **self.commands.select_processor(component_id) + ) + + if self._state.selected_component_id is None or self._state.selected_component_id not in self._state.components: + return None + else: + component_id = self._state.selected_component_id + component = self._state.components[component_id] + selected_processor_name = self._state.selected_processor_name.get(component_id, None) + icon = COMPONENT_TYPES[component.type]["icon"] + color = COMPONENT_TYPES[component.type]["color"] + return Form( + _mk_header(), + _mk_select(), + self._mk_processor_properties(component, selected_processor_name), + mk_dialog_buttons(cls="mt-4", + on_ok=self.commands.save_properties(component_id), + on_cancel=self.commands.cancel_properties(component_id)), + cls="font-mono text-sm", + ) + def _mk_properties(self): return Div( - Div( - H4("Properties"), - Div( - P("Workflow name:", cls="text-sm"), - Div(self._designer_settings.workflow_name, cls="text-base"), - cls="flex flex-col gap-2" - ), - ), - + self._mk_properties_details(), cls="p-2 bg-base-100 rounded-lg border", style=f"height:{self._get_properties_height()}px;", id=f"p_{self._id}", ) + @staticmethod + def _mk_jira_processor_details(component): + return Div( + Fieldset( + Legend("JQL", cls="fieldset-legend"), + Input(type="text", + name="jira_jql", + value=component.properties.get("jira_jql", ""), + placeholder="Enter JQL", + cls="input w-full"), + P("Write your jsl code"), + cls="fieldset bg-base-200 border-base-300 rounded-box border p-4" + ), + ) + + def _mk_repository_processor_details(self, component): + selected_repo = component.properties.get("repository", None) + selected_table = component.properties.get("table", None) + + return Div( + Fieldset( + Legend("Repository", cls="fieldset-legend"), + Div( + Select( + *[Option(repo.name, selected="selected" if repo.name == selected_repo else None) + for repo in DbManagementHelper.list_repositories(self._session)], + cls="select w-64", + id=f"repository_{self._id}", + name="repository", + **self.commands.on_processor_details_event(component.id, "OnRepositoryChanged"), + ), + Select( + *[Option(table, selected="selected" if table == selected_table else None) + for table in DbManagementHelper.list_tables(self._session, selected_repo)], + cls="select w-64 ml-4", + id=f"table_{self._id}", + name="table", + ), + cls="flex", + ), + P("Select the source table"), + cls="fieldset bg-base-200 border-base-300 rounded-box border p-4" + + ) + ) + def _get_properties_height(self): print(f"height: {self._boundaries['height']}") return self._boundaries["height"] - self._state.designer_height - 86 + + @staticmethod + def create_component_id(session, suffix=None): + prefix = f"{WORKFLOW_DESIGNER_INSTANCE_ID}{session['user_id']}" + if suffix is None: + suffix = get_unique_id() + + return make_safe_id(f"{prefix}{suffix}") + + @staticmethod + def _mk_toolbox_item(component_type: str, info: dict): + return Div( + mk_tooltip( + Div( + Span(info["icon"], cls="mb-2"), + H4(info["title"], cls="font-semibold text-xs"), + cls=f"p-2 rounded-lg border-2 {info['color']} flex text-center" + ), + tooltip=info["description"]), + cls="wkf-toolbox-item p-2", + draggable="true", + data_type=component_type + ) + + @staticmethod + def _mk_workflow_component(component: WorkflowComponent): + info = COMPONENT_TYPES[component.type] + return Div( + # Input connection point + Div(cls="wkf-connection-point wkf-input-point", + data_component_id=component.id, + data_point_type="input"), + + # Component content + Div( + Span(info["icon"], cls="text-xl mb-1"), + H4(component.title, cls="font-semibold text-xs"), + cls=f"p-3 rounded-lg border-2 {info['color']} bg-white shadow-lg flex items-center" + ), + + # Output connection point + Div(cls="wkf-connection-point wkf-output-point", + data_component_id=component.id, + data_point_type="output"), + + cls="wkf-workflow-component w-32", + style=f"left: {component.x}px; top: {component.y}px;", + data_component_id=component.id, + draggable="true" + ) diff --git a/src/components/workflows/constants.py b/src/components/workflows/constants.py index f8f2f70..4f0a910 100644 --- a/src/components/workflows/constants.py +++ b/src/components/workflows/constants.py @@ -17,4 +17,8 @@ class Routes: MoveComponent = "/move-component" DeleteComponent = "/delete-component" AddConnection = "/add-connection" - ResizeDesigner = "/resize-designer" \ No newline at end of file + ResizeDesigner = "/resize-designer" + SaveProperties = "/save-properties" + CancelProperties = "/cancel-properties" + SelectProcessor = "/select-processor" + OnProcessorDetailsEvent = "/on-processor-details-event" \ No newline at end of file diff --git a/src/components/workflows/db_management.py b/src/components/workflows/db_management.py index fa6d5c7..4b4a349 100644 --- a/src/components/workflows/db_management.py +++ b/src/components/workflows/db_management.py @@ -17,6 +17,7 @@ class WorkflowComponent: y: int title: str description: str + properties: dict = field(default_factory=dict) @dataclass @@ -37,6 +38,8 @@ class WorkflowsDesignerState: connections: list[Connection] = field(default_factory=list) component_counter = 0 designer_height = 230 + selected_component_id = None + selected_processor_name: dict = field(default_factory=dict) # selected processor for each component @dataclass diff --git a/src/utils/DbManagementHelper.py b/src/utils/DbManagementHelper.py new file mode 100644 index 0000000..53ac2ee --- /dev/null +++ b/src/utils/DbManagementHelper.py @@ -0,0 +1,14 @@ +from utils.ComponentsInstancesHelper import ComponentsInstancesHelper + + +class DbManagementHelper: + @staticmethod + def list_repositories(session): + return ComponentsInstancesHelper.get_repositories(session).db.get_repositories() + + @staticmethod + def list_tables(session, repository_name): + if not repository_name: + return [] + repository = ComponentsInstancesHelper.get_repositories(session).db.get_repository(repository_name) + return repository.tables