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::queries::{has_column_config, is_pivot_only_column};
38use crate::renderer::Renderer;
39use crate::session::{Session, SessionMetadataRc};
40use crate::tasks::{
41 delete_expr, delete_window, save_expr, save_window, update_expr, update_window,
42};
43use crate::ui::{Sidebar, TabList};
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
223 || next != Initials::of(old_props)
224 {
225 self.reset_to(ctx, next);
226 } else {
227 self.refresh_derived(ctx);
228 }
229
230 true
231 }
232
233 fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
234 match msg {
235 ColumnSettingsPanelMsg::SetExprValue(val) => {
236 if self.expr_value != val {
237 self.expr_value = val;
238 self.reset_enabled = true;
239 true
240 } else {
241 false
242 }
243 },
244 ColumnSettingsPanelMsg::SetExprValid(val) => {
245 self.expr_valid = val;
246 self.save_enabled_effect();
247 true
248 },
249 ColumnSettingsPanelMsg::SetHeaderValue(val) => {
250 if self.header_value != val {
251 self.header_value = val;
252 self.reset_enabled = true;
253 true
254 } else {
255 false
256 }
257 },
258 ColumnSettingsPanelMsg::SetHeaderValid(val) => {
259 self.header_valid = val;
260 self.save_enabled_effect();
261 true
262 },
263 ColumnSettingsPanelMsg::SetWindowValue(val) => {
264 if self.window_value != val {
265 self.window_value = val;
266 self.reset_enabled = true;
267 true
268 } else {
269 false
270 }
271 },
272 ColumnSettingsPanelMsg::SetSelectedTab((_, val)) => {
273 let rerender = ctx.props().selected_tab != Some(val);
274 ctx.props().on_select_tab.emit(val);
275 rerender
276 },
277 ColumnSettingsPanelMsg::OnResetAttributes(()) => {
278 self.header_value.clone_from(&self.initial_header_value);
279 self.expr_value.clone_from(&self.initial_expr_value);
280 self.window_value.clone_from(&self.initial_window_value);
281 self.save_enabled = false;
282 self.reset_enabled = false;
283 self.reset_count += 1;
284 true
285 },
286 ColumnSettingsPanelMsg::OnSaveAttributes(()) => {
287 if matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window)) {
288 if let Some(spec) = self.window_value.clone() {
289 let name = self
290 .header_value
291 .clone()
292 .unwrap_or_else(|| self.column_name.clone());
293 match &ctx.props().selected_column {
294 ColumnLocator::Window(old_name) => update_window(
295 &ctx.props().session,
296 &ctx.props().renderer,
297 &ctx.props().presentation,
298 old_name.clone(),
299 name,
300 spec.clone(),
301 ),
302 _ => {
303 if let Err(err) = save_window(
304 &ctx.props().session,
305 &ctx.props().renderer,
306 &ctx.props().presentation,
307 name,
308 spec.clone(),
309 ) {
310 tracing::warn!("{}", err);
311 }
312 },
313 }
314
315 self.initial_window_value = Some(spec);
316 self.initial_header_value.clone_from(&self.header_value);
317 self.save_enabled = false;
318 self.reset_enabled = false;
319 self.save_count += 1;
320 }
321
322 return true;
323 }
324
325 let new_expr = Expression::new(
326 self.header_value.clone().map(|s| s.into()),
327 (*(self.expr_value)).clone().into(),
328 );
329
330 match &ctx.props().selected_column {
331 ColumnLocator::Table(_) | ColumnLocator::Window(_) => {
332 tracing::error!("Tried to save non-expression column!")
333 },
334 ColumnLocator::Expression(name) => update_expr(
335 &ctx.props().session,
336 &ctx.props().renderer,
337 &ctx.props().presentation,
338 name.clone(),
339 new_expr,
340 ),
341 ColumnLocator::NewExpression => {
342 if let Err(err) = save_expr(
343 &ctx.props().session,
344 &ctx.props().renderer,
345 &ctx.props().presentation,
346 new_expr,
347 ) {
348 tracing::warn!("{}", err);
349 }
350 },
351 }
352
353 self.initial_expr_value.clone_from(&self.expr_value);
354 self.initial_header_value.clone_from(&self.header_value);
355 self.save_enabled = false;
356 self.reset_enabled = false;
357 self.save_count += 1;
358 true
359 },
360 ColumnSettingsPanelMsg::OnDelete(()) => {
361 if ctx.props().selected_column.is_saved_expr() {
362 delete_expr(
363 &ctx.props().session,
364 &ctx.props().renderer,
365 &self.column_name,
366 )
367 .unwrap_or_log();
368 } else if ctx.props().selected_column.is_saved_window() {
369 delete_window(
370 &ctx.props().session,
371 &ctx.props().renderer,
372 &self.column_name,
373 )
374 .unwrap_or_log();
375 }
376
377 ctx.props().on_close.emit(());
378 true
379 },
380 }
381 }
382
383 fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
384 let is_window_tab = matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window));
385
386 let header_placeholder = if is_window_tab {
387 Rc::new(self.column_name.clone())
388 } else {
389 self.expr_value.clone()
390 };
391
392 let header_props = props!(EditableHeaderProps {
393 value: self.header_value.clone(),
394 initial_value: self.initial_header_value.clone(),
395 placeholder: header_placeholder,
396 editable: (ctx.props().selected_column.is_expr()
397 && matches!(
398 ctx.props().selected_tab,
399 Some(ColumnSettingsTab::Attributes)
400 ))
401 || (ctx.props().selected_column.is_window_editable() && is_window_tab),
402 update_on_input: true,
403 icon_type: self
404 .maybe_ty
405 .map(|ty| ty.into())
406 .or(Some(TypeIconType::Expr)),
407 on_change: ctx.link().batch_callback(|(value, valid)| {
408 vec![
409 ColumnSettingsPanelMsg::SetHeaderValue(value),
410 ColumnSettingsPanelMsg::SetHeaderValid(valid),
411 ]
412 }),
413 metadata: ctx.props().metadata.clone(),
414 });
415
416 let expr_editor = props!(ExpressionEditorProps {
417 on_input: self.on_input.clone(),
418 on_save: self.on_save.clone(),
419 on_validate: self.on_validate.clone(),
420 alias: ctx.props().selected_column.name().cloned(),
421 initial_expr: self.initial_expr_value.clone(),
422 disabled: !ctx.props().selected_column.is_expr(),
423 reset_count: self.reset_count,
424 metadata: ctx.props().metadata.clone(),
425 selected_theme: ctx.props().selected_theme.clone(),
426 session: &ctx.props().session
427 });
428
429 let disable_delete = ctx
430 .props()
431 .selected_column
432 .name()
433 .map(|name| {
434 let config = &ctx.props().view_config;
435 config.columns.iter().any(|maybe_col| {
436 maybe_col
437 .as_ref()
438 .map(|col| col == name)
439 .unwrap_or_default()
440 }) || config.group_by.iter().any(|col| col == name)
441 || config.split_by.iter().any(|col| col == name)
442 || config.filter.iter().any(|col| col.column() == name)
443 || config.sort.iter().any(|col| &col.0 == name)
444 })
445 .unwrap_or_default();
446
447 let save_section = SaveSettingsProps {
448 save_enabled: self.save_enabled,
449 reset_enabled: self.reset_enabled,
450 is_save: ctx.props().selected_column.name().is_some(),
451 on_reset: ctx
452 .link()
453 .callback(ColumnSettingsPanelMsg::OnResetAttributes),
454 on_save: ctx
455 .link()
456 .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
457 on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
458 show_danger_zone: ctx.props().selected_column.is_saved_expr(),
459 disable_delete,
460 };
461
462 let attrs_tab = AttributesTabProps {
463 expr_editor,
464 save_section: save_section.clone(),
465 };
466
467 let window_changed = self.window_value != self.initial_window_value
468 || self.header_value != self.initial_header_value;
469 let window_tab = WindowTabProps {
470 editor: WindowEditorProps {
471 metadata: ctx.props().metadata.clone(),
472 initial: self.initial_window_value.clone(),
473 on_change: ctx.link().callback(ColumnSettingsPanelMsg::SetWindowValue),
474 reset_count: self.reset_count,
475 presentation: ctx.props().presentation.clone(),
476 session: ctx.props().session.clone(),
477 selected_theme: ctx.props().selected_theme.clone(),
478 },
479 save_section: SaveSettingsProps {
480 save_enabled: self.window_value.is_some() && window_changed && self.header_valid,
481 show_danger_zone: ctx.props().selected_column.is_saved_window(),
482 ..save_section
483 },
484 };
485
486 let style_tab = StyleTabProps {
487 ty: self.maybe_ty,
488 column_name: self.column_name.clone(),
489 group_by_depth: ctx.props().view_config.group_by.len() as u32,
490 view_config: ctx.props().view_config.clone(),
491 metadata: ctx.props().metadata.clone(),
492 column_stats: ctx.props().column_stats.clone(),
493 selected_theme: ctx.props().selected_theme.clone(),
494 presentation: ctx.props().presentation.clone(),
495 renderer: ctx.props().renderer.clone(),
496 session: ctx.props().session.clone(),
497 workspace: ctx.props().workspace.clone(),
498 };
499
500 let tab_children = self.tabs.iter().map(|tab| match tab {
501 ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
502 ColumnSettingsTab::Window => html! { <WindowTab ..window_tab.clone() /> },
503 ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
504 });
505
506 let selected_tab_idx = self
507 .tabs
508 .iter()
509 .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
510 .map(|(idx, _val)| idx)
511 .unwrap_or_default();
512
513 html! {
514 <>
515 <Sidebar
516 on_close={ctx.props().on_close.clone()}
517 id_prefix="column_settings"
518 width_override={ctx.props().width_override}
519 auto_width={ctx.props().auto_width}
520 on_auto_width={ctx.props().on_auto_width.clone()}
521 is_pinned={ctx.props().is_pinned}
522 on_toggle_pin={Some(ctx.props().on_toggle_pin.clone())}
523 selected_tab={selected_tab_idx}
524 header={html! { <EditableHeader ..header_props /> }}
525 >
526 <TabList<ColumnSettingsTab>
527 tabs={self.tabs.clone()}
528 on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
529 selected_tab={selected_tab_idx}
530 >
531 { for tab_children }
532 </TabList<ColumnSettingsTab>>
533 </Sidebar>
534 </>
535 }
536 }
537}
538
539impl ColumnSettingsPanel {
540 fn save_enabled_effect(&mut self) {
541 let changed = self.expr_value != self.initial_expr_value
542 || self.header_value != self.initial_header_value;
543 let valid = self.expr_valid && self.header_valid;
544 self.save_enabled = changed && valid;
545 }
546
547 fn refresh_derived(&mut self, ctx: &yew::prelude::Context<Self>) {
548 let view_ty = ctx
549 .props()
550 .metadata
551 .locator_view_type(&ctx.props().selected_column);
552
553 self.maybe_ty = match ctx.props().selected_column.name() {
554 Some(name) if is_pivot_only_column(name, &ctx.props().view_config) => {
555 ctx.props().metadata.get_column_table_type(name).or(view_ty)
556 },
557 _ => view_ty,
558 };
559
560 self.tabs = {
561 let mut tabs = vec![];
562 let is_new_expr = ctx.props().selected_column.is_new_expr();
563 let selected_name = ctx
564 .props()
565 .selected_column
566 .name()
567 .map(|x| x.to_string())
568 .unwrap_or_default();
569
570 let show_styles = !is_new_expr
571 && has_column_config(
572 &selected_name,
573 &ctx.props().view_config,
574 &ctx.props().metadata,
575 &ctx.props().renderer,
576 );
577
578 if !is_new_expr && show_styles {
579 tabs.push(ColumnSettingsTab::Style);
580 }
581
582 if ctx.props().selected_column.is_expr() {
583 tabs.push(ColumnSettingsTab::Attributes);
584 }
585
586 let supports_windows = ctx
587 .props()
588 .metadata
589 .get_features()
590 .map(|x| x.has_window_aggregates())
591 .unwrap_or_default();
592
593 if ctx.props().selected_column.is_window_editable() && supports_windows {
594 tabs.push(ColumnSettingsTab::Window);
595 }
596
597 tabs
598 };
599 }
600
601 fn reset_to(&mut self, ctx: &yew::prelude::Context<Self>, initials: Initials) {
602 let Initials {
603 column_name,
604 expr,
605 header,
606 window,
607 } = initials;
608
609 self.column_name = column_name;
610 self.expr_value = expr.clone();
611 self.initial_expr_value = expr;
612 self.header_value = header.clone();
613 self.initial_header_value = header;
614 self.window_value = window.clone();
615 self.initial_window_value = window;
616 self.header_valid = true;
617 self.save_enabled = false;
618 self.reset_enabled = false;
619 self.reset_count = self.reset_count.wrapping_add(1);
620 self.refresh_derived(ctx);
621 }
622}