perspective_viewer/components/
column_selector.rs1mod active_column;
14mod add_expression_button;
15mod aggregate_selector;
16mod column_selector_column_row;
17mod config_selector;
18mod empty_column;
19mod expr_edit_button;
20mod filter_column;
21mod inactive_column;
22mod invalid_column;
23mod pivot_column;
24mod sort_column;
25
26use std::iter::*;
27use std::rc::Rc;
28
29pub use column_selector_column_row::*;
30pub use empty_column::*;
31pub use invalid_column::*;
32use perspective_client::config::{ViewConfig, ViewConfigUpdate};
33pub use pivot_column::*;
34use web_sys::*;
35use yew::prelude::*;
36
37use self::active_column::*;
38use self::add_expression_button::AddExpressionButton;
39use self::config_selector::ConfigSelector;
40use self::inactive_column::*;
41use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
42use crate::config::PluginStaticConfig;
43use crate::presentation::{ColumnLocator, ColumnSettingsTarget, DragDropContainer, Presentation};
44use crate::queries::{
45 ActiveColumnState, ActiveColumnStateData, ColumnsIteratorSet, get_current_column_locator,
46};
47use crate::renderer::*;
48use crate::session::drag_drop_update::*;
49use crate::session::*;
50use crate::tasks::apply_and_render;
51use crate::ui::{Orientation, ScrollPanel, ScrollPanelItem, SplitPanel};
52use crate::utils::*;
53
54#[derive(Properties)]
55pub struct ColumnSelectorProps {
56 pub on_open_expr_panel: Callback<ColumnSettingsTarget>,
58
59 pub selected_column: Option<ColumnLocator>,
61
62 pub has_table: Option<TableLoadState>,
64 pub named_column_count: usize,
65
66 pub plugin_static_config: Rc<PluginStaticConfig>,
69 pub view_config: PtrEqRc<ViewConfig>,
70
71 pub columns_config: PtrEqRc<ColumnConfigMap>,
74 pub drag_column: Option<String>,
75
76 pub metadata: SessionMetadataRc,
79
80 pub selected_theme: Option<String>,
82
83 pub session: Session,
85 pub renderer: Renderer,
86 pub presentation: Presentation,
87
88 #[prop_or_default]
90 pub on_resize: Option<Rc<PubSub<()>>>,
91
92 #[prop_or_default]
96 pub initial_width: f64,
97
98 #[prop_or_default]
101 pub on_auto_width: Callback<f64>,
102
103 #[prop_or_default]
107 pub on_dimensions_reset: Option<Rc<PubSub<()>>>,
108}
109
110impl PartialEq for ColumnSelectorProps {
111 fn eq(&self, rhs: &Self) -> bool {
112 self.selected_column == rhs.selected_column
113 && self.has_table == rhs.has_table
114 && self.named_column_count == rhs.named_column_count
115 && self.plugin_static_config == rhs.plugin_static_config
116 && self.view_config == rhs.view_config
117 && self.columns_config == rhs.columns_config
118 && self.drag_column == rhs.drag_column
119 && self.metadata == rhs.metadata
120 && self.selected_theme == rhs.selected_theme
121 && self.initial_width == rhs.initial_width
122 }
123}
124
125#[derive(Debug)]
126pub enum ColumnSelectorMsg {
127 Redraw,
130 HoverActiveIndex(Option<usize>),
131 Drop((String, DragTarget, DragEffect, usize)),
132}
133
134use ColumnSelectorMsg::*;
135
136pub struct ColumnSelector {
139 _subscriptions: Vec<Subscription>,
140 drag_container: DragDropContainer,
141 column_dropdown: ColumnDropDownElement,
142 on_reset: Rc<PubSub<()>>,
143}
144
145fn close_column_settings_if_displaced(
146 presentation: &Presentation,
147 renderer: &Renderer,
148 metadata: &SessionMetadata,
149 view_config: &ViewConfig,
150 update: &ViewConfigUpdate,
151) {
152 let Some(columns) = &update.columns else {
153 return;
154 };
155
156 let ocs = presentation.get_open_column_settings();
157 if get_current_column_locator(&ocs, renderer, view_config, metadata).is_none() {
158 return;
159 }
160
161 let next_config = ViewConfig {
162 columns: columns.clone(),
163 ..view_config.clone()
164 };
165
166 if get_current_column_locator(&ocs, renderer, &next_config, metadata).is_none() {
167 presentation.set_open_column_settings(None);
168 }
169}
170
171impl Component for ColumnSelector {
172 type Message = ColumnSelectorMsg;
173 type Properties = ColumnSelectorProps;
174
175 fn create(ctx: &Context<Self>) -> Self {
176 let ColumnSelectorProps {
177 presentation,
178 session,
179 ..
180 } = ctx.props();
181
182 let drop_sub = {
183 let cb = ctx.link().callback(ColumnSelectorMsg::Drop);
184 presentation.drop_received.add_listener(cb)
185 };
186
187 let drag_container = DragDropContainer::new(|| {}, {
188 let link = ctx.link().clone();
189 move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None))
190 });
191
192 let column_dropdown = ColumnDropDownElement::new(session.clone());
193 let on_reset: Rc<PubSub<()>> = Default::default();
194 let mut subscriptions = vec![drop_sub];
195 if let Some(outer_reset) = ctx.props().on_dimensions_reset.as_ref() {
196 let on_reset = on_reset.clone();
197 subscriptions.push(outer_reset.add_listener(move |()| on_reset.emit(())));
198 }
199
200 Self {
201 _subscriptions: subscriptions,
202 drag_container,
203 column_dropdown,
204 on_reset,
205 }
206 }
207
208 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
209 match msg {
210 Redraw => true,
211 HoverActiveIndex(Some(to_index)) => ctx
212 .props()
213 .presentation
214 .notify_drag_enter(DragTarget::Active, to_index),
215 HoverActiveIndex(_) => {
216 ctx.props()
217 .presentation
218 .notify_drag_leave(DragTarget::Active);
219 true
220 },
221 Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => {
222 let is_invalid = {
223 let config = &ctx.props().view_config;
224 let from_index = config
225 .columns
226 .iter()
227 .position(|x| x.as_ref() == Some(&column));
228
229 let min_cols = ctx.props().renderer.metadata().min_config_columns;
230 let is_to_empty = !config
231 .columns
232 .get(index)
233 .map(|x| x.is_some())
234 .unwrap_or_default();
235
236 min_cols
237 .and_then(|x| from_index.map(|fi| fi < x))
238 .unwrap_or_default()
239 && is_to_empty
240 };
241 if !is_invalid {
242 let col_type = ctx
243 .props()
244 .metadata
245 .get_column_table_type(column.as_str())
246 .unwrap();
247
248 let update = ctx.props().view_config.create_drag_drop_update(
249 column,
250 col_type,
251 index,
252 DragTarget::Active,
253 DragEffect::Move(DragTarget::Active),
254 &ctx.props().renderer.metadata(),
255 ctx.props().metadata.get_features().unwrap(),
256 );
257
258 close_column_settings_if_displaced(
259 &ctx.props().presentation,
260 &ctx.props().renderer,
261 &ctx.props().metadata,
262 &ctx.props().view_config,
263 &update,
264 );
265
266 let session = ctx.props().session.clone();
267 let renderer = ctx.props().renderer.clone();
268 if let Ok(task) = apply_and_render(&session, &renderer, update) {
269 spawn_owned("column-selector", task);
270 }
271 }
272
273 true
274 },
275 Drop((column, DragTarget::Active, effect, index)) => {
276 let col_type = ctx
277 .props()
278 .metadata
279 .get_column_table_type(column.as_str())
280 .unwrap();
281 let update = ctx.props().view_config.create_drag_drop_update(
282 column,
283 col_type,
284 index,
285 DragTarget::Active,
286 effect,
287 &ctx.props().renderer.metadata(),
288 ctx.props().metadata.get_features().unwrap(),
289 );
290
291 close_column_settings_if_displaced(
292 &ctx.props().presentation,
293 &ctx.props().renderer,
294 &ctx.props().metadata,
295 &ctx.props().view_config,
296 &update,
297 );
298
299 let session = ctx.props().session.clone();
300 let renderer = ctx.props().renderer.clone();
301 if let Ok(task) = apply_and_render(&session, &renderer, update) {
302 spawn_owned("column-selector", task);
303 }
304
305 true
306 },
307 Drop((_, _, DragEffect::Move(DragTarget::Active), _)) => true,
308 Drop((..)) => true,
309 }
310 }
311
312 fn view(&self, ctx: &Context<Self>) -> Html {
313 let ColumnSelectorProps {
314 session,
315 renderer,
316 presentation,
317 ..
318 } = ctx.props();
319 let metadata = &ctx.props().metadata;
320
321 let prop_config = &ctx.props().view_config;
325 let config = if prop_config.columns.is_empty() {
326 if let Some(table_cols) = metadata.get_table_columns() {
327 ViewConfig {
328 columns: table_cols.iter().map(|c| Some(c.clone())).collect(),
329 ..(**prop_config).clone()
330 }
331 .into()
332 } else {
333 prop_config.clone()
334 }
335 } else {
336 prop_config.clone()
337 };
338
339 let is_last_column = config.columns.len() == 1;
340 let is_aggregated = config.is_aggregated();
341 let columns_iter = ColumnsIteratorSet::new(&config, metadata, renderer, presentation);
342 let onselect = ctx.link().callback(|()| Redraw);
343 let ondragenter = ctx.link().callback(HoverActiveIndex);
344 let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default());
345 let ondrop = Callback::from({
346 clone!(presentation);
347 move |event| presentation.notify_drop(&event)
348 });
349
350 let ondragend = Callback::from({
351 clone!(presentation);
352 move |_| presentation.notify_drag_end()
353 });
354
355 let mut active_classes = classes!("scrollable");
356 if ctx.props().drag_column.is_some() {
357 active_classes.push("dragdrop-highlight");
358 };
359
360 if is_aggregated {
361 active_classes.push("is-aggregated");
362 }
363
364 let size_hint = 28.0f64.mul_add(
365 (config.group_by.len()
366 + config.split_by.len()
367 + config.filter.len()
368 + config.sort.len()) as f64,
369 metadata
370 .get_features()
371 .map(|x| {
372 let mut y = 0.0;
373 if !x.filter_ops.is_empty() {
374 y += 1.0;
375 }
376
377 if x.group_by {
378 y += 1.0;
379 }
380
381 if x.split_by {
382 y += 1.0;
383 }
384
385 if x.sort {
386 y += 1.0;
387 }
388
389 y * 55.0
390 })
391 .unwrap_or_default(),
392 );
393
394 let config_selector = html_nested! {
395 <ScrollPanelItem key="config_selector" {size_hint}>
396 <ConfigSelector
397 onselect={onselect.clone()}
398 ondragenter={ctx.link().callback(|()| Redraw)}
399 view_config={ctx.props().view_config.clone()}
400 drag_column={ctx.props().drag_column.clone()}
401 metadata={metadata.clone()}
402 selected_theme={ctx.props().selected_theme.clone()}
403 plugin_static_config={ctx.props().plugin_static_config.clone()}
404 {presentation}
405 {renderer}
406 {session}
407 />
408 </ScrollPanelItem>
409 };
410
411 let mut named_count = ctx.props().named_column_count;
412 let mut active_columns: Vec<_> = columns_iter
413 .active()
414 .enumerate()
415 .map(|(idx, name): (usize, ActiveColumnState)| {
416 let ondragenter = ondragenter.reform(move |_| Some(idx));
417 let size_hint = if named_count > 0 { 50.0 } else { 28.0 };
418 named_count = named_count.saturating_sub(1);
419 let key = name
420 .get_name()
421 .map(|x| x.to_owned())
422 .unwrap_or_else(|| format!("__auto_{idx}__"));
423
424 let column_dropdown = self.column_dropdown.clone();
425 let is_editing = matches!(
426 &ctx.props().selected_column,
427 Some(ColumnLocator::Table(x))
428 | Some(ColumnLocator::Expression(x))
429 | Some(ColumnLocator::Window(x))
430 if x == &key );
431
432 let col_type = name
437 .get_name()
438 .and_then(|n| metadata.get_column_table_type(n))
439 .or_else(|| {
440 if matches!(name.state, ActiveColumnStateData::DragOver) {
441 presentation
442 .get_drag_column()
443 .and_then(|c| metadata.get_column_table_type(&c))
444 } else {
445 None
446 }
447 });
448
449 let is_expression = name
450 .get_name()
451 .map(|n| metadata.is_column_expression(n))
452 .unwrap_or(false);
453
454 let is_window = name
455 .get_name()
456 .map(|n| metadata.is_column_window(n))
457 .unwrap_or(false);
458
459 let can_render_styles =
460 name.get_name().is_some() && renderer.can_render_column_styles();
461
462 let show_edit_btn = is_expression || is_window || can_render_styles;
463 let is_modified = name
464 .get_name()
465 .and_then(|n| ctx.props().columns_config.get(n))
466 .is_some_and(|entry| !entry.is_empty());
467
468 let on_open_expr_panel = &ctx.props().on_open_expr_panel;
469 html_nested! {
470 <ScrollPanelItem {key} {size_hint}>
471 <ActiveColumn
472 {column_dropdown}
473 {idx}
474 {is_aggregated}
475 {is_editing}
476 {is_expression}
477 {is_window}
478 {is_modified}
479 {show_edit_btn}
480 {col_type}
481 {is_last_column}
482 view_config={config.clone()}
483 metadata={metadata.clone()}
484 {name}
485 {on_open_expr_panel}
486 {ondragenter}
487 ondragend={&ondragend}
488 onselect={&onselect}
489 {presentation}
490 {renderer}
491 {session}
492 />
493 </ScrollPanelItem>
494 }
495 })
496 .collect();
497
498 let mut inactive_children: Vec<_> = columns_iter
499 .expression()
500 .chain(columns_iter.window())
501 .chain(columns_iter.inactive())
502 .enumerate()
503 .map(|(idx, vc)| {
504 let selected_column = ctx.props().selected_column.as_ref();
505 let is_editing = matches!(
506 selected_column,
507 Some(ColumnLocator::Expression(x)) | Some(ColumnLocator::Window(x))
508 if x.as_str() == vc.name
509 );
510 let is_expression = metadata.is_column_expression(vc.name);
511 let is_window = metadata.is_column_window(vc.name);
512 html_nested! {
513 <ScrollPanelItem key={vc.name} size_hint=28.0>
514 <InactiveColumn
515 {idx}
516 visible={vc.is_visible}
517 name={vc.name.to_owned()}
518 {is_editing}
519 {is_expression}
520 {is_window}
521 view_config={config.clone()}
522 metadata={metadata.clone()}
523 onselect={&onselect}
524 ondragend={&ondragend}
525 on_open_expr_panel={&ctx.props().on_open_expr_panel}
526 {presentation}
527 {renderer}
528 {session}
529 />
530 </ScrollPanelItem>
531 }
532 })
533 .collect();
534
535 let size = 28.0;
536
537 let add_column = if metadata.get_features().unwrap().expressions {
538 html_nested! {
539 <ScrollPanelItem key="__add_expression__" size_hint={size}>
540 <AddExpressionButton
541 on_open_expr_panel={&ctx.props().on_open_expr_panel}
542 selected_column={ctx.props().selected_column.clone()}
543 />
544 </ScrollPanelItem>
545 }
546 } else {
547 html_nested! {
548 <ScrollPanelItem key="__add_expression__" size_hint=0_f64><span /></ScrollPanelItem>
549 }
550 };
551
552 if inactive_children.is_empty() {
553 active_columns.push(add_column)
554 } else {
555 inactive_children.insert(0, add_column);
556 }
557
558 let mut selected_columns = vec![html! {
559 <div id="selected-columns" key="__active_columns__">
560 <ScrollPanel
561 id="active-columns"
562 omit_autosize_div={true}
563 class={active_classes}
564 dragover={ondragover}
565 dragenter={&self.drag_container.dragenter}
566 dragleave={&self.drag_container.dragleave}
567 viewport_ref={&self.drag_container.noderef}
568 initial_width={ctx.props().initial_width}
569 on_auto_width={ctx.props().on_auto_width.clone()}
570 drop={ondrop}
571 on_resize={&ctx.props().on_resize}
572 on_dimensions_reset={&self.on_reset}
573 children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
574 />
575 </div>
576 }];
577
578 if !inactive_children.is_empty() {
579 selected_columns.push(html! {
580 <ScrollPanel
581 id="sub-columns"
582 key="__sub_columns__"
583 class={classes!("scrollable")}
584 on_resize={&ctx.props().on_resize}
585 on_dimensions_reset={&self.on_reset}
586 children={inactive_children}
587 />
588 })
589 }
590
591 html! {
592 <>
593 <SplitPanel
594 no_wrap=true
595 on_reset={self.on_reset.callback()}
596 skip_empty=true
597 orientation={Orientation::Vertical}
598 >
599 { for selected_columns }
600 </SplitPanel>
601 <ColumnDropDownPortal
602 element={self.column_dropdown.clone()}
603 theme={ctx.props().selected_theme.clone().unwrap_or_default()}
604 />
605 </>
606 }
607 }
608}