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