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 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
247 fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
248 let header_props = props!(EditableHeaderProps {
249 initial_value: self.initial_header_value.clone(),
250 placeholder: self.expr_value.clone(),
251 reset_count: self.reset_count,
252 editable: ctx.props().selected_column.is_expr()
253 && matches!(
254 ctx.props().selected_tab,
255 Some(ColumnSettingsTab::Attributes)
256 ),
257 icon_type: self
258 .maybe_ty
259 .map(|ty| ty.into())
260 .or(Some(TypeIconType::Expr)),
261 on_change: ctx.link().batch_callback(|(value, valid)| {
262 vec![
263 ColumnSettingsPanelMsg::SetHeaderValue(value),
264 ColumnSettingsPanelMsg::SetHeaderValid(valid),
265 ]
266 }),
267 session: &ctx.props().session
268 });
269
270 let expr_editor = props!(ExpressionEditorProps {
271 on_input: self.on_input.clone(),
272 on_save: self.on_save.clone(),
273 on_validate: self.on_validate.clone(),
274 alias: ctx.props().selected_column.name().cloned(),
275 disabled: !ctx.props().selected_column.is_expr(),
276 reset_count: self.reset_count,
277 session: &ctx.props().session
278 });
279
280 let disable_delete = ctx
281 .props()
282 .session
283 .is_locator_active(&ctx.props().selected_column);
284
285 let save_section = SaveSettingsProps {
286 save_enabled: self.save_enabled,
287 reset_enabled: self.reset_enabled,
288 is_save: ctx.props().selected_column.name().is_some(),
289 on_reset: ctx
290 .link()
291 .callback(ColumnSettingsPanelMsg::OnResetAttributes),
292 on_save: ctx
293 .link()
294 .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
295 on_delete: ctx.link().callback(ColumnSettingsPanelMsg::OnDelete),
296 show_danger_zone: ctx.props().selected_column.is_saved_expr(),
297 disable_delete,
298 };
299
300 let attrs_tab = AttributesTabProps {
301 expr_editor,
302 save_section,
303 };
304
305 let style_tab = props!(StyleTabProps {
306 ty: self.maybe_ty,
307 column_name: self.column_name.clone(),
308 group_by_depth: ctx.props().session.get_view_config().group_by.len() as u32,
309 custom_events: ctx.props().custom_events(),
310 presentation: ctx.props().presentation(),
311 renderer: ctx.props().renderer(),
312 session: ctx.props().session()
313 });
314
315 let tab_children = self.tabs.iter().map(|tab| match tab {
316 ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
317 ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
318 });
319
320 let selected_tab_idx = self
321 .tabs
322 .iter()
323 .find_position(|tab| Some(**tab) == ctx.props().selected_tab)
324 .map(|(idx, _val)| idx)
325 .unwrap_or_default();
326
327 html! {
328 <>
329 <LocalStyle href={css!("column-settings-panel")} />
330 <Sidebar
331 on_close={ctx.props().on_close.clone()}
332 id_prefix="column_settings"
333 width_override={ctx.props().width_override}
334 selected_tab={selected_tab_idx}
335 {header_props}
336 >
337 <TabList<ColumnSettingsTab>
338 tabs={self.tabs.clone()}
339 on_tab_change={ctx.link().callback(ColumnSettingsPanelMsg::SetSelectedTab)}
340 selected_tab={selected_tab_idx}
341 >
342 { for tab_children }
343 </TabList<ColumnSettingsTab>>
344 </Sidebar>
345 </>
346 }
347 }
348}
349
350impl ColumnSettingsPanel {
351 fn save_enabled_effect(&mut self) {
352 let changed = self.expr_value != self.initial_expr_value
353 || self.header_value != self.initial_header_value;
354 let valid = self.expr_valid && self.header_valid;
355 self.save_enabled = changed && valid;
356 }
357
358 fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
359 let column_name = ctx
360 .props()
361 .session
362 .locator_name_or_default(&ctx.props().selected_column);
363
364 let initial_expr_value = ctx
365 .props()
366 .session
367 .metadata()
368 .get_expression_by_alias(&column_name)
369 .unwrap_or_default();
370
371 let initial_expr_value = Rc::new(initial_expr_value);
372 let initial_header_value =
373 (*initial_expr_value != column_name).then_some(column_name.clone());
374
375 let maybe_ty = ctx
376 .props()
377 .session()
378 .locator_view_type(&ctx.props().selected_column);
379
380 let tabs = {
381 let mut tabs = vec![];
382 let is_new_expr = ctx.props().selected_column.is_new_expr();
383 let show_styles = !is_new_expr
384 && ctx
385 .props()
386 .can_render_column_styles(&column_name)
387 .unwrap_or_default();
388
389 if !is_new_expr && show_styles {
390 tabs.push(ColumnSettingsTab::Style);
391 }
392
393 if ctx.props().selected_column.is_expr() {
394 tabs.push(ColumnSettingsTab::Attributes);
395 }
396 tabs
397 };
398
399 let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
400 let on_save = ctx
401 .link()
402 .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
403
404 let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
405 *self = Self {
406 column_name,
407 expr_value: initial_expr_value.clone(),
408 initial_expr_value,
409 header_value: initial_header_value.clone(),
410 initial_header_value,
411 maybe_ty,
412 tabs,
413 header_valid: true,
414 on_input,
415 on_save,
416 on_validate,
417 _session_sub: self._session_sub.take(),
418 ..*self
419 }
420 }
421}