1use std::path::PathBuf;
18
19use components::{remove_button, IdOnlyLinkReference, LinkList, LinkReference};
20use eframe::egui::{self, Align, Layout};
21use egui_extras::{Column, TableBuilder};
22use indexmap::{IndexMap, IndexSet};
23use strum::IntoEnumIterator;
24
25use rafia_stpa::{
26 construct_link_map,
27 stpa::{
28 ca_analysis::{CaAnalyses, CaAnalysis, CaResult},
29 constraints::{Constraint, ConstraintType, Constraints, UcaLink},
30 control_loop_sequences::{ControlLoopSequence, ControlLoopSequences},
31 control_loops::{ControlLoop, ControlLoops},
32 elements::{Element, Elements},
33 hazards::{Hazard, Hazards, LossLink},
34 interactions::{Interaction, InteractionCategory, InteractionType, Interactions},
35 losses::{Loss, Losses},
36 scenarios::{CausalScenario, CausalScenarioResult, Scenario, Scenarios},
37 uca_contexts::{UcaContext, UcaContexts},
38 ucas::{Uca, Ucas},
39 CausalScenarioLink, ConstraintLink, ControlLoopLink, ControlLoopSequenceLink, ElementLink,
40 HazardLink, InteractionLink, StpaData, StpaProject, UcaContextLink, UcaType,
41 },
42};
43
44mod components;
45pub mod tabs;
46
47pub fn update_diagram(ui: &mut egui::Ui, diagram: Option<&url::Url>) {
48 if let Some(diagram) = diagram {
49 ui.image(diagram.as_str());
50 };
51}
52
53pub fn update_losses(ui: &mut egui::Ui, losses: &mut Losses) {
54 ui.with_layout(Layout::top_down(Align::Min), |ui| {
55 let new = if ui.button("Add loss").clicked() {
56 losses.losses.push(Loss {
57 id: format!("L{}", losses.losses.len() + 1),
58 description: "".to_string(),
59 category: IndexSet::new(),
60 });
61 true
62 } else {
63 false
64 };
65 ui.separator();
66
67 egui::ScrollArea::horizontal()
68 .auto_shrink(false)
69 .show(ui, |ui| {
70 let mut table = TableBuilder::new(ui)
71 .column(Column::auto().resizable(true))
72 .column(Column::auto().resizable(true))
73 .column(Column::remainder().at_least(250.0));
74 if new {
75 table = table.scroll_to_row(losses.losses.len() - 1, None);
76 }
77 table
78 .header(20.0, |mut header| {
79 header.col(|ui| {
80 ui.heading("Loss ID");
81 });
82 header.col(|ui| {
83 ui.heading("Loss Description");
84 });
85 header.col(|ui| {
86 ui.heading("Loss Categories");
87 });
88 })
89 .body(|mut body| {
90 let mut remove_loss = None;
91 for (index, loss) in &mut losses.losses.iter_mut().enumerate() {
92 body.row(30.0, |mut row| {
93 row.col(|ui| {
94 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
95 if remove_button(ui).clicked() {
96 remove_loss = Some(index);
97 };
98 egui::TextEdit::singleline(&mut loss.id)
99 .hint_text("Type something!")
100 .show(ui);
101 });
102 });
103
104 row.col(|ui| {
105 egui::TextEdit::singleline(&mut loss.description)
106 .hint_text("Type something!")
107 .show(ui);
108 });
109 row.col(|ui| {
110 components::EditableSet(&mut loss.category).show(ui);
111 });
112 });
113 }
114 if let Some(index) = remove_loss {
115 losses.losses.remove(index);
116 };
117 });
118 });
119 });
120}
121
122pub fn update_hazards(ui: &mut egui::Ui, hazards: &mut Hazards, losses: &Losses) {
123 let loss_map = construct_link_map!(table = losses, link_type = LossLink);
124
125 ui.with_layout(Layout::top_down(Align::Min), |ui| {
126 let new = if ui.button("Add hazard").clicked() {
127 hazards.hazards.push(Hazard {
128 id: format!("H{}", hazards.hazards.len() + 1),
129 description: "".to_string(),
130 losses: vec![],
131 notes: "".to_string(),
132 });
133 true
134 } else {
135 false
136 };
137 ui.separator();
138 egui::ScrollArea::horizontal()
139 .auto_shrink(false)
140 .show(ui, |ui| {
141 let mut table = TableBuilder::new(ui)
142 .column(Column::auto().resizable(true))
143 .column(Column::auto().resizable(true))
144 .column(Column::auto().resizable(true))
145 .column(Column::remainder());
146 if new {
147 table = table.scroll_to_row(hazards.hazards.len() - 1, None)
148 };
149 table
150 .header(20.0, |mut header| {
151 header.col(|ui| {
152 ui.heading("ID");
153 });
154 header.col(|ui| {
155 ui.heading("Description");
156 });
157 header.col(|ui| {
158 ui.heading("Losses");
159 });
160 header.col(|ui| {
161 ui.heading("Notes");
162 });
163 })
164 .body(|mut body| {
165 let mut remove_hazard = None;
166 for (index, hazards) in hazards.hazards.iter_mut().enumerate() {
167 body.row(30.0, |mut row| {
168 row.col(|ui| {
169 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
170 if remove_button(ui).clicked() {
171 remove_hazard = Some(index);
172 };
173 egui::TextEdit::singleline(&mut hazards.id)
174 .hint_text("Type something!")
175 .show(ui);
176 });
177 });
178 row.col(|ui| {
179 egui::TextEdit::singleline(&mut hazards.description)
180 .hint_text("Type something!")
181 .show(ui);
182 });
183 row.col(|ui| {
184 components::LinkList {
185 links: &mut hazards.losses,
186 mapping: &loss_map,
187 }
188 .show(ui);
189 });
190 row.col(|ui| {
191 egui::TextEdit::multiline(&mut hazards.notes)
192 .hint_text("Type something!")
193 .show(ui);
194 });
195 });
196 }
197 if let Some(index) = remove_hazard {
198 hazards.hazards.remove(index);
199 };
200 });
201 });
202 });
203}
204
205pub fn update_elements(ui: &mut egui::Ui, elements: &mut Elements) {
206 ui.with_layout(Layout::top_down(Align::Min), |ui| {
207 let new = if ui.button("Add element").clicked() {
208 elements.elements.push(Element {
209 id: format!("E{}", elements.elements.len() + 1),
210 name: "".to_string(),
211 responsibilities: vec![],
212 roles: IndexSet::new(),
213 notes: "".to_string(),
214 });
215 true
216 } else {
217 false
218 };
219 ui.separator();
220 egui::ScrollArea::horizontal()
221 .auto_shrink(false)
222 .show(ui, |ui| {
223 let mut table = TableBuilder::new(ui)
224 .column(Column::auto().resizable(true))
225 .column(Column::initial(250.0).resizable(true))
226 .column(Column::initial(300.0).resizable(true))
227 .column(Column::initial(250.0).resizable(true))
228 .column(Column::remainder().at_least(250.0));
229 if new {
230 table = table.scroll_to_row(elements.elements.len() - 1, None)
231 };
232
233 table
234 .header(40.0, |mut header| {
235 header.col(|ui| {
236 ui.heading("ID");
237 });
238 header.col(|ui| {
239 ui.heading("Name");
240 });
241 header.col(|ui| {
242 ui.heading("Responsibilities");
243 });
244 header.col(|ui| {
245 ui.heading("Roles");
246 });
247 header.col(|ui| {
248 ui.heading("Notes");
249 });
250 })
251 .body(|mut body| {
252 let mut remove_element = None;
253 for (index, element) in elements.elements.iter_mut().enumerate() {
254 body.row(30.0, |mut row| {
255 row.col(|ui| {
256 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
257 if remove_button(ui).clicked() {
258 remove_element = Some(index);
259 };
260 egui::TextEdit::singleline(&mut element.id)
261 .hint_text("Type something!")
262 .show(ui);
263 });
264 });
265 row.col(|ui| {
266 egui::TextEdit::singleline(&mut element.name)
267 .hint_text("Type something!")
268 .show(ui);
269 });
270 row.col(|ui| {
271 ui.vertical(|ui| {
272 components::TextList(&mut element.responsibilities)
273 .show(ui);
274 });
275 });
276 row.col(|ui| {
277 components::EditableSet(&mut element.roles).show(ui);
278 });
279 row.col(|ui| {
280 egui::TextEdit::multiline(&mut element.notes)
281 .hint_text("Type something!")
282 .show(ui);
283 });
284 });
285 }
286 if let Some(index) = remove_element {
287 elements.elements.remove(index);
288 };
289 });
290 });
291 });
292}
293
294pub fn update_constraints(
295 ui: &mut egui::Ui,
296 constraints: &mut Constraints,
297 hazards: &Hazards,
298 ucas: &Ucas,
299 scenarios: &Scenarios,
300) {
301 let constraint_map = construct_link_map!(table = constraints, link_type = ConstraintLink);
302 let hazard_map = construct_link_map!(table = hazards, link_type = HazardLink);
303 let uca_map = construct_link_map!(
304 table = ucas,
305 link_type = UcaLink,
306 linked_to = uca_definition
307 );
308 let cs_map = construct_link_map!(
309 table = scenarios,
310 link_type = CausalScenarioLink,
311 linked_to = causal_scenario_definition
312 );
313
314 ui.with_layout(Layout::top_down(Align::Min), |ui| {
315 let new = if ui.button("Add constraints").clicked() {
316 constraints.constraints.push(Constraint {
317 id: format!("{}", constraints.constraints.len() + 1),
318 description: "".to_string(),
319 constraint_type: ConstraintType::Missing,
320 link_to_constraints: vec![],
321 link_to_hazards: vec![],
322 link_to_uca: vec![],
323 link_to_cs: vec![],
324 link_to_tsf: vec![],
325 });
326 true
327 } else {
328 false
329 };
330 ui.separator();
331 egui::ScrollArea::horizontal()
332 .auto_shrink(false)
333 .show(ui, |ui| {
334 let mut table = TableBuilder::new(ui)
335 .column(Column::auto().resizable(true))
336 .column(Column::initial(250.0).resizable(true))
337 .column(Column::initial(300.0).resizable(true))
338 .column(Column::initial(250.0).resizable(true))
339 .column(Column::initial(250.0).resizable(true))
340 .column(Column::initial(300.0).resizable(true))
341 .column(Column::initial(250.0).resizable(true))
342 .column(Column::remainder().at_least(250.0));
343 if new {
344 table = table.scroll_to_row(constraints.constraints.len() - 1, None)
345 };
346 table
347 .header(40.0, |mut header| {
348 header.col(|ui| {
349 ui.heading("ID");
350 });
351 header.col(|ui| {
352 ui.heading("Description");
353 });
354 header.col(|ui| {
355 ui.heading("Constraint Type");
356 });
357 header.col(|ui| {
358 ui.heading("Constraints");
359 });
360 header.col(|ui| {
361 ui.heading("Hazards");
362 });
363 header.col(|ui| {
364 ui.heading("UCA");
365 });
366 header.col(|ui| {
367 ui.heading("CS");
368 });
369 header.col(|ui| {
370 ui.heading("TSF");
371 });
372 })
373 .body(|mut body| {
374 let mut remove_constraint = None;
375 for (index, constraint) in
376 &mut constraints.constraints.iter_mut().enumerate()
377 {
378 body.row(30.0, |mut row| {
379 row.col(|ui| {
380 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
381 if remove_button(ui).clicked() {
382 remove_constraint = Some(index);
383 };
384 egui::TextEdit::singleline(&mut constraint.id)
385 .hint_text("Type something!")
386 .show(ui);
387 });
388 });
389 row.col(|ui| {
390 egui::TextEdit::singleline(&mut constraint.description)
391 .hint_text("Type something!")
392 .show(ui);
393 });
394 row.col(|ui| {
395 components::Dropdown {
396 value: &mut constraint.constraint_type,
397 id_salt: format!("constraint_type{:?}", constraint.id),
398 }
399 .show(ui);
400 });
401 row.col(|ui| {
402 components::LinkList {
403 links: &mut constraint.link_to_constraints,
404 mapping: &constraint_map,
405 }
406 .show(ui);
407 });
408 row.col(|ui| {
409 LinkList {
410 links: &mut constraint.link_to_hazards,
411 mapping: &hazard_map,
412 }
413 .show(ui);
414 });
415 row.col(|ui| {
416 components::LinkList {
417 links: &mut constraint.link_to_uca,
418 mapping: &uca_map,
419 }
420 .show(ui);
421 });
422 row.col(|ui| {
423 components::LinkList {
424 links: &mut constraint.link_to_cs,
425 mapping: &cs_map,
426 }
427 .show(ui);
428 });
429 row.col(|ui| {
430 ui.vertical(|ui| {
431 constraint.link_to_tsf.retain_mut(|link| {
432 let mut keep = true;
433 ui.horizontal(|ui| {
434 egui::TextEdit::singleline(link)
435 .hint_text("Type something!")
436 .show(ui);
437 ui.with_layout(
438 Layout::right_to_left(Align::Center),
439 |ui| {
440 if ui.small_button("-").clicked() {
441 keep = false;
442 }
443 },
444 );
445 });
446
447 keep
448 });
449 });
450 if ui.button("Add TSF link").clicked() {
451 constraint.link_to_tsf.push(String::new());
452 }
453 });
454 });
455 }
456 if let Some(index) = remove_constraint {
457 constraints.constraints.remove(index);
458 };
459 });
460 });
461 });
462}
463
464pub fn update_interactions(
465 ui: &mut egui::Ui,
466 interactions: &mut Interactions,
467 elements: &Elements,
468) {
469 let element_map: IndexMap<ElementLink, String> = elements
470 .elements
471 .iter()
472 .map(|item| (ElementLink::from(item.id.as_ref()), item.name.clone()))
473 .collect();
474
475 let new = if ui.button("Add interactions").clicked() {
476 interactions.interactions.push(Interaction {
477 id: format!("{}", interactions.interactions.len() + 1),
478 category: InteractionCategory::Missing,
479 description: "".to_string(),
480 diagram_label: "".to_string(),
481 start: ElementLink::from(""),
482 end: ElementLink::from(""),
483 note: "".to_string(),
484 interaction_type: InteractionType::Missing,
485 });
486 true
487 } else {
488 false
489 };
490 ui.separator();
491 egui::ScrollArea::horizontal()
492 .auto_shrink(false)
493 .show(ui, |ui| {
494 let mut table = TableBuilder::new(ui)
495 .id_salt("interactions")
496 .column(Column::auto().resizable(true))
497 .column(Column::auto().resizable(true))
498 .column(Column::auto().resizable(true))
499 .column(Column::auto().resizable(true))
500 .column(Column::auto().resizable(true))
501 .column(Column::auto().resizable(true))
502 .column(Column::auto().resizable(true))
503 .column(Column::remainder().at_least(250.0));
504
505 if new {
506 table = table.scroll_to_row(interactions.interactions.len() - 1, None)
507 };
508 table
509 .header(20.0, |mut header| {
510 header.col(|ui| {
511 ui.heading("ID");
512 });
513 header.col(|ui| {
514 ui.heading("Diagram ID");
515 });
516 header.col(|ui| {
517 ui.heading("Description");
518 });
519 header.col(|ui| {
520 ui.heading("Type");
521 });
522 header.col(|ui| {
523 ui.heading("Provider");
524 });
525 header.col(|ui| {
526 ui.heading("Receiver");
527 });
528 header.col(|ui| {
529 ui.heading("Category");
530 });
531 header.col(|ui| {
532 ui.heading("Notes");
533 });
534 })
535 .body(|mut body| {
536 let mut remove_interaction = None;
537 for (index, interaction) in
538 &mut interactions.interactions.iter_mut().enumerate()
539 {
540 body.row(30.0, |mut row| {
541 row.col(|ui| {
542 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
543 if remove_button(ui).clicked() {
544 remove_interaction = Some(index);
545 };
546 egui::TextEdit::singleline(&mut interaction.id)
547 .hint_text("Type something!")
548 .show(ui);
549 });
550 });
551 row.col(|ui| {
552 egui::TextEdit::singleline(&mut interaction.diagram_label)
553 .hint_text("Type something!")
554 .show(ui);
555 });
556 row.col(|ui| {
557 egui::TextEdit::singleline(&mut interaction.description)
558 .hint_text("Type something!")
559 .show(ui);
560 });
561 row.col(|ui| {
562 components::Dropdown {
563 value: &mut interaction.interaction_type,
564 id_salt: format!("interaction_type{:?}", interaction.id),
565 }
566 .show(ui);
567 });
568 row.col(|ui| {
569 components::LinkReference {
570 id_salt: format!(
571 "interaction_start_combobox_{}",
572 interaction.id
573 ),
574 link: &mut interaction.start,
575 mapping: &element_map,
576 }
577 .show(ui);
578 });
579 row.col(|ui| {
580 components::LinkReference {
581 id_salt: format!("interaction_end_combobox_{}", interaction.id),
582 link: &mut interaction.end,
583 mapping: &element_map,
584 }
585 .show(ui)
586 });
587 row.col(|ui| {
588 components::Dropdown {
589 value: &mut interaction.category,
590 id_salt: format!("interaction_category{:?}", interaction.id),
591 }
592 .show(ui);
593 });
594 row.col(|ui| {
595 egui::TextEdit::multiline(&mut interaction.note)
596 .hint_text("Type something!")
597 .show(ui);
598 });
599 });
600 }
601 if let Some(index) = remove_interaction {
602 interactions.interactions.remove(index);
603 };
604 });
605 });
606}
607
608pub fn update_ca_analyses(
609 ui: &mut egui::Ui,
610 ca_analyses: &mut CaAnalyses,
611 uca_contexts: &UcaContexts,
612 hazards: &Hazards,
613 interactions: &Interactions,
614) {
615 let uca_context_map = construct_link_map!(
616 table = uca_contexts,
617 link_type = UcaContextLink,
618 linked_to = unsafe_context
619 );
620 let hazard_map = construct_link_map!(table = hazards, link_type = HazardLink);
621 let interaction_map = construct_link_map!(table = interactions, link_type = InteractionLink);
622
623 ui.with_layout(Layout::top_down(Align::Min), |ui| {
624 let new = if ui.button("Add control action analyses").clicked() {
625 ca_analyses.ca_analyses.push(CaAnalysis {
626 id: format!("{}", ca_analyses.ca_analyses.len() + 1),
627 analysis_result: CaResult::Missing,
628 ca_id: InteractionLink::from(""),
629 link_to_hazards: vec![],
630 uca_context: UcaContextLink::from(""),
631 uca_type: UcaType::Missing,
632 justification: "".to_string(),
633 });
634 true
635 } else {
636 false
637 };
638 ui.separator();
639 egui::ScrollArea::horizontal()
640 .auto_shrink(false)
641 .show(ui, |ui| {
642 let mut table = TableBuilder::new(ui)
643 .id_salt("ca_analysis")
644 .column(Column::auto().resizable(true))
645 .column(Column::auto().resizable(true))
646 .column(Column::auto().resizable(true))
647 .column(Column::auto().resizable(true))
648 .column(Column::auto().resizable(true))
649 .column(Column::auto().resizable(true))
650 .column(Column::remainder().at_least(250.0));
651
652 if new {
653 table = table.scroll_to_row(ca_analyses.ca_analyses.len() - 1, None)
654 };
655 table
656 .header(20.0, |mut header| {
657 header.col(|ui| {
658 ui.heading("ID");
659 });
660 header.col(|ui| {
661 ui.heading("CA ID");
662 });
663 header.col(|ui| {
664 ui.heading("UCA Type");
665 });
666 header.col(|ui| {
667 ui.heading("UCA Context");
668 });
669 header.col(|ui| {
670 ui.heading("Analyses Results");
671 });
672 header.col(|ui| {
673 ui.heading("Hazards");
674 });
675 header.col(|ui| {
676 ui.heading("justification");
677 });
678 })
679 .body(|mut body| {
680 let mut remove_ca_analysis = None;
681 for (index, ca_analysis) in
682 &mut ca_analyses.ca_analyses.iter_mut().enumerate()
683 {
684 body.row(30.0, |mut row| {
685 row.col(|ui| {
686 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
687 if remove_button(ui).clicked() {
688 remove_ca_analysis = Some(index);
689 };
690 egui::TextEdit::singleline(&mut ca_analysis.id)
691 .hint_text("Type something!")
692 .show(ui);
693 });
694 });
695 row.col(|ui| {
696 LinkReference {
697 id_salt: format!(
698 "ca_analysis_ca_id_combobox_{}",
699 ca_analysis.id
700 ),
701 link: &mut ca_analysis.ca_id,
702 mapping: &interaction_map,
703 }
704 .show(ui);
705 });
706 row.col(|ui| {
707 components::Dropdown {
708 value: &mut ca_analysis.uca_type,
709 id_salt: format!("uca_type{:?}", ca_analysis.id),
710 }
711 .show(ui);
712 });
713 row.col(|ui| {
714 LinkReference {
715 id_salt: format!(
716 "ca_analysis_uca_context_combobox_{}",
717 ca_analysis.id
718 ),
719 link: &mut ca_analysis.uca_context,
720 mapping: &uca_context_map,
721 }
722 .show(ui);
723 });
724 row.col(|ui| {
725 components::Dropdown {
726 value: &mut ca_analysis.analysis_result,
727 id_salt: format!("ca_analysis_result{:?}", ca_analysis.id),
728 }
729 .show(ui);
730 });
731 row.col(|ui| {
732 LinkList {
733 links: &mut ca_analysis.link_to_hazards,
734 mapping: &hazard_map,
735 }
736 .show(ui);
737 });
738 row.col(|ui| {
739 egui::TextEdit::multiline(&mut ca_analysis.justification)
740 .hint_text("Type something!")
741 .show(ui);
742 });
743 });
744 }
745 if let Some(index) = remove_ca_analysis {
746 ca_analyses.ca_analyses.remove(index);
747 };
748 });
749 });
750 });
751}
752
753pub fn update_uca_contexts(ui: &mut egui::Ui, uca_contexts: &mut UcaContexts) {
754 ui.with_layout(Layout::top_down(Align::Min), |ui| {
755 let new = if ui.button("Add unsafe control action context").clicked() {
756 uca_contexts.uca_contexts.push(UcaContext {
757 id: format!("CX{}", uca_contexts.uca_contexts.len() + 1),
758 unsafe_context: "".to_string(),
759 notes: "".to_string(),
760 });
761 true
762 } else {
763 false
764 };
765 ui.separator();
766 egui::ScrollArea::horizontal()
767 .auto_shrink(false)
768 .show(ui, |ui| {
769 let mut table = TableBuilder::new(ui)
770 .id_salt("uca_contexts")
771 .column(Column::auto().resizable(true))
772 .column(Column::auto().resizable(true))
773 .column(Column::remainder().at_least(250.0));
774
775 if new {
776 table = table.scroll_to_row(uca_contexts.uca_contexts.len() - 1, None)
777 };
778 table
779 .header(20.0, |mut header| {
780 header.col(|ui| {
781 ui.heading("ID");
782 });
783 header.col(|ui| {
784 ui.heading("Unsafe Context");
785 });
786 header.col(|ui| {
787 ui.heading("Notes");
788 });
789 })
790 .body(|mut body| {
791 let mut remove_uca_context = None;
792 for (index, uca_context) in
793 &mut uca_contexts.uca_contexts.iter_mut().enumerate()
794 {
795 body.row(30.0, |mut row| {
796 row.col(|ui| {
797 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
798 if remove_button(ui).clicked() {
799 remove_uca_context = Some(index);
800 };
801 egui::TextEdit::singleline(&mut uca_context.id)
802 .hint_text("Type something!")
803 .show(ui);
804 });
805 });
806 row.col(|ui| {
807 egui::TextEdit::multiline(&mut uca_context.unsafe_context)
808 .hint_text("Type something!")
809 .show(ui);
810 });
811 row.col(|ui| {
812 egui::TextEdit::multiline(&mut uca_context.notes)
813 .hint_text("Type something!")
814 .show(ui);
815 });
816 });
817 }
818 if let Some(index) = remove_uca_context {
819 uca_contexts.uca_contexts.remove(index);
820 };
821 });
822 });
823 });
824}
825
826pub fn update_ucas(
827 ui: &mut egui::Ui,
828 ucas: &mut Ucas,
829 uca_contexts: &UcaContexts,
830 constraints: &Constraints,
831 interactions: &Interactions,
832) {
833 let uca_context_map = construct_link_map!(
834 table = uca_contexts,
835 link_type = UcaContextLink,
836 linked_to = unsafe_context
837 );
838 let constraint_map = construct_link_map!(table = constraints, link_type = ConstraintLink);
839 let interaction_map = construct_link_map!(table = interactions, link_type = InteractionLink);
840
841 ui.with_layout(Layout::top_down(Align::Min), |ui| {
842 let new = if ui.button("Add unsafe control action").clicked() {
843 ucas.ucas.push(Uca {
844 id: format!("{}", ucas.ucas.len() + 1),
845 constraint_id: vec![],
846 ca: InteractionLink::from(""),
847 uca_context: UcaContextLink::from(""),
848 uca_definition: "".to_string(),
849 uca_type: UcaType::Missing,
850 });
851 true
852 } else {
853 false
854 };
855 ui.separator();
856 egui::ScrollArea::horizontal()
857 .auto_shrink(false)
858 .show(ui, |ui| {
859 let mut table = TableBuilder::new(ui)
860 .id_salt("ucas")
861 .column(Column::auto().resizable(true))
862 .column(Column::auto().resizable(true))
863 .column(Column::auto().resizable(true))
864 .column(Column::auto().resizable(true))
865 .column(Column::auto().resizable(true))
866 .column(Column::auto().resizable(true))
867 .column(Column::remainder().at_least(250.0));
868 if new {
869 table = table.scroll_to_row(ucas.ucas.len() - 1, None)
870 };
871 table
872 .header(20.0, |mut header| {
873 header.col(|ui| {
874 ui.heading("ID");
875 });
876 header.col(|ui| {
877 ui.heading("Uca Definition");
878 });
879 header.col(|ui| {
880 ui.heading("Ca");
881 });
882 header.col(|ui| {
883 ui.heading("UCA Type");
884 });
885 header.col(|ui| {
886 ui.heading("UCA Context");
887 });
888 header.col(|ui| {
889 ui.heading("Constraint(s)");
890 });
891 })
892 .body(|mut body| {
893 let mut remove_uca = None;
894 for (index, uca) in &mut ucas.ucas.iter_mut().enumerate() {
895 body.row(30.0, |mut row| {
896 row.col(|ui| {
897 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
898 if remove_button(ui).clicked() {
899 remove_uca = Some(index);
900 };
901 egui::TextEdit::singleline(&mut uca.id)
902 .hint_text("Type something!")
903 .show(ui);
904 });
905 });
906 row.col(|ui| {
907 egui::TextEdit::multiline(&mut uca.uca_definition)
908 .hint_text("Type something!")
909 .show(ui);
910 });
911 row.col(|ui| {
912 LinkReference {
913 id_salt: format!("uca_ca_id_combobox_{}", uca.id),
914 link: &mut uca.ca,
915 mapping: &interaction_map,
916 }
917 .show(ui);
918 });
919 row.col(|ui| {
920 egui::ComboBox::from_id_salt(format!(
921 "uca_uca_type{:?}",
922 uca.id
923 ))
924 .selected_text(format!("{:?}", uca.uca_type))
925 .show_ui(ui, |ui| {
926 for uca_type in UcaType::iter() {
927 ui.selectable_value(
928 &mut uca.uca_type,
929 uca_type,
930 format!("{uca_type:?}"),
931 );
932 }
933 });
934 });
935 row.col(|ui| {
936 LinkReference {
937 id_salt: format!("uca_uca_context_combobox_{}", uca.id),
938 link: &mut uca.uca_context,
939 mapping: &uca_context_map,
940 }
941 .show(ui);
942 });
943
944 row.col(|ui| {
945 components::LinkList {
946 links: &mut uca.constraint_id,
947 mapping: &constraint_map,
948 }
949 .show(ui);
950 });
951 });
952 }
953 if let Some(index) = remove_uca {
954 ucas.ucas.remove(index);
955 };
956 });
957 });
958 });
959}
960
961pub fn update_control_loops(
962 ui: &mut egui::Ui,
963 control_loops: &mut ControlLoops,
964 elements: &Elements,
965 constraints: &Constraints,
966) {
967 let element_map =
968 construct_link_map!(table = elements, link_type = ElementLink, linked_to = name);
969 let constraint_map = construct_link_map!(table = constraints, link_type = ConstraintLink);
970
971 ui.with_layout(Layout::top_down(Align::Min), |ui| {
972 let new = if ui.button("Add control loop").clicked() {
973 control_loops.control_loops.push(ControlLoop {
974 id: format!("CL{}", control_loops.control_loops.len() + 1),
975 controlled_process: ElementLink::from(""),
976 description: "".to_string(),
977 linked_slc: vec![],
978 });
979 true
980 } else {
981 false
982 };
983 ui.separator();
984 egui::ScrollArea::horizontal()
985 .auto_shrink(false)
986 .show(ui, |ui| {
987 let mut table = TableBuilder::new(ui)
988 .id_salt("control_loops")
989 .column(Column::auto().resizable(true))
990 .column(Column::auto().resizable(true))
991 .column(Column::auto().resizable(true))
992 .column(Column::remainder().at_least(250.0));
993 if new {
994 table = table.scroll_to_row(control_loops.control_loops.len() - 1, None)
995 };
996 table
997 .header(20.0, |mut header| {
998 header.col(|ui| {
999 ui.heading("ID");
1000 });
1001 header.col(|ui| {
1002 ui.heading("Description");
1003 });
1004 header.col(|ui| {
1005 ui.heading("Controlled Process");
1006 });
1007 header.col(|ui| {
1008 ui.heading("Linked SLC(s)");
1009 });
1010 })
1011 .body(|mut body| {
1012 let mut remove_control_loop = None;
1013 for (index, control_loop) in
1014 &mut control_loops.control_loops.iter_mut().enumerate()
1015 {
1016 body.row(30.0, |mut row| {
1017 row.col(|ui| {
1018 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1019 if remove_button(ui).clicked() {
1020 remove_control_loop = Some(index);
1021 };
1022
1023 egui::TextEdit::singleline(&mut control_loop.id)
1024 .hint_text("Type something!")
1025 .show(ui);
1026 });
1027 });
1028 row.col(|ui| {
1029 egui::TextEdit::multiline(&mut control_loop.description)
1030 .hint_text("Type something!")
1031 .show(ui);
1032 });
1033 row.col(|ui| {
1034 LinkReference {
1035 id_salt: format!(
1036 "control_loop_controlled_process_combobox_{}",
1037 control_loop.id
1038 ),
1039 link: &mut control_loop.controlled_process,
1040 mapping: &element_map,
1041 }
1042 .show(ui);
1043 });
1044 row.col(|ui| {
1045 components::LinkList {
1046 links: &mut control_loop.linked_slc,
1047 mapping: &constraint_map,
1048 }
1049 .show(ui);
1050 });
1051 });
1052 }
1053 if let Some(index) = remove_control_loop {
1054 control_loops.control_loops.remove(index);
1055 };
1056 });
1057 });
1058 });
1059}
1060
1061pub fn update_control_loop_sequences(
1062 ui: &mut egui::Ui,
1063 control_loop_sequences: &mut ControlLoopSequences,
1064 control_loops: &ControlLoops,
1065 interactions: &Interactions,
1066) {
1067 let interaction_map = construct_link_map!(table = interactions, link_type = InteractionLink);
1068 let control_loop_map = construct_link_map!(table = control_loops, link_type = ControlLoopLink);
1069 ui.with_layout(Layout::top_down(Align::Min), |ui| {
1070 let new = if ui.button("Add control loop sequences").clicked() {
1071 control_loop_sequences
1072 .control_loop_sequences
1073 .push(ControlLoopSequence {
1074 id: format!(
1075 "{}",
1076 control_loop_sequences.control_loop_sequences.len() + 1
1077 ),
1078 control_loop: ControlLoopLink::from(""),
1079 step: "".to_string(),
1080 interaction: InteractionLink::from(""),
1081 provider_process_model: "".to_string(),
1082 provider_logic: "".to_string(),
1083 expected_target_behaviour: "".to_string(),
1084 });
1085 true
1086 } else {
1087 false
1088 };
1089 ui.separator();
1090 egui::ScrollArea::horizontal()
1091 .auto_shrink(false)
1092 .show(ui, |ui| {
1093 let mut table = TableBuilder::new(ui)
1094 .id_salt("control_loop_sequences")
1095 .column(Column::auto().resizable(true))
1096 .column(Column::auto().resizable(true))
1097 .column(Column::auto().resizable(true))
1098 .column(Column::auto().resizable(true))
1099 .column(Column::auto().resizable(true))
1100 .column(Column::auto().resizable(true))
1101 .column(Column::remainder().at_least(250.0));
1102 if new {
1103 table = table.scroll_to_row(
1104 control_loop_sequences.control_loop_sequences.len() - 1,
1105 None,
1106 )
1107 };
1108 table
1109 .header(20.0, |mut header| {
1110 header.col(|ui| {
1111 ui.heading("ID");
1112 });
1113 header.col(|ui| {
1114 ui.heading("Loop");
1115 });
1116 header.col(|ui| {
1117 ui.heading("Step");
1118 });
1119 header.col(|ui| {
1120 ui.heading("Interaction");
1121 });
1122 header.col(|ui| {
1123 ui.heading("Provider process model or state");
1124 });
1125 header.col(|ui| {
1126 ui.heading("Provider logic");
1127 });
1128 header.col(|ui| {
1129 ui.heading("Expected target behaviour");
1130 });
1131 })
1132 .body(|mut body| {
1133 let mut remove_control_loop_sequence = None;
1134 for (index, control_loop_sequence) in &mut control_loop_sequences
1135 .control_loop_sequences
1136 .iter_mut()
1137 .enumerate()
1138 {
1139 body.row(30.0, |mut row| {
1140 row.col(|ui| {
1141 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1142 if remove_button(ui).clicked() {
1143 remove_control_loop_sequence = Some(index);
1144 };
1145
1146 egui::TextEdit::singleline(&mut control_loop_sequence.id)
1147 .hint_text("Type something!")
1148 .show(ui);
1149 });
1150 });
1151 row.col(|ui| {
1152 LinkReference {
1153 id_salt: format!(
1154 "control_loop_sequence_control_loop_combobox_{}",
1155 control_loop_sequence.id
1156 ),
1157 link: &mut control_loop_sequence.control_loop,
1158 mapping: &control_loop_map,
1159 }
1160 .show(ui);
1161 });
1162 row.col(|ui| {
1163 egui::TextEdit::singleline(&mut control_loop_sequence.step)
1164 .hint_text("Type something!")
1165 .show(ui);
1166 });
1167 row.col(|ui| {
1168 LinkReference {
1169 id_salt: format!(
1170 "control_loop_sequence_interaction_combobox_{}",
1171 control_loop_sequence.id
1172 ),
1173 link: &mut control_loop_sequence.interaction,
1174 mapping: &interaction_map,
1175 }
1176 .show(ui);
1177 });
1178 row.col(|ui| {
1179 egui::TextEdit::multiline(
1180 &mut control_loop_sequence.provider_process_model,
1181 )
1182 .hint_text("Type something!")
1183 .show(ui);
1184 });
1185 row.col(|ui| {
1186 egui::TextEdit::multiline(
1187 &mut control_loop_sequence.provider_logic,
1188 )
1189 .hint_text("Type something!")
1190 .show(ui);
1191 });
1192 row.col(|ui| {
1193 egui::TextEdit::multiline(
1194 &mut control_loop_sequence.expected_target_behaviour,
1195 )
1196 .hint_text("Type something!")
1197 .show(ui);
1198 });
1199 });
1200 }
1201 if let Some(index) = remove_control_loop_sequence {
1202 control_loop_sequences.control_loop_sequences.remove(index);
1203 };
1204 });
1205 });
1206 });
1207}
1208
1209pub fn update_scenarios(
1210 ui: &mut egui::Ui,
1211 scenarios: &mut Scenarios,
1212 control_loop_sequences: &ControlLoopSequences,
1213 ucas: &Ucas,
1214 hazards: &Hazards,
1215 constraints: &Constraints,
1216) {
1217 let sequence_ids: IndexSet<ControlLoopSequenceLink> = control_loop_sequences
1218 .control_loop_sequences
1219 .iter()
1220 .map(|seq| ControlLoopSequenceLink::from(seq.id.as_str()))
1221 .collect();
1222 let uca_map = construct_link_map!(
1223 table = ucas,
1224 link_type = UcaLink,
1225 linked_to = uca_definition
1226 );
1227 let hazard_map = construct_link_map!(table = hazards, link_type = HazardLink);
1228 let constraint_map = construct_link_map!(table = constraints, link_type = ConstraintLink);
1229
1230 ui.with_layout(Layout::top_down(Align::Min), |ui| {
1231 let new = if ui.button("Add scenario").clicked() {
1232 scenarios.scenarios.push(Scenario {
1233 id: format!("{}", scenarios.scenarios.len() + 1),
1234 sequence: ControlLoopSequenceLink::from(""),
1235 causal_scenario: CausalScenario::Missing,
1236 causal_scenario_prompt: "".to_string(),
1237 analysis_result: CausalScenarioResult::Missing,
1238 causal_scenario_definition: "".to_string(),
1239 ucas: vec![],
1240 hazards: vec![],
1241 constraint: vec![],
1242 notes: "".to_string(),
1243 });
1244 true
1245 } else {
1246 false
1247 };
1248 ui.separator();
1249 egui::ScrollArea::horizontal()
1250 .auto_shrink(false)
1251 .show(ui, |ui| {
1252 let mut table = TableBuilder::new(ui)
1253 .id_salt("uca_contexts")
1254 .column(Column::auto().resizable(true))
1255 .column(Column::auto().resizable(true))
1256 .column(Column::auto().resizable(true))
1257 .column(Column::auto().resizable(true))
1258 .column(Column::auto().resizable(true))
1259 .column(Column::auto().resizable(true))
1260 .column(Column::auto().resizable(true))
1261 .column(Column::auto().resizable(true))
1262 .column(Column::remainder().at_least(250.0));
1263 if new {
1264 table = table.scroll_to_row(scenarios.scenarios.len() - 1, None)
1265 };
1266 table
1267 .header(20.0, |mut header| {
1268 header.col(|ui| {
1269 ui.heading("ID");
1270 });
1271 header.col(|ui| {
1272 ui.heading("Control Loop Sequence");
1273 });
1274 header.col(|ui| {
1275 ui.heading("Causal Scenario Prompt");
1276 });
1277 header.col(|ui| {
1278 ui.heading("Control Scenario Type");
1279 });
1280 header.col(|ui| {
1281 ui.heading("Causal Scenario Definition");
1282 });
1283 header.col(|ui| {
1284 ui.heading("UCAs");
1285 });
1286 header.col(|ui| {
1287 ui.heading("Hazards");
1288 });
1289 header.col(|ui| {
1290 ui.heading("Constraint(s)");
1291 });
1292 header.col(|ui| {
1293 ui.heading("Notes");
1294 });
1295 })
1296 .body(|mut body| {
1297 let mut remove_senario = None;
1298 for (index, scenario) in &mut scenarios.scenarios.iter_mut().enumerate() {
1299 body.row(30.0, |mut row| {
1300 row.col(|ui| {
1301 ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
1302 if remove_button(ui).clicked() {
1303 remove_senario = Some(index);
1304 };
1305
1306 egui::TextEdit::singleline(&mut scenario.id)
1307 .hint_text("Type something!")
1308 .show(ui);
1309 });
1310 });
1311 row.col(|ui| {
1312 IdOnlyLinkReference {
1313 id_salt: format!(
1314 "scenario_control_loop_sequence_combobox_{}",
1315 scenario.id
1316 ),
1317 link: &mut scenario.sequence,
1318 valid_links: &sequence_ids,
1319 }
1320 .show(ui);
1321 });
1322 row.col(|ui| {
1323 ui.add_enabled(
1324 false,
1325 egui::TextEdit::multiline(
1326 &mut scenario.causal_scenario_prompt,
1327 )
1328 .hint_text("Type something!"),
1329 )
1330 .on_disabled_hover_text("Not yet supported");
1331 });
1332
1333 row.col(|ui| {
1334 components::Dropdown {
1335 value: &mut scenario.causal_scenario,
1336 id_salt: format!("causal_scenario{:?}", scenario.id),
1337 }
1338 .show(ui);
1339 });
1340 row.col(|ui| {
1341 egui::TextEdit::multiline(
1342 &mut scenario.causal_scenario_definition,
1343 )
1344 .hint_text("Type something!")
1345 .show(ui);
1346 });
1347 row.col(|ui| {
1348 components::LinkList {
1349 links: &mut scenario.ucas,
1350 mapping: &uca_map,
1351 }
1352 .show(ui);
1353 });
1354 row.col(|ui| {
1355 components::LinkList {
1356 links: &mut scenario.hazards,
1357 mapping: &hazard_map,
1358 }
1359 .show(ui);
1360 });
1361 row.col(|ui| {
1362 components::LinkList {
1363 links: &mut scenario.constraint,
1364 mapping: &constraint_map,
1365 }
1366 .show(ui);
1367 });
1368 row.col(|ui| {
1369 egui::TextEdit::multiline(&mut scenario.notes)
1370 .hint_text("Type something!")
1371 .show(ui);
1372 });
1373 });
1374 }
1375 if let Some(index) = remove_senario {
1376 scenarios.scenarios.remove(index);
1377 };
1378 });
1379 });
1380 });
1381}
1382
1383pub fn io_options(ui: &mut egui::Ui, stpa_data: &StpaData, project_settings: Option<&StpaProject>) {
1384 if ui.small_button("Save everything as ymal").clicked() {
1385 stpa_data.save_project(&PathBuf::from("."))
1386 }
1387 if let Some(project_settings) = project_settings {
1388 if ui
1389 .small_button("Save everything back to originals")
1390 .clicked()
1391 {
1392 if let Some(losses_file) = &project_settings.losses {
1393 stpa_data.losses.to_file(losses_file).unwrap();
1394 }
1395 if let Some(interactions_file) = &project_settings.interactions {
1396 stpa_data.interactions.to_file(interactions_file).unwrap();
1397 }
1398 }
1399 }
1400}