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            update_on_input: true,
268            icon_type: self
269                .maybe_ty
270                .map(|ty| ty.into())
271                .or(Some(TypeIconType::Expr)),
272            on_change: ctx.link().batch_callback(|(value, valid)| {
273                vec![
274                    ColumnSettingsPanelMsg::SetHeaderValue(value),
275                    ColumnSettingsPanelMsg::SetHeaderValid(valid),
276                ]
277            }),
278            metadata: ctx.props().metadata.clone(),
279            session: &ctx.props().session
280        });
281
282        let expr_editor = props!(ExpressionEditorProps {
283            on_input: self.on_input.clone(),
284            on_save: self.on_save.clone(),
285            on_validate: self.on_validate.clone(),
286            alias: ctx.props().selected_column.name().cloned(),
287            disabled: !ctx.props().selected_column.is_expr(),
288            reset_count: self.reset_count,
289            metadata: ctx.props().metadata.clone(),
290            selected_theme: ctx.props().selected_theme.clone(),
291            session: &ctx.props().session
292        });
293
294        let disable_delete = ctx
295            .props()
296            .selected_column
297            .name()
298            .map(|name| {
299                let config = &ctx.props().view_config;
300                config.columns.iter().any(|maybe_col| {
301                    maybe_col
302                        .as_ref()
303                        .map(|col| col == name)
304                        .unwrap_or_default()
305                }) || config.group_by.iter().any(|col| col == name)
306                    || config.split_by.iter().any(|col| col == name)
307                    || config.filter.iter().any(|col| col.column() == name)
308                    || config.sort.iter().any(|col| &col.0 == name)
309            })
310            .unwrap_or_default();
311
312        let save_section = SaveSettingsProps {
313            save_enabled: self.save_enabled,
314            reset_enabled: self.reset_enabled,
315            is_save: ctx.props().selected_column.name().is_some(),
316            on_reset: ctx
317                .link()
318                .callback(ColumnSettingsPanelMsg::OnResetAttributes),
319            on_save: ctx
320                .link()
321                .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
322            on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
323            show_danger_zone: ctx.props().selected_column.is_saved_expr(),
324            disable_delete,
325        };
326
327        let attrs_tab = AttributesTabProps {
328            expr_editor,
329            save_section,
330        };
331
332        let style_tab = StyleTabProps {
333            ty: self.maybe_ty,
334            column_name: self.column_name.clone(),
335            group_by_depth: ctx.props().view_config.group_by.len() as u32,
336            view_config: ctx.props().view_config.clone(),
337            metadata: ctx.props().metadata.clone(),
338            column_stats: ctx.props().column_stats.clone(),
339            selected_theme: ctx.props().selected_theme.clone(),
340            presentation: ctx.props().presentation.clone(),
341            renderer: ctx.props().renderer.clone(),
342            session: ctx.props().session.clone(),
343        };
344
345        let tab_children = self.tabs.iter().map(|tab| match tab {
346            ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
347            ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
348        });
349
350        let selected_tab_idx = self
351            .tabs
352            .iter()
353            .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
354            .map(|(idx, _val)| idx)
355            .unwrap_or_default();
356
357        html! {
358            <>
359                <LocalStyle href={css!("column-settings-panel")} />
360                <Sidebar
361                    on_close={ctx.props().on_close.clone()}
362                    id_prefix="column_settings"
363                    width_override={ctx.props().width_override}
364                    selected_tab={selected_tab_idx}
365                    {header_props}
366                >
367                    <TabList<ColumnSettingsTab>
368                        tabs={self.tabs.clone()}
369                        on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
370                        selected_tab={selected_tab_idx}
371                    >
372                        { for tab_children }
373                    </TabList<ColumnSettingsTab>>
374                </Sidebar>
375            </>
376        }
377    }
378}
379
380impl ColumnSettingsPanel {
381    fn save_enabled_effect(&mut self) {
382        let changed = self.expr_value != self.initial_expr_value
383            || self.header_value != self.initial_header_value;
384        let valid = self.expr_valid && self.header_valid;
385        self.save_enabled = changed && valid;
386    }
387
388    fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
389        let column_name = ctx
390            .props()
391            .metadata
392            .locator_name_or_default(&ctx.props().selected_column);
393
394        let initial_expr_value = ctx
395            .props()
396            .metadata
397            .get_expression_by_alias(&column_name)
398            .unwrap_or_default();
399
400        let initial_expr_value = Rc::new(initial_expr_value);
401        let initial_header_value =
402            (*initial_expr_value != column_name).then_some(column_name.clone());
403
404        let maybe_ty = ctx
405            .props()
406            .metadata
407            .locator_view_type(&ctx.props().selected_column);
408
409        let tabs = {
410            let mut tabs = vec![];
411            let is_new_expr = ctx.props().selected_column.is_new_expr();
412            let show_styles = !is_new_expr
413                && ctx.props().renderer.can_render_column_styles()
414                && ctx.props().view_config.columns.contains(&Some(
415                    ctx.props()
416                        .selected_column
417                        .name()
418                        .map(|x| x.to_string())
419                        .unwrap_or_default(),
420                ));
421
422            if !is_new_expr && show_styles {
423                tabs.push(ColumnSettingsTab::Style);
424            }
425
426            if ctx.props().selected_column.is_expr() {
427                tabs.push(ColumnSettingsTab::Attributes);
428            }
429
430            tabs
431        };
432
433        let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
434        let on_save = ctx
435            .link()
436            .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
437
438        let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
439        *self = Self {
440            column_name,
441            expr_value: initial_expr_value.clone(),
442            initial_expr_value,
443            header_value: initial_header_value.clone(),
444            initial_header_value,
445            maybe_ty,
446            tabs,
447            header_valid: true,
448            on_input,
449            on_save,
450            on_validate,
451            ..*self
452        }
453    }
454}