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}