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
15pub const TRANSACTIONS_FILE_EXTENSION: &str = "ftr";
17
18const ROW_HEIGHT: f32 = 15.;
20const SECTION_GAP: f32 = 5.;
21const SUBHEADER_GAP: f32 = 3.;
22const SUBHEADER_SIZE: f32 = 15.;
23
24const TRANSACTION_ROOT_NAME: &str = "tr";
26
27const 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
34const 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
44const 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
316fn 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}