mcai_workflow/workflow_panel/
panel.rs1use crate::{
2 Button, EditNotificationHook, EditNotificationHookMessage, EditStartParameter,
3 EditStartParameterMessage, Field, McaiField, SharedWorkflow,
4};
5use css_in_rust_next::Style;
6use mcai_graph::{Link, LinkType};
7use mcai_models::{ParameterType, SchemaVersion, Workflow, WorkflowDefinition};
8use std::ops::{Deref, DerefMut, Not};
9use yew::{html, Callback, Component, Context, Html, Properties};
10use yew_feather::{edit::Edit, plus::Plus, trash_2::Trash2};
11
12#[derive(PartialEq, Properties)]
13pub struct WorkflowPanelProperties {
14 pub workflow: SharedWorkflow,
15 pub height: String,
16 pub width: String,
17 pub step_id: Option<u32>,
18 pub link: Option<Link>,
19 pub event: Option<Callback<WorkflowPanelEvent>>,
20}
21
22pub enum WorkflowPanelEvent {
23 SetStepIconEvent(u32),
24 SetStepNameEvent(u32),
25 RemovedLink(Link),
26}
27
28pub enum ModalStatus {
29 Hidden,
30 New,
31 Edit(usize),
32}
33
34pub enum WorkflowPanelMessage {
35 UpdateField,
36 IconClick,
37 StepNameClick,
38 AddStartParameter,
39 AddNotificationHook,
40 AppendStartParameter(EditStartParameterMessage),
41 AppendNotificationHook(EditNotificationHookMessage),
42 EditStartParameter(usize),
43 EditNotificationHook(usize),
44 ToggleLive,
45 RemoveStep(u32),
46 RemoveLink(Link),
47}
48
49pub struct WorkflowPanel {
50 style: Style,
51 start_parameter_modal: ModalStatus,
52 notification_hook_modal: ModalStatus,
53}
54
55impl Component for WorkflowPanel {
56 type Message = WorkflowPanelMessage;
57 type Properties = WorkflowPanelProperties;
58
59 fn create(_ctx: &Context<Self>) -> Self {
60 let style = Style::create("Component", include_str!("panel.css")).unwrap();
61 Self {
62 style,
63 start_parameter_modal: ModalStatus::Hidden,
64 notification_hook_modal: ModalStatus::Hidden,
65 }
66 }
67
68 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
69 match msg {
70 WorkflowPanelMessage::UpdateField => true,
71 WorkflowPanelMessage::IconClick => {
72 if let Some(callback) = ctx.props().event.as_ref() {
73 if let Some(step_id) = ctx.props().step_id {
74 callback.emit(WorkflowPanelEvent::SetStepIconEvent(step_id));
75 return true;
76 }
77 }
78 false
79 }
80 WorkflowPanelMessage::StepNameClick => {
81 if let Some(callback) = ctx.props().event.as_ref() {
82 if let Some(step_id) = ctx.props().step_id {
83 callback.emit(WorkflowPanelEvent::SetStepNameEvent(step_id));
84 return true;
85 }
86 }
87 false
88 }
89 WorkflowPanelMessage::AddStartParameter => {
90 self.start_parameter_modal = ModalStatus::New;
91 true
92 }
93 WorkflowPanelMessage::AddNotificationHook => {
94 self.notification_hook_modal = ModalStatus::New;
95 true
96 }
97
98 WorkflowPanelMessage::AppendStartParameter(edit_start_message) => {
99 match edit_start_message {
100 EditStartParameterMessage::Submit(start_parameter) => {
101 if let Workflow::Definition(workflow_definition) =
102 ctx.props().workflow.lock().unwrap().deref_mut()
103 {
104 if let ModalStatus::Edit(index) = self.start_parameter_modal {
105 if let Some(parameter) = workflow_definition
106 .get_mut_start_parameters()
107 .get_mut(index)
108 {
109 *parameter = start_parameter;
110 }
111 } else {
112 workflow_definition
113 .get_mut_start_parameters()
114 .push(start_parameter);
115 }
116
117 self.start_parameter_modal = ModalStatus::Hidden;
118 } else {
119 return false;
120 }
121 }
122 EditStartParameterMessage::Cancel => {
123 self.start_parameter_modal = ModalStatus::Hidden;
124 }
125 EditStartParameterMessage::Delete => {
126 if let ModalStatus::Edit(index) = self.start_parameter_modal {
127 if let Workflow::Definition(workflow_definition) =
128 ctx.props().workflow.lock().unwrap().deref_mut()
129 {
130 workflow_definition.get_mut_start_parameters().remove(index);
131 }
132 }
133 self.start_parameter_modal = ModalStatus::Hidden;
134 }
135 }
136 true
137 }
138 WorkflowPanelMessage::AppendNotificationHook(edit_notification_hook) => {
139 match edit_notification_hook {
140 EditNotificationHookMessage::Submit(notification_hook) => {
141 if let Workflow::Definition(workflow_definition) =
142 ctx.props().workflow.lock().unwrap().deref_mut()
143 {
144 if let ModalStatus::Edit(index) = self.notification_hook_modal {
145 if let Some(notification_hooks) = workflow_definition.get_mut_notification_hooks() {
146 if let Some(parameter) = notification_hooks.get_mut(index) {
147 *parameter = notification_hook;
148 }
149 }
150 } else if let Some(notification_hooks) =
151 workflow_definition.get_mut_notification_hooks()
152 {
153 notification_hooks.push(notification_hook)
154 }
155
156 self.notification_hook_modal = ModalStatus::Hidden;
157 true
158 } else {
159 false
160 }
161 }
162 EditNotificationHookMessage::Cancel => {
163 self.notification_hook_modal = ModalStatus::Hidden;
164 true
165 }
166 EditNotificationHookMessage::Delete => {
167 if let ModalStatus::Edit(index) = self.notification_hook_modal {
168 if let Workflow::Definition(workflow_definition) =
169 ctx.props().workflow.lock().unwrap().deref_mut()
170 {
171 workflow_definition
172 .get_mut_notification_hooks()
173 .map(|notification_hooks| notification_hooks.remove(index));
174 }
175 }
176 self.notification_hook_modal = ModalStatus::Hidden;
177 true
178 }
179 }
180 }
181 WorkflowPanelMessage::EditStartParameter(index) => {
182 self.start_parameter_modal = ModalStatus::Edit(index);
183 true
184 }
185 WorkflowPanelMessage::EditNotificationHook(index) => {
186 self.notification_hook_modal = ModalStatus::Edit(index);
187 true
188 }
189 WorkflowPanelMessage::ToggleLive => {
190 if let Workflow::Definition(definition) = ctx.props().workflow.lock().unwrap().deref_mut() {
191 definition.toggle_is_live()
192 }
193 true
194 }
195 WorkflowPanelMessage::RemoveStep(step_id) => {
196 let mut workflow = ctx.props().workflow.lock().unwrap();
197
198 if let Workflow::Definition(workflow_definition) = workflow.deref_mut() {
199 let steps = workflow_definition.get_mut_steps();
200
201 steps.retain(|step| step.id != step_id);
202
203 steps.iter_mut().for_each(|step| {
204 step.parent_ids.retain(|parent_id| *parent_id != step_id);
205 step
206 .required_to_start
207 .retain(|required| *required != step_id);
208 });
209 }
210
211 true
212 }
213 WorkflowPanelMessage::RemoveLink(link) => {
214 let mut workflow = ctx.props().workflow.lock().unwrap();
215
216 if let Workflow::Definition(workflow_definition) = workflow.deref_mut() {
217 if let Some(step) = workflow_definition.get_mut_step(link.start_node_id()) {
218 let parents = match link.kind() {
219 LinkType::Parentage => &mut step.parent_ids,
220 LinkType::Requirement => &mut step.required_to_start,
221 };
222 parents.retain(|id| *id != link.end_node_id());
223 }
224 }
225
226 if let Some(callback) = ctx.props().event.as_ref() {
227 callback.emit(WorkflowPanelEvent::RemovedLink(link));
228 }
229
230 true
231 }
232 }
233 }
234
235 fn view(&self, ctx: &Context<Self>) -> Html {
236 let style = format!(
237 "height: {}; width: {};",
238 ctx.props().height,
239 ctx.props().width,
240 );
241
242 let modal_edit_start_parameter = match self.start_parameter_modal {
243 ModalStatus::Hidden => html!(),
244 ModalStatus::New => {
245 html!(<EditStartParameter event={ctx.link().callback(WorkflowPanelMessage::AppendStartParameter)} />)
246 }
247 ModalStatus::Edit(index) => {
248 let start_parameter = if let Workflow::Definition(workflow_definition) =
249 ctx.props().workflow.lock().unwrap().deref()
250 {
251 workflow_definition
252 .get_start_parameters()
253 .get(index)
254 .cloned()
255 } else {
256 None
257 };
258
259 html!(
260 <EditStartParameter event={ctx.link().callback(WorkflowPanelMessage::AppendStartParameter)} start_parameter={start_parameter} />
261 )
262 }
263 };
264
265 let modal_notification_hook = match self.notification_hook_modal {
266 ModalStatus::Hidden => html!(),
267 ModalStatus::New => {
268 html!(<EditNotificationHook event={ctx.link().callback(WorkflowPanelMessage::AppendNotificationHook)} />)
269 }
270 ModalStatus::Edit(index) => {
271 let notification_hook = if let Workflow::Definition(workflow_definition) =
272 ctx.props().workflow.lock().unwrap().deref()
273 {
274 if let Some(notification_hooks) = workflow_definition.get_notification_hooks() {
275 notification_hooks.get(index).cloned()
276 } else {
277 None
278 }
279 } else {
280 None
281 };
282
283 html!(<EditNotificationHook event={ctx.link().callback(WorkflowPanelMessage::AppendNotificationHook)} notification_hook={notification_hook} />)
284 }
285 };
286
287 let is_definition = ctx.props().workflow.lock().unwrap().is_definition();
288 let schema_version = ctx.props().workflow.lock().unwrap().schema_version();
289
290 let start_parameters: Html = ctx.props().workflow.lock().unwrap()
291 .get_start_parameters()
292 .iter()
293 .enumerate()
294 .map(|(index, start_parameter)| {
295 let edit_button = is_definition.then(|| html!(
296 <span class="edit_start_parameter">
297 <button onclick={ctx.link().callback(move |_| WorkflowPanelMessage::EditStartParameter(index))}>
298 <Edit />
299 </button>
300 </span>
301 )).unwrap_or_default();
302
303 html!(
304 <div class="start_parameter">
305 <label>
306 {&start_parameter.label}
307 </label>
308 {edit_button}
309 </div>
310 )
311 })
312 .collect();
313
314 let add_start_parameter = is_definition
315 .then(|| {
316 html!(
317 <div class="actions">
318 <Button
319 label="Add start parameter"
320 icon={html!(<Plus />)}
321 onclick={ctx.link().callback(|_| WorkflowPanelMessage::AddStartParameter)}
322 />
323 </div>
324 )
325 })
326 .unwrap_or_default();
327
328 let start_parameters = html!(
329 <>
330 <div>
331 <label class="sub_title">
332 {"Start parameters"}
333 </label>
334 </div>
335 {start_parameters}
336 {add_start_parameter}
337 </>
338 );
339
340 let notification_hooks: Html = if let Some(notification_hooks) = ctx
341 .props()
342 .workflow
343 .lock()
344 .unwrap()
345 .get_notification_hooks()
346 {
347 notification_hooks
348 .iter()
349 .enumerate()
350 .map(|(index, notification_hook)| {
351 let edit_button = is_definition.then(|| html!(
352 <span class="edit_notification_hook">
353 <button onclick={ctx.link().callback(move |_| WorkflowPanelMessage::EditNotificationHook(index))}>
354 <Edit />
355 </button>
356 </span>
357 )).unwrap_or_default();
358
359 html!(
360 <div class="notification_hook">
361 <label>
362 {¬ification_hook.label}
363 </label>
364 {edit_button}
365 </div>
366 )
367 })
368 .collect()
369 } else {
370 html!()
371 };
372
373 let add_notification_hook = is_definition
374 .then(|| {
375 html!(
376 <div class="actions">
377 <Button
378 label="Add notification hook"
379 icon={html!(<Plus />)}
380 onclick={ctx.link().callback(|_| WorkflowPanelMessage::AddNotificationHook)}
381 />
382 </div>
383 )
384 })
385 .unwrap_or_default();
386
387 let notifications_hooks = if schema_version == SchemaVersion::_1_11 {
388 html!(
389 <>
390 <div>
391 <label class="sub_title">
392 {"Notification hooks"}
393 </label>
394 </div>
395 {notification_hooks}
396 {add_notification_hook}
397 </>
398 )
399 } else {
400 html!()
401 };
402
403 let step_information = ctx
404 .props()
405 .step_id
406 .map(|step_id| {
407 let workflow = ctx
408 .props()
409 .workflow
410 .lock()
411 .unwrap();
412
413 let jobs: Vec<Html> = workflow
414 .jobs()
415 .map(|jobs|
416 jobs
417 .iter()
418 .filter(|job| job.step_id == step_id)
419 .map(|job| {
420 let parameters: Html = job.parameters
421 .iter()
422 .map(|parameter| {
423 html!(
424 <Field label={parameter.id.content.clone()}>
425 <McaiField
426 kind={parameter.kind.clone()}
427 step_id={Some(step_id)}
428 field_name={parameter.id.content.clone()}
429 workflow={ctx.props().workflow.clone()}
430 event={ctx.link().callback(|_| WorkflowPanelMessage::UpdateField)}
431 />
432 </Field>
433 )
434 }).collect();
435
436 let progression = job.get_last_progression();
437
438 let classes = format!("job {}", job.status.first().map(|job_status| job_status.state.clone()).unwrap_or_default());
439
440 html!(
441 <div class={classes}>
442 <Field label="Title">
443 {&job.name}
444 </Field>
445 <Field label="Status">
446 <span class="badge status">
447 {job.status.first().map(|job_status| html!({&job_status.state})).unwrap_or_default()}
448 </span>
449 </Field>
450 <Field label="Last Worker Instance ID">
451 {job.last_worker_instance_id.clone().unwrap_or_default()}
452 </Field>
453 <Field label="Progression">
454 {progression.map(|progression| progression.progression).unwrap_or_default()}
455 </Field>
456 <div>
457 <label class="sub_title">{"Parameters"}</label>
458 <span>
459 {parameters}
460 </span>
461 </div>
462 </div>
463 )
464 })
465 .collect()
466 )
467 .unwrap_or_default();
468
469 let jobs = jobs
470 .is_empty()
471 .not()
472 .then(||
473 html!(
474 <div class="jobs">
475 <label class="sub_title">{"Jobs"}</label>
476 <div class="inner">
477 {jobs}
478 </div>
479 </div>
480 )
481 )
482 .unwrap_or_default();
483
484 workflow
485 .steps()
486 .iter()
487 .find(|step| step.id == step_id)
488 .map(|step| {
489 let parameters: Html = step.parameters
490 .iter()
491 .map(|parameter| {
492 html!(
493 <div class="field parameter">
494 <label>
495 {¶meter.id.content}
496 </label>
497 <McaiField
498 kind={parameter.kind.clone()}
499 step_id={Some(step_id)}
500 field_name={parameter.id.content.clone()}
501 workflow={ctx.props().workflow.clone()}
502 event={ctx.link().callback(|_| WorkflowPanelMessage::UpdateField)}
503 />
504 </div>
505 )
506 }).collect();
507
508 let step_name = ParameterType::String {default: None, value: Some(step.name.clone()), required: true };
509 let step_icon = ParameterType::String {default: None, value: Some(step.icon.to_string()), required: false };
510 let step_id = step.id;
511
512 html! {
513 <div class="step">
514 <div class="title">
515 {format!("Step {}", step.label)}
516 </div>
517 <div class="content">
518 <Field label="Queue name">
519 <McaiField
520 kind={step_name}
521 step_id={step.id}
522 field_name="name"
523 workflow={ctx.props().workflow.clone()}
524 event={ctx.link().callback(|_| WorkflowPanelMessage::UpdateField)}
525 />
526 </Field>
527
528 <Field label="Icon">
529 <McaiField
530 kind={step_icon}
531 step_id={step.id}
532 field_name="icon"
533 is_icon=true
534 workflow={ctx.props().workflow.clone()}
535 event={ctx.link().callback(|_| WorkflowPanelMessage::UpdateField)}
536 />
537 </Field>
538 <div>
539 <label class="sub_title">{"Parameters"}</label>
540 <span>
541 {parameters}
542 </span>
543 </div>
544 {jobs}
545 </div>
546 <div class="footer">
547 <Button
548 label={"Remove this step"}
549 icon={html!(<Trash2 />)}
550 disabled={!is_definition}
551 onclick={ctx.link().callback(move |_| WorkflowPanelMessage::RemoveStep(step_id))}
552 />
553 </div>
554 </div>
555 }
556 })
557 .unwrap_or_default()
558 })
559 .unwrap_or_else(|| {
560 let scope = ctx.link();
561 let steps = ctx.props().workflow.lock().unwrap().steps().clone();
562
563 if let Some(html) = ctx.props().link.clone().map(|link| {
564 let start_step = steps.iter().find(|s| s.id == link.start_node_id());
565 let end_step = steps.iter().find(|s| s.id == link.end_node_id());
566
567 let title = format!("{} --> {}", start_step.map(|s| s.label.clone()).unwrap_or_default(), end_step.map(|s| s.label.clone()).unwrap_or_default());
568
569 html!(
570 <div class="link">
571 <div class="title">
572 {title}
573 </div>
574 <div class="footer">
575 <Button
576 label={"Remove this link"}
577 icon={html!(<Trash2 />)}
578 disabled={!is_definition}
579 onclick={scope.callback(move |_| WorkflowPanelMessage::RemoveLink(link.clone()))}
580 />
581 </div>
582 </div>
583 )
584 }) {
585 return html;
586 }
587
588 let reference = ctx
589 .props()
590 .workflow
591 .lock()
592 .unwrap()
593 .reference()
594 .map(|reference| html!(<Field label="Reference">{reference}</Field>))
595 .unwrap_or_default();
596
597 let workflow = ctx.props().workflow.lock().unwrap();
598 let workflow_identifier = workflow.identifier();
599 let workflow_label = workflow.label();
600 let version = workflow.version();
601 let is_live = workflow.is_live();
602
603 let schema_version = match workflow.deref() {
604 Workflow::Definition(WorkflowDefinition::Version1_8(_)) => "1.8".to_string(),
605 Workflow::Definition(WorkflowDefinition::Version1_9(_)) => "1.9".to_string(),
606 Workflow::Definition(WorkflowDefinition::Version1_10(_)) => "1.10".to_string(),
607 Workflow::Definition(WorkflowDefinition::Version1_11(_)) => "1.11".to_string(),
608 Workflow::Instance(workflow_instance) => workflow_instance.schema_version.clone()
609 };
610
611 let inner_identifier = if workflow.is_definition() {
612 html!(
613 <McaiField
614 kind={ParameterType::String{value: Some(workflow_identifier.to_string()), default: None, required: true}}
615 field_name={"identifier"}
616 workflow={ctx.props().workflow.clone()}
617 event={ctx.link().callback(|_| WorkflowPanelMessage::UpdateField)}
618 />
619 )
620 } else {
621 html!({workflow_identifier})
622 };
623
624 let inner_label = if workflow.is_definition() {
625 html!(
626 <McaiField
627 kind={ParameterType::String{value: Some(workflow_label.to_string()), default: None, required: true}}
628 field_name={"label"}
629 workflow={ctx.props().workflow.clone()}
630 event={ctx.link().callback(|_| WorkflowPanelMessage::UpdateField)}
631 />
632 )
633 } else {
634 html!({workflow_label})
635 };
636
637 let inner_tags = if workflow.is_definition() {
638 html!(
639 <McaiField
640 kind={ParameterType::ArrayOfStrings{value: workflow.tags().to_vec(), default: vec![], required: false}}
641 field_name={"tags"}
642 workflow={ctx.props().workflow.clone()}
643 event={ctx.link().callback(|_| WorkflowPanelMessage::UpdateField)}
644 />
645 )
646 } else {
647 workflow.tags().iter().map(|tag| html!(<span class="tag">{tag}</span>)).collect::<Html>()
648 };
649
650 html!(
651 <>
652 <div class="title">
653 <label>{format!("Workflow {}", workflow_label)}</label>
654 </div>
655 <div class="content">
656 <Field label="Identifier">{inner_identifier}</Field>
657 <Field label="Label">{inner_label}</Field>
658 <Field label="Version">{version}</Field>
659 <Field label="Schema version">{schema_version}</Field>
660 <Field label="Is live">
661 <input type="checkbox" checked={is_live} disabled={!workflow.is_definition()} onclick={ctx.link().callback(|_| WorkflowPanelMessage::ToggleLive)} />
662 </Field>
663 <Field label="Tags">{inner_tags}</Field>
664 {reference}
665 {start_parameters}
666 {notifications_hooks}
667 </div>
668 </>
669 )
670 });
671
672 html!(
673 <span class={self.style.clone()} style={style}>
674 {step_information}
675 {modal_edit_start_parameter}
676 {modal_notification_hook}
677 </span>
678 )
679 }
680}