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 icon_type: self
268 .maybe_ty
269 .map(|ty| ty.into())
270 .or(Some(TypeIconType::Expr)),
271 on_change: ctx.link().batch_callback(|(value, valid)| {
272 vec![
273 ColumnSettingsPanelMsg::SetHeaderValue(value),
274 ColumnSettingsPanelMsg::SetHeaderValid(valid),
275 ]
276 }),
277 metadata: ctx.props().metadata.clone(),
278 session: &ctx.props().session
279 });
280
281 let expr_editor = props!(ExpressionEditorProps {
282 on_input: self.on_input.clone(),
283 on_save: self.on_save.clone(),
284 on_validate: self.on_validate.clone(),
285 alias: ctx.props().selected_column.name().cloned(),
286 disabled: !ctx.props().selected_column.is_expr(),
287 reset_count: self.reset_count,
288 metadata: ctx.props().metadata.clone(),
289 selected_theme: ctx.props().selected_theme.clone(),
290 session: &ctx.props().session
291 });
292
293 let disable_delete = ctx
294 .props()
295 .selected_column
296 .name()
297 .map(|name| {
298 let config = &ctx.props().view_config;
299 config.columns.iter().any(|maybe_col| {
300 maybe_col
301 .as_ref()
302 .map(|col| col == name)
303 .unwrap_or_default()
304 }) || config.group_by.iter().any(|col| col == name)
305 || config.split_by.iter().any(|col| col == name)
306 || config.filter.iter().any(|col| col.column() == name)
307 || config.sort.iter().any(|col| &col.0 == name)
308 })
309 .unwrap_or_default();
310
311 let save_section = SaveSettingsProps {
312 save_enabled: self.save_enabled,
313 reset_enabled: self.reset_enabled,
314 is_save: ctx.props().selected_column.name().is_some(),
315 on_reset: ctx
316 .link()
317 .callback(ColumnSettingsPanelMsg::OnResetAttributes),
318 on_save: ctx
319 .link()
320 .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
321 on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
322 show_danger_zone: ctx.props().selected_column.is_saved_expr(),
323 disable_delete,
324 };
325
326 let attrs_tab = AttributesTabProps {
327 expr_editor,
328 save_section,
329 };
330
331 let style_tab = StyleTabProps {
332 ty: self.maybe_ty,
333 column_name: self.column_name.clone(),
334 group_by_depth: ctx.props().view_config.group_by.len() as u32,
335 view_config: ctx.props().view_config.clone(),
336 metadata: ctx.props().metadata.clone(),
337 column_stats: ctx.props().column_stats.clone(),
338 selected_theme: ctx.props().selected_theme.clone(),
339 presentation: ctx.props().presentation.clone(),
340 renderer: ctx.props().renderer.clone(),
341 session: ctx.props().session.clone(),
342 };
343
344 let tab_children = self.tabs.iter().map(|tab| match tab {
345 ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
346 ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
347 });
348
349 let selected_tab_idx = self
350 .tabs
351 .iter()
352 .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
353 .map(|(idx, _val)| idx)
354 .unwrap_or_default();
355
356 html! {
357 <>
358 <LocalStyle href={css!("column-settings-panel")} />
359 <Sidebar
360 on_close={ctx.props().on_close.clone()}
361 id_prefix="column_settings"
362 width_override={ctx.props().width_override}
363 selected_tab={selected_tab_idx}
364 {header_props}
365 >
366 <TabList<ColumnSettingsTab>
367 tabs={self.tabs.clone()}
368 on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
369 selected_tab={selected_tab_idx}
370 >
371 { for tab_children }
372 </TabList<ColumnSettingsTab>>
373 </Sidebar>
374 </>
375 }
376 }
377}
378
379impl ColumnSettingsPanel {
380 fn save_enabled_effect(&mut self) {
381 let changed = self.expr_value != self.initial_expr_value
382 || self.header_value != self.initial_header_value;
383 let valid = self.expr_valid && self.header_valid;
384 self.save_enabled = changed && valid;
385 }
386
387 fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
388 let column_name = ctx
389 .props()
390 .metadata
391 .locator_name_or_default(&ctx.props().selected_column);
392
393 let initial_expr_value = ctx
394 .props()
395 .metadata
396 .get_expression_by_alias(&column_name)
397 .unwrap_or_default();
398
399 let initial_expr_value = Rc::new(initial_expr_value);
400 let initial_header_value =
401 (*initial_expr_value != column_name).then_some(column_name.clone());
402
403 let maybe_ty = ctx
404 .props()
405 .metadata
406 .locator_view_type(&ctx.props().selected_column);
407
408 let tabs = {
409 let mut tabs = vec![];
410 let is_new_expr = ctx.props().selected_column.is_new_expr();
411 let show_styles = !is_new_expr
412 && ctx.props().renderer.can_render_column_styles()
413 && ctx.props().view_config.columns.contains(&Some(
414 ctx.props()
415 .selected_column
416 .name()
417 .map(|x| x.to_string())
418 .unwrap_or_default(),
419 ));
420
421 if !is_new_expr && show_styles {
422 tabs.push(ColumnSettingsTab::Style);
423 }
424
425 if ctx.props().selected_column.is_expr() {
426 tabs.push(ColumnSettingsTab::Attributes);
427 }
428
429 tabs
430 };
431
432 let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
433 let on_save = ctx
434 .link()
435 .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
436
437 let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
438 *self = Self {
439 column_name,
440 expr_value: initial_expr_value.clone(),
441 initial_expr_value,
442 header_value: initial_header_value.clone(),
443 initial_header_value,
444 maybe_ty,
445 tabs,
446 header_valid: true,
447 on_input,
448 on_save,
449 on_validate,
450 ..*self
451 }
452 }
453}