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