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