mcai_workflow/workflow_graph/
workflow.rs

1use super::{Moving, Node, SvgLink};
2use crate::{colors::*, DragAndDropMessage, ToggleButton, MCAI_DRAG_AND_DROP_ID};
3use by_address::ByAddress;
4use css_in_rust_next::Style;
5use mcai_graph::{Graph, GraphConfiguration, Link, LinkType, NodeConfiguration, ToGraph};
6use mcai_models::{Icon, Parameter, Step, StepMode, Workflow};
7use mcai_types::Coordinates;
8use std::{
9  ops::DerefMut,
10  sync::{Arc, Mutex},
11};
12use web_sys::{DragEvent, MouseEvent, WheelEvent};
13use yew::{html, Callback, Component, Context, Html, Properties};
14
15pub type SharedWorkflow = ByAddress<Arc<Mutex<Workflow>>>;
16
17#[derive(PartialEq, Properties)]
18pub struct WorkflowGraphProperties {
19  pub workflow: SharedWorkflow,
20  pub height: String,
21  pub width: String,
22  pub selected_step: Option<u32>,
23  #[prop_or_default]
24  pub selected_link: Option<Link>,
25  pub events: Option<Callback<WorkflowGraphEvent>>,
26}
27
28pub enum WorkflowGraphEvent {
29  StepSelected(u32),
30  StepDeselected,
31  LinkSelected(Link),
32  LinkDeselected,
33}
34
35#[derive(Debug, PartialEq)]
36pub enum WorkflowGraphMessage {
37  DeselectStep,
38  SelectStep(u32),
39  RemoveStep(u32),
40  SelectLink(Link),
41  RemoveLink(u32, u32),
42  DeselectMovingStep,
43  SelectMovingStep(u32, Coordinates),
44  SelectInputStep(u32, Coordinates),
45  SelectOutputStep(u32, Coordinates),
46  OnDragOver(DragEvent),
47  OnDrop(DragEvent),
48  OnMouseDown(MouseEvent),
49  OnMouseMove(MouseEvent),
50  OnMouseUp(MouseEvent),
51  OnMouseUpStepInput(u32),
52  OnMouseUpStepOutput(u32),
53  OnWheel(WheelEvent),
54  ToggleRequirementLinks(MouseEvent),
55}
56
57pub struct WorkflowGraph {
58  style: Style,
59  offset: Coordinates,
60  moving: Option<Moving>,
61  zoom: f32,
62  configuration: GraphConfiguration,
63  graph: Graph,
64  drawing_link: Option<Link>,
65  link_type: LinkType,
66}
67
68impl WorkflowGraph {
69  fn emit(&self, ctx: &Context<Self>, event: WorkflowGraphEvent) {
70    if let Some(events) = ctx.props().events.as_ref() {
71      events.emit(event)
72    }
73  }
74}
75
76impl Component for WorkflowGraph {
77  type Message = WorkflowGraphMessage;
78  type Properties = WorkflowGraphProperties;
79
80  fn create(ctx: &Context<Self>) -> Self {
81    let style = Style::create("Component", include_str!("workflow.css")).unwrap();
82
83    let configuration = GraphConfiguration::new(NodeConfiguration::new(200, 50, 30, 30));
84    let graph = ctx
85      .props()
86      .workflow
87      .lock()
88      .unwrap()
89      .to_graph(configuration.clone());
90
91    ctx
92      .props()
93      .workflow
94      .lock()
95      .unwrap()
96      .update_steps_coordinates_from_graph(&graph);
97
98    let offset = Coordinates { x: 30, y: 30 };
99
100    WorkflowGraph {
101      style,
102      offset,
103      moving: None,
104      zoom: 1.0,
105      configuration,
106      graph,
107      drawing_link: None,
108      link_type: LinkType::Parentage,
109    }
110  }
111
112  fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
113    let is_definition = ctx.props().workflow.lock().unwrap().is_definition();
114
115    match msg {
116      WorkflowGraphMessage::SelectStep(step_id) => {
117        if ctx
118          .props()
119          .selected_step
120          .map(|id| id == step_id)
121          .unwrap_or_default()
122        {
123          self.emit(ctx, WorkflowGraphEvent::StepDeselected);
124        } else {
125          self.emit(ctx, WorkflowGraphEvent::StepSelected(step_id));
126        }
127      }
128      WorkflowGraphMessage::DeselectStep => {
129        self.emit(ctx, WorkflowGraphEvent::StepDeselected);
130      }
131      WorkflowGraphMessage::RemoveStep(step_id) => {
132        let mut workflow = ctx.props().workflow.lock().unwrap();
133
134        if let Workflow::Definition(workflow_definition) = workflow.deref_mut() {
135          let steps = workflow_definition.get_mut_steps();
136
137          steps.retain(|step| step.id != step_id);
138
139          steps.iter_mut().for_each(|step| {
140            step.parent_ids.retain(|parent_id| *parent_id != step_id);
141            step
142              .required_to_start
143              .retain(|required| *required != step_id);
144          });
145        }
146
147        self.emit(ctx, WorkflowGraphEvent::StepDeselected);
148      }
149      WorkflowGraphMessage::SelectLink(link) => {
150        self.emit(ctx, WorkflowGraphEvent::StepDeselected);
151        self.emit(ctx, WorkflowGraphEvent::LinkSelected(link));
152      }
153      WorkflowGraphMessage::RemoveLink(child_id, parent_id) => {
154        let mut workflow = ctx.props().workflow.lock().unwrap();
155
156        if let Workflow::Definition(workflow_definition) = workflow.deref_mut() {
157          if let Some(step) = workflow_definition.get_mut_step(child_id) {
158            let parents = match self.link_type {
159              LinkType::Parentage => &mut step.parent_ids,
160              LinkType::Requirement => &mut step.required_to_start,
161            };
162            parents.retain(|id| *id != parent_id);
163          }
164          self.graph.disconnect(parent_id, child_id, &self.link_type);
165        }
166      }
167      WorkflowGraphMessage::SelectMovingStep(step_id, coordinates) => {
168        if is_definition {
169          self.moving = Some(Moving::Step(step_id, coordinates));
170        }
171      }
172      WorkflowGraphMessage::SelectInputStep(step_id, coordinates) => {
173        if is_definition {
174          self.moving = Some(Moving::Input(step_id, coordinates));
175        }
176      }
177      WorkflowGraphMessage::SelectOutputStep(step_id, coordinates) => {
178        if is_definition {
179          self.moving = Some(Moving::Output(step_id, coordinates));
180        }
181      }
182      WorkflowGraphMessage::DeselectMovingStep => {
183        self.moving = None;
184        self.drawing_link = None;
185      }
186      WorkflowGraphMessage::OnMouseDown(mouse_event) => {
187        if self.moving.is_none() {
188          let coordinates = Coordinates {
189            x: mouse_event.client_x() as isize - self.offset.x,
190            y: mouse_event.client_y() as isize - self.offset.y,
191          };
192
193          self.moving = Some(Moving::View(coordinates));
194        }
195      }
196      WorkflowGraphMessage::OnDragOver(event) => {
197        event.prevent_default();
198      }
199      WorkflowGraphMessage::OnDrop(event) => {
200        if let Some(data) = event.data_transfer() {
201          if let Ok(data) = data.get_data(MCAI_DRAG_AND_DROP_ID) {
202            let message: DragAndDropMessage = serde_json::from_str(&data).unwrap();
203
204            match message {
205              DragAndDropMessage::Worker(worker_definition) => {
206                if is_definition {
207                  if let Workflow::Definition(definition) =
208                    ctx.props().workflow.lock().unwrap().deref_mut()
209                  {
210                    let steps = definition.get_mut_steps();
211
212                    let index = steps.len() as u32;
213
214                    let parameters: Vec<_> = worker_definition
215                      .parameters
216                      .schema
217                      .object
218                      .unwrap()
219                      .properties
220                      .iter()
221                      .filter_map(|(key, value)| Parameter::new(key, value).ok())
222                      .collect();
223
224                    let coord = Coordinates {
225                      x: event.offset_x() as isize - 30 - 100,
226                      y: event.offset_y() as isize - 30 - 25,
227                    };
228
229                    steps.push(Step {
230                      icon: Icon {
231                        icon: Some("settings".to_string()),
232                      },
233                      id: index,
234                      label: worker_definition.description.label,
235                      name: worker_definition.description.queue_name,
236                      parameters,
237                      mode: StepMode::OneForOne,
238                      multiple_jobs: None,
239                      condition: None,
240                      skip_destination_path: false,
241                      parent_ids: vec![],
242                      required_to_start: vec![],
243                      work_dir: None,
244                      jobs: None,
245                      coordinates: Some(coord.clone()),
246                    });
247
248                    self.graph.add_node(index, coord);
249                  }
250                }
251              }
252            }
253          }
254        }
255      }
256      WorkflowGraphMessage::ToggleRequirementLinks(mouse_event) => {
257        mouse_event.prevent_default();
258        self.link_type = match self.link_type {
259          LinkType::Parentage => LinkType::Requirement,
260          LinkType::Requirement => LinkType::Parentage,
261        };
262      }
263      WorkflowGraphMessage::OnMouseMove(mouse_event) => {
264        mouse_event.prevent_default();
265
266        match &self.moving {
267          Some(Moving::View(moving_view)) => {
268            self.offset.x = mouse_event.client_x() as isize - moving_view.x;
269            self.offset.y = mouse_event.client_y() as isize - moving_view.y;
270          }
271          Some(Moving::Step(step_id, offset)) => {
272            if is_definition {
273              let coordinate = Coordinates {
274                x: mouse_event.client_x() as isize - offset.x,
275                y: mouse_event.client_y() as isize - offset.y,
276              };
277              self.graph.move_node(*step_id, coordinate);
278            }
279          }
280          Some(Moving::Input(step_id, offset)) => {
281            if is_definition {
282              if let Some(node) = self.graph.get_node(*step_id) {
283                let start = node.borrow().get_input_coordinates();
284
285                self.drawing_link = Some(Link::new(
286                  0,
287                  0,
288                  start,
289                  Coordinates {
290                    x: mouse_event.client_x() as isize - offset.x,
291                    y: mouse_event.client_y() as isize - offset.y,
292                  },
293                  self.link_type,
294                ))
295              }
296            }
297          }
298          Some(Moving::Output(step_id, offset)) => {
299            if is_definition {
300              if let Some(node) = self.graph.get_node(*step_id) {
301                let end = node.borrow().get_output_coordinates();
302
303                self.drawing_link = Some(Link::new(
304                  0,
305                  0,
306                  Coordinates {
307                    x: mouse_event.client_x() as isize - offset.x,
308                    y: mouse_event.client_y() as isize - offset.y,
309                  },
310                  end,
311                  self.link_type,
312                ))
313              }
314            }
315          }
316          None => {}
317        }
318      }
319      WorkflowGraphMessage::OnMouseUp(mouse_event) => {
320        if let Some(Moving::Step(step_id, offset)) = &self.moving {
321          if is_definition {
322            let coordinate = Coordinates {
323              x: mouse_event.client_x() as isize - offset.x,
324              y: mouse_event.client_y() as isize - offset.y,
325            };
326
327            let mut workflow = ctx.props().workflow.lock().unwrap();
328
329            if let Workflow::Definition(workflow_definition) = workflow.deref_mut() {
330              if let Some(current_step) = workflow_definition.get_mut_step(*step_id) {
331                current_step.coordinates = Some(coordinate.clone());
332              }
333            }
334            self.graph.move_node(*step_id, coordinate);
335          }
336        }
337
338        self.moving = None;
339        self.drawing_link = None;
340      }
341      WorkflowGraphMessage::OnMouseUpStepInput(child_step_id) => {
342        if let Some(Moving::Output(step_id, _offset)) = &self.moving {
343          self.graph.connect(*step_id, child_step_id, &self.link_type);
344
345          let mut workflow = ctx.props().workflow.lock().unwrap();
346
347          if let Workflow::Definition(workflow_definition) = workflow.deref_mut() {
348            if let Some(child_step) = workflow_definition.get_mut_step(child_step_id) {
349              match self.link_type {
350                LinkType::Parentage => child_step.add_parent(step_id),
351                LinkType::Requirement => child_step.add_required(step_id),
352              }
353            }
354          }
355        }
356        self.moving = None;
357        self.drawing_link = None;
358      }
359      WorkflowGraphMessage::OnMouseUpStepOutput(parent_step_id) => {
360        if let Some(Moving::Input(step_id, _offset)) = &self.moving {
361          self
362            .graph
363            .connect(parent_step_id, *step_id, &self.link_type);
364
365          let mut workflow = ctx.props().workflow.lock().unwrap();
366
367          if let Workflow::Definition(workflow_definition) = workflow.deref_mut() {
368            if let Some(child_step) = workflow_definition.get_mut_step(*step_id) {
369              match self.link_type {
370                LinkType::Parentage => child_step.add_parent(&parent_step_id),
371                LinkType::Requirement => child_step.add_required(&parent_step_id),
372              }
373            }
374          }
375        }
376
377        self.moving = None;
378        self.drawing_link = None;
379      }
380      WorkflowGraphMessage::OnWheel(wheel_event) => {
381        wheel_event.prevent_default();
382        self.zoom = 0.01f32.max(self.zoom - wheel_event.delta_y() as f32 / 100.0);
383      }
384    }
385    true
386  }
387
388  fn changed(&mut self, ctx: &Context<Self>) -> bool {
389    self.graph = ctx
390      .props()
391      .workflow
392      .lock()
393      .unwrap()
394      .to_graph(self.configuration.clone());
395
396    ctx
397      .props()
398      .workflow
399      .lock()
400      .unwrap()
401      .update_steps_coordinates_from_graph(&self.graph);
402    true
403  }
404
405  fn view(&self, ctx: &Context<Self>) -> Html {
406    let mut links = self.graph.get_links(self.link_type);
407    if let Some(link) = self.drawing_link.clone() {
408      links.push(link);
409    }
410
411    let links: Html = match self.link_type {
412      LinkType::Parentage => {
413        links
414          .iter()
415          .map(|link| {
416            let color = ctx
417              .props()
418              .selected_step
419              .map(|id| {
420                if id == link.start_node_id() {
421                  COLOR_GREEN
422                } else if id == link.end_node_id() {
423                  COLOR_BLUE
424                } else {
425                  COLOR_GRAY
426                }
427              })
428              .unwrap_or_else(|| COLOR_GRAY);
429
430            let selected = ctx.props().selected_link.as_ref().map(|selected_link| selected_link == link).unwrap_or_default();
431
432            html!(<SvgLink link={link.clone()} color={color} callback={ctx.link().callback(|message| message)} {selected}></SvgLink>)
433          })
434          .collect()
435      }
436      LinkType::Requirement => {
437        links
438          .iter()
439          .map(|link| {
440            let selected = ctx.props().selected_link.as_ref().map(|selected_link| selected_link == link).unwrap_or_default();
441
442            html!(<SvgLink link={link.clone()} color={COLOR_BLUE} dashed=true width=1 callback={ctx.link().callback(|message| message)} {selected}></SvgLink>)
443          })
444          .collect()
445      }
446    };
447
448    let is_definition = ctx.props().workflow.lock().unwrap().is_definition();
449
450    let nodes: Html = ctx
451      .props()
452      .workflow
453      .lock()
454      .unwrap()
455      .steps()
456      .iter()
457      .map(|step| {
458        let selected = ctx.props().selected_step.map(|id| id == step.id).unwrap_or_default();
459
460        self.graph.get_node(step.id).map(|node| {
461          html!(<Node step={step.clone()} position={node.borrow().coordinates()} selected={selected} editable={is_definition} callback={ctx.link().callback(|message| message)}></Node>)
462        }).unwrap_or_default()
463      })
464      .collect();
465
466    let style = format!(
467      "height: {}; width: {};",
468      ctx.props().height,
469      ctx.props().width,
470    );
471
472    let dataflow_style = format!(
473      "transform: translate({}px, {}px) scale({});",
474      self.offset.x, self.offset.y, self.zoom
475    );
476
477    let toggle_label = match self.link_type {
478      LinkType::Parentage => "Parentage links",
479      LinkType::Requirement => "Requirement links",
480    };
481
482    html!(
483      <div class={self.style.clone()}
484          onwheel={ctx.link().callback(WorkflowGraphMessage::OnWheel)}
485          onmousedown={ctx.link().callback(WorkflowGraphMessage::OnMouseDown)}
486          onmousemove={ctx.link().callback(WorkflowGraphMessage::OnMouseMove)}
487          onmouseup={ctx.link().callback(WorkflowGraphMessage::OnMouseUp)}
488          ondrop={ctx.link().callback(WorkflowGraphMessage::OnDrop)}
489          ondragover={ctx.link().callback(WorkflowGraphMessage::OnDragOver)}
490          style={style}>
491        <ToggleButton class="toggle"
492          label={toggle_label}
493          onclick={ctx.link().callback(WorkflowGraphMessage::ToggleRequirementLinks)}
494          checked={self.link_type == LinkType::Requirement}/>
495        <div class="drawflow" style={dataflow_style}
496          >
497          {links}
498          {nodes}
499        </div>
500      </div>
501    )
502  }
503}