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