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;
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 || next != self.initials() {
223            self.reset_to(ctx, next);
224        } else {
225            self.refresh_derived(ctx);
226        }
227
228        true
229    }
230
231    fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
232        match msg {
233            ColumnSettingsPanelMsg::SetExprValue(val) => {
234                if self.expr_value != val {
235                    self.expr_value = val;
236                    self.reset_enabled = true;
237                    true
238                } else {
239                    false
240                }
241            },
242            ColumnSettingsPanelMsg::SetExprValid(val) => {
243                self.expr_valid = val;
244                self.save_enabled_effect();
245                true
246            },
247            ColumnSettingsPanelMsg::SetHeaderValue(val) => {
248                if self.header_value != val {
249                    self.header_value = val;
250                    self.reset_enabled = true;
251                    true
252                } else {
253                    false
254                }
255            },
256            ColumnSettingsPanelMsg::SetHeaderValid(val) => {
257                self.header_valid = val;
258                self.save_enabled_effect();
259                true
260            },
261            ColumnSettingsPanelMsg::SetWindowValue(val) => {
262                if self.window_value != val {
263                    self.window_value = val;
264                    self.reset_enabled = true;
265                    true
266                } else {
267                    false
268                }
269            },
270            ColumnSettingsPanelMsg::SetSelectedTab((_, val)) => {
271                let rerender = ctx.props().selected_tab != Some(val);
272                ctx.props().on_select_tab.emit(val);
273                rerender
274            },
275            ColumnSettingsPanelMsg::OnResetAttributes(()) => {
276                self.header_value.clone_from(&self.initial_header_value);
277                self.expr_value.clone_from(&self.initial_expr_value);
278                self.window_value.clone_from(&self.initial_window_value);
279                self.save_enabled = false;
280                self.reset_enabled = false;
281                self.reset_count += 1;
282                true
283            },
284            ColumnSettingsPanelMsg::OnSaveAttributes(()) => {
285                if matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window)) {
286                    if let Some(spec) = self.window_value.clone() {
287                        let name = self
288                            .header_value
289                            .clone()
290                            .unwrap_or_else(|| self.column_name.clone());
291                        match &ctx.props().selected_column {
292                            ColumnLocator::Window(old_name) => update_window(
293                                &ctx.props().session,
294                                &ctx.props().renderer,
295                                &ctx.props().presentation,
296                                old_name.clone(),
297                                name,
298                                spec.clone(),
299                            ),
300                            _ => {
301                                if let Err(err) = save_window(
302                                    &ctx.props().session,
303                                    &ctx.props().renderer,
304                                    &ctx.props().presentation,
305                                    name,
306                                    spec.clone(),
307                                ) {
308                                    tracing::warn!("{}", err);
309                                }
310                            },
311                        }
312
313                        self.initial_window_value = Some(spec);
314                        self.initial_header_value.clone_from(&self.header_value);
315                        self.save_enabled = false;
316                        self.reset_enabled = false;
317                        self.save_count += 1;
318                    }
319
320                    return true;
321                }
322
323                let new_expr = Expression::new(
324                    self.header_value.clone().map(|s| s.into()),
325                    (*(self.expr_value)).clone().into(),
326                );
327
328                match &ctx.props().selected_column {
329                    ColumnLocator::Table(_) | ColumnLocator::Window(_) => {
330                        tracing::error!("Tried to save non-expression column!")
331                    },
332                    ColumnLocator::Expression(name) => update_expr(
333                        &ctx.props().session,
334                        &ctx.props().renderer,
335                        &ctx.props().presentation,
336                        name.clone(),
337                        new_expr,
338                    ),
339                    ColumnLocator::NewExpression => {
340                        if let Err(err) = save_expr(
341                            &ctx.props().session,
342                            &ctx.props().renderer,
343                            &ctx.props().presentation,
344                            new_expr,
345                        ) {
346                            tracing::warn!("{}", err);
347                        }
348                    },
349                }
350
351                self.initial_expr_value.clone_from(&self.expr_value);
352                self.initial_header_value.clone_from(&self.header_value);
353                self.save_enabled = false;
354                self.reset_enabled = false;
355                self.save_count += 1;
356                true
357            },
358            ColumnSettingsPanelMsg::OnDelete(()) => {
359                if ctx.props().selected_column.is_saved_expr() {
360                    delete_expr(
361                        &ctx.props().session,
362                        &ctx.props().renderer,
363                        &self.column_name,
364                    )
365                    .unwrap_or_log();
366                } else if ctx.props().selected_column.is_saved_window() {
367                    delete_window(
368                        &ctx.props().session,
369                        &ctx.props().renderer,
370                        &self.column_name,
371                    )
372                    .unwrap_or_log();
373                }
374
375                ctx.props().on_close.emit(());
376                true
377            },
378        }
379    }
380
381    fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
382        let is_window_tab = matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window));
383
384        let header_placeholder = if is_window_tab {
385            Rc::new(self.column_name.clone())
386        } else {
387            self.expr_value.clone()
388        };
389
390        let header_props = props!(EditableHeaderProps {
391            value: self.header_value.clone(),
392            initial_value: self.initial_header_value.clone(),
393            placeholder: header_placeholder,
394            editable: (ctx.props().selected_column.is_expr()
395                && matches!(
396                    ctx.props().selected_tab,
397                    Some(ColumnSettingsTab::Attributes)
398                ))
399                || (ctx.props().selected_column.is_window_editable() && is_window_tab),
400            update_on_input: true,
401            icon_type: self
402                .maybe_ty
403                .map(|ty| ty.into())
404                .or(Some(TypeIconType::Expr)),
405            on_change: ctx.link().batch_callback(|(value, valid)| {
406                vec![
407                    ColumnSettingsPanelMsg::SetHeaderValue(value),
408                    ColumnSettingsPanelMsg::SetHeaderValid(valid),
409                ]
410            }),
411            metadata: ctx.props().metadata.clone(),
412        });
413
414        let expr_editor = props!(ExpressionEditorProps {
415            on_input: self.on_input.clone(),
416            on_save: self.on_save.clone(),
417            on_validate: self.on_validate.clone(),
418            alias: ctx.props().selected_column.name().cloned(),
419            initial_expr: self.initial_expr_value.clone(),
420            disabled: !ctx.props().selected_column.is_expr(),
421            reset_count: self.reset_count,
422            metadata: ctx.props().metadata.clone(),
423            selected_theme: ctx.props().selected_theme.clone(),
424            session: &ctx.props().session
425        });
426
427        let disable_delete = ctx
428            .props()
429            .selected_column
430            .name()
431            .map(|name| {
432                let config = &ctx.props().view_config;
433                config.columns.iter().any(|maybe_col| {
434                    maybe_col
435                        .as_ref()
436                        .map(|col| col == name)
437                        .unwrap_or_default()
438                }) || config.group_by.iter().any(|col| col == name)
439                    || config.split_by.iter().any(|col| col == name)
440                    || config.filter.iter().any(|col| col.column() == name)
441                    || config.sort.iter().any(|col| &col.0 == name)
442            })
443            .unwrap_or_default();
444
445        let save_section = SaveSettingsProps {
446            save_enabled: self.save_enabled,
447            reset_enabled: self.reset_enabled,
448            is_save: ctx.props().selected_column.name().is_some(),
449            on_reset: ctx
450                .link()
451                .callback(ColumnSettingsPanelMsg::OnResetAttributes),
452            on_save: ctx
453                .link()
454                .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
455            on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
456            show_danger_zone: ctx.props().selected_column.is_saved_expr(),
457            disable_delete,
458        };
459
460        let attrs_tab = AttributesTabProps {
461            expr_editor,
462            save_section: save_section.clone(),
463        };
464
465        let window_changed = self.window_value != self.initial_window_value
466            || self.header_value != self.initial_header_value;
467        let window_tab = WindowTabProps {
468            editor: WindowEditorProps {
469                metadata: ctx.props().metadata.clone(),
470                initial: self.initial_window_value.clone(),
471                on_change: ctx.link().callback(ColumnSettingsPanelMsg::SetWindowValue),
472                reset_count: self.reset_count,
473                presentation: ctx.props().presentation.clone(),
474                session: ctx.props().session.clone(),
475                selected_theme: ctx.props().selected_theme.clone(),
476            },
477            save_section: SaveSettingsProps {
478                save_enabled: self.window_value.is_some() && window_changed && self.header_valid,
479                show_danger_zone: ctx.props().selected_column.is_saved_window(),
480                ..save_section
481            },
482        };
483
484        let style_tab = StyleTabProps {
485            ty: self.maybe_ty,
486            column_name: self.column_name.clone(),
487            group_by_depth: ctx.props().view_config.group_by.len() as u32,
488            view_config: ctx.props().view_config.clone(),
489            metadata: ctx.props().metadata.clone(),
490            column_stats: ctx.props().column_stats.clone(),
491            selected_theme: ctx.props().selected_theme.clone(),
492            presentation: ctx.props().presentation.clone(),
493            renderer: ctx.props().renderer.clone(),
494            session: ctx.props().session.clone(),
495            workspace: ctx.props().workspace.clone(),
496        };
497
498        let tab_children = self.tabs.iter().map(|tab| match tab {
499            ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
500            ColumnSettingsTab::Window => html! { <WindowTab ..window_tab.clone() /> },
501            ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
502        });
503
504        let selected_tab_idx = self
505            .tabs
506            .iter()
507            .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
508            .map(|(idx, _val)| idx)
509            .unwrap_or_default();
510
511        html! {
512            <>
513                <Sidebar
514                    on_close={ctx.props().on_close.clone()}
515                    id_prefix="column_settings"
516                    width_override={ctx.props().width_override}
517                    auto_width={ctx.props().auto_width}
518                    on_auto_width={ctx.props().on_auto_width.clone()}
519                    is_pinned={ctx.props().is_pinned}
520                    on_toggle_pin={Some(ctx.props().on_toggle_pin.clone())}
521                    selected_tab={selected_tab_idx}
522                    {header_props}
523                >
524                    <TabList<ColumnSettingsTab>
525                        tabs={self.tabs.clone()}
526                        on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
527                        selected_tab={selected_tab_idx}
528                    >
529                        { for tab_children }
530                    </TabList<ColumnSettingsTab>>
531                </Sidebar>
532            </>
533        }
534    }
535}
536
537impl ColumnSettingsPanel {
538    fn save_enabled_effect(&mut self) {
539        let changed = self.expr_value != self.initial_expr_value
540            || self.header_value != self.initial_header_value;
541        let valid = self.expr_valid && self.header_valid;
542        self.save_enabled = changed && valid;
543    }
544
545    fn initials(&self) -> Initials {
546        Initials {
547            column_name: self.column_name.clone(),
548            expr: self.initial_expr_value.clone(),
549            header: self.initial_header_value.clone(),
550            window: self.initial_window_value.clone(),
551        }
552    }
553
554    fn refresh_derived(&mut self, ctx: &yew::prelude::Context<Self>) {
555        self.maybe_ty = ctx
556            .props()
557            .metadata
558            .locator_view_type(&ctx.props().selected_column);
559
560        self.tabs = {
561            let mut tabs = vec![];
562            let is_new_expr = ctx.props().selected_column.is_new_expr();
563            let show_styles = !is_new_expr
564                && ctx.props().renderer.can_render_column_styles()
565                && ctx.props().view_config.columns.contains(&Some(
566                    ctx.props()
567                        .selected_column
568                        .name()
569                        .map(|x| x.to_string())
570                        .unwrap_or_default(),
571                ));
572
573            if !is_new_expr && show_styles {
574                tabs.push(ColumnSettingsTab::Style);
575            }
576
577            if ctx.props().selected_column.is_expr() {
578                tabs.push(ColumnSettingsTab::Attributes);
579            }
580
581            let supports_windows = ctx
582                .props()
583                .metadata
584                .get_features()
585                .map(|x| x.has_window_aggregates())
586                .unwrap_or_default();
587
588            if ctx.props().selected_column.is_window_editable() && supports_windows {
589                tabs.push(ColumnSettingsTab::Window);
590            }
591
592            tabs
593        };
594    }
595
596    fn reset_to(&mut self, ctx: &yew::prelude::Context<Self>, initials: Initials) {
597        let Initials {
598            column_name,
599            expr,
600            header,
601            window,
602        } = initials;
603
604        self.column_name = column_name;
605        self.expr_value = expr.clone();
606        self.initial_expr_value = expr;
607        self.header_value = header.clone();
608        self.initial_header_value = header;
609        self.window_value = window.clone();
610        self.initial_window_value = window;
611        self.header_valid = true;
612        self.save_enabled = false;
613        self.reset_enabled = false;
614        self.reset_count = self.reset_count.wrapping_add(1);
615        self.refresh_derived(ctx);
616    }
617}