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