Skip to main content

perspective_viewer/components/
settings_panel.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use 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::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::*;
31
32#[derive(Clone, Properties)]
33pub struct SettingsPanelProps {
34    pub on_close: Callback<()>,
35    pub on_resize: Rc<PubSub<()>>,
36    pub on_select_column: Callback<Option<ColumnLocator>>,
37    pub on_debug: Callback<()>,
38    pub is_debug: bool,
39
40    /// Value props threaded from the root's `RendererProps` / `SessionProps`.
41    pub plugin_name: Option<String>,
42    pub available_plugins: PtrEqRc<Vec<String>>,
43    pub has_table: Option<TableLoadState>,
44    pub named_column_count: usize,
45    pub view_config: PtrEqRc<ViewConfig>,
46
47    /// Snapshot of the active plugin's `plugin_config` bucket, threaded
48    /// from `RendererProps`. Forwarded into `PluginTab` so the tab is
49    /// prop-driven instead of reading `Renderer` directly.
50    pub plugin_config: PtrEqRc<serde_json::Map<String, serde_json::Value>>,
51
52    /// Column currently being dragged (if any) — threaded to show drag
53    /// highlights without per-component `DragDrop` PubSub subscriptions.
54    pub drag_column: Option<String>,
55
56    /// Cloned session metadata snapshot — threaded from `SessionProps`
57    /// so that metadata changes trigger re-renders via prop diffing.
58    pub metadata: SessionMetadataRc,
59
60    /// Snapshot of the column-settings sidebar state — threaded from
61    /// `PresentationProps` so that open/close triggers re-renders.
62    pub open_column_settings: OpenColumnSettings,
63
64    /// Selected theme name, threaded for PortalModal consumers.
65    pub selected_theme: Option<String>,
66
67    /// Controlled: the currently selected tab. Lifted to `PerspectiveViewer`
68    /// so that messages like `OpenColumnSettings` can revert the tab without
69    /// the panel owning the state.
70    pub selected_tab: SelectedTab,
71
72    /// Controlled: the running max of measured tab widths. Lifted so that
73    /// `SettingsPanelSizeUpdate(None)` (divider reset) can clear it.
74    pub auto_width: f64,
75
76    /// Callback invoked when the user clicks a tab.
77    pub on_select_tab: Callback<SelectedTab>,
78
79    /// Callback invoked by tab subtrees reporting their natural width.
80    pub on_auto_width: Callback<f64>,
81
82    /// Fires when the outer split-panel divider is reset; threaded into
83    /// `ColumnSelector` so its inner `ScrollPanel` can drop its persistent
84    /// `viewport_width` and re-measure honestly. Without this, the
85    /// `auto_width` reset in `PerspectiveViewer` rebounds immediately as
86    /// the ScrollPanel republishes its stale cached width.
87    pub on_dimensions_reset: Rc<PubSub<()>>,
88
89    /// State
90    pub session: Session,
91    pub renderer: Renderer,
92    pub presentation: Presentation,
93}
94
95impl PartialEq for SettingsPanelProps {
96    fn eq(&self, rhs: &Self) -> bool {
97        self.is_debug == rhs.is_debug
98            && self.plugin_name == rhs.plugin_name
99            && self.available_plugins == rhs.available_plugins
100            && self.has_table == rhs.has_table
101            && self.named_column_count == rhs.named_column_count
102            && self.view_config == rhs.view_config
103            && self.plugin_config == rhs.plugin_config
104            && self.drag_column == rhs.drag_column
105            && self.metadata == rhs.metadata
106            && self.open_column_settings == rhs.open_column_settings
107            && self.selected_theme == rhs.selected_theme
108            && self.selected_tab == rhs.selected_tab
109            && self.auto_width == rhs.auto_width
110    }
111}
112
113#[derive(Debug, PartialEq, Clone, Copy, Default)]
114pub enum SelectedTab {
115    #[default]
116    Query,
117    Plugin,
118    Debug,
119}
120
121#[function_component]
122pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
123    let SettingsPanelProps {
124        presentation,
125        renderer,
126        session,
127        ..
128    } = &props;
129
130    let selected_column = {
131        let locator = props.open_column_settings.locator.clone();
132        let config = &props.view_config;
133        locator.filter(|locator| match locator {
134            ColumnLocator::Table(_name) => {
135                locator
136                    .name()
137                    .map(|n| {
138                        config.columns.iter().any(|maybe_col| {
139                            maybe_col.as_ref().map(|col| col == n).unwrap_or_default()
140                        }) || config.group_by.iter().any(|col| col == n)
141                            || config.split_by.iter().any(|col| col == n)
142                            || config.filter.iter().any(|col| col.column() == n)
143                            || config.sort.iter().any(|col| &col.0 == n)
144                    })
145                    .unwrap_or_default()
146                    && props.renderer.can_render_column_styles()
147            },
148            _ => true,
149        })
150    };
151
152    let plugin_name = props.plugin_name.clone();
153    let available_plugins = props.available_plugins.clone();
154    let selected = props.selected_tab;
155
156    // Shared trap-door width across tabs. Each tab subtree measures its
157    // natural width and feeds the result back through `on_auto_width`;
158    // the parent keeps the running max so a tab switch never shrinks the
159    // panel, and clears it on divider reset.
160    let width = props.auto_width;
161    let on_auto_width = props.on_auto_width.clone();
162
163    // Dispatch callback: captures engine handles, constructs config update,
164    // hands the apply+draw work to `tasks::pipeline`.
165    let on_select_plugin = {
166        clone!(renderer, session, presentation);
167        let session_metadata = props.metadata.clone();
168        let view_config = props.view_config.clone();
169        Callback::from(move |plugin_name: String| {
170            if session.is_errored() {
171                return;
172            }
173            // Pure resolve — the swap itself is committed inside the locked
174            // draw task by `update_plugin_and_render`, never staged on the
175            // `Renderer` where a concurrent draw could observe it.
176            let resolved_plugin =
177                renderer.resolve_plugin_update(&PluginUpdate::Update(plugin_name));
178            let prev_metadata = renderer.metadata();
179            let plugin_config = resolved_plugin
180                .as_ref()
181                .map(|(_, metadata)| &**metadata)
182                .unwrap_or(&*prev_metadata);
183            let rollup_features = session_metadata
184                .get_features()
185                .map(|x| x.get_group_rollup_modes())
186                .unwrap();
187
188            let group_rollups = plugin_config.get_group_rollups(&rollup_features);
189            let mut update = ViewConfigUpdate {
190                group_rollup_mode: group_rollups.first().cloned(),
191                ..ViewConfigUpdate::default()
192            };
193
194            update.set_update_column_defaults(
195                &session_metadata,
196                &view_config.columns,
197                plugin_config,
198            );
199
200            let plugin_idx = resolved_plugin.map(|(idx, _)| idx);
201            if let Ok(task) = update_plugin_and_render(&session, &renderer, update, plugin_idx) {
202                ApiFuture::spawn(task);
203            }
204
205            presentation.set_open_column_settings(None);
206        })
207    };
208
209    let cb1 = props.on_select_column.clone();
210    let set_debug = use_callback(
211        props.on_select_tab.clone(),
212        move |_: PointerEvent, on_select_tab| {
213            on_select_tab.emit(SelectedTab::Debug);
214            cb1.emit(None)
215        },
216    );
217
218    let cb2 = props.on_select_column.clone();
219    let set_plugin = use_callback(
220        props.on_select_tab.clone(),
221        move |_: PointerEvent, on_select_tab| {
222            on_select_tab.emit(SelectedTab::Plugin);
223            cb2.emit(None)
224        },
225    );
226
227    let set_query = use_callback(
228        props.on_select_tab.clone(),
229        |_: PointerEvent, on_select_tab| on_select_tab.emit(SelectedTab::Query),
230    );
231
232    let tab_class = |l_tab: SelectedTab, r_tab: SelectedTab| {
233        if l_tab == r_tab {
234            "settings_tab selected_tab"
235        } else {
236            "settings_tab"
237        }
238    };
239
240    let on_open_expr_panel = use_callback(props.on_select_column.clone(), |c, on_select| {
241        on_select.emit(Some(c))
242    });
243
244    html! {
245        <div id="settings_panel" class="sidebar_column noselect split-panel orient-vertical">
246            if selected_column.is_none() {
247                <SidebarCloseButton
248                    id="settings_close_button"
249                    on_close_sidebar={&props.on_close.clone()}
250                />
251            }
252            <PluginSelector
253                {plugin_name}
254                {available_plugins}
255                {on_select_plugin}
256            />
257            <div id="settings_tab_bar" class="settings_tab_bar_scroll_offset">
258                <div
259                    id="query_tabbar_tab"
260                    class={tab_class(selected, SelectedTab::Query)}
261                    onpointerdown={set_query}
262                />
263                <div
264                    id="plugin_tabbar_tab"
265                    class={tab_class(selected, SelectedTab::Plugin)}
266                    onpointerdown={set_plugin}
267                />
268                <div
269                    id="debug_tabbar_tab"
270                    class={tab_class(selected, SelectedTab::Debug)}
271                    onpointerdown={set_debug}
272                />
273            </div>
274            if selected == SelectedTab::Query {
275                <ColumnSelector
276                    on_resize={&props.on_resize}
277                    {on_open_expr_panel}
278                    {selected_column}
279                    has_table={props.has_table.clone()}
280                    named_column_count={props.named_column_count}
281                    view_config={props.view_config.clone()}
282                    drag_column={props.drag_column.clone()}
283                    metadata={props.metadata.clone()}
284                    selected_theme={props.selected_theme.clone()}
285                    presentation={presentation.clone()}
286                    renderer={renderer.clone()}
287                    session={session.clone()}
288                    initial_width={width}
289                    on_auto_width={on_auto_width.clone()}
290                    on_dimensions_reset={&props.on_dimensions_reset}
291                />
292            } else if selected == SelectedTab::Plugin {
293                <PluginTab
294                    view_config={props.view_config.clone()}
295                    plugin_config={props.plugin_config.clone()}
296                    renderer={renderer.clone()}
297                    session={session.clone()}
298                // initial_width={width}
299                // on_auto_width={on_auto_width.clone()}
300                />
301            } else {
302                <DebugPanel
303                    {presentation}
304                    {renderer}
305                    {session}
306                    initial_width={width}
307                    on_auto_width={on_auto_width.clone()}
308                />
309            }
310            // Sibling sizer keeps the panel width pinned across tab
311            // switches; lives outside the tab-body so it survives the
312            // tab subtree's unmount.
313            <div class="scroll-panel-auto-width" style={format!("width:{}px", width)} />
314        </div>
315    }
316}