perspective_viewer/components/
settings_panel.rs1use std::rc::Rc;
14
15use perspective_client::config::{ViewConfig, ViewConfigUpdate};
16use perspective_js::utils::ApiFuture;
17use yew::prelude::*;
18
19use super::column_selector::ColumnSelector;
20use super::plugin_selector::PluginSelector;
21use super::plugin_tab::PluginTab;
22use crate::components::debug_panel::DebugPanel;
23use crate::config::{PluginStaticConfig, PluginUpdate};
24use crate::presentation::{ColumnLocator, ColumnSettingsTarget, OpenColumnSettings, Presentation};
25use crate::queries::classify_column;
26use crate::renderer::*;
27use crate::session::column_defaults_update::*;
28use crate::session::*;
29use crate::tasks::update_plugin_and_render;
30use crate::ui::SidebarCloseButton;
31use crate::utils::*;
32use crate::workspace::Workspace;
33
34#[derive(Clone, Properties)]
35pub struct SettingsPanelProps {
36 pub on_close: Callback<()>,
37 pub on_resize: Rc<PubSub<()>>,
38 pub on_select_column: Callback<Option<ColumnSettingsTarget>>,
39 pub on_debug: Callback<()>,
40 pub is_debug: bool,
41
42 pub plugin_name: Option<String>,
44 pub available_plugins: PtrEqRc<Vec<String>>,
45 pub has_table: Option<TableLoadState>,
46 pub named_column_count: usize,
47
48 pub plugin_static_config: Rc<PluginStaticConfig>,
56 pub view_config: PtrEqRc<ViewConfig>,
57
58 pub plugin_config: PtrEqRc<serde_json::Map<String, serde_json::Value>>,
62
63 pub columns_config: PtrEqRc<ColumnConfigMap>,
66
67 pub drag_column: Option<String>,
70
71 pub metadata: SessionMetadataRc,
74
75 pub open_column_settings: OpenColumnSettings,
78
79 pub selected_theme: Option<String>,
81
82 pub selected_tab: SelectedTab,
86
87 pub auto_width: f64,
90
91 pub on_select_tab: Callback<SelectedTab>,
93
94 pub on_auto_width: Callback<f64>,
96
97 pub on_dimensions_reset: Rc<PubSub<()>>,
103
104 pub session: Session,
106 pub renderer: Renderer,
107 pub presentation: Presentation,
108 pub workspace: Workspace,
109}
110
111impl PartialEq for SettingsPanelProps {
112 fn eq(&self, rhs: &Self) -> bool {
113 self.is_debug == rhs.is_debug
114 && self.plugin_name == rhs.plugin_name
115 && self.available_plugins == rhs.available_plugins
116 && self.has_table == rhs.has_table
117 && self.named_column_count == rhs.named_column_count
118 && self.plugin_static_config == rhs.plugin_static_config
119 && self.view_config == rhs.view_config
120 && self.plugin_config == rhs.plugin_config
121 && self.columns_config == rhs.columns_config
122 && self.drag_column == rhs.drag_column
123 && self.metadata == rhs.metadata
124 && self.open_column_settings == rhs.open_column_settings
125 && self.selected_theme == rhs.selected_theme
126 && self.selected_tab == rhs.selected_tab
127 && self.auto_width == rhs.auto_width
128 }
129}
130
131#[derive(Debug, PartialEq, Clone, Copy, Default)]
132pub enum SelectedTab {
133 #[default]
134 Query,
135 Plugin,
136 Debug,
137
138 Chat,
142}
143
144#[function_component]
145pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
146 let SettingsPanelProps {
147 presentation,
148 renderer,
149 session,
150 ..
151 } = &props;
152
153 let selected_column = {
154 let config = &props.view_config;
155 props
156 .open_column_settings
157 .target
158 .as_ref()
159 .and_then(|target| match target {
160 ColumnSettingsTarget::NewExpression => Some(ColumnLocator::NewExpression),
161 ColumnSettingsTarget::Column(n) => {
162 let locator = classify_column(n, config, &props.metadata)?;
163 if !matches!(locator, ColumnLocator::Table(_)) {
164 return Some(locator);
165 }
166
167 let used = config.columns.iter().any(|maybe_col| {
168 maybe_col.as_ref().map(|col| col == n).unwrap_or_default()
169 }) || config.group_by.iter().any(|col| col == n)
170 || config.split_by.iter().any(|col| col == n)
171 || config.filter.iter().any(|col| col.column() == n)
172 || config.sort.iter().any(|col| &col.0 == n);
173 (used && props.renderer.can_render_column_styles()).then_some(locator)
174 },
175 })
176 };
177
178 let plugin_name = props.plugin_name.clone();
179 let available_plugins = props.available_plugins.clone();
180 let selected = props.selected_tab;
181
182 let width = props.auto_width;
187 let on_auto_width = props.on_auto_width.clone();
188
189 let on_select_plugin = {
192 clone!(renderer, session, presentation);
193 let session_metadata = props.metadata.clone();
194 let view_config = props.view_config.clone();
195 Callback::from(move |plugin_name: String| {
196 if session.is_errored() {
197 return;
198 }
199 let resolved_plugin =
203 renderer.resolve_plugin_update(&PluginUpdate::Update(plugin_name));
204 let prev_metadata = renderer.metadata();
205 let plugin_config = resolved_plugin
206 .as_ref()
207 .map(|(_, metadata)| &**metadata)
208 .unwrap_or(&*prev_metadata);
209 let rollup_features = session_metadata
210 .get_features()
211 .map(|x| x.get_group_rollup_modes())
212 .unwrap();
213
214 let group_rollups = plugin_config.get_group_rollups(&rollup_features);
215 let split_rollup_features = session_metadata
216 .get_features()
217 .map(|x| x.get_split_rollup_modes())
218 .unwrap();
219
220 let split_rollups = plugin_config.get_split_rollups(&split_rollup_features);
221 let mut update = ViewConfigUpdate {
222 group_rollup_mode: group_rollups.first().cloned(),
223 split_rollup_mode: split_rollups.first().cloned(),
224 ..ViewConfigUpdate::default()
225 };
226
227 update.set_update_column_defaults(
228 &session_metadata,
229 &view_config,
230 &view_config.columns,
231 plugin_config,
232 );
233
234 let plugin_idx = resolved_plugin.map(|(idx, _)| idx);
235 if let Ok(task) = update_plugin_and_render(&session, &renderer, update, plugin_idx) {
236 ApiFuture::spawn(task);
237 }
238
239 presentation.set_open_column_settings(None);
240 })
241 };
242
243 let cb1 = props.on_select_column.clone();
244 let set_debug = use_callback(
245 props.on_select_tab.clone(),
246 move |_: PointerEvent, on_select_tab| {
247 on_select_tab.emit(SelectedTab::Debug);
248 cb1.emit(None)
249 },
250 );
251
252 let cb2 = props.on_select_column.clone();
253 let set_plugin = use_callback(
254 props.on_select_tab.clone(),
255 move |_: PointerEvent, on_select_tab| {
256 on_select_tab.emit(SelectedTab::Plugin);
257 cb2.emit(None)
258 },
259 );
260
261 let set_query = use_callback(
262 props.on_select_tab.clone(),
263 |_: PointerEvent, on_select_tab| on_select_tab.emit(SelectedTab::Query),
264 );
265
266 let tab_class = |l_tab: SelectedTab, r_tab: SelectedTab| {
267 if l_tab == r_tab {
268 "settings_tab selected_tab"
269 } else {
270 "settings_tab"
271 }
272 };
273
274 #[cfg(feature = "llm-agent")]
278 let (chat_tab_button, chat_body) = {
279 let update = use_force_update();
280 let agent = presentation.agent.clone();
281 use_effect_with((), move |_| {
282 let sub = agent
283 .on_update
284 .add_notify_listener(&Callback::from(move |_| update.force_update()));
285
286 move || drop(sub)
287 });
288
289 let button = if presentation.agent.is_configured() {
290 let on_select_column = props.on_select_column.clone();
291 let set_chat = {
292 let on_select_tab = props.on_select_tab.clone();
293 Callback::from(move |_: PointerEvent| {
294 on_select_tab.emit(SelectedTab::Chat);
295 on_select_column.emit(None)
296 })
297 };
298
299 html! {
300 <div
301 id="chat_tabbar_tab"
302 class={tab_class(selected, SelectedTab::Chat)}
303 onpointerdown={set_chat}
304 />
305 }
306 } else {
307 html! {}
308 };
309
310 let body = html! {
311 <crate::components::chat_panel::ChatPanel agent={presentation.agent.clone()} />
312 };
313
314 (button, body)
315 };
316
317 #[cfg(not(feature = "llm-agent"))]
318 let (chat_tab_button, chat_body) = (html! {}, html! {});
319
320 let on_open_expr_panel = use_callback(props.on_select_column.clone(), |c, on_select| {
321 on_select.emit(Some(c))
322 });
323
324 html! {
325 <div id="settings_panel" class="sidebar_column noselect split-panel orient-vertical">
326 if selected_column.is_none() {
327 <SidebarCloseButton
328 id="settings_close_button"
329 on_close_sidebar={&props.on_close.clone()}
330 />
331 }
332 <PluginSelector
333 {plugin_name}
334 {available_plugins}
335 {on_select_plugin}
336 />
337 <div id="settings_tab_bar" class="settings_tab_bar_scroll_offset">
338 <div
339 id="query_tabbar_tab"
340 class={tab_class(selected, SelectedTab::Query)}
341 onpointerdown={set_query}
342 />
343 <div
344 id="plugin_tabbar_tab"
345 class={tab_class(selected, SelectedTab::Plugin)}
346 onpointerdown={set_plugin}
347 />
348 <div
349 id="debug_tabbar_tab"
350 class={tab_class(selected, SelectedTab::Debug)}
351 onpointerdown={set_debug}
352 />
353 { chat_tab_button }
354 </div>
355 if selected == SelectedTab::Query {
356 <ColumnSelector
357 on_resize={&props.on_resize}
358 {on_open_expr_panel}
359 {selected_column}
360 has_table={props.has_table.clone()}
361 named_column_count={props.named_column_count}
362 plugin_static_config={props.plugin_static_config.clone()}
363 view_config={props.view_config.clone()}
364 columns_config={props.columns_config.clone()}
365 drag_column={props.drag_column.clone()}
366 metadata={props.metadata.clone()}
367 selected_theme={props.selected_theme.clone()}
368 presentation={presentation.clone()}
369 renderer={renderer.clone()}
370 session={session.clone()}
371 initial_width={width}
372 on_auto_width={on_auto_width.clone()}
373 on_dimensions_reset={&props.on_dimensions_reset}
374 />
375 } else if selected == SelectedTab::Plugin {
376 <PluginTab
377 view_config={props.view_config.clone()}
378 plugin_config={props.plugin_config.clone()}
379 presentation={presentation.clone()}
380 renderer={renderer.clone()}
381 session={session.clone()}
382 />
385 } else if selected == SelectedTab::Chat {
386 { chat_body }
387 } else {
388 <DebugPanel
389 {presentation}
390 {renderer}
391 {session}
392 workspace={props.workspace.clone()}
393 initial_width={width}
394 on_auto_width={on_auto_width.clone()}
395 />
396 }
397 <div
401 class="scroll-panel-auto-width"
402 style={format!("width:{}px", width)}
403 />
404 </div>
405 }
406}