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