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