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