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
16pub const TRANSACTIONS_FILE_EXTENSION: &str = "ftr";
18
19const ROW_HEIGHT: f32 = 15.;
21const SECTION_GAP: f32 = 5.;
22const SUBHEADER_GAP: f32 = 3.;
23const SUBHEADER_SIZE: f32 = 15.;
24
25const TRANSACTION_ROOT_NAME: &str = "tr";
27
28const 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
35const 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
45const 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 .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
427fn 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}