From 4b06a0fe9b2c0f9a2ff5f1f5146650a3b4f5b724 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Tue, 1 Jul 2025 22:07:12 +0200 Subject: [PATCH] Adding workflow management I --- src/assets/icons.py | 9 ++ src/assets/main.css | 17 ++- src/components/BaseComponent.py | 17 +++ .../drawerlayout/components/DrawerLayout.py | 3 + src/components/repositories/db_management.py | 2 +- src/components/tabs/components/MyTabs.py | 2 +- src/components/workflows/WorkflowsApp.py | 26 +++++ src/components/workflows/__init__.py | 0 src/components/workflows/assets/__init__.py | 0 src/components/workflows/commands.py | 22 ++++ .../workflows/components/WorkflowDesigner.py | 0 .../workflows/components/Workflows.py | 100 ++++++++++++++++++ .../workflows/components/__init__.py | 0 src/components/workflows/constants.py | 8 ++ src/components/workflows/db_management.py | 99 +++++++++++++++++ src/main.py | 1 + src/utils/ComponentsInstancesHelper.py | 1 + 17 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 src/components/workflows/WorkflowsApp.py create mode 100644 src/components/workflows/__init__.py create mode 100644 src/components/workflows/assets/__init__.py create mode 100644 src/components/workflows/commands.py create mode 100644 src/components/workflows/components/WorkflowDesigner.py create mode 100644 src/components/workflows/components/Workflows.py create mode 100644 src/components/workflows/components/__init__.py create mode 100644 src/components/workflows/constants.py create mode 100644 src/components/workflows/db_management.py diff --git a/src/assets/icons.py b/src/assets/icons.py index 2964ea1..782bb90 100644 --- a/src/assets/icons.py +++ b/src/assets/icons.py @@ -10,3 +10,12 @@ icon_dismiss_regular = NotStr( """ ) + +# Fluent Add16Regular +icon_add_regular = NotStr(""" + + + + + +""") \ No newline at end of file diff --git a/src/assets/main.css b/src/assets/main.css index d42db94..f97bf85 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -28,6 +28,21 @@ transition: opacity 0.3s ease; /* No delay when becoming visible */ } +.mmt-visible-on-hover { + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0s linear 0.2s; +} + + +/* When parent is hovered, show the child elements with this class */ +*:hover > .mmt-visible-on-hover { + opacity: 1; + visibility: visible; + transition: opacity 0.2s ease; +} + + .icon-32 { width: 32px; height: 32px; @@ -65,7 +80,6 @@ padding-top: 4px; } - .icon-16 { width: 16px; min-width: 16px; @@ -82,7 +96,6 @@ padding-top: 5px; } - .icon-bool { display: block; width: 20px; diff --git a/src/components/BaseComponent.py b/src/components/BaseComponent.py index 20109af..5b98cfb 100644 --- a/src/components/BaseComponent.py +++ b/src/components/BaseComponent.py @@ -34,3 +34,20 @@ class BaseComponent: @staticmethod def create_component_id(session): pass + + +class BaseComponentSingleton(BaseComponent): + """ + Base class for components that will have a single instance per user + """ + + COMPONENT_INSTANCE_ID = None + + def __init__(self, session, _id=None, settings_manager=None, tabs_manager=None, **kwargs): + super().__init__(session, _id, **kwargs) + self._settings_manager = settings_manager + self.tabs_manager = tabs_manager + + @classmethod + def create_component_id(cls, session): + return f"{cls.COMPONENT_INSTANCE_ID}{session['user_id']}" diff --git a/src/components/drawerlayout/components/DrawerLayout.py b/src/components/drawerlayout/components/DrawerLayout.py index 747057a..1dc10f4 100644 --- a/src/components/drawerlayout/components/DrawerLayout.py +++ b/src/components/drawerlayout/components/DrawerLayout.py @@ -11,6 +11,7 @@ from components.drawerlayout.assets.icons import icon_panel_contract_regular, ic from components.drawerlayout.constants import DRAWER_LAYOUT_INSTANCE_ID from components.repositories.components.Repositories import Repositories from components.tabs.components.MyTabs import MyTabs +from components.workflows.components.Workflows import Workflows from core.instance_manager import InstanceManager from core.settings_management import SettingsManager @@ -24,6 +25,7 @@ class DrawerLayout(BaseComponent): self._settings_manager = settings_manager self._tabs = InstanceManager.get(session, MyTabs.create_component_id(session), MyTabs) self._repositories = self._create_component(Repositories) + self._workflows = self._create_component(Workflows) self._debugger = self._create_component(Debugger) self._add_stuff = self._create_component(AddStuffMenu) self._ai_buddy = self._create_component(AIBuddy) @@ -41,6 +43,7 @@ class DrawerLayout(BaseComponent): self._ai_buddy, self._applications, self._repositories, + self._workflows, self._admin, self._debugger, ), diff --git a/src/components/repositories/db_management.py b/src/components/repositories/db_management.py index c666e84..01837cb 100644 --- a/src/components/repositories/db_management.py +++ b/src/components/repositories/db_management.py @@ -5,7 +5,7 @@ from core.settings_management import SettingsManager REPOSITORIES_SETTINGS_ENTRY = "Repositories" -logger = logging.getLogger("AddStuffSettings") +logger = logging.getLogger("RepositoriesSettings") @dataclasses.dataclass diff --git a/src/components/tabs/components/MyTabs.py b/src/components/tabs/components/MyTabs.py index e37dcb2..965cbfb 100644 --- a/src/components/tabs/components/MyTabs.py +++ b/src/components/tabs/components/MyTabs.py @@ -157,7 +157,7 @@ class MyTabs(BaseComponent): def render(self, oob=False): active_content = self.get_active_tab_content() - if hasattr(active_content, "on_htmx_after_settle"): + if hasattr(active_content, "on_htmx_after_settle") and active_content.on_htmx_after_settle is not None: extra_params = {"hx-on::after-settle": active_content.on_htmx_after_settle()} else: extra_params = {} diff --git a/src/components/workflows/WorkflowsApp.py b/src/components/workflows/WorkflowsApp.py new file mode 100644 index 0000000..f8b57ee --- /dev/null +++ b/src/components/workflows/WorkflowsApp.py @@ -0,0 +1,26 @@ +import json +import logging + +from fasthtml.fastapp import fast_app + +from components.workflows.constants import Routes +from core.instance_manager import InstanceManager, debug_session + +logger = logging.getLogger("WorkflowsApp") + +repositories_app, rt = fast_app() + + +@rt(Routes.AddWorkflow) +def get(session, _id: str): + logger.debug(f"Entering {Routes.AddWorkflow} with args {debug_session(session)}, {_id=}") + instance = InstanceManager.get(session, _id) + return instance.request_new_workflow() + + +@rt(Routes.AddWorkflow) +def post(session, _id: str, tab_id: str, form_id: str, name: str, tab_boundaries: str): + logger.debug( + f"Entering {Routes.AddWorkflow} with args {debug_session(session)}, {_id=}, {tab_id=}, {form_id=}, {name=}, {tab_boundaries=}") + instance = InstanceManager.get(session, _id) + return instance.add_new_workflow(tab_id, form_id, name, json.loads(tab_boundaries)) diff --git a/src/components/workflows/__init__.py b/src/components/workflows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/components/workflows/assets/__init__.py b/src/components/workflows/assets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/components/workflows/commands.py b/src/components/workflows/commands.py new file mode 100644 index 0000000..0e2dc82 --- /dev/null +++ b/src/components/workflows/commands.py @@ -0,0 +1,22 @@ +from components.BaseCommandManager import BaseCommandManager +from components.workflows.constants import Routes, ROUTE_ROOT + + +class WorkflowsCommandManager(BaseCommandManager): + def __init__(self, owner): + super().__init__(owner) + + def request_add_workflow(self): + return { + "hx-get": f"{ROUTE_ROOT}{Routes.AddWorkflow}", + "hx-target": f"#{self._owner.tabs_manager.get_id()}", + "hx-swap": "outerHTML", + "hx-vals": f'{{"_id": "{self._id}"}}', + } + + def add_workflow(self, tab_id: str): + return { + "hx-post": f"{ROUTE_ROOT}{Routes.AddWorkflow}", + "hx-target": f"#w_{self._id}", + "hx-vals": f'js:{{"_id": "{self._id}", "tab_id": "{tab_id}", "tab_boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}', + } diff --git a/src/components/workflows/components/WorkflowDesigner.py b/src/components/workflows/components/WorkflowDesigner.py new file mode 100644 index 0000000..e69de29 diff --git a/src/components/workflows/components/Workflows.py b/src/components/workflows/components/Workflows.py new file mode 100644 index 0000000..14e88a2 --- /dev/null +++ b/src/components/workflows/components/Workflows.py @@ -0,0 +1,100 @@ +import logging + +from fasthtml.components import * + +from assets.icons import icon_add_regular +from components.BaseComponent import BaseComponentSingleton +from components.form.components.MyForm import MyForm, FormField +from components.workflows.commands import WorkflowsCommandManager +from components.workflows.constants import WORKFLOWS_INSTANCE_ID +from components.workflows.db_management import WorkflowsDbManager +from components_helpers import mk_ellipsis, mk_icon +from core.instance_manager import InstanceManager + +logger = logging.getLogger("Workflows") + + +class Workflows(BaseComponentSingleton): + COMPONENT_INSTANCE_ID = WORKFLOWS_INSTANCE_ID + + def __init__(self, session, _id, settings_manager=None, tabs_manager=None): + super().__init__(session, _id, settings_manager, tabs_manager) + self.commands = WorkflowsCommandManager(self) + self.db = WorkflowsDbManager(session, settings_manager) + + def request_new_workflow(self): + # request for a new tab_id + new_tab_id = self.tabs_manager.request_new_tab_id() + + # create a new form to ask for the details of the new database + add_workflow_form = self._mk_add_workflow_form(new_tab_id) + + # create and display the form in a new tab + self.tabs_manager.add_tab("Add Workflow", add_workflow_form, tab_id=new_tab_id) + return self.tabs_manager + + def add_new_workflow(self, tab_id: str, form_id: str, workflow_name: str, tab_boundaries: dict): + """ + + :param tab_id: tab id where the table content will be displayed (and where the form was displayed) + :param form_id: form used to give the repository name (to be used in case of error) + :param workflow_name: new workflow name + :param tab_boundaries: tab boundaries + :return: + """ + try: + # Add the new repository and its default table to the list of repositories + self.db.add_workflow(workflow_name) + + # update the tab content with table content + self.tabs_manager.set_tab_content(tab_id, + self._get_workflow_designer(workflow_name, tab_boundaries), + title=workflow_name, + key=workflow_name, + active=True) + + return self._mk_workflows(), self.tabs_manager.refresh() + + except ValueError as ex: + logger.error(f" Workflow '{workflow_name}' already exists.") + add_repository_form = InstanceManager.get(self._session, form_id) + add_repository_form.set_error(ex) + + return self.tabs_manager.refresh() + + def __ft__(self): + return Div( + Div(cls="divider"), + Div( + mk_ellipsis("Workflows", cls="text-sm font-medium mb-1"), + mk_icon(icon_add_regular, + size=16, + tooltip="Add Workflow", + cls="ml-2 mmt-visible-on-hover", + **self.commands.request_add_workflow()), + cls="flex" + ), + self._mk_workflows(), + id=f"{self._id}" + ) + + def _get_workflow_designer(self, workflow_name: str, tab_boundaries: dict): + return Div(f"Workflow Designer for {workflow_name}") + + def _mk_add_workflow_form(self, tab_id: str): + return InstanceManager.get(self._session, MyForm.create_component_id(self._session), MyForm, + title="Add Workflow", + fields=[FormField("name", 'Workflow Name', 'input')], + htmx_request=self.commands.add_workflow(tab_id), + ) + + def _mk_workflow(self, workflow_name: str, selected: bool): + return mk_ellipsis(workflow_name, cls="text-sm") + + def _mk_workflows(self, oob=False): + return Div( + *[self._mk_workflow(workflow_name, workflow_name == self.db.get_selected_workflow()) + for workflow_name in self.db.get_workflows()], + id=f"w_{self._id}", + hx_swap_oob="true" if oob else None, + ) diff --git a/src/components/workflows/components/__init__.py b/src/components/workflows/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/components/workflows/constants.py b/src/components/workflows/constants.py new file mode 100644 index 0000000..32f06bf --- /dev/null +++ b/src/components/workflows/constants.py @@ -0,0 +1,8 @@ +WORKFLOWS_INSTANCE_ID = "__Workflows__" +WORKFLOWS_SETTINGS_ENTRY = "Workflows" +ROUTE_ROOT = "/workflows" + + +class Routes: + AddWorkflow = "/add-workflow" + SelectWorkflow = "/select-workflow" diff --git a/src/components/workflows/db_management.py b/src/components/workflows/db_management.py new file mode 100644 index 0000000..5cbc6dd --- /dev/null +++ b/src/components/workflows/db_management.py @@ -0,0 +1,99 @@ +import logging +from dataclasses import dataclass, field + +from components.workflows.constants import WORKFLOWS_SETTINGS_ENTRY +from core.settings_management import SettingsManager + +logger = logging.getLogger("WorkflowsSettings") + + +@dataclass +class WorkflowsSettings: + workflows: list[str] = field(default_factory=list) + selected_workflow: str = None + + +class WorkflowsDbManager: + def __init__(self, session: dict, settings_manager: SettingsManager): + self.session = session + self.settings_manager = settings_manager + + def add_workflow(self, workflow_name: str): + settings = self._get_settings() + + if not workflow_name: + raise ValueError("Workflow name cannot be empty.") + + if workflow_name in settings.workflows: + raise ValueError(f"Workflow '{workflow_name}' already exists.") + + settings.workflows.append(workflow_name) + self.settings_manager.save(self.session, WORKFLOWS_SETTINGS_ENTRY, settings) + return True + + def get_workflow(self, workflow_name: str): + if not workflow_name: + raise ValueError("Workflow name cannot be empty.") + + settings = self._get_settings() + if workflow_name not in settings.workflows: + raise ValueError(f"Workflow '{workflow_name}' does not exist.") + + return next(filter(lambda r: r == workflow_name, settings.workflows)) + + # def modify_workflow(self, old_workflow_name, new_workflow_name: str, tables: list[str]): + # if not old_workflow_name or not new_workflow_name: + # raise ValueError("Workflow name cannot be empty.") + # + # settings = self._get_settings() + # for workflow in settings.workflows: + # if workflow == old_workflow_name: + # workflow.name = new_workflow_name + # workflow.tables = tables + # + # self.settings_manager.save(self.session, workflows_SETTINGS_ENTRY, settings) + # return workflow + # + # else: + # raise ValueError(f"workflow '{old_workflow_name}' not found.") + + def remove_workflow(self, workflow_name): + if not workflow_name: + raise ValueError("Workflow name cannot be empty.") + + settings = self._get_settings() + if workflow_name not in settings.workflows: + raise ValueError(f"workflow '{workflow_name}' does not exist.") + + settings.workflows.remove(workflow_name) + self.settings_manager.save(self.session, WORKFLOWS_SETTINGS_ENTRY, settings) + return True + + def exists_workflow(self, workflow_name): + if not workflow_name: + raise ValueError("workflow name cannot be empty.") + + settings = self._get_settings() + return workflow_name in settings.workflows + + def get_workflows(self): + return self._get_settings().workflows + + def select_workflow(self, workflow_name: str): + """ + Select and save the specified workflow name in the current session's settings. + + :param workflow_name: The name of the workflow to be selected and stored. + :type workflow_name: str + :return: None + """ + settings = self._get_settings() + settings.selected_workflow = workflow_name + self.settings_manager.save(self.session, WORKFLOWS_SETTINGS_ENTRY, settings) + + def get_selected_workflow(self): + settings = self._get_settings() + return settings.selected_workflow + + def _get_settings(self): + return self.settings_manager.load(self.session, WORKFLOWS_SETTINGS_ENTRY, default=WorkflowsSettings()) diff --git a/src/main.py b/src/main.py index 51bcb49..a88a769 100644 --- a/src/main.py +++ b/src/main.py @@ -148,6 +148,7 @@ register_component("main_layout", "components.drawerlayout", "DrawerLayoutApp") register_component("tabs", "components.tabs", "TabsApp") # before repositories register_component("applications", "components.applications", "ApplicationsApp") register_component("repositories", "components.repositories", "RepositoriesApp") +register_component("workflows", "components.workflows", "WorkflowsApp") register_component("add_stuff", "components.addstuff", None) register_component("form", "components.form", "FormApp") register_component("datagrid_new", "components.datagrid_new", "DataGridApp") diff --git a/src/utils/ComponentsInstancesHelper.py b/src/utils/ComponentsInstancesHelper.py index 89a6136..01baf1f 100644 --- a/src/utils/ComponentsInstancesHelper.py +++ b/src/utils/ComponentsInstancesHelper.py @@ -6,3 +6,4 @@ class ComponentsInstancesHelper: @staticmethod def get_repositories(session): return InstanceManager.get(session, Repositories.create_component_id(session)) + \ No newline at end of file