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