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