Skip to main content

libsurfer/
transactions.rs

1use egui::{Layout, RichText, TextWrapMode, Ui};
2use egui_extras::{Column, TableBody, TableBuilder};
3use emath::Align;
4use ftr_parser::types::Transaction;
5use itertools::Itertools;
6use num::BigUint;
7
8use crate::SystemState;
9use crate::displayed_item::DisplayedItem;
10use crate::message::Message;
11use crate::transaction_container::{StreamScopeRef, TransactionContainer};
12use crate::transaction_container::{TransactionRef, TransactionStreamRef};
13use crate::wave_data::ScopeType;
14use crate::wave_data::WaveData;
15
16// Transactions file extension
17pub const TRANSACTIONS_FILE_EXTENSION: &str = "ftr";
18
19// Constants for transaction table drawing and UI labels
20const ROW_HEIGHT: f32 = 15.;
21const SECTION_GAP: f32 = 5.;
22const SUBHEADER_GAP: f32 = 3.;
23const SUBHEADER_SIZE: f32 = 15.;
24
25// Root stream name
26const TRANSACTION_ROOT_NAME: &str = "tr";
27
28// Header / section titles
29const FOCUSED_TX_DETAILS_HDR: &str = "Focused Transaction Details";
30const PROPERTIES_HDR: &str = "Properties";
31const ATTRIBUTES_SECTION_TITLE: &str = "Attributes";
32const INCOMING_RELATIONS_TITLE: &str = "Incoming Relations";
33const OUTGOING_RELATIONS_TITLE: &str = "Outgoing Relations";
34
35// Column / field labels
36const TX_ID_LABEL: &str = "Transaction ID";
37const TX_TYPE_LABEL: &str = "Type";
38const START_TIME_LABEL: &str = "Start Time";
39const END_TIME_LABEL: &str = "End Time";
40const SOURCE_TX_LABEL: &str = "Source Tx";
41const SINK_TX_LABEL: &str = "Sink Tx";
42const ATTR_NAME_LABEL: &str = "Name";
43const ATTR_VALUE_LABEL: &str = "Value";
44
45// Information label
46const STREAM_NOT_FOUND_LABEL: &str = "Stream not found";
47
48impl SystemState {
49    pub fn draw_transaction_detail_panel(
50        &self,
51        ui: &mut Ui,
52        max_width: f32,
53        msgs: &mut Vec<Message>,
54    ) {
55        let Some(waves) = self.user.waves.as_ref() else {
56            return;
57        };
58        let (Some(transaction_ref), focused_transaction) = &waves.focused_transaction else {
59            return;
60        };
61        let Some(transactions) = waves.inner.as_transactions() else {
62            return;
63        };
64        let Some(focused_transaction) = focused_transaction
65            .as_ref()
66            .or_else(|| transactions.get_transaction(transaction_ref))
67        else {
68            return;
69        };
70
71        egui::Panel::right("Transaction Details")
72            .default_size(330.)
73            .size_range(10.0..=max_width)
74            .show_inside(ui, |ui| {
75                ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
76                self.handle_pointer_in_ui(ui, msgs);
77                draw_focused_transaction_details(ui, transactions, focused_transaction);
78            });
79    }
80}
81
82impl WaveData {
83    pub fn add_stream_or_generator_from_name(
84        &mut self,
85        scope: Option<StreamScopeRef>,
86        name: String,
87    ) -> Option<()> {
88        let inner = self.inner.as_transactions()?;
89        match scope {
90            Some(StreamScopeRef::Root) => {
91                let (stream_id, name) = inner
92                    .get_stream_from_name(name)
93                    .map(|s| (s.id, s.name.clone()))?;
94
95                self.add_stream(TransactionStreamRef::new_stream(stream_id, name));
96            }
97            Some(StreamScopeRef::Stream(stream)) => {
98                let (stream_id, id, name) = inner
99                    .get_generator_from_name(Some(stream.stream_id), name)
100                    .map(|g| (g.stream_id, g.id, g.name.clone()))?;
101
102                self.add_generator(TransactionStreamRef::new_gen(stream_id, id, name));
103            }
104            Some(StreamScopeRef::Empty(_)) => {}
105            None => {
106                let (stream_id, id, name) = inner
107                    .get_generator_from_name(None, name)
108                    .map(|g| (g.stream_id, g.id, g.name.clone()))?;
109
110                self.add_generator(TransactionStreamRef::new_gen(stream_id, id, name));
111            }
112        };
113        Some(())
114    }
115
116    pub fn add_all_from_stream_scope(&mut self, scope_name: String) -> Option<()> {
117        if scope_name == "tr" {
118            self.add_all_streams();
119        } else {
120            let inner = self.inner.as_transactions()?;
121            let stream = inner.get_stream_from_name(scope_name)?;
122            let gens = stream
123                .generators
124                .iter()
125                .map(|gen_id| inner.get_generator(*gen_id).unwrap())
126                .map(|g| (g.stream_id, g.id, g.name.clone()))
127                // Sort by name to get deterministic order
128                .sorted_by(|a, b| numeric_sort::cmp(&a.2, &b.2))
129                .collect_vec();
130
131            for (stream_id, id, name) in gens {
132                self.add_generator(TransactionStreamRef::new_gen(stream_id, id, name.clone()));
133            }
134        };
135        Some(())
136    }
137
138    pub fn move_to_transaction(&mut self, next: bool) -> Option<()> {
139        let inner = self.inner.as_transactions()?;
140        let mut transactions = self
141            .items_tree
142            .iter_visible()
143            .flat_map(|node| {
144                let item = &self.displayed_items[&node.item_ref];
145                match item {
146                    DisplayedItem::Stream(s) => {
147                        let stream_ref = &s.transaction_stream_ref;
148                        let stream_id = stream_ref.stream_id;
149                        if let Some(gen_id) = stream_ref.gen_id {
150                            inner.get_transactions_from_generator(gen_id)
151                        } else {
152                            inner.get_transactions_from_stream(stream_id)
153                        }
154                    }
155                    _ => vec![],
156                }
157            })
158            .collect_vec();
159        transactions.sort_unstable_by(|a, b| a.0.cmp(&b.0));
160        let tx = if let Some(focused_tx) = &self.focused_transaction.0 {
161            let next_id = transactions
162                .iter()
163                .enumerate()
164                .find(|(_, tx)| **tx == focused_tx.id)
165                .map_or(
166                    if next { transactions.len() - 1 } else { 0 },
167                    |(vec_idx, _)| {
168                        if next {
169                            if vec_idx + 1 < transactions.len() {
170                                vec_idx + 1
171                            } else {
172                                transactions.len() - 1
173                            }
174                        } else {
175                            vec_idx.saturating_sub(1)
176                        }
177                    },
178                );
179            Some(TransactionRef {
180                id: *transactions.get(next_id)?,
181            })
182        } else {
183            transactions
184                .first()
185                .map(|first| TransactionRef { id: *first })
186        };
187        self.focused_transaction = (tx, self.focused_transaction.1.clone());
188        Some(())
189    }
190}
191
192fn draw_focused_transaction_details(
193    ui: &mut Ui,
194    transactions: &TransactionContainer,
195    focused_transaction: &Transaction,
196) {
197    ui.with_layout(
198        Layout::top_down(Align::LEFT).with_cross_justify(true),
199        |ui| {
200            ui.label(FOCUSED_TX_DETAILS_HDR);
201            let column_width = ui.available_width() / 2.;
202            TableBuilder::new(ui)
203                .column(Column::exact(column_width))
204                .column(Column::auto())
205                .header(20.0, |mut header| {
206                    header.col(|ui| {
207                        ui.heading(PROPERTIES_HDR);
208                    });
209                })
210                .body(|mut body| {
211                    table_row(
212                        &mut body,
213                        TX_ID_LABEL,
214                        &focused_transaction.get_tx_id().to_string(),
215                    );
216                    table_row(&mut body, TX_TYPE_LABEL, {
217                        let generator = transactions
218                            .get_generator(focused_transaction.get_gen_id())
219                            .unwrap();
220                        &generator.name
221                    });
222                    table_row(
223                        &mut body,
224                        START_TIME_LABEL,
225                        &focused_transaction.get_start_time().to_string(),
226                    );
227                    table_row(
228                        &mut body,
229                        END_TIME_LABEL,
230                        &focused_transaction.get_end_time().to_string(),
231                    );
232                    section_header(&mut body, ATTRIBUTES_SECTION_TITLE);
233                    subheader(&mut body, ATTR_NAME_LABEL, ATTR_VALUE_LABEL);
234
235                    for attr in &focused_transaction.attributes {
236                        table_row(&mut body, &attr.name, &attr.value().to_string());
237                    }
238
239                    if !focused_transaction.inc_relations.is_empty() {
240                        section_header(&mut body, INCOMING_RELATIONS_TITLE);
241                        subheader(&mut body, SOURCE_TX_LABEL, SINK_TX_LABEL);
242
243                        for rel in &focused_transaction.inc_relations {
244                            table_row(
245                                &mut body,
246                                &rel.source_tx_id.to_string(),
247                                &rel.sink_tx_id.to_string(),
248                            );
249                        }
250                    }
251
252                    if !focused_transaction.out_relations.is_empty() {
253                        section_header(&mut body, OUTGOING_RELATIONS_TITLE);
254                        subheader(&mut body, SOURCE_TX_LABEL, SINK_TX_LABEL);
255
256                        for rel in &focused_transaction.out_relations {
257                            table_row(
258                                &mut body,
259                                &rel.source_tx_id.to_string(),
260                                &rel.sink_tx_id.to_string(),
261                            );
262                        }
263                    }
264                });
265        },
266    );
267}
268
269pub fn calculate_rows_of_stream(
270    transactions: &[Transaction],
271    last_times_on_row: &mut Vec<(BigUint, BigUint)>,
272) {
273    for transaction in transactions {
274        let mut curr_row = 0;
275        let start_time = transaction.get_start_time();
276        let end_time = transaction.get_end_time();
277
278        while last_times_on_row[curr_row].1 > start_time {
279            curr_row += 1;
280            if last_times_on_row.len() <= curr_row {
281                last_times_on_row.push((BigUint::ZERO, BigUint::ZERO));
282            }
283        }
284        last_times_on_row[curr_row] = (start_time, end_time);
285    }
286}
287
288pub fn draw_transaction_variable_list(
289    msgs: &mut Vec<Message>,
290    streams: &WaveData,
291    ui: &mut Ui,
292    active_stream: &StreamScopeRef,
293) {
294    let Some(inner) = streams.inner.as_transactions() else {
295        return;
296    };
297    match active_stream {
298        StreamScopeRef::Root => {
299            draw_transaction_root_variables(msgs, ui, inner);
300        }
301        StreamScopeRef::Stream(stream_ref) => {
302            draw_transaction_stream_variables(msgs, ui, inner, stream_ref);
303        }
304        StreamScopeRef::Empty(_) => {}
305    }
306}
307
308pub fn draw_transaction_root(msgs: &mut Vec<Message>, streams: &WaveData, ui: &mut Ui) {
309    egui::collapsing_header::CollapsingState::load_with_default_open(
310        ui.ctx(),
311        egui::Id::from("Streams"),
312        false,
313    )
314    .show_header(ui, |ui| {
315        ui.with_layout(
316            Layout::top_down(Align::LEFT).with_cross_justify(true),
317            |ui| {
318                let response = ui.selectable_label(
319                    streams.active_scope == Some(ScopeType::StreamScope(StreamScopeRef::Root)),
320                    TRANSACTION_ROOT_NAME,
321                );
322                if response.clicked() {
323                    msgs.push(Message::SetActiveScope(Some(ScopeType::StreamScope(
324                        StreamScopeRef::Root,
325                    ))));
326                }
327            },
328        );
329    })
330    .body(|ui| {
331        if let Some(tx_container) = streams.inner.as_transactions() {
332            for (id, stream) in &tx_container.inner.tx_streams {
333                let selected = streams.active_scope.as_ref().is_some_and(|s| {
334                    if let ScopeType::StreamScope(StreamScopeRef::Stream(scope_stream)) = s {
335                        scope_stream.stream_id == *id
336                    } else {
337                        false
338                    }
339                });
340                let response = ui.selectable_label(selected, &stream.name);
341                if response.clicked() {
342                    msgs.push(Message::SetActiveScope(Some(ScopeType::StreamScope(
343                        StreamScopeRef::Stream(TransactionStreamRef::new_stream(
344                            *id,
345                            stream.name.clone(),
346                        )),
347                    ))));
348                }
349            }
350        }
351    });
352}
353
354fn draw_transaction_stream_variables(
355    msgs: &mut Vec<Message>,
356    ui: &mut Ui,
357    inner: &TransactionContainer,
358    stream_ref: &TransactionStreamRef,
359) {
360    if let Some(stream) = inner.get_stream(stream_ref.stream_id) {
361        let sorted_generators = stream
362            .generators
363            .iter()
364            .filter_map(|gen_id| {
365                if let Some(g) = inner.get_generator(*gen_id) {
366                    Some((*gen_id, g))
367                } else {
368                    tracing::warn!(
369                        "Generator ID {} not found in stream {}",
370                        gen_id,
371                        stream_ref.stream_id
372                    );
373                    None
374                }
375            })
376            .sorted_by(|(_, a), (_, b)| numeric_sort::cmp(&a.name, &b.name));
377
378        for (gen_id, generator) in sorted_generators {
379            ui.with_layout(
380                Layout::top_down(Align::LEFT).with_cross_justify(true),
381                |ui| {
382                    if ui.selectable_label(false, &generator.name).clicked() {
383                        msgs.push(Message::AddStreamOrGenerator(
384                            TransactionStreamRef::new_gen(
385                                stream_ref.stream_id,
386                                gen_id,
387                                generator.name.clone(),
388                            ),
389                        ));
390                    }
391                },
392            );
393        }
394    } else {
395        ui.label(STREAM_NOT_FOUND_LABEL);
396        tracing::warn!(
397            "Stream ID {} not found in transaction container",
398            stream_ref.stream_id
399        );
400    }
401}
402
403fn draw_transaction_root_variables(
404    msgs: &mut Vec<Message>,
405    ui: &mut Ui,
406    inner: &TransactionContainer,
407) {
408    let streams = inner.get_streams();
409    let sorted_streams = streams
410        .iter()
411        .sorted_by(|a, b| numeric_sort::cmp(&a.name, &b.name));
412    for stream in sorted_streams {
413        ui.with_layout(
414            Layout::top_down(Align::LEFT).with_cross_justify(true),
415            |ui| {
416                let response = ui.selectable_label(false, &stream.name);
417                if response.clicked() {
418                    msgs.push(Message::AddStreamOrGenerator(
419                        TransactionStreamRef::new_stream(stream.id, stream.name.clone()),
420                    ));
421                }
422            },
423        );
424    }
425}
426
427// Helper functions for drawing transaction details table
428
429fn table_row(body: &mut TableBody, key: &str, val: &str) {
430    body.row(ROW_HEIGHT, |mut row| {
431        row.col(|ui| {
432            ui.label(key);
433        });
434        row.col(|ui| {
435            ui.label(val);
436        });
437    });
438}
439
440fn section_header(body: &mut TableBody, title: &str) {
441    body.row(ROW_HEIGHT + SECTION_GAP, |mut row| {
442        row.col(|ui| {
443            ui.heading(title);
444        });
445    });
446}
447
448fn subheader(body: &mut TableBody, left: &str, right: &str) {
449    body.row(ROW_HEIGHT + SUBHEADER_GAP, |mut row| {
450        row.col(|ui| {
451            ui.label(RichText::new(left).size(SUBHEADER_SIZE));
452        });
453        row.col(|ui| {
454            ui.label(RichText::new(right).size(SUBHEADER_SIZE));
455        });
456    });
457}