perspective_viewer/components/
column_selector.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13mod active_column;
14mod add_expression_button;
15mod aggregate_selector;
16mod config_selector;
17mod empty_column;
18mod expression_toolbar;
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 web_sys::*;
31use yew::prelude::*;
32
33use self::active_column::*;
34use self::add_expression_button::AddExpressionButton;
35use self::config_selector::ConfigSelector;
36use self::inactive_column::*;
37use super::containers::scroll_panel::*;
38use super::containers::split_panel::{Orientation, SplitPanel};
39use super::style::LocalStyle;
40use super::viewer::ColumnLocator;
41use crate::components::containers::scroll_panel_item::ScrollPanelItem;
42use crate::custom_elements::ColumnDropDownElement;
43use crate::dragdrop::*;
44use crate::model::*;
45use crate::presentation::Presentation;
46use crate::renderer::*;
47use crate::session::*;
48use crate::utils::*;
49use crate::*;
50
51#[derive(Properties)]
52pub struct ColumnSelectorProps {
53    pub session: Session,
54    pub renderer: Renderer,
55    pub dragdrop: DragDrop,
56    pub presentation: Presentation,
57
58    pub on_open_expr_panel: Callback<ColumnLocator>,
59
60    /// This is passed to the add_expression_button for styling.
61    pub selected_column: Option<ColumnLocator>,
62
63    #[prop_or_default]
64    pub on_resize: Option<Rc<PubSub<()>>>,
65
66    #[prop_or_default]
67    pub on_dimensions_reset: Option<Rc<PubSub<()>>>,
68}
69
70derive_model!(DragDrop, Renderer, Session for ColumnSelectorProps);
71
72impl PartialEq for ColumnSelectorProps {
73    fn eq(&self, rhs: &Self) -> bool {
74        self.selected_column == rhs.selected_column
75    }
76}
77
78#[derive(Debug)]
79pub enum ColumnSelectorMsg {
80    TableLoaded,
81    ViewCreated,
82    HoverActiveIndex(Option<usize>),
83    Drag(DragEffect),
84    DragEnd,
85    Drop((String, DragTarget, DragEffect, usize)),
86}
87
88use ColumnSelectorMsg::*;
89
90/// A `ColumnSelector` controls the `columns` field of the `ViewConfig`,
91/// deriving its options from the table columns and `ViewConfig` expressions.
92pub struct ColumnSelector {
93    _subscriptions: [Subscription; 5],
94    named_row_count: usize,
95    drag_container: DragDropContainer,
96    column_dropdown: ColumnDropDownElement,
97    on_reset: Rc<PubSub<()>>,
98}
99
100impl Component for ColumnSelector {
101    type Message = ColumnSelectorMsg;
102    type Properties = ColumnSelectorProps;
103
104    fn create(ctx: &Context<Self>) -> Self {
105        let table_sub = {
106            let cb = ctx.link().callback(|_| ColumnSelectorMsg::TableLoaded);
107            ctx.props().session.table_loaded.add_listener(cb)
108        };
109
110        let view_sub = {
111            let cb = ctx.link().callback(|_| ColumnSelectorMsg::ViewCreated);
112            ctx.props().session.view_created.add_listener(cb)
113        };
114
115        let drop_sub = {
116            let cb = ctx.link().callback(ColumnSelectorMsg::Drop);
117            ctx.props().dragdrop.drop_received.add_listener(cb)
118        };
119
120        let drag_sub = {
121            let cb = ctx.link().callback(ColumnSelectorMsg::Drag);
122            ctx.props().dragdrop.dragstart_received.add_listener(cb)
123        };
124
125        let dragend_sub = {
126            let cb = ctx.link().callback(|_| ColumnSelectorMsg::DragEnd);
127            ctx.props().dragdrop.dragend_received.add_listener(cb)
128        };
129
130        let named = maybe! {
131            let plugin =
132                ctx.props().renderer.get_active_plugin().ok()?;
133
134            Some(plugin.config_column_names()?.length() as usize)
135        };
136
137        let named_row_count = named.unwrap_or_default();
138        let drag_container = DragDropContainer::new(|| {}, {
139            let link = ctx.link().clone();
140            move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None))
141        });
142
143        let column_dropdown = ColumnDropDownElement::new(ctx.props().session.clone());
144        Self {
145            _subscriptions: [table_sub, view_sub, drop_sub, drag_sub, dragend_sub],
146            named_row_count,
147            drag_container,
148            column_dropdown,
149            on_reset: Default::default(),
150        }
151    }
152
153    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
154        match msg {
155            Drag(DragEffect::Move(DragTarget::Active)) => false,
156            Drag(_) | DragEnd | TableLoaded => true,
157            ViewCreated => {
158                let named = maybe! {
159                    let plugin =
160                        ctx.props().renderer.get_active_plugin().ok()?;
161
162                    Some(plugin.config_column_names()?.length() as usize)
163                };
164
165                self.named_row_count = named.unwrap_or_default();
166                true
167            },
168            HoverActiveIndex(Some(to_index)) => ctx
169                .props()
170                .dragdrop
171                .notify_drag_enter(DragTarget::Active, to_index),
172            HoverActiveIndex(_) => {
173                ctx.props().dragdrop.notify_drag_leave(DragTarget::Active);
174                true
175            },
176            Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => {
177                if !ctx.props().is_invalid_columns_column(&column, index) {
178                    let update = ctx.props().session.create_drag_drop_update(
179                        column,
180                        index,
181                        DragTarget::Active,
182                        DragEffect::Move(DragTarget::Active),
183                        &ctx.props().renderer.metadata(),
184                    );
185
186                    if let Ok(task) = ctx.props().update_and_render(update) {
187                        ApiFuture::spawn(task);
188                    }
189                }
190
191                true
192            },
193            Drop((column, DragTarget::Active, effect, index)) => {
194                let update = ctx.props().session.create_drag_drop_update(
195                    column,
196                    index,
197                    DragTarget::Active,
198                    effect,
199                    &ctx.props().renderer.metadata(),
200                );
201
202                if let Ok(task) = ctx.props().update_and_render(update) {
203                    ApiFuture::spawn(task);
204                }
205
206                true
207            },
208            Drop((_, _, DragEffect::Move(DragTarget::Active), _)) => true,
209            Drop((..)) => true,
210        }
211    }
212
213    fn view(&self, ctx: &Context<Self>) -> Html {
214        let config = ctx.props().session.get_view_config();
215
216        let is_aggregated = config.is_aggregated();
217        let columns_iter = ctx.props().column_selector_iter_set(&config);
218        let onselect = ctx.link().callback(|()| ViewCreated);
219        let ondragenter = ctx.link().callback(HoverActiveIndex);
220        let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default());
221        let ondrop = Callback::from({
222            let dragdrop = ctx.props().dragdrop.clone();
223            move |event| dragdrop.notify_drop(&event)
224        });
225
226        let ondragend = Callback::from({
227            let dragdrop = ctx.props().dragdrop.clone();
228            move |_| dragdrop.notify_drag_end()
229        });
230
231        let mut active_classes = classes!();
232        if ctx.props().dragdrop.get_drag_column().is_some() {
233            active_classes.push("dragdrop-highlight");
234        };
235
236        if is_aggregated {
237            active_classes.push("is-aggregated");
238        }
239
240        let size_hint = 28.0f64.mul_add(
241            (config.group_by.len()
242                + config.split_by.len()
243                + config.filter.len()
244                + config.sort.len()) as f64,
245            ctx.props()
246                .session
247                .metadata()
248                .get_features()
249                .map(|x| {
250                    let mut y = 0.0;
251                    if !x.filter_ops.is_empty() {
252                        y += 1.0;
253                    }
254
255                    if x.group_by {
256                        y += 1.0;
257                    }
258
259                    if x.split_by {
260                        y += 1.0;
261                    }
262
263                    if x.sort {
264                        y += 1.0;
265                    }
266
267                    y * 55.0
268                })
269                .unwrap_or_default(),
270        );
271
272        let config_selector = html_nested! {
273            <ScrollPanelItem key="config_selector" {size_hint}>
274                <ConfigSelector
275                    dragdrop={&ctx.props().dragdrop}
276                    session={&ctx.props().session}
277                    renderer={&ctx.props().renderer}
278                    onselect={onselect.clone()}
279                    ondragenter={ctx.link().callback(|()| ViewCreated)}
280                />
281            </ScrollPanelItem>
282        };
283
284        let mut named_count = self.named_row_count;
285        let mut active_columns: Vec<_> = columns_iter
286            .active()
287            .enumerate()
288            .map(|(idx, name)| {
289                let ondragenter = ondragenter.reform(move |_| Some(idx));
290                let size_hint = if named_count > 0 { 50.0 } else { 28.0 };
291                named_count = named_count.saturating_sub(1);
292                let key = name
293                    .get_name()
294                    .map(|x| x.to_owned())
295                    .unwrap_or_else(|| format!("__auto_{idx}__"));
296
297                let column_dropdown = self.column_dropdown.clone();
298                let is_editing = matches!(
299                    &ctx.props().selected_column,
300                    Some(ColumnLocator::Table(x)) | Some(ColumnLocator::Expression(x))
301                if x == &key );
302
303                let on_open_expr_panel = &ctx.props().on_open_expr_panel;
304                html_nested! {
305                    <ScrollPanelItem {key} {size_hint}>
306                        <ActiveColumn
307                            {column_dropdown}
308                            {idx}
309                            {is_aggregated}
310                            {is_editing}
311                            {name}
312                            {on_open_expr_panel}
313                            dragdrop={&ctx.props().dragdrop}
314                            session={&ctx.props().session}
315                            renderer={&ctx.props().renderer}
316                            presentation={&ctx.props().presentation}
317                            {ondragenter}
318                            ondragend={&ondragend}
319                            onselect={&onselect}
320                        />
321                    </ScrollPanelItem>
322                }
323            })
324            .collect();
325
326        let mut inactive_children: Vec<_> = columns_iter
327            .expression()
328            .chain(columns_iter.inactive())
329            .enumerate()
330            .map(|(idx, vc)| {
331                let selected_column = ctx.props().selected_column.as_ref();
332                let is_editing = matches!(selected_column, Some(ColumnLocator::Expression(x)) if x.as_str() == vc.name);
333                html_nested! {
334                    <ScrollPanelItem key={vc.name} size_hint=28.0>
335                        <InactiveColumn
336                            {idx}
337                            visible={vc.is_visible}
338                            name={vc.name.to_owned()}
339                            dragdrop={&ctx.props().dragdrop}
340                            session={&ctx.props().session}
341                            renderer={&ctx.props().renderer}
342                            presentation={&ctx.props().presentation}
343                            {is_editing}
344                            onselect={&onselect}
345                            ondragend={&ondragend}
346                            on_open_expr_panel={&ctx.props().on_open_expr_panel}
347                        />
348                    </ScrollPanelItem>
349                }
350            })
351            .collect();
352
353        let size = if !inactive_children.is_empty() {
354            56.0
355        } else {
356            28.0
357        };
358
359        let add_column = if ctx
360            .props()
361            .session
362            .metadata()
363            .get_features()
364            .unwrap()
365            .expressions
366        {
367            html_nested! {
368                <ScrollPanelItem key="__add_expression__" size_hint={size}>
369                    <AddExpressionButton
370                        on_open_expr_panel={&ctx.props().on_open_expr_panel}
371                        selected_column={ctx.props().selected_column.clone()}
372                    />
373                </ScrollPanelItem>
374            }
375        } else {
376            html_nested! {
377                <ScrollPanelItem key="__add_expression__" size_hint=0_f64><span /></ScrollPanelItem>
378            }
379        };
380
381        if inactive_children.is_empty() {
382            active_columns.push(add_column)
383        } else {
384            inactive_children.insert(0, add_column);
385        }
386
387        let selected_columns = html! {
388            <div id="selected-columns">
389                <ScrollPanel
390                    id="active-columns"
391                    class={active_classes}
392                    dragover={ondragover}
393                    dragenter={&self.drag_container.dragenter}
394                    dragleave={&self.drag_container.dragleave}
395                    viewport_ref={&self.drag_container.noderef}
396                    drop={ondrop}
397                    on_resize={&ctx.props().on_resize}
398                    on_dimensions_reset={&self.on_reset}
399                    children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
400                />
401            </div>
402        };
403
404        html! {
405            <>
406                <LocalStyle href={css!("column-selector")} />
407                <SplitPanel
408                    no_wrap=true
409                    on_reset={self.on_reset.callback()}
410                    skip_empty=true
411                    orientation={Orientation::Vertical}
412                >
413                    { selected_columns }
414                    if !inactive_children.is_empty() {
415                        <ScrollPanel
416                            id="sub-columns"
417                            on_resize={&ctx.props().on_resize}
418                            on_dimensions_reset={&self.on_reset}
419                            children={inactive_children}
420                        />
421                    }
422                </SplitPanel>
423            </>
424        }
425    }
426}