Skip to main content

perspective_viewer/components/
plugin_tab.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
13//! Plugin-scoped settings tab. Mirrors `style_tab` but operates on the
14//! active plugin's `save()`/`restore()` token rather than a per-column
15//! config map. The schema comes from `plugin.plugin_config_schema()`;
16//! field updates are dispatched through `tasks::send_plugin_config`.
17
18use itertools::Itertools;
19use perspective_client::config::ViewConfig;
20use yew::prelude::*;
21
22use crate::components::column_settings_sidebar::style_tab::primitive_field::{
23    BoolField, ColorField, ColorRangeField, EnumField, NumberFieldPrimitive,
24};
25use crate::config::ControlSpec;
26use crate::queries::get_plugin_config_schema;
27use crate::renderer::Renderer;
28use crate::session::Session;
29use crate::tasks::send_plugin_config;
30use crate::utils::PtrEqRc;
31
32#[derive(Clone, PartialEq, Properties)]
33pub struct PluginTabProps {
34    /// View config snapshot — passed to the plugin schema callback in
35    /// case the plugin wants to gate fields based on it.
36    pub view_config: PtrEqRc<ViewConfig>,
37
38    /// Active plugin's `plugin_config` bucket — threaded as a value
39    /// snapshot from `RendererProps`. Changes on every mutation path
40    /// that fires `plugin_config_changed` (in-tab edit,
41    /// `restore_and_render` JSON paste, `reset_all` with `all=true`)
42    /// AND on plugin switch (the active bucket is keyed by plugin
43    /// name, so `to_props()` produces a fresh `Rc` after
44    /// `commit_plugin_idx`). PluginTab is a pure function of this
45    /// prop — no `Renderer::get_plugin_config()` reads against the
46    /// interior-mutable handle.
47    pub plugin_config: PtrEqRc<serde_json::Map<String, serde_json::Value>>,
48
49    // State
50    pub renderer: Renderer,
51    pub session: Session,
52}
53
54#[function_component]
55pub fn PluginTab(props: &PluginTabProps) -> Html {
56    // Memoize the JS-side `plugin_config_schema` call. The schema is a
57    // function of (active plugin, current plugin_config values,
58    // view_config); each of those arrives as a prop so the deps tuple
59    // uses cheap pointer-equality / value-equality. Yew re-runs the
60    // closure only when one of them actually changed, so the JS
61    // round-trip doesn't fire on unrelated re-renders.
62    //
63    // The closure captures `renderer` to dispatch `_plugin_config_schema`
64    // through the active plugin handle, but resolves it via the props
65    // at call time so the schema query is bound to the same atomic
66    // snapshot the rendered controls read from. No race window between
67    // a plugin swap and the schema fetch — both observe the same
68    // `RendererProps` value.
69    let schema = {
70        let renderer = props.renderer.clone();
71        let view_config = props.view_config.clone();
72        use_memo(
73            (props.plugin_config.clone(), props.view_config.clone()),
74            move |_| match get_plugin_config_schema(&renderer, &view_config) {
75                Ok(schema) => schema.fields,
76                Err(error) => {
77                    tracing::error!("{}", error);
78                    vec![]
79                },
80            },
81        )
82    };
83
84    let on_change = {
85        let session = props.session.clone();
86        let renderer = props.renderer.clone();
87        yew::Callback::from(move |update: crate::config::ColumnConfigFieldUpdate| {
88            // `send_plugin_config` emits `plugin_config_changed`,
89            // which the root component's subscription
90            // (`create_subscriptions`) turns into an `UpdateRenderer`
91            // dispatch carrying a fresh `RendererProps`. Yew's prop
92            // diff propagates the new `plugin_config` into this
93            // component automatically — no manual revision bump.
94            send_plugin_config(&session, &renderer, update);
95        })
96    };
97
98    let raw_config = &*props.plugin_config;
99    let components = schema
100        .iter()
101        .cloned()
102        .filter_map(|spec| {
103            let component = match spec {
104                ControlSpec::Enum {
105                    key,
106                    variants,
107                    default,
108                } => {
109                    let current = raw_config
110                        .get(&key)
111                        .and_then(|v| v.as_str().map(|s| s.to_string()));
112                    html! {
113                        <EnumField
114                            field_key={key}
115                            {variants}
116                            {default}
117                            {current}
118                            on_change={on_change.clone()}
119                        />
120                    }
121                },
122                ControlSpec::Bool { key, default } => {
123                    let current = raw_config.get(&key).and_then(|v| v.as_bool());
124                    html! {
125                        <BoolField
126                            field_key={key}
127                            {default}
128                            {current}
129                            on_change={on_change.clone()}
130                        />
131                    }
132                },
133                ControlSpec::Color { key, default } => {
134                    let current = raw_config
135                        .get(&key)
136                        .and_then(|v| v.as_str().map(|s| s.to_string()));
137                    html! {
138                        <ColorField
139                            field_key={key}
140                            {default}
141                            {current}
142                            on_change={on_change.clone()}
143                        />
144                    }
145                },
146                ControlSpec::ColorRange {
147                    key_pos,
148                    key_neg,
149                    default_pos,
150                    default_neg,
151                    is_gradient,
152                } => {
153                    let current_pos = raw_config
154                        .get(&key_pos)
155                        .and_then(|v| v.as_str().map(|s| s.to_string()));
156                    let current_neg = raw_config
157                        .get(&key_neg)
158                        .and_then(|v| v.as_str().map(|s| s.to_string()));
159                    html! {
160                        <ColorRangeField
161                            field_key_pos={key_pos}
162                            field_key_neg={key_neg}
163                            {default_pos}
164                            {default_neg}
165                            {current_pos}
166                            {current_neg}
167                            {is_gradient}
168                            on_change={on_change.clone()}
169                        />
170                    }
171                },
172                ControlSpec::Number {
173                    key,
174                    default,
175                    min,
176                    max,
177                    step,
178                    include,
179                } => {
180                    let current = raw_config.get(&key).and_then(|v| v.as_f64());
181                    html! {
182                        <NumberFieldPrimitive
183                            field_key={key}
184                            {default}
185                            {current}
186                            {min}
187                            {max}
188                            {step}
189                            {include}
190                            on_change={on_change.clone()}
191                        />
192                    }
193                },
194                // Column-scoped variants don't apply to
195                // plugin-level config; drop silently.
196                ControlSpec::AggregateDepth
197                | ControlSpec::NumberSeriesStyle { .. }
198                | ControlSpec::DatetimeFormat
199                | ControlSpec::StringFormat
200                | ControlSpec::Symbols { .. }
201                | ControlSpec::NumberFormat
202                | ControlSpec::String { .. } => {
203                    return None;
204                },
205            };
206
207            Some(html! { <fieldset class="style-control">{ component }</fieldset> })
208        })
209        .collect_vec();
210
211    html! {
212        <div id="plugin-tab" class="sidebar_column scrollable">
213            <div id="plugin-config-container" class="tab-section">{ components }</div>
214        </div>
215    }
216}