Skip to main content

libsurfer/
transactions.rs

1use egui::{Context, 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::message::Message;
10use crate::transaction_container::TransactionStreamRef;
11use crate::transaction_container::{StreamScopeRef, TransactionContainer};
12use crate::wave_data::ScopeType;
13use crate::wave_data::WaveData;
14
15// Transactions file extension
16pub const TRANSACTIONS_FILE_EXTENSION: &str = "ftr";
17
18// Constants for transaction table drawing and UI labels
19const ROW_HEIGHT: f32 = 15.;
20const SECTION_GAP: f32 = 5.;
21const SUBHEADER_GAP: f32 = 3.;
22const SUBHEADER_SIZE: f32 = 15.;
23
24// Root stream name
25const TRANSACTION_ROOT_NAME: &str = "tr";
26
27// Header / section titles
28const FOCUSED_TX_DETAILS_HDR: &str = "Focused Transaction Details";
29const PROPERTIES_HDR: &str = "Properties";
30const ATTRIBUTES_SECTION_TITLE: &str = "Attributes";
31const INCOMING_RELATIONS_TITLE: &str = "Incoming Relations";
32const OUTGOING_RELATIONS_TITLE: &str = "Outgoing Relations";
33
34// Column / field labels
35const TX_ID_LABEL: &str = "Transaction ID";
36const TX_TYPE_LABEL: &str = "Type";
37const START_TIME_LABEL: &str = "Start Time";
38const END_TIME_LABEL: &str = "End Time";
39const SOURCE_TX_LABEL: &str = "Source Tx";
40const SINK_TX_LABEL: &str = "Sink Tx";
41const ATTR_NAME_LABEL: &str = "Name";
42const ATTR_VALUE_LABEL: &str = "Value";
43
44// Information label
45const STREAM_NOT_FOUND_LABEL: &str = "Stream not found";
46
47impl SystemState {
48    pub fn draw_transaction_detail_panel(
49        &self,
50        ctx: &Context,
51        max_width: f32,
52        msgs: &mut Vec<Message>,
53    ) {
54        let Some(waves) = self.user.waves.as_ref() else {
55            return;
56        };
57        let (Some(transaction_ref), focused_transaction) = &waves.focused_transaction else {
58            return;
59        };
60        let Some(transactions) = waves.inner.as_transactions() else {
61            return;
62        };
63        let Some(focused_transaction) = focused_transaction
64            .as_ref()
65            .or_else(|| transactions.get_transaction(transaction_ref))
66        else {
67            return;
68        };
69
70        egui::SidePanel::right("Transaction Details")
71            .default_width(330.)
72            .width_range(10.0..=max_width)
73            .show(ctx, |ui| {
74                ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
75                self.handle_pointer_in_ui(ui, msgs);
76                draw_focused_transaction_details(ui, transactions, focused_transaction);
77            });
78    }
79}
80
81fn draw_focused_transaction_details(
82    ui: &mut Ui,
83    transactions: &TransactionContainer,
84    focused_transaction: &Transaction,
85) {
86    ui.with_layout(
87        Layout::top_down(Align::LEFT).with_cross_justify(true),
88        |ui| {
89            ui.label(FOCUSED_TX_DETAILS_HDR);
90            let column_width = ui.available_width() / 2.;
91            TableBuilder::new(ui)
92                .column(Column::exact(column_width))
93                .column(Column::auto())
94                .header(20.0, |mut header| {
95                    header.col(|ui| {
96                        ui.heading(PROPERTIES_HDR);
97                    });
98                })
99                .body(|mut body| {
100                    table_row(
101                        &mut body,
102                        TX_ID_LABEL,
103                        &focused_transaction.get_tx_id().to_string(),
104                    );
105                    table_row(&mut body, TX_TYPE_LABEL, {
106                        let generator = transactions
107                            .get_generator(focused_transaction.get_gen_id())
108                            .unwrap();
109                        &generator.name
110                    });
111                    table_row(
112                        &mut body,
113                        START_TIME_LABEL,
114                        &focused_transaction.get_start_time().to_string(),
115                    );
116                    table_row(
117                        &mut body,
118                        END_TIME_LABEL,
119                        &focused_transaction.get_end_time().to_string(),
120                    );
121                    section_header(&mut body, ATTRIBUTES_SECTION_TITLE);
122                    subheader(&mut body, ATTR_NAME_LABEL, ATTR_VALUE_LABEL);
123
124                    for attr in &focused_transaction.attributes {
125                        table_row(&mut body, &attr.name, &attr.value().to_string());
126                    }
127
128                    if !focused_transaction.inc_relations.is_empty() {
129                        section_header(&mut body, INCOMING_RELATIONS_TITLE);
130                        subheader(&mut body, SOURCE_TX_LABEL, SINK_TX_LABEL);
131
132                        for rel in &focused_transaction.inc_relations {
133                            table_row(
134                                &mut body,
135                                &rel.source_tx_id.to_string(),
136                                &rel.sink_tx_id.to_string(),
137                            );
138                        }
139                    }
140
141                    if !focused_transaction.out_relations.is_empty() {
142                        section_header(&mut body, OUTGOING_RELATIONS_TITLE);
143                        subheader(&mut body, SOURCE_TX_LABEL, SINK_TX_LABEL);
144
145                        for rel in &focused_transaction.out_relations {
146                            table_row(
147                                &mut body,
148                                &rel.source_tx_id.to_string(),
149                                &rel.sink_tx_id.to_string(),
150                            );
151                        }
152                    }
153                });
154        },
155    );
156}
157
158pub fn calculate_rows_of_stream(
159    transactions: &[Transaction],
160    last_times_on_row: &mut Vec<(BigUint, BigUint)>,
161) {
162    for transaction in transactions {
163        let mut curr_row = 0;
164        let start_time = transaction.get_start_time();
165        let end_time = transaction.get_end_time();
166
167        while last_times_on_row[curr_row].1 > start_time {
168            curr_row += 1;
169            if last_times_on_row.len() <= curr_row {
170                last_times_on_row.push((BigUint::ZERO, BigUint::ZERO));
171            }
172        }
173        last_times_on_row[curr_row] = (start_time, end_time);
174    }
175}
176
177pub fn draw_transaction_variable_list(
178    msgs: &mut Vec<Message>,
179    streams: &WaveData,
180    ui: &mut Ui,
181    active_stream: &StreamScopeRef,
182) {
183    let Some(inner) = streams.inner.as_transactions() else {
184        return;
185    };
186    match active_stream {
187        StreamScopeRef::Root => {
188            draw_transaction_root_variables(msgs, ui, inner);
189        }
190        StreamScopeRef::Stream(stream_ref) => {
191            draw_transaction_stream_variables(msgs, ui, inner, stream_ref);
192        }
193        StreamScopeRef::Empty(_) => {}
194    }
195}
196
197pub fn draw_transaction_root(msgs: &mut Vec<Message>, streams: &WaveData, ui: &mut Ui) {
198    egui::collapsing_header::CollapsingState::load_with_default_open(
199        ui.ctx(),
200        egui::Id::from("Streams"),
201        false,
202    )
203    .show_header(ui, |ui| {
204        ui.with_layout(
205            Layout::top_down(Align::LEFT).with_cross_justify(true),
206            |ui| {
207                let response = ui.selectable_label(
208                    streams.active_scope == Some(ScopeType::StreamScope(StreamScopeRef::Root)),
209                    TRANSACTION_ROOT_NAME,
210                );
211                if response.clicked() {
212                    msgs.push(Message::SetActiveScope(Some(ScopeType::StreamScope(
213                        StreamScopeRef::Root,
214                    ))));
215                }
216            },
217        );
218    })
219    .body(|ui| {
220        if let Some(tx_container) = streams.inner.as_transactions() {
221            for (id, stream) in &tx_container.inner.tx_streams {
222                let selected = streams.active_scope.as_ref().is_some_and(|s| {
223                    if let ScopeType::StreamScope(StreamScopeRef::Stream(scope_stream)) = s {
224                        scope_stream.stream_id == *id
225                    } else {
226                        false
227                    }
228                });
229                let response = ui.selectable_label(selected, &stream.name);
230                if response.clicked() {
231                    msgs.push(Message::SetActiveScope(Some(ScopeType::StreamScope(
232                        StreamScopeRef::Stream(TransactionStreamRef::new_stream(
233                            *id,
234                            stream.name.clone(),
235                        )),
236                    ))));
237                }
238            }
239        }
240    });
241}
242
243fn draw_transaction_stream_variables(
244    msgs: &mut Vec<Message>,
245    ui: &mut Ui,
246    inner: &TransactionContainer,
247    stream_ref: &TransactionStreamRef,
248) {
249    if let Some(stream) = inner.get_stream(stream_ref.stream_id) {
250        let sorted_generators = stream
251            .generators
252            .iter()
253            .filter_map(|gen_id| {
254                if let Some(g) = inner.get_generator(*gen_id) {
255                    Some((*gen_id, g))
256                } else {
257                    tracing::warn!(
258                        "Generator ID {} not found in stream {}",
259                        gen_id,
260                        stream_ref.stream_id
261                    );
262                    None
263                }
264            })
265            .sorted_by(|(_, a), (_, b)| numeric_sort::cmp(&a.name, &b.name));
266
267        for (gen_id, generator) in sorted_generators {
268            ui.with_layout(
269                Layout::top_down(Align::LEFT).with_cross_justify(true),
270                |ui| {
271                    if ui.selectable_label(false, &generator.name).clicked() {
272                        msgs.push(Message::AddStreamOrGenerator(
273                            TransactionStreamRef::new_gen(
274                                stream_ref.stream_id,
275                                gen_id,
276                                generator.name.clone(),
277                            ),
278                        ));
279                    }
280                },
281            );
282        }
283    } else {
284        ui.label(STREAM_NOT_FOUND_LABEL);
285        tracing::warn!(
286            "Stream ID {} not found in transaction container",
287            stream_ref.stream_id
288        );
289    }
290}
291
292fn draw_transaction_root_variables(
293    msgs: &mut Vec<Message>,
294    ui: &mut Ui,
295    inner: &TransactionContainer,
296) {
297    let streams = inner.get_streams();
298    let sorted_streams = streams
299        .iter()
300        .sorted_by(|a, b| numeric_sort::cmp(&a.name, &b.name));
301    for stream in sorted_streams {
302        ui.with_layout(
303            Layout::top_down(Align::LEFT).with_cross_justify(true),
304            |ui| {
305                let response = ui.selectable_label(false, &stream.name);
306                if response.clicked() {
307                    msgs.push(Message::AddStreamOrGenerator(
308                        TransactionStreamRef::new_stream(stream.id, stream.name.clone()),
309                    ));
310                }
311            },
312        );
313    }
314}
315
316// Helper functions for drawing transaction details table
317
318fn table_row(body: &mut TableBody, key: &str, val: &str) {
319    body.row(ROW_HEIGHT, |mut row| {
320        row.col(|ui| {
321            ui.label(key);
322        });
323        row.col(|ui| {
324            ui.label(val);
325        });
326    });
327}
328
329fn section_header(body: &mut TableBody, title: &str) {
330    body.row(ROW_HEIGHT + SECTION_GAP, |mut row| {
331        row.col(|ui| {
332            ui.heading(title);
333        });
334    });
335}
336
337fn subheader(body: &mut TableBody, left: &str, right: &str) {
338    body.row(ROW_HEIGHT + SUBHEADER_GAP, |mut row| {
339        row.col(|ui| {
340            ui.label(RichText::new(left).size(SUBHEADER_SIZE));
341        });
342        row.col(|ui| {
343            ui.label(RichText::new(right).size(SUBHEADER_SIZE));
344        });
345    });
346}