perspective_viewer/components/column_settings_sidebar/
sidebar.rs1use 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}