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