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