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