I can load and save Jira and Table Processor details

This commit is contained in:
2025-07-04 19:03:32 +02:00
parent 8e718ecb67
commit f86f4852c7
8 changed files with 321 additions and 78 deletions

View File

@@ -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)
@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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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}"}}',
}

View File

@@ -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,13 +121,49 @@ 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
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(
H1(f"{self._designer_settings.workflow_name}", cls="text-xl font-bold"),
@@ -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_properties(self):
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(
H4("Properties"),
Div(
P("Workflow name:", cls="text-sm"),
Div(self._designer_settings.workflow_name, cls="text-base"),
cls="flex flex-col gap-2"
),
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(
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"
)

View File

@@ -18,3 +18,7 @@ class Routes:
DeleteComponent = "/delete-component"
AddConnection = "/add-connection"
ResizeDesigner = "/resize-designer"
SaveProperties = "/save-properties"
CancelProperties = "/cancel-properties"
SelectProcessor = "/select-processor"
OnProcessorDetailsEvent = "/on-processor-details-event"

View File

@@ -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

View File

@@ -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