Skip to main content

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