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;
16
17use std::rc::Rc;
18
19use derivative::Derivative;
20use itertools::Itertools;
21use perspective_client::config::{ColumnType, Expression, ViewConfig};
22use perspective_client::utils::PerspectiveResultExt;
23use yew::{Callback, Component, Html, Properties, html, props};
24
25use self::attributes_tab::AttributesTabProps;
26use self::style_tab::StyleTabProps;
27use crate::components::column_settings_sidebar::attributes_tab::AttributesTab;
28use crate::components::column_settings_sidebar::save_settings::SaveSettingsProps;
29use crate::components::column_settings_sidebar::style_tab::StyleTab;
30use crate::components::containers::sidebar::Sidebar;
31use crate::components::containers::tab_list::TabList;
32use crate::components::editable_header::EditableHeaderProps;
33use crate::components::expression_editor::ExpressionEditorProps;
34use crate::components::style::LocalStyle;
35use crate::components::type_icon::TypeIconType;
36use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
37use crate::renderer::Renderer;
38use crate::session::{Session, SessionMetadataRc};
39use crate::tasks::{delete_expr, save_expr, update_expr};
40use crate::utils::PtrEqRc;
41use crate::*;
42
43#[derive(Clone, Derivative, Properties)]
44#[derivative(Debug)]
45pub struct ColumnSettingsPanelProps {
46    pub selected_column: ColumnLocator,
47    pub selected_tab: Option<ColumnSettingsTab>,
48    pub on_close: Callback<()>,
49    pub width_override: Option<i32>,
50    pub on_select_tab: Callback<ColumnSettingsTab>,
51
52    /// Active plugin name threaded as a value prop so that plugin changes
53    /// trigger re-initialization via `changed()` rather than a PubSub
54    /// `render_limits_changed` subscription.
55    pub plugin_name: Option<String>,
56
57    /// Session metadata snapshot — threaded from `SessionProps`.
58    pub metadata: SessionMetadataRc,
59
60    /// View config snapshot — threaded from `SessionProps`.
61    pub view_config: PtrEqRc<ViewConfig>,
62
63    /// Per-column stats snapshot — threaded from `SessionProps`.
64    pub column_stats: PtrEqRc<std::collections::HashMap<String, crate::session::ColumnStats>>,
65
66    /// Selected theme name, threaded for PortalModal consumers.
67    pub selected_theme: Option<String>,
68
69    // State
70    #[derivative(Debug = "ignore")]
71    pub presentation: Presentation,
72
73    #[derivative(Debug = "ignore")]
74    pub renderer: Renderer,
75
76    #[derivative(Debug = "ignore")]
77    pub session: Session,
78}
79
80impl PartialEq for ColumnSettingsPanelProps {
81    fn eq(&self, other: &Self) -> bool {
82        self.selected_column == other.selected_column
83            && self.selected_tab == other.selected_tab
84            && self.plugin_name == other.plugin_name
85            && self.metadata == other.metadata
86            && self.view_config == other.view_config
87            && self.column_stats == other.column_stats
88            && self.selected_theme == other.selected_theme
89    }
90}
91
92#[derive(Debug)]
93pub enum ColumnSettingsPanelMsg {
94    SetExprValue(Rc<String>),
95    SetExprValid(bool),
96    SetHeaderValue(Option<String>),
97    SetHeaderValid(bool),
98    SetSelectedTab((usize, ColumnSettingsTab)),
99    OnSaveAttributes(()),
100    OnResetAttributes(()),
101    OnDelete(()),
102}
103
104#[derive(Derivative)]
105#[derivative(Debug)]
106pub struct ColumnSettingsPanel {
107    column_name: String,
108    expr_valid: bool,
109    expr_value: Rc<String>,
110    header_valid: bool,
111    header_value: Option<String>,
112    initial_expr_value: Rc<String>,
113    initial_header_value: Option<String>,
114    maybe_ty: Option<ColumnType>,
115    on_input: Callback<Rc<String>>,
116    on_save: Callback<()>,
117    on_validate: Callback<bool>,
118    reset_count: u8,
119    reset_enabled: bool,
120    save_count: u8,
121    save_enabled: bool,
122    tabs: Vec<ColumnSettingsTab>,
123}
124
125impl Component for ColumnSettingsPanel {
126    type Message = ColumnSettingsPanelMsg;
127    type Properties = ColumnSettingsPanelProps;
128
129    fn create(ctx: &yew::prelude::Context<Self>) -> Self {
130        let mut this = Self {
131            initial_expr_value: Rc::default(),
132            expr_value: Rc::default(),
133            expr_valid: false,
134            initial_header_value: None,
135            header_value: None,
136            header_valid: false,
137            save_enabled: false,
138            save_count: 0,
139            reset_enabled: false,
140            reset_count: 0,
141            column_name: "".to_owned(),
142            maybe_ty: None,
143            tabs: vec![],
144            on_input: Callback::default(),
145            on_save: Callback::default(),
146            on_validate: Callback::default(),
147        };
148
149        this.initialize(ctx);
150        this
151    }
152
153    fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
154        if ctx.props() != old_props {
155            self.initialize(ctx);
156            true
157        } else {
158            false
159        }
160    }
161
162    fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
163        match msg {
164            ColumnSettingsPanelMsg::SetExprValue(val) => {
165                if self.expr_value != val {
166                    self.expr_value = val;
167                    self.reset_enabled = true;
168                    true
169                } else {
170                    false
171                }
172            },
173            ColumnSettingsPanelMsg::SetExprValid(val) => {
174                self.expr_valid = val;
175                self.save_enabled_effect();
176                true
177            },
178            ColumnSettingsPanelMsg::SetHeaderValue(val) => {
179                if self.header_value != val {
180                    self.header_value = val;
181                    self.reset_enabled = true;
182                    true
183                } else {
184                    false
185                }
186            },
187            ColumnSettingsPanelMsg::SetHeaderValid(val) => {
188                self.header_valid = val;
189                self.save_enabled_effect();
190                true
191            },
192            ColumnSettingsPanelMsg::SetSelectedTab((_, val)) => {
193                let rerender = ctx.props().selected_tab != Some(val);
194                ctx.props().on_select_tab.emit(val);
195                rerender
196            },
197            ColumnSettingsPanelMsg::OnResetAttributes(()) => {
198                self.header_value.clone_from(&self.initial_header_value);
199                self.expr_value.clone_from(&self.initial_expr_value);
200                self.save_enabled = false;
201                self.reset_enabled = false;
202                self.reset_count += 1;
203                true
204            },
205            ColumnSettingsPanelMsg::OnSaveAttributes(()) => {
206                let new_expr = Expression::new(
207                    self.header_value.clone().map(|s| s.into()),
208                    (*(self.expr_value)).clone().into(),
209                );
210
211                match &ctx.props().selected_column {
212                    ColumnLocator::Table(_) => {
213                        tracing::error!("Tried to save non-expression column!")
214                    },
215                    ColumnLocator::Expression(name) => update_expr(
216                        &ctx.props().session,
217                        &ctx.props().renderer,
218                        &ctx.props().presentation,
219                        name.clone(),
220                        new_expr,
221                    ),
222                    ColumnLocator::NewExpression => {
223                        if let Err(err) = save_expr(
224                            &ctx.props().session,
225                            &ctx.props().renderer,
226                            &ctx.props().presentation,
227                            new_expr,
228                        ) {
229                            tracing::warn!("{}", err);
230                        }
231                    },
232                }
233
234                self.initial_expr_value.clone_from(&self.expr_value);
235                self.initial_header_value.clone_from(&self.header_value);
236                self.save_enabled = false;
237                self.reset_enabled = false;
238                self.save_count += 1;
239                true
240            },
241            ColumnSettingsPanelMsg::OnDelete(()) => {
242                if ctx.props().selected_column.is_saved_expr() {
243                    delete_expr(
244                        &ctx.props().session,
245                        &ctx.props().renderer,
246                        &self.column_name,
247                    )
248                    .unwrap_or_log();
249                }
250
251                ctx.props().on_close.emit(());
252                true
253            },
254        }
255    }
256
257    fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
258        let header_props = props!(EditableHeaderProps {
259            initial_value: self.initial_header_value.clone(),
260            placeholder: self.expr_value.clone(),
261            reset_count: self.reset_count,
262            editable: ctx.props().selected_column.is_expr()
263                && matches!(
264                    ctx.props().selected_tab,
265                    Some(ColumnSettingsTab::Attributes)
266                ),
267            icon_type: self
268                .maybe_ty
269                .map(|ty| ty.into())
270                .or(Some(TypeIconType::Expr)),
271            on_change: ctx.link().batch_callback(|(value, valid)| {
272                vec![
273                    ColumnSettingsPanelMsg::SetHeaderValue(value),
274                    ColumnSettingsPanelMsg::SetHeaderValid(valid),
275                ]
276            }),
277            metadata: ctx.props().metadata.clone(),
278            session: &ctx.props().session
279        });
280
281        let expr_editor = props!(ExpressionEditorProps {
282            on_input: self.on_input.clone(),
283            on_save: self.on_save.clone(),
284            on_validate: self.on_validate.clone(),
285            alias: ctx.props().selected_column.name().cloned(),
286            disabled: !ctx.props().selected_column.is_expr(),
287            reset_count: self.reset_count,
288            metadata: ctx.props().metadata.clone(),
289            selected_theme: ctx.props().selected_theme.clone(),
290            session: &ctx.props().session
291        });
292
293        let disable_delete = ctx
294            .props()
295            .selected_column
296            .name()
297            .map(|name| {
298                let config = &ctx.props().view_config;
299                config.columns.iter().any(|maybe_col| {
300                    maybe_col
301                        .as_ref()
302                        .map(|col| col == name)
303                        .unwrap_or_default()
304                }) || config.group_by.iter().any(|col| col == name)
305                    || config.split_by.iter().any(|col| col == name)
306                    || config.filter.iter().any(|col| col.column() == name)
307                    || config.sort.iter().any(|col| &col.0 == name)
308            })
309            .unwrap_or_default();
310
311        let save_section = SaveSettingsProps {
312            save_enabled: self.save_enabled,
313            reset_enabled: self.reset_enabled,
314            is_save: ctx.props().selected_column.name().is_some(),
315            on_reset: ctx
316                .link()
317                .callback(ColumnSettingsPanelMsg::OnResetAttributes),
318            on_save: ctx
319                .link()
320                .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
321            on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
322            show_danger_zone: ctx.props().selected_column.is_saved_expr(),
323            disable_delete,
324        };
325
326        let attrs_tab = AttributesTabProps {
327            expr_editor,
328            save_section,
329        };
330
331        let style_tab = StyleTabProps {
332            ty: self.maybe_ty,
333            column_name: self.column_name.clone(),
334            group_by_depth: ctx.props().view_config.group_by.len() as u32,
335            view_config: ctx.props().view_config.clone(),
336            metadata: ctx.props().metadata.clone(),
337            column_stats: ctx.props().column_stats.clone(),
338            selected_theme: ctx.props().selected_theme.clone(),
339            presentation: ctx.props().presentation.clone(),
340            renderer: ctx.props().renderer.clone(),
341            session: ctx.props().session.clone(),
342        };
343
344        let tab_children = self.tabs.iter().map(|tab| match tab {
345            ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
346            ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
347        });
348
349        let selected_tab_idx = self
350            .tabs
351            .iter()
352            .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
353            .map(|(idx, _val)| idx)
354            .unwrap_or_default();
355
356        html! {
357            <>
358                <LocalStyle href={css!("column-settings-panel")} />
359                <Sidebar
360                    on_close={ctx.props().on_close.clone()}
361                    id_prefix="column_settings"
362                    width_override={ctx.props().width_override}
363                    selected_tab={selected_tab_idx}
364                    {header_props}
365                >
366                    <TabList<ColumnSettingsTab>
367                        tabs={self.tabs.clone()}
368                        on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
369                        selected_tab={selected_tab_idx}
370                    >
371                        { for tab_children }
372                    </TabList<ColumnSettingsTab>>
373                </Sidebar>
374            </>
375        }
376    }
377}
378
379impl ColumnSettingsPanel {
380    fn save_enabled_effect(&mut self) {
381        let changed = self.expr_value != self.initial_expr_value
382            || self.header_value != self.initial_header_value;
383        let valid = self.expr_valid && self.header_valid;
384        self.save_enabled = changed && valid;
385    }
386
387    fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
388        let column_name = ctx
389            .props()
390            .metadata
391            .locator_name_or_default(&ctx.props().selected_column);
392
393        let initial_expr_value = ctx
394            .props()
395            .metadata
396            .get_expression_by_alias(&column_name)
397            .unwrap_or_default();
398
399        let initial_expr_value = Rc::new(initial_expr_value);
400        let initial_header_value =
401            (*initial_expr_value != column_name).then_some(column_name.clone());
402
403        let maybe_ty = ctx
404            .props()
405            .metadata
406            .locator_view_type(&ctx.props().selected_column);
407
408        let tabs = {
409            let mut tabs = vec![];
410            let is_new_expr = ctx.props().selected_column.is_new_expr();
411            let show_styles = !is_new_expr
412                && ctx.props().renderer.can_render_column_styles()
413                && ctx.props().view_config.columns.contains(&Some(
414                    ctx.props()
415                        .selected_column
416                        .name()
417                        .map(|x| x.to_string())
418                        .unwrap_or_default(),
419                ));
420
421            if !is_new_expr && show_styles {
422                tabs.push(ColumnSettingsTab::Style);
423            }
424
425            if ctx.props().selected_column.is_expr() {
426                tabs.push(ColumnSettingsTab::Attributes);
427            }
428
429            tabs
430        };
431
432        let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
433        let on_save = ctx
434            .link()
435            .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
436
437        let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
438        *self = Self {
439            column_name,
440            expr_value: initial_expr_value.clone(),
441            initial_expr_value,
442            header_value: initial_header_value.clone(),
443            initial_header_value,
444            maybe_ty,
445            tabs,
446            header_valid: true,
447            on_input,
448            on_save,
449            on_validate,
450            ..*self
451        }
452    }
453}