Skip to main content

perspective_viewer/components/
column_settings_sidebar.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// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12mod attributes_tab;
13
14mod save_settings;
15pub(crate) mod style_tab;
16mod window_tab;
17
18use std::rc::Rc;
19
20use derivative::Derivative;
21use itertools::Itertools;
22use perspective_client::config::{ColumnType, Expression, ViewConfig, WindowSpec};
23use perspective_client::utils::PerspectiveResultExt;
24use yew::{Callback, Component, Html, Properties, html, props};
25
26use self::attributes_tab::AttributesTabProps;
27use self::style_tab::StyleTabProps;
28use crate::components::column_settings_sidebar::attributes_tab::AttributesTab;
29use crate::components::column_settings_sidebar::save_settings::SaveSettingsProps;
30use crate::components::column_settings_sidebar::style_tab::StyleTab;
31use crate::components::column_settings_sidebar::window_tab::{WindowTab, WindowTabProps};
32use crate::components::containers::sidebar::Sidebar;
33use crate::components::containers::tab_list::TabList;
34use crate::components::editable_header::EditableHeaderProps;
35use crate::components::expression_editor::ExpressionEditorProps;
36use crate::components::type_icon::TypeIconType;
37use crate::components::window_editor::WindowEditorProps;
38use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
39use crate::renderer::Renderer;
40use crate::session::{Session, SessionMetadataRc};
41use crate::tasks::{
42    delete_expr, delete_window, save_expr, save_window, update_expr, update_window,
43};
44use crate::utils::PtrEqRc;
45
46#[derive(Clone, Derivative, Properties)]
47#[derivative(Debug)]
48pub struct ColumnSettingsPanelProps {
49    pub selected_column: ColumnLocator,
50    pub selected_tab: Option<ColumnSettingsTab>,
51    pub on_close: Callback<()>,
52    pub width_override: Option<i32>,
53    pub on_select_tab: Callback<ColumnSettingsTab>,
54
55    /// Shared trap-door width across the drawer's Style/Attributes/Window
56    /// tabs.
57    #[prop_or_default]
58    pub auto_width: f64,
59
60    #[prop_or_default]
61    pub on_auto_width: Callback<f64>,
62
63    /// Whether the drawer is pinned into the layout.
64    #[prop_or_default]
65    pub is_pinned: bool,
66
67    #[prop_or_default]
68    pub on_toggle_pin: Callback<()>,
69
70    /// Active plugin name.
71    pub plugin_name: Option<String>,
72
73    /// Session metadata snapshot — threaded from `SessionProps`.
74    pub metadata: SessionMetadataRc,
75
76    /// View config snapshot — threaded from `SessionProps`.
77    pub view_config: PtrEqRc<ViewConfig>,
78
79    /// Per-column stats snapshot — threaded from `SessionProps`.
80    pub column_stats: PtrEqRc<std::collections::HashMap<String, crate::session::ColumnStats>>,
81
82    /// Selected theme name, threaded for PortalModal consumers.
83    pub selected_theme: Option<String>,
84
85    // State
86    #[derivative(Debug = "ignore")]
87    pub presentation: Presentation,
88
89    #[derivative(Debug = "ignore")]
90    pub renderer: Renderer,
91
92    #[derivative(Debug = "ignore")]
93    pub session: Session,
94}
95
96impl ColumnSettingsPanelProps {
97    /// Everything EXCEPT the trap-door `auto_width`: the props whose change
98    /// invalidates the drafts that `initialize` rebuilds.
99    fn identity_eq(&self, other: &Self) -> bool {
100        self.selected_column == other.selected_column
101            && self.selected_tab == other.selected_tab
102            && self.plugin_name == other.plugin_name
103            && self.metadata == other.metadata
104            && self.view_config == other.view_config
105            && self.column_stats == other.column_stats
106            && self.selected_theme == other.selected_theme
107    }
108}
109
110impl PartialEq for ColumnSettingsPanelProps {
111    fn eq(&self, other: &Self) -> bool {
112        self.identity_eq(other)
113            && self.auto_width == other.auto_width
114            && self.is_pinned == other.is_pinned
115    }
116}
117
118#[derive(Debug)]
119pub enum ColumnSettingsPanelMsg {
120    SetExprValue(Rc<String>),
121    SetExprValid(bool),
122    SetHeaderValue(Option<String>),
123    SetHeaderValid(bool),
124    SetWindowValue(Option<WindowSpec>),
125    SetSelectedTab((usize, ColumnSettingsTab)),
126    OnSaveAttributes(()),
127    OnResetAttributes(()),
128    OnDelete(()),
129}
130
131#[derive(Derivative)]
132#[derivative(Debug)]
133pub struct ColumnSettingsPanel {
134    column_name: String,
135    expr_valid: bool,
136    expr_value: Rc<String>,
137    header_valid: bool,
138    header_value: Option<String>,
139    initial_expr_value: Rc<String>,
140    initial_header_value: Option<String>,
141    maybe_ty: Option<ColumnType>,
142    on_input: Callback<Rc<String>>,
143    on_save: Callback<()>,
144    on_validate: Callback<bool>,
145    reset_count: u8,
146    reset_enabled: bool,
147    save_count: u8,
148    save_enabled: bool,
149    initial_window_value: Option<WindowSpec>,
150    window_value: Option<WindowSpec>,
151    tabs: Vec<ColumnSettingsTab>,
152}
153
154impl Component for ColumnSettingsPanel {
155    type Message = ColumnSettingsPanelMsg;
156    type Properties = ColumnSettingsPanelProps;
157
158    fn create(ctx: &yew::prelude::Context<Self>) -> Self {
159        let mut this = Self {
160            initial_expr_value: Rc::default(),
161            expr_value: Rc::default(),
162            expr_valid: false,
163            initial_header_value: None,
164            header_value: None,
165            header_valid: false,
166            save_enabled: false,
167            save_count: 0,
168            reset_enabled: false,
169            reset_count: 0,
170            column_name: "".to_owned(),
171            maybe_ty: None,
172            initial_window_value: None,
173            window_value: None,
174            tabs: vec![],
175            on_input: Callback::default(),
176            on_save: Callback::default(),
177            on_validate: Callback::default(),
178        };
179
180        this.initialize(ctx);
181        this
182    }
183
184    fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
185        // Only reached when props are unequal. Re-`initialize` (which wipes
186        // in-progress expression/window drafts) only on IDENTITY changes -
187        // a trap-door `auto_width` change re-renders the `Sidebar` sizer
188        // alone.
189        if !ctx.props().identity_eq(old_props) {
190            self.initialize(ctx);
191        }
192
193        true
194    }
195
196    fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
197        match msg {
198            ColumnSettingsPanelMsg::SetExprValue(val) => {
199                if self.expr_value != val {
200                    self.expr_value = val;
201                    self.reset_enabled = true;
202                    true
203                } else {
204                    false
205                }
206            },
207            ColumnSettingsPanelMsg::SetExprValid(val) => {
208                self.expr_valid = val;
209                self.save_enabled_effect();
210                true
211            },
212            ColumnSettingsPanelMsg::SetHeaderValue(val) => {
213                if self.header_value != val {
214                    self.header_value = val;
215                    self.reset_enabled = true;
216                    true
217                } else {
218                    false
219                }
220            },
221            ColumnSettingsPanelMsg::SetHeaderValid(val) => {
222                self.header_valid = val;
223                self.save_enabled_effect();
224                true
225            },
226            ColumnSettingsPanelMsg::SetWindowValue(val) => {
227                if self.window_value != val {
228                    self.window_value = val;
229                    self.reset_enabled = true;
230                    true
231                } else {
232                    false
233                }
234            },
235            ColumnSettingsPanelMsg::SetSelectedTab((_, val)) => {
236                let rerender = ctx.props().selected_tab != Some(val);
237                ctx.props().on_select_tab.emit(val);
238                rerender
239            },
240            ColumnSettingsPanelMsg::OnResetAttributes(()) => {
241                self.header_value.clone_from(&self.initial_header_value);
242                self.expr_value.clone_from(&self.initial_expr_value);
243                self.window_value.clone_from(&self.initial_window_value);
244                self.save_enabled = false;
245                self.reset_enabled = false;
246                self.reset_count += 1;
247                true
248            },
249            ColumnSettingsPanelMsg::OnSaveAttributes(()) => {
250                if matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window)) {
251                    if let Some(spec) = self.window_value.clone() {
252                        let name = self
253                            .header_value
254                            .clone()
255                            .unwrap_or_else(|| self.column_name.clone());
256                        match &ctx.props().selected_column {
257                            ColumnLocator::Window(old_name) => update_window(
258                                &ctx.props().session,
259                                &ctx.props().renderer,
260                                &ctx.props().presentation,
261                                old_name.clone(),
262                                name,
263                                spec.clone(),
264                            ),
265                            _ => {
266                                if let Err(err) = save_window(
267                                    &ctx.props().session,
268                                    &ctx.props().renderer,
269                                    &ctx.props().presentation,
270                                    name,
271                                    spec.clone(),
272                                ) {
273                                    tracing::warn!("{}", err);
274                                }
275                            },
276                        }
277
278                        self.initial_window_value = Some(spec);
279                        self.initial_header_value.clone_from(&self.header_value);
280                        self.save_enabled = false;
281                        self.reset_enabled = false;
282                        self.save_count += 1;
283                    }
284
285                    return true;
286                }
287
288                let new_expr = Expression::new(
289                    self.header_value.clone().map(|s| s.into()),
290                    (*(self.expr_value)).clone().into(),
291                );
292
293                match &ctx.props().selected_column {
294                    ColumnLocator::Table(_) | ColumnLocator::Window(_) => {
295                        tracing::error!("Tried to save non-expression column!")
296                    },
297                    ColumnLocator::Expression(name) => update_expr(
298                        &ctx.props().session,
299                        &ctx.props().renderer,
300                        &ctx.props().presentation,
301                        name.clone(),
302                        new_expr,
303                    ),
304                    ColumnLocator::NewExpression => {
305                        if let Err(err) = save_expr(
306                            &ctx.props().session,
307                            &ctx.props().renderer,
308                            &ctx.props().presentation,
309                            new_expr,
310                        ) {
311                            tracing::warn!("{}", err);
312                        }
313                    },
314                }
315
316                self.initial_expr_value.clone_from(&self.expr_value);
317                self.initial_header_value.clone_from(&self.header_value);
318                self.save_enabled = false;
319                self.reset_enabled = false;
320                self.save_count += 1;
321                true
322            },
323            ColumnSettingsPanelMsg::OnDelete(()) => {
324                if ctx.props().selected_column.is_saved_expr() {
325                    delete_expr(
326                        &ctx.props().session,
327                        &ctx.props().renderer,
328                        &self.column_name,
329                    )
330                    .unwrap_or_log();
331                } else if ctx.props().selected_column.is_saved_window() {
332                    delete_window(
333                        &ctx.props().session,
334                        &ctx.props().renderer,
335                        &self.column_name,
336                    )
337                    .unwrap_or_log();
338                }
339
340                ctx.props().on_close.emit(());
341                true
342            },
343        }
344    }
345
346    fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
347        let is_window_tab = matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window));
348
349        let header_placeholder = if is_window_tab {
350            Rc::new(self.column_name.clone())
351        } else {
352            self.expr_value.clone()
353        };
354
355        let header_props = props!(EditableHeaderProps {
356            initial_value: self.initial_header_value.clone(),
357            placeholder: header_placeholder,
358            reset_count: self.reset_count,
359            editable: (ctx.props().selected_column.is_expr()
360                && matches!(
361                    ctx.props().selected_tab,
362                    Some(ColumnSettingsTab::Attributes)
363                ))
364                || (ctx.props().selected_column.is_window_editable() && is_window_tab),
365            update_on_input: true,
366            icon_type: self
367                .maybe_ty
368                .map(|ty| ty.into())
369                .or(Some(TypeIconType::Expr)),
370            on_change: ctx.link().batch_callback(|(value, valid)| {
371                vec![
372                    ColumnSettingsPanelMsg::SetHeaderValue(value),
373                    ColumnSettingsPanelMsg::SetHeaderValid(valid),
374                ]
375            }),
376            metadata: ctx.props().metadata.clone(),
377            session: &ctx.props().session
378        });
379
380        let expr_editor = props!(ExpressionEditorProps {
381            on_input: self.on_input.clone(),
382            on_save: self.on_save.clone(),
383            on_validate: self.on_validate.clone(),
384            alias: ctx.props().selected_column.name().cloned(),
385            disabled: !ctx.props().selected_column.is_expr(),
386            reset_count: self.reset_count,
387            metadata: ctx.props().metadata.clone(),
388            selected_theme: ctx.props().selected_theme.clone(),
389            session: &ctx.props().session
390        });
391
392        let disable_delete = ctx
393            .props()
394            .selected_column
395            .name()
396            .map(|name| {
397                let config = &ctx.props().view_config;
398                config.columns.iter().any(|maybe_col| {
399                    maybe_col
400                        .as_ref()
401                        .map(|col| col == name)
402                        .unwrap_or_default()
403                }) || config.group_by.iter().any(|col| col == name)
404                    || config.split_by.iter().any(|col| col == name)
405                    || config.filter.iter().any(|col| col.column() == name)
406                    || config.sort.iter().any(|col| &col.0 == name)
407            })
408            .unwrap_or_default();
409
410        let save_section = SaveSettingsProps {
411            save_enabled: self.save_enabled,
412            reset_enabled: self.reset_enabled,
413            is_save: ctx.props().selected_column.name().is_some(),
414            on_reset: ctx
415                .link()
416                .callback(ColumnSettingsPanelMsg::OnResetAttributes),
417            on_save: ctx
418                .link()
419                .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
420            on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
421            show_danger_zone: ctx.props().selected_column.is_saved_expr(),
422            disable_delete,
423        };
424
425        let attrs_tab = AttributesTabProps {
426            expr_editor,
427            save_section: save_section.clone(),
428        };
429
430        let window_changed = self.window_value != self.initial_window_value
431            || self.header_value != self.initial_header_value;
432        let window_tab = WindowTabProps {
433            editor: WindowEditorProps {
434                metadata: ctx.props().metadata.clone(),
435                initial: self.initial_window_value.clone(),
436                on_change: ctx.link().callback(ColumnSettingsPanelMsg::SetWindowValue),
437                reset_count: self.reset_count,
438                presentation: ctx.props().presentation.clone(),
439                session: ctx.props().session.clone(),
440                selected_theme: ctx.props().selected_theme.clone(),
441            },
442            save_section: SaveSettingsProps {
443                save_enabled: self.window_value.is_some() && window_changed && self.header_valid,
444                show_danger_zone: ctx.props().selected_column.is_saved_window(),
445                ..save_section
446            },
447        };
448
449        let style_tab = StyleTabProps {
450            ty: self.maybe_ty,
451            column_name: self.column_name.clone(),
452            group_by_depth: ctx.props().view_config.group_by.len() as u32,
453            view_config: ctx.props().view_config.clone(),
454            metadata: ctx.props().metadata.clone(),
455            column_stats: ctx.props().column_stats.clone(),
456            selected_theme: ctx.props().selected_theme.clone(),
457            presentation: ctx.props().presentation.clone(),
458            renderer: ctx.props().renderer.clone(),
459            session: ctx.props().session.clone(),
460        };
461
462        let tab_children = self.tabs.iter().map(|tab| match tab {
463            ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
464            ColumnSettingsTab::Window => html! { <WindowTab ..window_tab.clone() /> },
465            ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
466        });
467
468        let selected_tab_idx = self
469            .tabs
470            .iter()
471            .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
472            .map(|(idx, _val)| idx)
473            .unwrap_or_default();
474
475        html! {
476            <>
477                <Sidebar
478                    on_close={ctx.props().on_close.clone()}
479                    id_prefix="column_settings"
480                    width_override={ctx.props().width_override}
481                    auto_width={ctx.props().auto_width}
482                    on_auto_width={ctx.props().on_auto_width.clone()}
483                    is_pinned={ctx.props().is_pinned}
484                    on_toggle_pin={Some(ctx.props().on_toggle_pin.clone())}
485                    selected_tab={selected_tab_idx}
486                    {header_props}
487                >
488                    <TabList<ColumnSettingsTab>
489                        tabs={self.tabs.clone()}
490                        on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
491                        selected_tab={selected_tab_idx}
492                    >
493                        { for tab_children }
494                    </TabList<ColumnSettingsTab>>
495                </Sidebar>
496            </>
497        }
498    }
499}
500
501impl ColumnSettingsPanel {
502    fn save_enabled_effect(&mut self) {
503        let changed = self.expr_value != self.initial_expr_value
504            || self.header_value != self.initial_header_value;
505        let valid = self.expr_valid && self.header_valid;
506        self.save_enabled = changed && valid;
507    }
508
509    fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
510        let column_name = ctx
511            .props()
512            .metadata
513            .locator_name_or_default(&ctx.props().selected_column);
514
515        let initial_expr_value = ctx
516            .props()
517            .metadata
518            .get_expression_by_alias(&column_name)
519            .unwrap_or_default();
520
521        let initial_expr_value = Rc::new(initial_expr_value);
522        let initial_header_value =
523            (*initial_expr_value != column_name).then_some(column_name.clone());
524
525        let maybe_ty = ctx
526            .props()
527            .metadata
528            .locator_view_type(&ctx.props().selected_column);
529
530        // Specs are unnamed (the `windows` map key names them, and the
531        // editable header owns naming in the UI), so drafts and saved specs
532        // compare directly.
533        let initial_window_value = ctx
534            .props()
535            .selected_column
536            .name()
537            .and_then(|name| ctx.props().view_config.windows.get(name))
538            .cloned();
539
540        let tabs = {
541            let mut tabs = vec![];
542            let is_new_expr = ctx.props().selected_column.is_new_expr();
543            let show_styles = !is_new_expr
544                && ctx.props().renderer.can_render_column_styles()
545                && ctx.props().view_config.columns.contains(&Some(
546                    ctx.props()
547                        .selected_column
548                        .name()
549                        .map(|x| x.to_string())
550                        .unwrap_or_default(),
551                ));
552
553            if !is_new_expr && show_styles {
554                tabs.push(ColumnSettingsTab::Style);
555            }
556
557            if ctx.props().selected_column.is_expr() {
558                tabs.push(ColumnSettingsTab::Attributes);
559            }
560
561            let supports_windows = ctx
562                .props()
563                .metadata
564                .get_features()
565                .map(|x| x.has_window_aggregates())
566                .unwrap_or_default();
567
568            if ctx.props().selected_column.is_window_editable() && supports_windows {
569                tabs.push(ColumnSettingsTab::Window);
570            }
571
572            tabs
573        };
574
575        let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
576        let on_save = ctx
577            .link()
578            .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
579
580        let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
581        *self = Self {
582            column_name,
583            expr_value: initial_expr_value.clone(),
584            initial_expr_value,
585            header_value: initial_header_value.clone(),
586            initial_header_value,
587            maybe_ty,
588            window_value: initial_window_value.clone(),
589            initial_window_value,
590            tabs,
591            header_valid: true,
592            on_input,
593            on_save,
594            on_validate,
595            ..*self
596        }
597    }
598}