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