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