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