mcai_workflow/workflow_panel/
panel.rs

1use 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                {&notification_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                      {&parameter.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}