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;
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 super::containers::scroll_panel::*;
42use super::containers::split_panel::{Orientation, SplitPanel};
43use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
44use crate::components::containers::scroll_panel_item::ScrollPanelItem;
45use crate::config::PluginStaticConfig;
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::tasks::apply_and_render;
52use crate::utils::*;
53
54#[derive(Properties)]
55pub struct ColumnSelectorProps {
56 pub on_open_expr_panel: Callback<ColumnLocator>,
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
140impl Component for ColumnSelector {
141 type Message = ColumnSelectorMsg;
142 type Properties = ColumnSelectorProps;
143
144 fn create(ctx: &Context<Self>) -> Self {
145 let ColumnSelectorProps {
146 presentation,
147 session,
148 ..
149 } = ctx.props();
150
151 let drop_sub = {
152 let cb = ctx.link().callback(ColumnSelectorMsg::Drop);
153 presentation.drop_received.add_listener(cb)
154 };
155
156 let drag_container = DragDropContainer::new(|| {}, {
157 let link = ctx.link().clone();
158 move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None))
159 });
160
161 let column_dropdown = ColumnDropDownElement::new(session.clone());
162 let on_reset: Rc<PubSub<()>> = Default::default();
163 let mut subscriptions = vec![drop_sub];
164 if let Some(outer_reset) = ctx.props().on_dimensions_reset.as_ref() {
165 let on_reset = on_reset.clone();
166 subscriptions.push(outer_reset.add_listener(move |()| on_reset.emit(())));
167 }
168
169 Self {
170 _subscriptions: subscriptions,
171 drag_container,
172 column_dropdown,
173 on_reset,
174 }
175 }
176
177 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
178 match msg {
179 Redraw => true,
180 HoverActiveIndex(Some(to_index)) => ctx
181 .props()
182 .presentation
183 .notify_drag_enter(DragTarget::Active, to_index),
184 HoverActiveIndex(_) => {
185 ctx.props()
186 .presentation
187 .notify_drag_leave(DragTarget::Active);
188 true
189 },
190 Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => {
191 let is_invalid = {
192 let config = &ctx.props().view_config;
193 let from_index = config
194 .columns
195 .iter()
196 .position(|x| x.as_ref() == Some(&column));
197
198 let min_cols = ctx.props().renderer.metadata().min_config_columns;
199 let is_to_empty = !config
200 .columns
201 .get(index)
202 .map(|x| x.is_some())
203 .unwrap_or_default();
204
205 min_cols
206 .and_then(|x| from_index.map(|fi| fi < x))
207 .unwrap_or_default()
208 && is_to_empty
209 };
210 if !is_invalid {
211 let col_type = ctx
212 .props()
213 .metadata
214 .get_column_table_type(column.as_str())
215 .unwrap();
216
217 let update = ctx.props().view_config.create_drag_drop_update(
218 column,
219 col_type,
220 index,
221 DragTarget::Active,
222 DragEffect::Move(DragTarget::Active),
223 &ctx.props().renderer.metadata(),
224 ctx.props().metadata.get_features().unwrap(),
225 );
226
227 let session = ctx.props().session.clone();
228 let renderer = ctx.props().renderer.clone();
229 if let Ok(task) = apply_and_render(&session, &renderer, update) {
230 spawn_owned("column-selector", task);
231 }
232 }
233
234 true
235 },
236 Drop((column, DragTarget::Active, effect, index)) => {
237 let col_type = ctx
238 .props()
239 .metadata
240 .get_column_table_type(column.as_str())
241 .unwrap();
242 let update = ctx.props().view_config.create_drag_drop_update(
243 column,
244 col_type,
245 index,
246 DragTarget::Active,
247 effect,
248 &ctx.props().renderer.metadata(),
249 ctx.props().metadata.get_features().unwrap(),
250 );
251
252 let session = ctx.props().session.clone();
253 let renderer = ctx.props().renderer.clone();
254 if let Ok(task) = apply_and_render(&session, &renderer, update) {
255 spawn_owned("column-selector", task);
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 plugin_static_config={ctx.props().plugin_static_config.clone()}
356 {presentation}
357 {renderer}
358 {session}
359 />
360 </ScrollPanelItem>
361 };
362
363 let mut named_count = ctx.props().named_column_count;
364 let mut active_columns: Vec<_> = columns_iter
365 .active()
366 .enumerate()
367 .map(|(idx, name): (usize, ActiveColumnState)| {
368 let ondragenter = ondragenter.reform(move |_| Some(idx));
369 let size_hint = if named_count > 0 { 50.0 } else { 28.0 };
370 named_count = named_count.saturating_sub(1);
371 let key = name
372 .get_name()
373 .map(|x| x.to_owned())
374 .unwrap_or_else(|| format!("__auto_{idx}__"));
375
376 let column_dropdown = self.column_dropdown.clone();
377 let is_editing = matches!(
378 &ctx.props().selected_column,
379 Some(ColumnLocator::Table(x))
380 | Some(ColumnLocator::Expression(x))
381 | Some(ColumnLocator::Window(x))
382 if x == &key );
383
384 let col_type = name
389 .get_name()
390 .and_then(|n| metadata.get_column_table_type(n))
391 .or_else(|| {
392 if matches!(name.state, ActiveColumnStateData::DragOver) {
393 presentation
394 .get_drag_column()
395 .and_then(|c| metadata.get_column_table_type(&c))
396 } else {
397 None
398 }
399 });
400
401 let is_expression = name
402 .get_name()
403 .map(|n| metadata.is_column_expression(n))
404 .unwrap_or(false);
405
406 let is_window = name
407 .get_name()
408 .map(|n| metadata.is_column_window(n))
409 .unwrap_or(false);
410
411 let can_render_styles =
412 name.get_name().is_some() && renderer.can_render_column_styles();
413
414 let show_edit_btn = is_expression || is_window || can_render_styles;
415 let on_open_expr_panel = &ctx.props().on_open_expr_panel;
416 html_nested! {
417 <ScrollPanelItem {key} {size_hint}>
418 <ActiveColumn
419 {column_dropdown}
420 {idx}
421 {is_aggregated}
422 {is_editing}
423 {is_expression}
424 {is_window}
425 {show_edit_btn}
426 {col_type}
427 view_config={config.clone()}
428 metadata={metadata.clone()}
429 {name}
430 {on_open_expr_panel}
431 {ondragenter}
432 ondragend={&ondragend}
433 onselect={&onselect}
434 {presentation}
435 {renderer}
436 {session}
437 />
438 </ScrollPanelItem>
439 }
440 })
441 .collect();
442
443 let mut inactive_children: Vec<_> = columns_iter
444 .expression()
445 .chain(columns_iter.window())
446 .chain(columns_iter.inactive())
447 .enumerate()
448 .map(|(idx, vc)| {
449 let selected_column = ctx.props().selected_column.as_ref();
450 let is_editing = matches!(
451 selected_column,
452 Some(ColumnLocator::Expression(x)) | Some(ColumnLocator::Window(x))
453 if x.as_str() == vc.name
454 );
455 let is_expression = metadata.is_column_expression(vc.name);
456 let is_window = metadata.is_column_window(vc.name);
457 html_nested! {
458 <ScrollPanelItem key={vc.name} size_hint=28.0>
459 <InactiveColumn
460 {idx}
461 visible={vc.is_visible}
462 name={vc.name.to_owned()}
463 {is_editing}
464 {is_expression}
465 {is_window}
466 view_config={config.clone()}
467 metadata={metadata.clone()}
468 onselect={&onselect}
469 ondragend={&ondragend}
470 on_open_expr_panel={&ctx.props().on_open_expr_panel}
471 {presentation}
472 {renderer}
473 {session}
474 />
475 </ScrollPanelItem>
476 }
477 })
478 .collect();
479
480 let size = 28.0;
481
482 let add_column = if metadata.get_features().unwrap().expressions {
483 html_nested! {
484 <ScrollPanelItem key="__add_expression__" size_hint={size}>
485 <AddExpressionButton
486 on_open_expr_panel={&ctx.props().on_open_expr_panel}
487 selected_column={ctx.props().selected_column.clone()}
488 />
489 </ScrollPanelItem>
490 }
491 } else {
492 html_nested! {
493 <ScrollPanelItem key="__add_expression__" size_hint=0_f64><span /></ScrollPanelItem>
494 }
495 };
496
497 if inactive_children.is_empty() {
498 active_columns.push(add_column)
499 } else {
500 inactive_children.insert(0, add_column);
501 }
502
503 let mut selected_columns = vec![html! {
504 <div id="selected-columns" key="__active_columns__">
505 <ScrollPanel
506 id="active-columns"
507 omit_autosize_div={true}
508 class={active_classes}
509 dragover={ondragover}
510 dragenter={&self.drag_container.dragenter}
511 dragleave={&self.drag_container.dragleave}
512 viewport_ref={&self.drag_container.noderef}
513 initial_width={ctx.props().initial_width}
514 on_auto_width={ctx.props().on_auto_width.clone()}
515 drop={ondrop}
516 on_resize={&ctx.props().on_resize}
517 on_dimensions_reset={&self.on_reset}
518 children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
519 />
520 </div>
521 }];
522
523 if !inactive_children.is_empty() {
524 selected_columns.push(html! {
525 <ScrollPanel
526 id="sub-columns"
527 key="__sub_columns__"
528 class={classes!("scrollable")}
529 on_resize={&ctx.props().on_resize}
530 on_dimensions_reset={&self.on_reset}
531 children={inactive_children}
532 />
533 })
534 }
535
536 html! {
537 <>
538 <SplitPanel
539 no_wrap=true
540 on_reset={self.on_reset.callback()}
541 skip_empty=true
542 orientation={Orientation::Vertical}
543 >
544 { for selected_columns }
545 </SplitPanel>
546 <ColumnDropDownPortal
547 element={self.column_dropdown.clone()}
548 theme={ctx.props().selected_theme.clone().unwrap_or_default()}
549 />
550 </>
551 }
552 }
553}