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