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