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