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