I can drag and drop items into the canvas
I
This commit is contained in:
@@ -1,10 +1,36 @@
|
||||
from fastcore.basics import NotStr
|
||||
from fasthtml.components import *
|
||||
from fasthtml.xtend import Script
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID
|
||||
from components.workflows.db_management import WorkflowsDesignerSettings
|
||||
from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowsDesignerState, WorkflowComponent, \
|
||||
Connection
|
||||
from components_helpers import apply_boundaries, mk_tooltip
|
||||
from core.utils import get_unique_id
|
||||
|
||||
# Component templates
|
||||
COMPONENT_TYPES = {
|
||||
"producer": {
|
||||
"title": "Data Producer",
|
||||
"description": "Generates or loads data",
|
||||
"icon": "📊",
|
||||
"color": "bg-green-100 border-green-300"
|
||||
},
|
||||
"filter": {
|
||||
"title": "Data Filter",
|
||||
"description": "Filters and transforms data",
|
||||
"icon": "🔍",
|
||||
"color": "bg-blue-100 border-blue-300"
|
||||
},
|
||||
"presenter": {
|
||||
"title": "Data Presenter",
|
||||
"description": "Displays or exports data",
|
||||
"icon": "📋",
|
||||
"color": "bg-purple-100 border-purple-300"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class WorkflowDesigner(BaseComponent):
|
||||
def __init__(self, session,
|
||||
@@ -15,13 +41,71 @@ class WorkflowDesigner(BaseComponent):
|
||||
super().__init__(session, _id)
|
||||
self._settings_manager = settings_manager
|
||||
self._designer_settings = designer_settings
|
||||
self.boundaries = boundaries
|
||||
self._state = WorkflowsDesignerState()
|
||||
self._boundaries = boundaries
|
||||
|
||||
def set_boundaries(self, boundaries: dict):
|
||||
self.boundaries = boundaries
|
||||
self._boundaries = boundaries
|
||||
|
||||
def refresh(self, oob=False):
|
||||
return self._mk_elements()
|
||||
|
||||
def add_component(self, component_type, x, y):
|
||||
self._state.component_counter += 1
|
||||
|
||||
component_id = f"comp_{self._state.component_counter}"
|
||||
info = COMPONENT_TYPES[component_type]
|
||||
|
||||
component = WorkflowComponent(
|
||||
id=component_id,
|
||||
type=component_type,
|
||||
x=int(x),
|
||||
y=int(y),
|
||||
title=info["title"],
|
||||
description=info["description"]
|
||||
)
|
||||
|
||||
self._state.components[component_id] = component
|
||||
return self.refresh()
|
||||
|
||||
def move_component(self, component_id, x, y):
|
||||
if component_id in self._state.components:
|
||||
self._state.components[component_id].x = int(x)
|
||||
self._state.components[component_id].y = int(y)
|
||||
|
||||
return self.refresh()
|
||||
|
||||
def delete_component(self, component_id):
|
||||
# Remove component
|
||||
if component_id in self._state.components:
|
||||
del self._state.components[component_id]
|
||||
|
||||
# Remove related connections
|
||||
self._state.connections = [connection for connection in self._state.connections
|
||||
if connection.from_id != component_id and connection.to_id != component_id]
|
||||
|
||||
return self.refresh()
|
||||
|
||||
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")
|
||||
|
||||
connection_id = f"conn_{len(self._state.connections) + 1}"
|
||||
connection = Connection(id=connection_id, from_id=from_id, to_id=to_id)
|
||||
self._state.connections.append(connection)
|
||||
return self.refresh()
|
||||
|
||||
def __ft__(self):
|
||||
return Div(f"Workflow Designer - {self._designer_settings.workflow_name}")
|
||||
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_designer(),
|
||||
Script(f"bindWorkflowDesigner('{self._id}');"),
|
||||
**apply_boundaries(self._boundaries),
|
||||
id=f"{self._id}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session, suffix=None):
|
||||
@@ -30,3 +114,114 @@ class WorkflowDesigner(BaseComponent):
|
||||
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="text-2xl mb-2"),
|
||||
H4(info["title"], cls="font-semibold text-sm"),
|
||||
cls=f"p-3 rounded-lg border-2 {info['color']} 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-sm"),
|
||||
P(info["description"], cls="text-xs text-gray-600"),
|
||||
cls=f"p-3 rounded-lg border-2 {info['color']} bg-white shadow-lg"
|
||||
),
|
||||
|
||||
# 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 ""
|
||||
|
||||
from_comp = self._state.components[conn.from_id]
|
||||
to_comp = self._state.components[conn.to_id]
|
||||
|
||||
# Calculate connection points (approximate)
|
||||
x1 = from_comp.x + 128 # component width + output point
|
||||
y1 = from_comp.y + 40 # component height / 2
|
||||
x2 = to_comp.x
|
||||
y2 = to_comp.y + 40
|
||||
|
||||
# Create curved path
|
||||
mid_x = (x1 + x2) / 2
|
||||
path = f"M {x1} {y1} C {mid_x} {y1}, {mid_x} {y2}, {x2} {y2}"
|
||||
|
||||
return f"""
|
||||
<svg class="wkf-connection-line" style="left: 0; top: 0; width: 100%; height: 100%;"
|
||||
data-from-id="{conn.from_id}" data-to-id="{conn.to_id}">
|
||||
<path d="{path}" stroke="#3b82f6" stroke-width="2" fill="none" marker-end="url(#arrowhead)"/>
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#3b82f6"/>
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
def _mk_elements(self):
|
||||
return Div(
|
||||
# Render connections
|
||||
*[NotStr(self._mk_connection_svg(conn)) for conn in self._state.connections],
|
||||
|
||||
# Render components
|
||||
*[self._mk_workflow_component(comp) for comp in self._state.components.values()],
|
||||
|
||||
cls="wkf-canvas bg-white rounded-lg border relative",
|
||||
),
|
||||
|
||||
def _mk_canvas(self, oob=False):
|
||||
return Div(
|
||||
self._mk_elements(),
|
||||
cls="flex-1 ",
|
||||
id=f"c_{self._id}",
|
||||
hx_swap_oob='true' if oob else None,
|
||||
),
|
||||
|
||||
def _mk_toolbox(self):
|
||||
return Div(
|
||||
H2("Toolbox", cls="text-lg font-semibold mb-4 text-gray-700"),
|
||||
Div(
|
||||
*[self._mk_toolbox_item(comp_type, info)
|
||||
for comp_type, info in COMPONENT_TYPES.items()],
|
||||
cls="space-y-3"
|
||||
),
|
||||
cls="w-32 p-4 bg-gray-50 rounded-lg border"
|
||||
)
|
||||
|
||||
def _mk_designer(self):
|
||||
return Div(
|
||||
self._mk_toolbox(), # (Left side)
|
||||
self._mk_canvas(), # (Right side)
|
||||
|
||||
cls="flex gap-4",
|
||||
id=f"d_{self._id}",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user