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