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}