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;
31pub use pivot_column::*;
32use web_sys::*;
33use yew::prelude::*;
34
35use self::active_column::*;
36use self::add_expression_button::AddExpressionButton;
37use self::config_selector::ConfigSelector;
38use self::inactive_column::*;
39use super::containers::scroll_panel::*;
40use super::containers::split_panel::{Orientation, SplitPanel};
41use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
42use crate::components::containers::scroll_panel_item::ScrollPanelItem;
43use crate::presentation::{ColumnLocator, DragDropContainer, Presentation};
44use crate::queries::{ActiveColumnState, ActiveColumnStateData, ColumnsIteratorSet};
45use crate::renderer::*;
46use crate::session::drag_drop_update::*;
47use crate::session::*;
48use crate::tasks::apply_and_render;
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 let Ok(task) = apply_and_render(&session, &renderer, update) {
222 spawn_owned("column-selector", task);
223 }
224 }
225
226 true
227 },
228 Drop((column, DragTarget::Active, effect, index)) => {
229 let col_type = ctx
230 .props()
231 .metadata
232 .get_column_table_type(column.as_str())
233 .unwrap();
234 let update = ctx.props().view_config.create_drag_drop_update(
235 column,
236 col_type,
237 index,
238 DragTarget::Active,
239 effect,
240 &ctx.props().renderer.metadata(),
241 ctx.props().metadata.get_features().unwrap(),
242 );
243
244 let session = ctx.props().session.clone();
245 let renderer = ctx.props().renderer.clone();
246 if let Ok(task) = apply_and_render(&session, &renderer, update) {
247 spawn_owned("column-selector", task);
248 }
249
250 true
251 },
252 Drop((_, _, DragEffect::Move(DragTarget::Active), _)) => true,
253 Drop((..)) => true,
254 }
255 }
256
257 fn view(&self, ctx: &Context<Self>) -> Html {
258 let ColumnSelectorProps {
259 session,
260 renderer,
261 presentation,
262 ..
263 } = ctx.props();
264 let metadata = &ctx.props().metadata;
265
266 let prop_config = &ctx.props().view_config;
270 let config = if prop_config.columns.is_empty() {
271 if let Some(table_cols) = metadata.get_table_columns() {
272 ViewConfig {
273 columns: table_cols.iter().map(|c| Some(c.clone())).collect(),
274 ..(**prop_config).clone()
275 }
276 .into()
277 } else {
278 prop_config.clone()
279 }
280 } else {
281 prop_config.clone()
282 };
283
284 let is_aggregated = config.is_aggregated();
285 let columns_iter = ColumnsIteratorSet::new(&config, metadata, renderer, presentation);
286 let onselect = ctx.link().callback(|()| Redraw);
287 let ondragenter = ctx.link().callback(HoverActiveIndex);
288 let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default());
289 let ondrop = Callback::from({
290 clone!(presentation);
291 move |event| presentation.notify_drop(&event)
292 });
293
294 let ondragend = Callback::from({
295 clone!(presentation);
296 move |_| presentation.notify_drag_end()
297 });
298
299 let mut active_classes = classes!("scrollable");
300 if ctx.props().drag_column.is_some() {
301 active_classes.push("dragdrop-highlight");
302 };
303
304 if is_aggregated {
305 active_classes.push("is-aggregated");
306 }
307
308 let size_hint = 28.0f64.mul_add(
309 (config.group_by.len()
310 + config.split_by.len()
311 + config.filter.len()
312 + config.sort.len()) as f64,
313 metadata
314 .get_features()
315 .map(|x| {
316 let mut y = 0.0;
317 if !x.filter_ops.is_empty() {
318 y += 1.0;
319 }
320
321 if x.group_by {
322 y += 1.0;
323 }
324
325 if x.split_by {
326 y += 1.0;
327 }
328
329 if x.sort {
330 y += 1.0;
331 }
332
333 y * 55.0
334 })
335 .unwrap_or_default(),
336 );
337
338 let config_selector = html_nested! {
339 <ScrollPanelItem key="config_selector" {size_hint}>
340 <ConfigSelector
341 onselect={onselect.clone()}
342 ondragenter={ctx.link().callback(|()| Redraw)}
343 view_config={ctx.props().view_config.clone()}
344 drag_column={ctx.props().drag_column.clone()}
345 metadata={metadata.clone()}
346 selected_theme={ctx.props().selected_theme.clone()}
347 {presentation}
348 {renderer}
349 {session}
350 />
351 </ScrollPanelItem>
352 };
353
354 let mut named_count = ctx.props().named_column_count;
355 let mut active_columns: Vec<_> = columns_iter
356 .active()
357 .enumerate()
358 .map(|(idx, name): (usize, ActiveColumnState)| {
359 let ondragenter = ondragenter.reform(move |_| Some(idx));
360 let size_hint = if named_count > 0 { 50.0 } else { 28.0 };
361 named_count = named_count.saturating_sub(1);
362 let key = name
363 .get_name()
364 .map(|x| x.to_owned())
365 .unwrap_or_else(|| format!("__auto_{idx}__"));
366
367 let column_dropdown = self.column_dropdown.clone();
368 let is_editing = matches!(
369 &ctx.props().selected_column,
370 Some(ColumnLocator::Table(x)) | Some(ColumnLocator::Expression(x))
371 if x == &key );
372
373 let col_type = name
378 .get_name()
379 .and_then(|n| metadata.get_column_table_type(n))
380 .or_else(|| {
381 if matches!(name.state, ActiveColumnStateData::DragOver) {
382 presentation
383 .get_drag_column()
384 .and_then(|c| metadata.get_column_table_type(&c))
385 } else {
386 None
387 }
388 });
389
390 let is_expression = name
391 .get_name()
392 .map(|n| metadata.is_column_expression(n))
393 .unwrap_or(false);
394
395 let can_render_styles =
396 name.get_name().is_some() && renderer.can_render_column_styles();
397
398 let show_edit_btn = is_expression || can_render_styles;
399 let on_open_expr_panel = &ctx.props().on_open_expr_panel;
400 html_nested! {
401 <ScrollPanelItem {key} {size_hint}>
402 <ActiveColumn
403 {column_dropdown}
404 {idx}
405 {is_aggregated}
406 {is_editing}
407 {is_expression}
408 {show_edit_btn}
409 {col_type}
410 view_config={config.clone()}
411 metadata={metadata.clone()}
412 {name}
413 {on_open_expr_panel}
414 {ondragenter}
415 ondragend={&ondragend}
416 onselect={&onselect}
417 {presentation}
418 {renderer}
419 {session}
420 />
421 </ScrollPanelItem>
422 }
423 })
424 .collect();
425
426 let mut inactive_children: Vec<_> = columns_iter
427 .expression()
428 .chain(columns_iter.inactive())
429 .enumerate()
430 .map(|(idx, vc)| {
431 let selected_column = ctx.props().selected_column.as_ref();
432 let is_editing = matches!(selected_column, Some(ColumnLocator::Expression(x)) if x.as_str() == vc.name);
433 let is_expression = metadata.is_column_expression(vc.name);
434 html_nested! {
435 <ScrollPanelItem key={vc.name} size_hint=28.0>
436 <InactiveColumn
437 {idx}
438 visible={vc.is_visible}
439 name={vc.name.to_owned()}
440 {is_editing}
441 {is_expression}
442 view_config={config.clone()}
443 metadata={metadata.clone()}
444 onselect={&onselect}
445 ondragend={&ondragend}
446 on_open_expr_panel={&ctx.props().on_open_expr_panel}
447 {presentation}
448 {renderer}
449 {session}
450 />
451 </ScrollPanelItem>
452 }
453 })
454 .collect();
455
456 let size = 28.0;
457
458 let add_column = if metadata.get_features().unwrap().expressions {
459 html_nested! {
460 <ScrollPanelItem key="__add_expression__" size_hint={size}>
461 <AddExpressionButton
462 on_open_expr_panel={&ctx.props().on_open_expr_panel}
463 selected_column={ctx.props().selected_column.clone()}
464 />
465 </ScrollPanelItem>
466 }
467 } else {
468 html_nested! {
469 <ScrollPanelItem key="__add_expression__" size_hint=0_f64><span /></ScrollPanelItem>
470 }
471 };
472
473 if inactive_children.is_empty() {
474 active_columns.push(add_column)
475 } else {
476 inactive_children.insert(0, add_column);
477 }
478
479 let mut selected_columns = vec![html! {
480 <div id="selected-columns" key="__active_columns__">
481 <ScrollPanel
482 id="active-columns"
483 omit_autosize_div={true}
484 class={active_classes}
485 dragover={ondragover}
486 dragenter={&self.drag_container.dragenter}
487 dragleave={&self.drag_container.dragleave}
488 viewport_ref={&self.drag_container.noderef}
489 initial_width={ctx.props().initial_width}
490 on_auto_width={ctx.props().on_auto_width.clone()}
491 drop={ondrop}
492 on_resize={&ctx.props().on_resize}
493 on_dimensions_reset={&self.on_reset}
494 children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
495 />
496 </div>
497 }];
498
499 if !inactive_children.is_empty() {
500 selected_columns.push(html! {
501 <ScrollPanel
502 id="sub-columns"
503 key="__sub_columns__"
504 class={classes!("scrollable")}
505 on_resize={&ctx.props().on_resize}
506 on_dimensions_reset={&self.on_reset}
507 children={inactive_children}
508 />
509 })
510 }
511
512 html! {
513 <>
514 <SplitPanel
515 no_wrap=true
516 on_reset={self.on_reset.callback()}
517 skip_empty=true
518 orientation={Orientation::Vertical}
519 >
520 { for selected_columns }
521 </SplitPanel>
522 <ColumnDropDownPortal
523 element={self.column_dropdown.clone()}
524 theme={ctx.props().selected_theme.clone().unwrap_or_default()}
525 />
526 </>
527 }
528 }
529}