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        let result = 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        result
247    }
248
249    fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
250        let header_props = props!(EditableHeaderProps {
251            initial_value: self.initial_header_value.clone(),
252            placeholder: self.expr_value.clone(),
253            reset_count: self.reset_count,
254            editable: ctx.props().selected_column.is_expr()
255                && matches!(
256                    ctx.props().selected_tab,
257                    Some(ColumnSettingsTab::Attributes)
258                ),
259            icon_type: self
260                .maybe_ty
261                .map(|ty| ty.into())
262                .or(Some(TypeIconType::Expr)),
263            on_change: ctx.link().batch_callback(|(value, valid)| {
264                vec![
265                    ColumnSettingsPanelMsg::SetHeaderValue(value),
266                    ColumnSettingsPanelMsg::SetHeaderValid(valid),
267                ]
268            }),
269            session: &ctx.props().session
270        });
271
272        let expr_editor = props!(ExpressionEditorProps {
273            on_input: self.on_input.clone(),
274            on_save: self.on_save.clone(),
275            on_validate: self.on_validate.clone(),
276            alias: ctx.props().selected_column.name().cloned(),
277            disabled: !ctx.props().selected_column.is_expr(),
278            reset_count: self.reset_count,
279            session: &ctx.props().session
280        });
281
282        let disable_delete = ctx
283            .props()
284            .session
285            .is_locator_active(&ctx.props().selected_column);
286
287        let save_section = SaveSettingsProps {
288            save_enabled: self.save_enabled,
289            reset_enabled: self.reset_enabled,
290            is_save: ctx.props().selected_column.name().is_some(),
291            on_reset: ctx
292                .link()
293                .callback(ColumnSettingsPanelMsg::OnResetAttributes),
294            on_save: ctx
295                .link()
296                .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
297            on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
298            show_danger_zone: ctx.props().selected_column.is_saved_expr(),
299            disable_delete,
300        };
301
302        let attrs_tab = AttributesTabProps {
303            expr_editor,
304            save_section,
305        };
306
307        let style_tab = props!(StyleTabProps {
308            ty: self.maybe_ty,
309            column_name: self.column_name.clone(),
310            group_by_depth: ctx.props().session.get_view_config().group_by.len() as u32,
311            custom_events: ctx.props().custom_events(),
312            presentation: ctx.props().presentation(),
313            renderer: ctx.props().renderer(),
314            session: ctx.props().session()
315        });
316
317        let tab_children = self.tabs.iter().map(|tab| match tab {
318            ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
319            ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
320        });
321
322        let selected_tab_idx = self
323            .tabs
324            .iter()
325            .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
326            .map(|(idx, _val)| idx)
327            .unwrap_or_default();
328
329        html! {
330            <>
331                <LocalStyle href={css!("column-settings-panel")} />
332                <Sidebar
333                    on_close={ctx.props().on_close.clone()}
334                    id_prefix="column_settings"
335                    width_override={ctx.props().width_override}
336                    selected_tab={selected_tab_idx}
337                    {header_props}
338                >
339                    <TabList<ColumnSettingsTab>
340                        tabs={self.tabs.clone()}
341                        on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
342                        selected_tab={selected_tab_idx}
343                    >
344                        { for tab_children }
345                    </TabList<ColumnSettingsTab>>
346                </Sidebar>
347            </>
348        }
349    }
350}
351
352impl ColumnSettingsPanel {
353    fn save_enabled_effect(&mut self) {
354        let changed = self.expr_value != self.initial_expr_value
355            || self.header_value != self.initial_header_value;
356        let valid = self.expr_valid && self.header_valid;
357        self.save_enabled = changed && valid;
358    }
359
360    fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
361        let column_name = ctx
362            .props()
363            .session
364            .locator_name_or_default(&ctx.props().selected_column);
365
366        let initial_expr_value = ctx
367            .props()
368            .session
369            .metadata()
370            .get_expression_by_alias(&column_name)
371            .unwrap_or_default();
372
373        let initial_expr_value = Rc::new(initial_expr_value);
374        let initial_header_value =
375            (*initial_expr_value != column_name).then_some(column_name.clone());
376
377        let maybe_ty = ctx
378            .props()
379            .session()
380            .locator_view_type(&ctx.props().selected_column);
381
382        let tabs = {
383            let mut tabs = vec![];
384            let is_new_expr = ctx.props().selected_column.is_new_expr();
385            let show_styles = !is_new_expr
386                && ctx
387                    .props()
388                    .can_render_column_styles(&column_name)
389                    .unwrap_or_default();
390
391            if !is_new_expr && show_styles {
392                tabs.push(ColumnSettingsTab::Style);
393            }
394
395            if ctx.props().selected_column.is_expr() {
396                tabs.push(ColumnSettingsTab::Attributes);
397            }
398            tabs
399        };
400
401        let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
402        let on_save = ctx
403            .link()
404            .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
405
406        let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
407        *self = Self {
408            column_name,
409            expr_value: initial_expr_value.clone(),
410            initial_expr_value,
411            header_value: initial_header_value.clone(),
412            initial_header_value,
413            maybe_ty,
414            tabs,
415            header_valid: true,
416            on_input,
417            on_save,
418            on_validate,
419            _session_sub: self._session_sub.take(),
420            ..*self
421        }
422    }
423}