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