perspective_viewer/components/
column_settings_sidebar.rs1mod attributes_tab;
13
14mod save_settings;
15pub(crate) mod style_tab;
16
17use std::rc::Rc;
18
19use derivative::Derivative;
20use itertools::Itertools;
21use perspective_client::config::{ColumnType, Expression, ViewConfig};
22use perspective_client::utils::PerspectiveResultExt;
23use yew::{Callback, Component, Html, Properties, html, props};
24
25use self::attributes_tab::AttributesTabProps;
26use self::style_tab::StyleTabProps;
27use crate::components::column_settings_sidebar::attributes_tab::AttributesTab;
28use crate::components::column_settings_sidebar::save_settings::SaveSettingsProps;
29use crate::components::column_settings_sidebar::style_tab::StyleTab;
30use crate::components::containers::sidebar::Sidebar;
31use crate::components::containers::tab_list::TabList;
32use crate::components::editable_header::EditableHeaderProps;
33use crate::components::expression_editor::ExpressionEditorProps;
34use crate::components::type_icon::TypeIconType;
35use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
36use crate::renderer::Renderer;
37use crate::session::{Session, SessionMetadataRc};
38use crate::tasks::{delete_expr, save_expr, update_expr};
39use crate::utils::PtrEqRc;
40
41#[derive(Clone, Derivative, Properties)]
42#[derivative(Debug)]
43pub struct ColumnSettingsPanelProps {
44 pub selected_column: ColumnLocator,
45 pub selected_tab: Option<ColumnSettingsTab>,
46 pub on_close: Callback<()>,
47 pub width_override: Option<i32>,
48 pub on_select_tab: Callback<ColumnSettingsTab>,
49
50 pub plugin_name: Option<String>,
54
55 pub metadata: SessionMetadataRc,
57
58 pub view_config: PtrEqRc<ViewConfig>,
60
61 pub column_stats: PtrEqRc<std::collections::HashMap<String, crate::session::ColumnStats>>,
63
64 pub selected_theme: Option<String>,
66
67 #[derivative(Debug = "ignore")]
69 pub presentation: Presentation,
70
71 #[derivative(Debug = "ignore")]
72 pub renderer: Renderer,
73
74 #[derivative(Debug = "ignore")]
75 pub session: Session,
76}
77
78impl PartialEq for ColumnSettingsPanelProps {
79 fn eq(&self, other: &Self) -> bool {
80 self.selected_column == other.selected_column
81 && self.selected_tab == other.selected_tab
82 && self.plugin_name == other.plugin_name
83 && self.metadata == other.metadata
84 && self.view_config == other.view_config
85 && self.column_stats == other.column_stats
86 && self.selected_theme == other.selected_theme
87 }
88}
89
90#[derive(Debug)]
91pub enum ColumnSettingsPanelMsg {
92 SetExprValue(Rc<String>),
93 SetExprValid(bool),
94 SetHeaderValue(Option<String>),
95 SetHeaderValid(bool),
96 SetSelectedTab((usize, ColumnSettingsTab)),
97 OnSaveAttributes(()),
98 OnResetAttributes(()),
99 OnDelete(()),
100}
101
102#[derive(Derivative)]
103#[derivative(Debug)]
104pub struct ColumnSettingsPanel {
105 column_name: String,
106 expr_valid: bool,
107 expr_value: Rc<String>,
108 header_valid: bool,
109 header_value: Option<String>,
110 initial_expr_value: Rc<String>,
111 initial_header_value: Option<String>,
112 maybe_ty: Option<ColumnType>,
113 on_input: Callback<Rc<String>>,
114 on_save: Callback<()>,
115 on_validate: Callback<bool>,
116 reset_count: u8,
117 reset_enabled: bool,
118 save_count: u8,
119 save_enabled: bool,
120 tabs: Vec<ColumnSettingsTab>,
121}
122
123impl Component for ColumnSettingsPanel {
124 type Message = ColumnSettingsPanelMsg;
125 type Properties = ColumnSettingsPanelProps;
126
127 fn create(ctx: &yew::prelude::Context<Self>) -> Self {
128 let mut this = Self {
129 initial_expr_value: Rc::default(),
130 expr_value: Rc::default(),
131 expr_valid: false,
132 initial_header_value: None,
133 header_value: None,
134 header_valid: false,
135 save_enabled: false,
136 save_count: 0,
137 reset_enabled: false,
138 reset_count: 0,
139 column_name: "".to_owned(),
140 maybe_ty: None,
141 tabs: vec![],
142 on_input: Callback::default(),
143 on_save: Callback::default(),
144 on_validate: Callback::default(),
145 };
146
147 this.initialize(ctx);
148 this
149 }
150
151 fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
152 if ctx.props() != old_props {
153 self.initialize(ctx);
154 true
155 } else {
156 false
157 }
158 }
159
160 fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
161 match msg {
162 ColumnSettingsPanelMsg::SetExprValue(val) => {
163 if self.expr_value != val {
164 self.expr_value = val;
165 self.reset_enabled = true;
166 true
167 } else {
168 false
169 }
170 },
171 ColumnSettingsPanelMsg::SetExprValid(val) => {
172 self.expr_valid = val;
173 self.save_enabled_effect();
174 true
175 },
176 ColumnSettingsPanelMsg::SetHeaderValue(val) => {
177 if self.header_value != val {
178 self.header_value = val;
179 self.reset_enabled = true;
180 true
181 } else {
182 false
183 }
184 },
185 ColumnSettingsPanelMsg::SetHeaderValid(val) => {
186 self.header_valid = val;
187 self.save_enabled_effect();
188 true
189 },
190 ColumnSettingsPanelMsg::SetSelectedTab((_, val)) => {
191 let rerender = ctx.props().selected_tab != Some(val);
192 ctx.props().on_select_tab.emit(val);
193 rerender
194 },
195 ColumnSettingsPanelMsg::OnResetAttributes(()) => {
196 self.header_value.clone_from(&self.initial_header_value);
197 self.expr_value.clone_from(&self.initial_expr_value);
198 self.save_enabled = false;
199 self.reset_enabled = false;
200 self.reset_count += 1;
201 true
202 },
203 ColumnSettingsPanelMsg::OnSaveAttributes(()) => {
204 let new_expr = Expression::new(
205 self.header_value.clone().map(|s| s.into()),
206 (*(self.expr_value)).clone().into(),
207 );
208
209 match &ctx.props().selected_column {
210 ColumnLocator::Table(_) => {
211 tracing::error!("Tried to save non-expression column!")
212 },
213 ColumnLocator::Expression(name) => update_expr(
214 &ctx.props().session,
215 &ctx.props().renderer,
216 &ctx.props().presentation,
217 name.clone(),
218 new_expr,
219 ),
220 ColumnLocator::NewExpression => {
221 if let Err(err) = save_expr(
222 &ctx.props().session,
223 &ctx.props().renderer,
224 &ctx.props().presentation,
225 new_expr,
226 ) {
227 tracing::warn!("{}", err);
228 }
229 },
230 }
231
232 self.initial_expr_value.clone_from(&self.expr_value);
233 self.initial_header_value.clone_from(&self.header_value);
234 self.save_enabled = false;
235 self.reset_enabled = false;
236 self.save_count += 1;
237 true
238 },
239 ColumnSettingsPanelMsg::OnDelete(()) => {
240 if ctx.props().selected_column.is_saved_expr() {
241 delete_expr(
242 &ctx.props().session,
243 &ctx.props().renderer,
244 &self.column_name,
245 )
246 .unwrap_or_log();
247 }
248
249 ctx.props().on_close.emit(());
250 true
251 },
252 }
253 }
254
255 fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
256 let header_props = props!(EditableHeaderProps {
257 initial_value: self.initial_header_value.clone(),
258 placeholder: self.expr_value.clone(),
259 reset_count: self.reset_count,
260 editable: ctx.props().selected_column.is_expr()
261 && matches!(
262 ctx.props().selected_tab,
263 Some(ColumnSettingsTab::Attributes)
264 ),
265 update_on_input: true,
266 icon_type: self
267 .maybe_ty
268 .map(|ty| ty.into())
269 .or(Some(TypeIconType::Expr)),
270 on_change: ctx.link().batch_callback(|(value, valid)| {
271 vec![
272 ColumnSettingsPanelMsg::SetHeaderValue(value),
273 ColumnSettingsPanelMsg::SetHeaderValid(valid),
274 ]
275 }),
276 metadata: ctx.props().metadata.clone(),
277 session: &ctx.props().session
278 });
279
280 let expr_editor = props!(ExpressionEditorProps {
281 on_input: self.on_input.clone(),
282 on_save: self.on_save.clone(),
283 on_validate: self.on_validate.clone(),
284 alias: ctx.props().selected_column.name().cloned(),
285 disabled: !ctx.props().selected_column.is_expr(),
286 reset_count: self.reset_count,
287 metadata: ctx.props().metadata.clone(),
288 selected_theme: ctx.props().selected_theme.clone(),
289 session: &ctx.props().session
290 });
291
292 let disable_delete = ctx
293 .props()
294 .selected_column
295 .name()
296 .map(|name| {
297 let config = &ctx.props().view_config;
298 config.columns.iter().any(|maybe_col| {
299 maybe_col
300 .as_ref()
301 .map(|col| col == name)
302 .unwrap_or_default()
303 }) || config.group_by.iter().any(|col| col == name)
304 || config.split_by.iter().any(|col| col == name)
305 || config.filter.iter().any(|col| col.column() == name)
306 || config.sort.iter().any(|col| &col.0 == name)
307 })
308 .unwrap_or_default();
309
310 let save_section = SaveSettingsProps {
311 save_enabled: self.save_enabled,
312 reset_enabled: self.reset_enabled,
313 is_save: ctx.props().selected_column.name().is_some(),
314 on_reset: ctx
315 .link()
316 .callback(ColumnSettingsPanelMsg::OnResetAttributes),
317 on_save: ctx
318 .link()
319 .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
320 on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
321 show_danger_zone: ctx.props().selected_column.is_saved_expr(),
322 disable_delete,
323 };
324
325 let attrs_tab = AttributesTabProps {
326 expr_editor,
327 save_section,
328 };
329
330 let style_tab = StyleTabProps {
331 ty: self.maybe_ty,
332 column_name: self.column_name.clone(),
333 group_by_depth: ctx.props().view_config.group_by.len() as u32,
334 view_config: ctx.props().view_config.clone(),
335 metadata: ctx.props().metadata.clone(),
336 column_stats: ctx.props().column_stats.clone(),
337 selected_theme: ctx.props().selected_theme.clone(),
338 presentation: ctx.props().presentation.clone(),
339 renderer: ctx.props().renderer.clone(),
340 session: ctx.props().session.clone(),
341 };
342
343 let tab_children = self.tabs.iter().map(|tab| match tab {
344 ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
345 ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
346 });
347
348 let selected_tab_idx = self
349 .tabs
350 .iter()
351 .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
352 .map(|(idx, _val)| idx)
353 .unwrap_or_default();
354
355 html! {
356 <>
357 <Sidebar
358 on_close={ctx.props().on_close.clone()}
359 id_prefix="column_settings"
360 width_override={ctx.props().width_override}
361 selected_tab={selected_tab_idx}
362 {header_props}
363 >
364 <TabList<ColumnSettingsTab>
365 tabs={self.tabs.clone()}
366 on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
367 selected_tab={selected_tab_idx}
368 >
369 { for tab_children }
370 </TabList<ColumnSettingsTab>>
371 </Sidebar>
372 </>
373 }
374 }
375}
376
377impl ColumnSettingsPanel {
378 fn save_enabled_effect(&mut self) {
379 let changed = self.expr_value != self.initial_expr_value
380 || self.header_value != self.initial_header_value;
381 let valid = self.expr_valid && self.header_valid;
382 self.save_enabled = changed && valid;
383 }
384
385 fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
386 let column_name = ctx
387 .props()
388 .metadata
389 .locator_name_or_default(&ctx.props().selected_column);
390
391 let initial_expr_value = ctx
392 .props()
393 .metadata
394 .get_expression_by_alias(&column_name)
395 .unwrap_or_default();
396
397 let initial_expr_value = Rc::new(initial_expr_value);
398 let initial_header_value =
399 (*initial_expr_value != column_name).then_some(column_name.clone());
400
401 let maybe_ty = ctx
402 .props()
403 .metadata
404 .locator_view_type(&ctx.props().selected_column);
405
406 let tabs = {
407 let mut tabs = vec![];
408 let is_new_expr = ctx.props().selected_column.is_new_expr();
409 let show_styles = !is_new_expr
410 && ctx.props().renderer.can_render_column_styles()
411 && ctx.props().view_config.columns.contains(&Some(
412 ctx.props()
413 .selected_column
414 .name()
415 .map(|x| x.to_string())
416 .unwrap_or_default(),
417 ));
418
419 if !is_new_expr && show_styles {
420 tabs.push(ColumnSettingsTab::Style);
421 }
422
423 if ctx.props().selected_column.is_expr() {
424 tabs.push(ColumnSettingsTab::Attributes);
425 }
426
427 tabs
428 };
429
430 let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
431 let on_save = ctx
432 .link()
433 .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
434
435 let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
436 *self = Self {
437 column_name,
438 expr_value: initial_expr_value.clone(),
439 initial_expr_value,
440 header_value: initial_header_value.clone(),
441 initial_header_value,
442 maybe_ty,
443 tabs,
444 header_valid: true,
445 on_input,
446 on_save,
447 on_validate,
448 ..*self
449 }
450 }
451}