Refactoring Properties component
This commit is contained in:
@@ -4,12 +4,20 @@
|
||||
|
||||
using `_id={WORKFLOW_DESIGNER_INSTANCE_ID}{session['user_id']}{get_unique_id()}`
|
||||
|
||||
| Name | value |
|
||||
|-----------------|--------------------|
|
||||
| Canvas | `c_{self._id}` |
|
||||
| Designer | `d_{self._id}` |
|
||||
| Error Message | `err_{self._id}` |
|
||||
| Properties | `p_{self._id}` |
|
||||
| Spliter | `s_{self._id}` |
|
||||
| Top element | `t_{self._id}` |
|
||||
| Name | value |
|
||||
|----------------------------------|--------------------------------|
|
||||
| Canvas | `c_{self._id}` |
|
||||
| Designer | `d_{self._id}` |
|
||||
| Error Message | `err_{self._id}` |
|
||||
| Properties | `p_{self._id}` |
|
||||
| Properties Input Section | `pi_{self._id}` |
|
||||
| Properties Output Section | `po_{self._id}` |
|
||||
| Properties Properties Section | `pp_{self._id}` |
|
||||
| Properties Properties drag top | `ppt_{self._id}` |
|
||||
| Properties Properties drag left | `ppl_{self._id}` |
|
||||
| Properties Properties drag right | `ppr_{self._id}` |
|
||||
| Spliter | `s_{self._id}` |
|
||||
| Top element | `t_{self._id}` |
|
||||
| Form for properties | `f_{self._id}_{component_id}` |
|
||||
| Form for output properties | `fo_{self._id}_{component_id}` |
|
||||
|
||||
|
||||
@@ -82,6 +82,14 @@ def post(session, _id: str, designer_height: int):
|
||||
return instance.set_designer_height(designer_height)
|
||||
|
||||
|
||||
@rt(Routes.UpdatePropertiesLayout)
|
||||
def post(session, _id: str, input_width: int, properties_width: int, output_width: int):
|
||||
logger.debug(
|
||||
f"Entering {Routes.UpdatePropertiesLayout} with args {debug_session(session)}, {_id=}, {input_width=}, {properties_width=}, {output_width=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.update_properties_layout(input_width, properties_width, output_width)
|
||||
|
||||
|
||||
@rt(Routes.SelectComponent)
|
||||
def post(session, _id: str, component_id: str):
|
||||
logger.debug(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
function bindWorkflowDesigner(elementId) {
|
||||
bindWorkflowDesignerToolbox(elementId)
|
||||
bindWorkflowDesignerSplitter(elementId)
|
||||
bindWorkflowProperties(elementId)
|
||||
}
|
||||
|
||||
function bindWorkflowDesignerToolbox(elementId) {
|
||||
@@ -612,3 +613,153 @@ function bindWorkflowDesignerSplitter(elementId) {
|
||||
}
|
||||
}
|
||||
|
||||
function bindWorkflowProperties(elementId) {
|
||||
let isDragging = false;
|
||||
let isResizing = false;
|
||||
let startX = 0;
|
||||
let startWidths = {};
|
||||
let resizeType = '';
|
||||
|
||||
console.debug("Binding Properties component for "+ elementId)
|
||||
properties_component = document.getElementById(`p_${elementId}`);
|
||||
if (properties_component == null) {
|
||||
console.error(`'Component ' p_${elementId}' is not found !' `)
|
||||
return
|
||||
}
|
||||
const totalWidth = properties_component.getBoundingClientRect().width
|
||||
console.debug("totalWidth", totalWidth)
|
||||
|
||||
const minPropertiesWidth = Math.floor(totalWidth * 0.2); // 20% minimum
|
||||
|
||||
const inputSection = document.getElementById(`pi_${elementId}`);
|
||||
const propertiesSection = document.getElementById(`pp_${elementId}`);
|
||||
const outputSection = document.getElementById(`po_${elementId}`);
|
||||
const dragHandle = document.getElementById(`ppt_${elementId}`);
|
||||
const leftHandle = document.getElementById(`ppl_${elementId}`);
|
||||
const rightHandle = document.getElementById(`ppr_${elementId}`);
|
||||
|
||||
// Drag and drop for moving properties section
|
||||
dragHandle.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startWidths = {
|
||||
input: parseInt(inputSection.style.width),
|
||||
properties: parseInt(propertiesSection.style.width),
|
||||
output: parseInt(outputSection.style.width)
|
||||
};
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Left resize handle
|
||||
leftHandle.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
resizeType = 'left';
|
||||
startX = e.clientX;
|
||||
startWidths = {
|
||||
input: parseInt(inputSection.style.width),
|
||||
properties: parseInt(propertiesSection.style.width),
|
||||
output: parseInt(outputSection.style.width)
|
||||
};
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Right resize handle
|
||||
rightHandle.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
resizeType = 'right';
|
||||
startX = e.clientX;
|
||||
startWidths = {
|
||||
input: parseInt(inputSection.style.width),
|
||||
properties: parseInt(propertiesSection.style.width),
|
||||
output: parseInt(outputSection.style.width)
|
||||
};
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Mouse move
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (isDragging) {
|
||||
const deltaX = e.clientX - startX;
|
||||
let newInputWidth = startWidths.input + deltaX;
|
||||
let newOutputWidth = startWidths.output - deltaX;
|
||||
|
||||
// Constraints
|
||||
if (newInputWidth < 0) {
|
||||
newInputWidth = 0;
|
||||
newOutputWidth = totalWidth - startWidths.properties;
|
||||
}
|
||||
if (newOutputWidth < 0) {
|
||||
newOutputWidth = 0;
|
||||
newInputWidth = totalWidth - startWidths.properties;
|
||||
}
|
||||
|
||||
inputSection.style.width = newInputWidth + 'px';
|
||||
outputSection.style.width = newOutputWidth + 'px';
|
||||
}
|
||||
|
||||
if (isResizing) {
|
||||
const deltaX = e.clientX - startX;
|
||||
let newInputWidth = startWidths.input;
|
||||
let newPropertiesWidth = startWidths.properties;
|
||||
let newOutputWidth = startWidths.output;
|
||||
|
||||
if (resizeType === 'left') {
|
||||
newInputWidth = startWidths.input + deltaX;
|
||||
newPropertiesWidth = startWidths.properties - deltaX;
|
||||
|
||||
if (newInputWidth < 0) {
|
||||
newInputWidth = 0;
|
||||
newPropertiesWidth = startWidths.input + startWidths.properties;
|
||||
}
|
||||
if (newPropertiesWidth < minPropertiesWidth) {
|
||||
newPropertiesWidth = minPropertiesWidth;
|
||||
newInputWidth = totalWidth - minPropertiesWidth - startWidths.output;
|
||||
}
|
||||
} else if (resizeType === 'right') {
|
||||
newPropertiesWidth = startWidths.properties + deltaX;
|
||||
newOutputWidth = startWidths.output - deltaX;
|
||||
|
||||
if (newOutputWidth < 0) {
|
||||
newOutputWidth = 0;
|
||||
newPropertiesWidth = startWidths.properties + startWidths.output;
|
||||
}
|
||||
if (newPropertiesWidth < minPropertiesWidth) {
|
||||
newPropertiesWidth = minPropertiesWidth;
|
||||
newOutputWidth = totalWidth - startWidths.input - minPropertiesWidth;
|
||||
}
|
||||
}
|
||||
|
||||
inputSection.style.width = newInputWidth + 'px';
|
||||
propertiesSection.style.width = newPropertiesWidth + 'px';
|
||||
outputSection.style.width = newOutputWidth + 'px';
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse up
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (isDragging || isResizing) {
|
||||
// Send HTMX request with new dimensions
|
||||
const currentWidths = {
|
||||
input_width: parseInt(inputSection.style.width),
|
||||
properties_width: parseInt(propertiesSection.style.width),
|
||||
output_width: parseInt(outputSection.style.width)
|
||||
};
|
||||
|
||||
try {
|
||||
htmx.ajax('POST', '/workflows/update-properties-layout', {
|
||||
target: `#${elementId}`,
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
swap: "outerHTML",
|
||||
values: { _id: elementId, ...currentWidths }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('HTMX request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
isResizing = false;
|
||||
resizeType = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -9,10 +9,11 @@ from components.BaseComponent import BaseComponent
|
||||
from components.undo_redo.constants import UndoRedoAttrs
|
||||
from components.workflows.assets.icons import icon_play, icon_pause, icon_stop, icon_refresh
|
||||
from components.workflows.commands import WorkflowDesignerCommandManager
|
||||
from components.workflows.components.WorkflowDesignerProperties import WorkflowDesignerProperties
|
||||
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, ComponentState
|
||||
Connection, WorkflowsDesignerDbManager, ComponentState, WorkflowsDesignerState
|
||||
from components_helpers import apply_boundaries, mk_tooltip, mk_dialog_buttons, mk_icon
|
||||
from core.instance_manager import InstanceManager
|
||||
from core.jira import JiraRequestTypes, DEFAULT_SEARCH_FIELDS
|
||||
@@ -65,9 +66,10 @@ class WorkflowDesigner(BaseComponent):
|
||||
self._designer_settings = designer_settings
|
||||
self._db = WorkflowsDesignerDbManager(session, settings_manager)
|
||||
self._undo_redo = ComponentsInstancesHelper.get_undo_redo(session)
|
||||
self._state = self._db.load_state(key)
|
||||
self._state: WorkflowsDesignerState = self._db.load_state(key)
|
||||
self._boundaries = boundaries
|
||||
self.commands = WorkflowDesignerCommandManager(self)
|
||||
self.properties = WorkflowDesignerProperties(self._session, f"{self._id}", self)
|
||||
|
||||
workflow_name = self._designer_settings.workflow_name
|
||||
self._player = InstanceManager.get(self._session,
|
||||
@@ -83,7 +85,10 @@ class WorkflowDesigner(BaseComponent):
|
||||
def set_boundaries(self, boundaries: dict):
|
||||
self._boundaries = boundaries
|
||||
|
||||
def get_state(self):
|
||||
def get_boundaries(self):
|
||||
return self._boundaries
|
||||
|
||||
def get_state(self) -> WorkflowsDesignerState:
|
||||
return self._state
|
||||
|
||||
def get_db(self):
|
||||
@@ -106,6 +111,7 @@ class WorkflowDesigner(BaseComponent):
|
||||
|
||||
def refresh_state(self):
|
||||
self._state = self._db.load_state(self._key)
|
||||
self.properties.update_layout()
|
||||
return self.__ft__(oob=True)
|
||||
|
||||
def add_component(self, component_type, x, y):
|
||||
@@ -140,7 +146,7 @@ class WorkflowDesigner(BaseComponent):
|
||||
undo_redo_attrs = UndoRedoAttrs(f"Move Component '{component.title}'", on_undo=self.refresh_state)
|
||||
self._db.save_state(self._key, self._state, undo_redo_attrs) # update db
|
||||
|
||||
return self.refresh_designer(), self.refresh_properties(True), self._undo_redo.refresh()
|
||||
return self.refresh_designer(), self.properties.refresh(), self._undo_redo.refresh()
|
||||
|
||||
def delete_component(self, component_id):
|
||||
# Remove component
|
||||
@@ -192,6 +198,16 @@ class WorkflowDesigner(BaseComponent):
|
||||
self._db.save_state(self._key, self._state, undo_redo_attrs)
|
||||
return self.__ft__(), self._undo_redo.refresh() # refresh the whole component
|
||||
|
||||
def update_properties_layout(self, input_width, properties_width, output_width):
|
||||
self._state.properties_input_width = input_width
|
||||
self._state.properties_properties_width = properties_width
|
||||
self._state.properties_output_width = output_width
|
||||
self.properties.update_layout()
|
||||
|
||||
undo_redo_attrs = UndoRedoAttrs(f"Resize Properties", on_undo=lambda: self.refresh_state())
|
||||
self._db.save_state(self._key, self._state, undo_redo_attrs)
|
||||
return self.__ft__(), self._undo_redo.refresh() # refresh the whole component
|
||||
|
||||
def select_component(self, component_id):
|
||||
if component_id in self._state.components:
|
||||
self._state.selected_component_id = component_id
|
||||
@@ -273,7 +289,7 @@ class WorkflowDesigner(BaseComponent):
|
||||
def __ft__(self, oob=False):
|
||||
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"),
|
||||
# P("Drag components from the toolbox to the canvas to create your workflow.", cls="text-sm"),
|
||||
Div(
|
||||
self._mk_media(),
|
||||
# self._mk_refresh_button(),
|
||||
@@ -365,6 +381,11 @@ class WorkflowDesigner(BaseComponent):
|
||||
)
|
||||
|
||||
def _mk_elements(self):
|
||||
if len(self._state.components) == 0:
|
||||
return Div("Drag components from the toolbox to the canvas to create your workflow.",
|
||||
cls="flex items-center justify-center h-full w-full"
|
||||
)
|
||||
|
||||
return Div(
|
||||
# Render connections
|
||||
*[NotStr(self._mk_connection_svg(conn)) for conn in self._state.connections],
|
||||
@@ -436,6 +457,17 @@ class WorkflowDesigner(BaseComponent):
|
||||
|
||||
return Div('Not defined yet !')
|
||||
|
||||
def _mk_properties_output(self, component):
|
||||
return Div(
|
||||
"Output name",
|
||||
Input(type="input",
|
||||
name="output_name",
|
||||
placeholder="data",
|
||||
value=component.properties.get("output_name", None),
|
||||
cls="input w-xs"),
|
||||
cls="join"
|
||||
)
|
||||
|
||||
def _mk_properties_details(self, component_id, allow_component_selection=False):
|
||||
def _mk_header():
|
||||
return Div(
|
||||
@@ -469,11 +501,32 @@ class WorkflowDesigner(BaseComponent):
|
||||
return Div(
|
||||
Form(
|
||||
_mk_header(),
|
||||
_mk_select(),
|
||||
self._mk_processor_properties(component, selected_processor_name),
|
||||
Div(
|
||||
Input(type="radio", name=f"pt_{self._id}", cls="tab", aria_label="Properties", checked="checked"),
|
||||
Div(
|
||||
_mk_select(),
|
||||
self._mk_processor_properties(component, selected_processor_name),
|
||||
cls="tab-content"
|
||||
),
|
||||
|
||||
Input(type="radio", name=f"pt_{self._id}", cls="tab", aria_label="Inputs"),
|
||||
Div(
|
||||
"Inputs",
|
||||
cls="tab-content"
|
||||
),
|
||||
|
||||
Input(type="radio", name=f"pt_{self._id}", cls="tab", aria_label="Output"),
|
||||
Div(
|
||||
self._mk_properties_output(component),
|
||||
cls="tab-content"
|
||||
),
|
||||
|
||||
cls="tabs tabs-border"
|
||||
),
|
||||
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",
|
||||
id=f"f_{self._id}_{component_id}",
|
||||
),
|
||||
@@ -481,13 +534,7 @@ class WorkflowDesigner(BaseComponent):
|
||||
)
|
||||
|
||||
def _mk_properties(self, oob=False):
|
||||
return Div(
|
||||
self._mk_properties_details(self._state.selected_component_id),
|
||||
cls="p-2 bg-base-100 rounded-lg border",
|
||||
style=f"height:{self._get_properties_height()}px;",
|
||||
hx_swap_oob='true' if oob else None,
|
||||
id=f"p_{self._id}",
|
||||
)
|
||||
return self.properties
|
||||
|
||||
def _mk_jira_processor_details(self, component):
|
||||
def _mk_option(name):
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
from fasthtml.common import *
|
||||
from dataclasses import dataclass
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
|
||||
|
||||
@dataclass
|
||||
class DesignerLayout:
|
||||
input_width: int
|
||||
properties_width: int
|
||||
output_width: int
|
||||
|
||||
|
||||
class WorkflowDesignerProperties(BaseComponent):
|
||||
def __init__(self, session, instance_id, owner):
|
||||
super().__init__(session, instance_id)
|
||||
self._owner = owner
|
||||
self._boundaries = self._owner.get_boundaries()
|
||||
self._commands = self._owner.commands
|
||||
self.layout = self.compute_layout()
|
||||
|
||||
def update_layout(self):
|
||||
self.layout = self.compute_layout()
|
||||
|
||||
def compute_layout(self) -> DesignerLayout:
|
||||
if self._owner.get_state().properties_input_width is None:
|
||||
input_width = self._boundaries["width"] // 3
|
||||
properties_width = self._boundaries["width"] // 3
|
||||
output_width = self._boundaries["width"] - input_width - properties_width - 10
|
||||
else:
|
||||
input_width = self._owner.get_state().properties_input_width
|
||||
properties_width = self._owner.get_state().properties_properties_width
|
||||
output_width = self._owner.get_state().properties_output_width
|
||||
|
||||
return DesignerLayout(
|
||||
input_width=input_width,
|
||||
properties_width=properties_width,
|
||||
output_width=output_width
|
||||
)
|
||||
|
||||
def refresh(self, oob=True):
|
||||
return self.__ft__(oob=oob)
|
||||
|
||||
def _mk_input(self):
|
||||
return Div(
|
||||
"Input",
|
||||
id=f"pi_{self._id}",
|
||||
style=f"width: {self.layout.input_width}px; height: {self._boundaries['height']}px; background-color: #f0f0f0; border: 1px solid #ccc; display: inline-block; vertical-align: top; padding: 10px; box-sizing: border-box;"
|
||||
)
|
||||
|
||||
def _mk_output(self):
|
||||
return Div(
|
||||
"Output",
|
||||
id=f"po_{self._id}",
|
||||
style=f"width: {self.layout.output_width}px; height: {self._boundaries['height']}px; background-color: #f0f0f0; border: 1px solid #ccc; display: inline-block; vertical-align: top; padding: 10px; box-sizing: border-box;"
|
||||
)
|
||||
|
||||
def _mk_properties(self):
|
||||
return Div(
|
||||
# Drag handle (20px height)
|
||||
Div(
|
||||
"Properties",
|
||||
id=f"ppt_{self._id}",
|
||||
style="height: 20px; background-color: #ddd; cursor: move; text-align: center; line-height: 20px; font-weight: bold; font-size: 12px; border-bottom: 1px solid #bbb;"
|
||||
),
|
||||
|
||||
# Properties content
|
||||
Div(
|
||||
Input(placeholder="output name", style="width: 100%; margin-bottom: 10px;"),
|
||||
Select(
|
||||
Option("Repository", value="repository"),
|
||||
Option("Jira", value="jira"),
|
||||
style="width: 100%; margin-bottom: 10px;"
|
||||
),
|
||||
Div(
|
||||
Button("Save", **self._commands.on_save(), style="margin-right: 5px; padding: 5px 10px;"),
|
||||
Button("Cancel", **self._commands.on_cancel(), style="padding: 5px 10px;"),
|
||||
style="text-align: center;"
|
||||
),
|
||||
style=f"padding: 10px; height: {self._boundaries['height'] - 20}px; box-sizing: border-box;"
|
||||
),
|
||||
|
||||
# Left resize handle
|
||||
Div(
|
||||
id=f"ppl_{self._id}",
|
||||
style="position: absolute; left: 0; top: 0; width: 5px; height: 100%; cursor: ew-resize; background-color: transparent;"
|
||||
),
|
||||
|
||||
# Right resize handle
|
||||
Div(
|
||||
id=f"ppr_{self._id}",
|
||||
style="position: absolute; right: 0; top: 0; width: 5px; height: 100%; cursor: ew-resize; background-color: transparent;"
|
||||
),
|
||||
|
||||
id=f"pp_{self._id}",
|
||||
style=f"width: {self.layout.properties_width}px; height: {self._boundaries['height']}px; background-color: #f8f8f8; border: 1px solid #ccc; display: inline-block; vertical-align: top; position: relative; box-sizing: border-box;"
|
||||
)
|
||||
|
||||
def _mk_layout(self):
|
||||
return Div(
|
||||
self._mk_input(),
|
||||
self._mk_properties(),
|
||||
self._mk_output(),
|
||||
)
|
||||
|
||||
def __ft__(self, oob=False):
|
||||
# return self.render()
|
||||
return Div(
|
||||
self._mk_layout(),
|
||||
style=f"height: {self._boundaries['height']}px; border: 2px solid #333; position: relative; font-family: Arial, sans-serif;",
|
||||
id=f"p_{self._id}",
|
||||
hx_swap_oob='true' if oob else None,
|
||||
)
|
||||
@@ -25,6 +25,7 @@ class Routes:
|
||||
AddConnection = "/add-connection"
|
||||
DeleteConnection = "/delete-connection"
|
||||
ResizeDesigner = "/resize-designer"
|
||||
UpdatePropertiesLayout = "/update-properties-layout"
|
||||
SaveProperties = "/save-properties"
|
||||
CancelProperties = "/cancel-properties"
|
||||
SelectProcessor = "/select-processor"
|
||||
|
||||
@@ -61,6 +61,9 @@ class WorkflowsDesignerState:
|
||||
connections: list[Connection] = field(default_factory=list)
|
||||
component_counter: int = 0
|
||||
designer_height: int = 230
|
||||
properties_input_width: int = None
|
||||
properties_properties_width : int = None
|
||||
properties_output_width: int = None
|
||||
selected_component_id: str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_i_can_render_no_component(designer):
|
||||
actual = designer.__ft__()
|
||||
expected = Div(
|
||||
H1("Workflow Name"),
|
||||
P("Drag components from the toolbox to the canvas to create your workflow."),
|
||||
# P("Drag components from the toolbox to the canvas to create your workflow."),
|
||||
Div(id=f"t_{designer.get_id()}"), # media + error message
|
||||
Div(id=f"d_{designer.get_id()}"), # designer container
|
||||
Div(cls="wkf-splitter"),
|
||||
|
||||
Reference in New Issue
Block a user