1mod attributes_tab;
13
14mod save_settings;
15pub(crate) mod style_tab;
16mod window_tab;
17
18use std::rc::Rc;
19
20use derivative::Derivative;
21use itertools::Itertools;
22use perspective_client::config::{ColumnType, Expression, ViewConfig, WindowSpec};
23use perspective_client::utils::PerspectiveResultExt;
24use yew::{Callback, Component, Html, Properties, html, props};
25
26use self::attributes_tab::AttributesTabProps;
27use self::style_tab::StyleTabProps;
28use crate::components::column_settings_sidebar::attributes_tab::AttributesTab;
29use crate::components::column_settings_sidebar::save_settings::SaveSettingsProps;
30use crate::components::column_settings_sidebar::style_tab::StyleTab;
31use crate::components::column_settings_sidebar::window_tab::{WindowTab, WindowTabProps};
32use crate::components::containers::sidebar::Sidebar;
33use crate::components::containers::tab_list::TabList;
34use crate::components::editable_header::EditableHeaderProps;
35use crate::components::expression_editor::ExpressionEditorProps;
36use crate::components::type_icon::TypeIconType;
37use crate::components::window_editor::WindowEditorProps;
38use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
39use crate::renderer::Renderer;
40use crate::session::{Session, SessionMetadataRc};
41use crate::tasks::{
42 delete_expr, delete_window, save_expr, save_window, update_expr, update_window,
43};
44use crate::utils::PtrEqRc;
45
46#[derive(Clone, Derivative, Properties)]
47#[derivative(Debug)]
48pub struct ColumnSettingsPanelProps {
49 pub selected_column: ColumnLocator,
50 pub selected_tab: Option<ColumnSettingsTab>,
51 pub on_close: Callback<()>,
52 pub width_override: Option<i32>,
53 pub on_select_tab: Callback<ColumnSettingsTab>,
54
55 #[prop_or_default]
58 pub auto_width: f64,
59
60 #[prop_or_default]
61 pub on_auto_width: Callback<f64>,
62
63 #[prop_or_default]
65 pub is_pinned: bool,
66
67 #[prop_or_default]
68 pub on_toggle_pin: Callback<()>,
69
70 pub plugin_name: Option<String>,
72
73 pub metadata: SessionMetadataRc,
75
76 pub view_config: PtrEqRc<ViewConfig>,
78
79 pub column_stats: PtrEqRc<std::collections::HashMap<String, crate::session::ColumnStats>>,
81
82 pub selected_theme: Option<String>,
84
85 #[derivative(Debug = "ignore")]
87 pub presentation: Presentation,
88
89 #[derivative(Debug = "ignore")]
90 pub renderer: Renderer,
91
92 #[derivative(Debug = "ignore")]
93 pub session: Session,
94}
95
96impl ColumnSettingsPanelProps {
97 fn identity_eq(&self, other: &Self) -> bool {
100 self.selected_column == other.selected_column
101 && self.selected_tab == other.selected_tab
102 && self.plugin_name == other.plugin_name
103 && self.metadata == other.metadata
104 && self.view_config == other.view_config
105 && self.column_stats == other.column_stats
106 && self.selected_theme == other.selected_theme
107 }
108}
109
110impl PartialEq for ColumnSettingsPanelProps {
111 fn eq(&self, other: &Self) -> bool {
112 self.identity_eq(other)
113 && self.auto_width == other.auto_width
114 && self.is_pinned == other.is_pinned
115 }
116}
117
118#[derive(Debug)]
119pub enum ColumnSettingsPanelMsg {
120 SetExprValue(Rc<String>),
121 SetExprValid(bool),
122 SetHeaderValue(Option<String>),
123 SetHeaderValid(bool),
124 SetWindowValue(Option<WindowSpec>),
125 SetSelectedTab((usize, ColumnSettingsTab)),
126 OnSaveAttributes(()),
127 OnResetAttributes(()),
128 OnDelete(()),
129}
130
131#[derive(Derivative)]
132#[derivative(Debug)]
133pub struct ColumnSettingsPanel {
134 column_name: String,
135 expr_valid: bool,
136 expr_value: Rc<String>,
137 header_valid: bool,
138 header_value: Option<String>,
139 initial_expr_value: Rc<String>,
140 initial_header_value: Option<String>,
141 maybe_ty: Option<ColumnType>,
142 on_input: Callback<Rc<String>>,
143 on_save: Callback<()>,
144 on_validate: Callback<bool>,
145 reset_count: u8,
146 reset_enabled: bool,
147 save_count: u8,
148 save_enabled: bool,
149 initial_window_value: Option<WindowSpec>,
150 window_value: Option<WindowSpec>,
151 tabs: Vec<ColumnSettingsTab>,
152}
153
154impl Component for ColumnSettingsPanel {
155 type Message = ColumnSettingsPanelMsg;
156 type Properties = ColumnSettingsPanelProps;
157
158 fn create(ctx: &yew::prelude::Context<Self>) -> Self {
159 let mut this = Self {
160 initial_expr_value: Rc::default(),
161 expr_value: Rc::default(),
162 expr_valid: false,
163 initial_header_value: None,
164 header_value: None,
165 header_valid: false,
166 save_enabled: false,
167 save_count: 0,
168 reset_enabled: false,
169 reset_count: 0,
170 column_name: "".to_owned(),
171 maybe_ty: None,
172 initial_window_value: None,
173 window_value: None,
174 tabs: vec![],
175 on_input: Callback::default(),
176 on_save: Callback::default(),
177 on_validate: Callback::default(),
178 };
179
180 this.initialize(ctx);
181 this
182 }
183
184 fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
185 if !ctx.props().identity_eq(old_props) {
190 self.initialize(ctx);
191 }
192
193 true
194 }
195
196 fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
197 match msg {
198 ColumnSettingsPanelMsg::SetExprValue(val) => {
199 if self.expr_value != val {
200 self.expr_value = val;
201 self.reset_enabled = true;
202 true
203 } else {
204 false
205 }
206 },
207 ColumnSettingsPanelMsg::SetExprValid(val) => {
208 self.expr_valid = val;
209 self.save_enabled_effect();
210 true
211 },
212 ColumnSettingsPanelMsg::SetHeaderValue(val) => {
213 if self.header_value != val {
214 self.header_value = val;
215 self.reset_enabled = true;
216 true
217 } else {
218 false
219 }
220 },
221 ColumnSettingsPanelMsg::SetHeaderValid(val) => {
222 self.header_valid = val;
223 self.save_enabled_effect();
224 true
225 },
226 ColumnSettingsPanelMsg::SetWindowValue(val) => {
227 if self.window_value != val {
228 self.window_value = val;
229 self.reset_enabled = true;
230 true
231 } else {
232 false
233 }
234 },
235 ColumnSettingsPanelMsg::SetSelectedTab((_, val)) => {
236 let rerender = ctx.props().selected_tab != Some(val);
237 ctx.props().on_select_tab.emit(val);
238 rerender
239 },
240 ColumnSettingsPanelMsg::OnResetAttributes(()) => {
241 self.header_value.clone_from(&self.initial_header_value);
242 self.expr_value.clone_from(&self.initial_expr_value);
243 self.window_value.clone_from(&self.initial_window_value);
244 self.save_enabled = false;
245 self.reset_enabled = false;
246 self.reset_count += 1;
247 true
248 },
249 ColumnSettingsPanelMsg::OnSaveAttributes(()) => {
250 if matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window)) {
251 if let Some(spec) = self.window_value.clone() {
252 let name = self
253 .header_value
254 .clone()
255 .unwrap_or_else(|| self.column_name.clone());
256 match &ctx.props().selected_column {
257 ColumnLocator::Window(old_name) => update_window(
258 &ctx.props().session,
259 &ctx.props().renderer,
260 &ctx.props().presentation,
261 old_name.clone(),
262 name,
263 spec.clone(),
264 ),
265 _ => {
266 if let Err(err) = save_window(
267 &ctx.props().session,
268 &ctx.props().renderer,
269 &ctx.props().presentation,
270 name,
271 spec.clone(),
272 ) {
273 tracing::warn!("{}", err);
274 }
275 },
276 }
277
278 self.initial_window_value = Some(spec);
279 self.initial_header_value.clone_from(&self.header_value);
280 self.save_enabled = false;
281 self.reset_enabled = false;
282 self.save_count += 1;
283 }
284
285 return true;
286 }
287
288 let new_expr = Expression::new(
289 self.header_value.clone().map(|s| s.into()),
290 (*(self.expr_value)).clone().into(),
291 );
292
293 match &ctx.props().selected_column {
294 ColumnLocator::Table(_) | ColumnLocator::Window(_) => {
295 tracing::error!("Tried to save non-expression column!")
296 },
297 ColumnLocator::Expression(name) => update_expr(
298 &ctx.props().session,
299 &ctx.props().renderer,
300 &ctx.props().presentation,
301 name.clone(),
302 new_expr,
303 ),
304 ColumnLocator::NewExpression => {
305 if let Err(err) = save_expr(
306 &ctx.props().session,
307 &ctx.props().renderer,
308 &ctx.props().presentation,
309 new_expr,
310 ) {
311 tracing::warn!("{}", err);
312 }
313 },
314 }
315
316 self.initial_expr_value.clone_from(&self.expr_value);
317 self.initial_header_value.clone_from(&self.header_value);
318 self.save_enabled = false;
319 self.reset_enabled = false;
320 self.save_count += 1;
321 true
322 },
323 ColumnSettingsPanelMsg::OnDelete(()) => {
324 if ctx.props().selected_column.is_saved_expr() {
325 delete_expr(
326 &ctx.props().session,
327 &ctx.props().renderer,
328 &self.column_name,
329 )
330 .unwrap_or_log();
331 } else if ctx.props().selected_column.is_saved_window() {
332 delete_window(
333 &ctx.props().session,
334 &ctx.props().renderer,
335 &self.column_name,
336 )
337 .unwrap_or_log();
338 }
339
340 ctx.props().on_close.emit(());
341 true
342 },
343 }
344 }
345
346 fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
347 let is_window_tab = matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window));
348
349 let header_placeholder = if is_window_tab {
350 Rc::new(self.column_name.clone())
351 } else {
352 self.expr_value.clone()
353 };
354
355 let header_props = props!(EditableHeaderProps {
356 initial_value: self.initial_header_value.clone(),
357 placeholder: header_placeholder,
358 reset_count: self.reset_count,
359 editable: (ctx.props().selected_column.is_expr()
360 && matches!(
361 ctx.props().selected_tab,
362 Some(ColumnSettingsTab::Attributes)
363 ))
364 || (ctx.props().selected_column.is_window_editable() && is_window_tab),
365 update_on_input: true,
366 icon_type: self
367 .maybe_ty
368 .map(|ty| ty.into())
369 .or(Some(TypeIconType::Expr)),
370 on_change: ctx.link().batch_callback(|(value, valid)| {
371 vec![
372 ColumnSettingsPanelMsg::SetHeaderValue(value),
373 ColumnSettingsPanelMsg::SetHeaderValid(valid),
374 ]
375 }),
376 metadata: ctx.props().metadata.clone(),
377 session: &ctx.props().session
378 });
379
380 let expr_editor = props!(ExpressionEditorProps {
381 on_input: self.on_input.clone(),
382 on_save: self.on_save.clone(),
383 on_validate: self.on_validate.clone(),
384 alias: ctx.props().selected_column.name().cloned(),
385 disabled: !ctx.props().selected_column.is_expr(),
386 reset_count: self.reset_count,
387 metadata: ctx.props().metadata.clone(),
388 selected_theme: ctx.props().selected_theme.clone(),
389 session: &ctx.props().session
390 });
391
392 let disable_delete = ctx
393 .props()
394 .selected_column
395 .name()
396 .map(|name| {
397 let config = &ctx.props().view_config;
398 config.columns.iter().any(|maybe_col| {
399 maybe_col
400 .as_ref()
401 .map(|col| col == name)
402 .unwrap_or_default()
403 }) || config.group_by.iter().any(|col| col == name)
404 || config.split_by.iter().any(|col| col == name)
405 || config.filter.iter().any(|col| col.column() == name)
406 || config.sort.iter().any(|col| &col.0 == name)
407 })
408 .unwrap_or_default();
409
410 let save_section = SaveSettingsProps {
411 save_enabled: self.save_enabled,
412 reset_enabled: self.reset_enabled,
413 is_save: ctx.props().selected_column.name().is_some(),
414 on_reset: ctx
415 .link()
416 .callback(ColumnSettingsPanelMsg::OnResetAttributes),
417 on_save: ctx
418 .link()
419 .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
420 on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
421 show_danger_zone: ctx.props().selected_column.is_saved_expr(),
422 disable_delete,
423 };
424
425 let attrs_tab = AttributesTabProps {
426 expr_editor,
427 save_section: save_section.clone(),
428 };
429
430 let window_changed = self.window_value != self.initial_window_value
431 || self.header_value != self.initial_header_value;
432 let window_tab = WindowTabProps {
433 editor: WindowEditorProps {
434 metadata: ctx.props().metadata.clone(),
435 initial: self.initial_window_value.clone(),
436 on_change: ctx.link().callback(ColumnSettingsPanelMsg::SetWindowValue),
437 reset_count: self.reset_count,
438 presentation: ctx.props().presentation.clone(),
439 session: ctx.props().session.clone(),
440 selected_theme: ctx.props().selected_theme.clone(),
441 },
442 save_section: SaveSettingsProps {
443 save_enabled: self.window_value.is_some() && window_changed && self.header_valid,
444 show_danger_zone: ctx.props().selected_column.is_saved_window(),
445 ..save_section
446 },
447 };
448
449 let style_tab = StyleTabProps {
450 ty: self.maybe_ty,
451 column_name: self.column_name.clone(),
452 group_by_depth: ctx.props().view_config.group_by.len() as u32,
453 view_config: ctx.props().view_config.clone(),
454 metadata: ctx.props().metadata.clone(),
455 column_stats: ctx.props().column_stats.clone(),
456 selected_theme: ctx.props().selected_theme.clone(),
457 presentation: ctx.props().presentation.clone(),
458 renderer: ctx.props().renderer.clone(),
459 session: ctx.props().session.clone(),
460 };
461
462 let tab_children = self.tabs.iter().map(|tab| match tab {
463 ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
464 ColumnSettingsTab::Window => html! { <WindowTab ..window_tab.clone() /> },
465 ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
466 });
467
468 let selected_tab_idx = self
469 .tabs
470 .iter()
471 .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
472 .map(|(idx, _val)| idx)
473 .unwrap_or_default();
474
475 html! {
476 <>
477 <Sidebar
478 on_close={ctx.props().on_close.clone()}
479 id_prefix="column_settings"
480 width_override={ctx.props().width_override}
481 auto_width={ctx.props().auto_width}
482 on_auto_width={ctx.props().on_auto_width.clone()}
483 is_pinned={ctx.props().is_pinned}
484 on_toggle_pin={Some(ctx.props().on_toggle_pin.clone())}
485 selected_tab={selected_tab_idx}
486 {header_props}
487 >
488 <TabList<ColumnSettingsTab>
489 tabs={self.tabs.clone()}
490 on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
491 selected_tab={selected_tab_idx}
492 >
493 { for tab_children }
494 </TabList<ColumnSettingsTab>>
495 </Sidebar>
496 </>
497 }
498 }
499}
500
501impl ColumnSettingsPanel {
502 fn save_enabled_effect(&mut self) {
503 let changed = self.expr_value != self.initial_expr_value
504 || self.header_value != self.initial_header_value;
505 let valid = self.expr_valid && self.header_valid;
506 self.save_enabled = changed && valid;
507 }
508
509 fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
510 let column_name = ctx
511 .props()
512 .metadata
513 .locator_name_or_default(&ctx.props().selected_column);
514
515 let initial_expr_value = ctx
516 .props()
517 .metadata
518 .get_expression_by_alias(&column_name)
519 .unwrap_or_default();
520
521 let initial_expr_value = Rc::new(initial_expr_value);
522 let initial_header_value =
523 (*initial_expr_value != column_name).then_some(column_name.clone());
524
525 let maybe_ty = ctx
526 .props()
527 .metadata
528 .locator_view_type(&ctx.props().selected_column);
529
530 let initial_window_value = ctx
534 .props()
535 .selected_column
536 .name()
537 .and_then(|name| ctx.props().view_config.windows.get(name))
538 .cloned();
539
540 let tabs = {
541 let mut tabs = vec![];
542 let is_new_expr = ctx.props().selected_column.is_new_expr();
543 let show_styles = !is_new_expr
544 && ctx.props().renderer.can_render_column_styles()
545 && ctx.props().view_config.columns.contains(&Some(
546 ctx.props()
547 .selected_column
548 .name()
549 .map(|x| x.to_string())
550 .unwrap_or_default(),
551 ));
552
553 if !is_new_expr && show_styles {
554 tabs.push(ColumnSettingsTab::Style);
555 }
556
557 if ctx.props().selected_column.is_expr() {
558 tabs.push(ColumnSettingsTab::Attributes);
559 }
560
561 let supports_windows = ctx
562 .props()
563 .metadata
564 .get_features()
565 .map(|x| x.has_window_aggregates())
566 .unwrap_or_default();
567
568 if ctx.props().selected_column.is_window_editable() && supports_windows {
569 tabs.push(ColumnSettingsTab::Window);
570 }
571
572 tabs
573 };
574
575 let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
576 let on_save = ctx
577 .link()
578 .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
579
580 let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
581 *self = Self {
582 column_name,
583 expr_value: initial_expr_value.clone(),
584 initial_expr_value,
585 header_value: initial_header_value.clone(),
586 initial_header_value,
587 maybe_ty,
588 window_value: initial_window_value.clone(),
589 initial_window_value,
590 tabs,
591 header_valid: true,
592 on_input,
593 on_save,
594 on_validate,
595 ..*self
596 }
597 }
598}