@perspective-dev/viewer 4.5.0 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/perspective-viewer.js +1 -1
- package/dist/cdn/perspective-viewer.js.map +2 -2
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/esm/perspective-viewer.inline.js +1 -1
- package/dist/esm/perspective-viewer.inline.js.map +2 -2
- package/dist/esm/perspective-viewer.js +1 -1
- package/dist/esm/perspective-viewer.js.map +2 -2
- package/dist/wasm/perspective-viewer.d.ts +13 -13
- package/dist/wasm/perspective-viewer.js +87 -82
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +13 -13
- package/package.json +1 -1
- package/src/css/column-settings-panel.css +9 -3
- package/src/css/column-style.css +9 -1
- package/src/css/dom/checkbox.css +2 -2
- package/src/css/viewer.css +79 -1
- package/src/rust/components/column_selector/active_column.rs +3 -0
- package/src/rust/components/column_selector/inactive_column.rs +2 -1
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -0
- package/src/rust/components/column_settings_sidebar.rs +1 -0
- package/src/rust/components/containers/dragdrop_list.rs +27 -0
- package/src/rust/components/editable_header.rs +15 -0
- package/src/rust/components/status_bar.rs +1 -1
- package/src/rust/components/viewer.rs +78 -2
- package/src/rust/renderer.rs +72 -18
- package/src/rust/tasks/reset_all.rs +38 -18
- package/src/rust/tasks/restore_and_render.rs +23 -12
- package/src/rust/tasks/send_column_config.rs +3 -2
- package/src/rust/tasks/send_plugin_config.rs +3 -2
- package/src/rust/tasks/update_and_render.rs +11 -4
- package/src/svg/checkbox-checked-icon.svg +1 -1
- package/src/svg/checkbox-unchecked-icon.svg +1 -1
- package/src/themes/icons.css +2 -0
- package/src/themes/intl/de.css +1 -0
- package/src/themes/intl/es.css +1 -0
- package/src/themes/intl/fr.css +1 -0
- package/src/themes/intl/ja.css +1 -0
- package/src/themes/intl/pt.css +1 -0
- package/src/themes/intl/zh.css +1 -0
- package/src/themes/intl.css +1 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-3cd58f0374935772}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-3cd58f0374935772}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-3cd58f0374935772}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-3cd58f0374935772}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-3cd58f0374935772}/inline4.js +0 -0
package/src/css/viewer.css
CHANGED
|
@@ -16,10 +16,88 @@
|
|
|
16
16
|
--psp--color: #ff0000;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/* Shift-key affordance: while the user holds Shift, any mask-image icon
|
|
20
|
+
* tagged `.shift-alt-icon` recolors to advertise that a Shift-modified
|
|
21
|
+
* action is available on the underlying control. `!important` is required
|
|
22
|
+
* to beat per-icon ID-scoped `background-color` rules. */
|
|
23
|
+
:host(.shift-active) .shift-alt-icon {
|
|
24
|
+
background-color: var(
|
|
25
|
+
--shift-active--color,
|
|
26
|
+
var(--psp-datagrid--pos-cell--color, #1078d1)
|
|
27
|
+
) !important;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:host(.shift-active) {
|
|
31
|
+
#sub-columns {
|
|
32
|
+
.is_column_active.toggle-mode {
|
|
33
|
+
-webkit-mask-image: var(--psp-icon--radio-off--mask-image);
|
|
34
|
+
mask-image: var(--psp-icon--radio-off--mask-image);
|
|
35
|
+
|
|
36
|
+
&:before {
|
|
37
|
+
content: var(--psp-icon--radio-off--mask-image);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&:hover {
|
|
41
|
+
-webkit-mask-image: var(--psp-icon--radio-hover--mask-image);
|
|
42
|
+
mask-image: var(--psp-icon--radio-hover--mask-image);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.is_column_active.select-mode {
|
|
47
|
+
-webkit-mask-image: var(--psp-icon--checkbox-off--mask-image);
|
|
48
|
+
mask-image: var(--psp-icon--checkbox-off--mask-image);
|
|
49
|
+
|
|
50
|
+
&:before {
|
|
51
|
+
content: var(--psp-icon--checkbox-off--mask-image);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:hover {
|
|
55
|
+
-webkit-mask-image: var(--psp-icon--checkbox-hover--mask-image);
|
|
56
|
+
mask-image: var(--psp-icon--checkbox-hover--mask-image);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#active-columns {
|
|
62
|
+
.is_column_active.toggle-mode {
|
|
63
|
+
-webkit-mask-image: var(--psp-icon--radio-on--mask-image);
|
|
64
|
+
mask-image: var(--psp-icon--radio-on--mask-image);
|
|
65
|
+
|
|
66
|
+
&:before {
|
|
67
|
+
content: var(--psp-icon--radio-on--mask-image);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
&:not(.required):hover {
|
|
71
|
+
-webkit-mask-image: var(--psp-icon--radio-hover--mask-image);
|
|
72
|
+
mask-image: var(--psp-icon--radio-hover--mask-image);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.is_column_active.select-mode {
|
|
77
|
+
-webkit-mask-image: var(--psp-icon--checkbox-on--mask-image);
|
|
78
|
+
mask-image: var(--psp-icon--checkbox-on--mask-image);
|
|
79
|
+
|
|
80
|
+
&:before {
|
|
81
|
+
content: var(--psp-icon--checkbox-on--mask-image);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&:not(.required):hover {
|
|
85
|
+
-webkit-mask-image: var(--psp-icon--checkbox-hover--mask-image);
|
|
86
|
+
mask-image: var(--psp-icon--checkbox-hover--mask-image);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
19
92
|
::slotted(*) {
|
|
20
93
|
pointer-events: var(--override-content-pointer-events);
|
|
21
94
|
}
|
|
22
95
|
|
|
96
|
+
:host input[type="number"]::-webkit-inner-spin-button {
|
|
97
|
+
transform: scale(1.5); /* Makes the arrows 50% larger */
|
|
98
|
+
filter: invert(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
23
101
|
:host .sidebar_close_button {
|
|
24
102
|
position: absolute;
|
|
25
103
|
top: 0;
|
|
@@ -398,13 +476,13 @@
|
|
|
398
476
|
cursor: pointer;
|
|
399
477
|
padding: 6px 8px;
|
|
400
478
|
font-size: var(--label--font-size, 0.75em);
|
|
479
|
+
text-transform: uppercase;
|
|
401
480
|
flex: 0 1 100px;
|
|
402
481
|
background-color: #00000020;
|
|
403
482
|
border-bottom: 1px solid var(--psp-inactive--color);
|
|
404
483
|
color: var(--psp-inactive--color);
|
|
405
484
|
margin-left: -1px;
|
|
406
485
|
border-left: 1px solid var(--psp-inactive--color);
|
|
407
|
-
|
|
408
486
|
&:hover {
|
|
409
487
|
color: inherit;
|
|
410
488
|
}
|
|
@@ -149,7 +149,8 @@ impl Component for InactiveColumn {
|
|
|
149
149
|
|
|
150
150
|
let is_expression = ctx.props().is_expression;
|
|
151
151
|
|
|
152
|
-
let is_active_class = ctx.props().renderer.metadata().select_mode.css();
|
|
152
|
+
let mut is_active_class = ctx.props().renderer.metadata().select_mode.css();
|
|
153
|
+
is_active_class.push("shift-alt-icon");
|
|
153
154
|
let mut class = classes!("column-selector-column");
|
|
154
155
|
if !ctx.props().visible {
|
|
155
156
|
class.push("column-selector-column-hidden");
|
|
@@ -193,6 +193,12 @@ where
|
|
|
193
193
|
}
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
// Held by per-row `ondragenter` closures below so they can re-arm
|
|
197
|
+
// the `safaridragleave` flag on the container element when the
|
|
198
|
+
// row stops dragenter from bubbling. See the comment inside the
|
|
199
|
+
// closure for why this matters.
|
|
200
|
+
let container_noderef = drag_container.noderef.clone();
|
|
201
|
+
|
|
196
202
|
let invalid_drag: bool;
|
|
197
203
|
let mut valid_duplicate_drag = false;
|
|
198
204
|
|
|
@@ -253,9 +259,30 @@ where
|
|
|
253
259
|
let close = ctx.props().parent.callback(move |_| V::close(idx));
|
|
254
260
|
let dragenter = ctx.props().parent.callback({
|
|
255
261
|
let link = ctx.link().clone();
|
|
262
|
+
let container_noderef = container_noderef.clone();
|
|
256
263
|
move |event: DragEvent| {
|
|
257
264
|
event.stop_propagation();
|
|
258
265
|
event.prevent_default();
|
|
266
|
+
// Safari: `relatedTarget` is always null on
|
|
267
|
+
// dragleave, so `dragleave_helper` uses a
|
|
268
|
+
// `data-safaridragleave` flag set by the
|
|
269
|
+
// container's own dragenter to distinguish
|
|
270
|
+
// child-crossing leaves (consume the flag)
|
|
271
|
+
// from real leaves (no flag → fire callback).
|
|
272
|
+
// The `stop_propagation` above blocks the
|
|
273
|
+
// container's dragenter, so the flag would
|
|
274
|
+
// never be re-armed after the first consume —
|
|
275
|
+
// any further internal boundary crossing
|
|
276
|
+
// would demote the state out of
|
|
277
|
+
// `DragOverInProgress` and the next drop
|
|
278
|
+
// would be silently rejected. Set the flag
|
|
279
|
+
// here so each row-targeted dragenter
|
|
280
|
+
// refills the pool.
|
|
281
|
+
if event.related_target().is_none()
|
|
282
|
+
&& let Some(elem) = container_noderef.cast::<HtmlElement>()
|
|
283
|
+
{
|
|
284
|
+
let _ = elem.dataset().set("safaridragleave", "true");
|
|
285
|
+
}
|
|
259
286
|
link.send_message(DragDropListMsg::Freeze(true));
|
|
260
287
|
V::dragenter(idx)
|
|
261
288
|
}
|
|
@@ -28,9 +28,13 @@ pub struct EditableHeaderProps {
|
|
|
28
28
|
pub initial_value: Option<String>,
|
|
29
29
|
pub placeholder: Rc<String>,
|
|
30
30
|
|
|
31
|
+
// TODO remove this pattern
|
|
31
32
|
#[prop_or_default]
|
|
32
33
|
pub reset_count: u8,
|
|
33
34
|
|
|
35
|
+
#[prop_or_default]
|
|
36
|
+
pub update_on_input: bool,
|
|
37
|
+
|
|
34
38
|
/// Session metadata snapshot — threaded from `SessionProps`.
|
|
35
39
|
pub metadata: SessionMetadataRc,
|
|
36
40
|
|
|
@@ -163,6 +167,16 @@ impl Component for EditableHeader {
|
|
|
163
167
|
EditableHeaderMsg::SetNewValue(value)
|
|
164
168
|
});
|
|
165
169
|
|
|
170
|
+
let update_on_input = ctx.props().update_on_input;
|
|
171
|
+
let oninput = ctx.link().batch_callback(move |e: yew::InputEvent| {
|
|
172
|
+
if update_on_input {
|
|
173
|
+
let value = e.target_unchecked_into::<HtmlInputElement>().value();
|
|
174
|
+
vec![EditableHeaderMsg::SetNewValue(value)]
|
|
175
|
+
} else {
|
|
176
|
+
vec![]
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
166
180
|
html! {
|
|
167
181
|
<div class={classes} onclick={ctx.link().callback(|_| EditableHeaderMsg::OnClick(()))}>
|
|
168
182
|
if let Some(icon) = ctx.props().icon_type { <TypeIcon ty={icon} /> }
|
|
@@ -173,6 +187,7 @@ impl Component for EditableHeader {
|
|
|
173
187
|
disabled={!ctx.props().editable}
|
|
174
188
|
{onblur}
|
|
175
189
|
{onkeyup}
|
|
190
|
+
{oninput}
|
|
176
191
|
value={self.value.clone()}
|
|
177
192
|
placeholder={self.placeholder.clone()}
|
|
178
193
|
/>
|
|
@@ -377,7 +377,7 @@ impl Component for StatusBar {
|
|
|
377
377
|
<div id="plugin-settings"><slot name="statusbar-extra" /></div>
|
|
378
378
|
<span class="hover-target">
|
|
379
379
|
<span id="reset" class="button" onmousedown={&onreset}>
|
|
380
|
-
<span class="icon" />
|
|
380
|
+
<span class="icon shift-alt-icon" />
|
|
381
381
|
<span class="icon-label" />
|
|
382
382
|
</span>
|
|
383
383
|
</span>
|
|
@@ -14,7 +14,9 @@ use std::rc::Rc;
|
|
|
14
14
|
|
|
15
15
|
use futures::channel::oneshot::*;
|
|
16
16
|
use perspective_js::utils::*;
|
|
17
|
+
use wasm_bindgen::JsCast;
|
|
17
18
|
use wasm_bindgen::prelude::*;
|
|
19
|
+
use web_sys::{FocusEvent, KeyboardEvent};
|
|
18
20
|
use yew::prelude::*;
|
|
19
21
|
|
|
20
22
|
use super::containers::split_panel::SplitPanel;
|
|
@@ -121,6 +123,70 @@ pub struct PerspectiveViewer {
|
|
|
121
123
|
/// Counts in-flight renders (incremented on `view_config_changed`,
|
|
122
124
|
/// decremented on `view_created`). Threaded to `StatusIndicator`.
|
|
123
125
|
update_count: u32,
|
|
126
|
+
|
|
127
|
+
/// Window listeners that toggle the `.shift-active` class on the host
|
|
128
|
+
/// element while the Shift key is held, making Shift-modified affordances
|
|
129
|
+
/// (e.g. inactive column add, active column remove, status-bar reset)
|
|
130
|
+
/// visually discoverable. Stored so the closures outlive `create`.
|
|
131
|
+
_shift_listeners: ShiftListeners,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
struct ShiftListeners {
|
|
135
|
+
elem: web_sys::HtmlElement,
|
|
136
|
+
keydown: Closure<dyn FnMut(KeyboardEvent)>,
|
|
137
|
+
keyup: Closure<dyn FnMut(KeyboardEvent)>,
|
|
138
|
+
blur: Closure<dyn FnMut(FocusEvent)>,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
impl Drop for ShiftListeners {
|
|
142
|
+
fn drop(&mut self) {
|
|
143
|
+
let win = global::window();
|
|
144
|
+
let _ = win
|
|
145
|
+
.remove_event_listener_with_callback("keydown", self.keydown.as_ref().unchecked_ref());
|
|
146
|
+
let _ =
|
|
147
|
+
win.remove_event_listener_with_callback("keyup", self.keyup.as_ref().unchecked_ref());
|
|
148
|
+
let _ = win.remove_event_listener_with_callback("blur", self.blur.as_ref().unchecked_ref());
|
|
149
|
+
let _ = self.elem.class_list().remove_1("shift-active");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn install_shift_listeners(elem: web_sys::HtmlElement) -> ShiftListeners {
|
|
154
|
+
let keydown = {
|
|
155
|
+
let elem = elem.clone();
|
|
156
|
+
Closure::wrap(Box::new(move |event: KeyboardEvent| {
|
|
157
|
+
if event.key() == "Shift" {
|
|
158
|
+
let _ = elem.class_list().add_1("shift-active");
|
|
159
|
+
}
|
|
160
|
+
}) as Box<dyn FnMut(KeyboardEvent)>)
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
let keyup = {
|
|
164
|
+
let elem = elem.clone();
|
|
165
|
+
Closure::wrap(Box::new(move |event: KeyboardEvent| {
|
|
166
|
+
if event.key() == "Shift" {
|
|
167
|
+
let _ = elem.class_list().remove_1("shift-active");
|
|
168
|
+
}
|
|
169
|
+
}) as Box<dyn FnMut(KeyboardEvent)>)
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
let blur = {
|
|
173
|
+
let elem = elem.clone();
|
|
174
|
+
Closure::wrap(Box::new(move |_: FocusEvent| {
|
|
175
|
+
let _ = elem.class_list().remove_1("shift-active");
|
|
176
|
+
}) as Box<dyn FnMut(FocusEvent)>)
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let win = global::window();
|
|
180
|
+
let _ = win.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref());
|
|
181
|
+
let _ = win.add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref());
|
|
182
|
+
let _ = win.add_event_listener_with_callback("blur", blur.as_ref().unchecked_ref());
|
|
183
|
+
|
|
184
|
+
ShiftListeners {
|
|
185
|
+
elem,
|
|
186
|
+
keydown,
|
|
187
|
+
keyup,
|
|
188
|
+
blur,
|
|
189
|
+
}
|
|
124
190
|
}
|
|
125
191
|
|
|
126
192
|
impl Component for PerspectiveViewer {
|
|
@@ -160,6 +226,8 @@ impl Component for PerspectiveViewer {
|
|
|
160
226
|
});
|
|
161
227
|
}
|
|
162
228
|
|
|
229
|
+
let shift_listeners = install_shift_listeners(elem);
|
|
230
|
+
|
|
163
231
|
Self {
|
|
164
232
|
_subscriptions: subscriptions,
|
|
165
233
|
column_settings_panel_width_override: None,
|
|
@@ -178,6 +246,7 @@ impl Component for PerspectiveViewer {
|
|
|
178
246
|
presentation_props,
|
|
179
247
|
dragdrop_props: DragDropProps::default(),
|
|
180
248
|
update_count: 0,
|
|
249
|
+
_shift_listeners: shift_listeners,
|
|
181
250
|
}
|
|
182
251
|
}
|
|
183
252
|
|
|
@@ -548,8 +617,15 @@ impl Component for PerspectiveViewer {
|
|
|
548
617
|
skip_empty=true
|
|
549
618
|
initial_size={self.settings_panel_width_override}
|
|
550
619
|
on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
|
|
551
|
-
on_resize={
|
|
552
|
-
|
|
620
|
+
on_resize={{
|
|
621
|
+
let size_cb = on_split_panel_resize.clone();
|
|
622
|
+
let resize_cb = resize_callback(&ctx.props().session, &ctx.props().renderer);
|
|
623
|
+
move |x| {
|
|
624
|
+
size_cb.emit(x);
|
|
625
|
+
resize_cb.emit(());
|
|
626
|
+
}
|
|
627
|
+
}}
|
|
628
|
+
on_resize_finished={resize_callback(&ctx.props().session, &ctx.props().renderer)}
|
|
553
629
|
>
|
|
554
630
|
{ settings_panel }
|
|
555
631
|
<div id="main_column_container">
|
package/src/rust/renderer.rs
CHANGED
|
@@ -31,11 +31,10 @@ use std::ops::Deref;
|
|
|
31
31
|
use std::pin::Pin;
|
|
32
32
|
use std::rc::Rc;
|
|
33
33
|
|
|
34
|
-
use futures::future::select_all;
|
|
34
|
+
use futures::future::{join_all, select_all};
|
|
35
35
|
use perspective_client::config::ViewConfig;
|
|
36
36
|
use perspective_client::utils::*;
|
|
37
37
|
use perspective_client::{View, ViewWindow};
|
|
38
|
-
use perspective_js::json;
|
|
39
38
|
use perspective_js::utils::{ApiResult, JsValueSerdeExt, ResultTApiErrorExt};
|
|
40
39
|
use serde_json::Value;
|
|
41
40
|
use wasm_bindgen::prelude::*;
|
|
@@ -52,6 +51,7 @@ pub use self::registry::*;
|
|
|
52
51
|
use self::render_timer::*;
|
|
53
52
|
use crate::config::*;
|
|
54
53
|
use crate::js::plugin::*;
|
|
54
|
+
use crate::queries::resolve_abs_max;
|
|
55
55
|
use crate::session::Session;
|
|
56
56
|
use crate::utils::*;
|
|
57
57
|
|
|
@@ -169,15 +169,6 @@ impl Renderer {
|
|
|
169
169
|
}))
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
pub async fn reset(&self, columns_config: Option<&ColumnConfigMap>) -> ApiResult<()> {
|
|
173
|
-
self.0.borrow_mut().plugins_idx = None;
|
|
174
|
-
if let Ok(plugin) = self.get_active_plugin() {
|
|
175
|
-
plugin.restore(&json!({}), columns_config)?;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
Ok(())
|
|
179
|
-
}
|
|
180
|
-
|
|
181
172
|
pub fn delete(&self) -> ApiResult<()> {
|
|
182
173
|
self.get_active_plugin().map(|x| x.delete()).unwrap_or_log();
|
|
183
174
|
self.plugin_data.borrow().viewer_elem.set_inner_text("");
|
|
@@ -239,12 +230,69 @@ impl Renderer {
|
|
|
239
230
|
/// `include` fields off other fields (e.g. Datagrid's
|
|
240
231
|
/// `fg_gradient` revealed when `number_fg_mode = "bar"`) will
|
|
241
232
|
/// reach the plugin without their default value populated.
|
|
242
|
-
|
|
233
|
+
///
|
|
234
|
+
/// `async` because per-column stats (e.g. `abs_max` for Datagrid's
|
|
235
|
+
/// `fg_gradient`) may need to be fetched before the schema's
|
|
236
|
+
/// `default` is meaningful. Pass 1 sync-scans the schema for any
|
|
237
|
+
/// column whose `include: true` Number key is missing from its
|
|
238
|
+
/// entry AND has no cached stats — `view_config_changed` clears the
|
|
239
|
+
/// stats cache, so "missing in cache" subsumes "stale". Pass 2
|
|
240
|
+
/// blocks on a parallel `resolve_abs_max` for that set, then runs
|
|
241
|
+
/// the materialize loop with the now-warm cache. Columns never
|
|
242
|
+
/// touched in a stats-dependent mode never trigger a fetch.
|
|
243
|
+
pub async fn all_columns_configs_materialized(
|
|
243
244
|
&self,
|
|
244
245
|
view_config: &ViewConfig,
|
|
245
246
|
session: &Session,
|
|
246
247
|
) -> ColumnConfigMap {
|
|
247
248
|
let mut configs = self.all_columns_configs();
|
|
249
|
+
|
|
250
|
+
// Pass 1: identify columns whose schema demands an `include:
|
|
251
|
+
// true` Number default we don't have stats for.
|
|
252
|
+
let mut to_warm: Vec<String> = vec![];
|
|
253
|
+
for (col, entry) in &configs {
|
|
254
|
+
if session
|
|
255
|
+
.get_column_stats(col)
|
|
256
|
+
.and_then(|s| s.abs_max)
|
|
257
|
+
.is_some()
|
|
258
|
+
{
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
let Ok(schema) =
|
|
262
|
+
self.query_column_config_schema(view_config, session, col, Some(entry))
|
|
263
|
+
else {
|
|
264
|
+
continue;
|
|
265
|
+
};
|
|
266
|
+
let needs_warm = schema.fields.iter().any(|f| {
|
|
267
|
+
matches!(
|
|
268
|
+
f,
|
|
269
|
+
ControlSpec::Number {
|
|
270
|
+
key,
|
|
271
|
+
include: Some(true),
|
|
272
|
+
..
|
|
273
|
+
} if !entry.contains_key(key)
|
|
274
|
+
)
|
|
275
|
+
});
|
|
276
|
+
if needs_warm {
|
|
277
|
+
to_warm.push(col.clone());
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Block on the (typically tiny) warm set. Clone the metadata
|
|
282
|
+
// and resolve the view ref *before* the .await — `metadata()`
|
|
283
|
+
// returns a live `Ref<>` guard that must not cross an await
|
|
284
|
+
// boundary.
|
|
285
|
+
if !to_warm.is_empty() {
|
|
286
|
+
let metadata = session.metadata().clone();
|
|
287
|
+
let view = session.get_view();
|
|
288
|
+
let futs = to_warm
|
|
289
|
+
.iter()
|
|
290
|
+
.map(|c| resolve_abs_max(session, &metadata, view.as_ref(), c.as_str()));
|
|
291
|
+
join_all(futs).await;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Pass 2: materialize. With stats now in cache, the schema
|
|
295
|
+
// returns a real `default` instead of the placeholder `0`.
|
|
248
296
|
for (col, entry) in &mut configs {
|
|
249
297
|
let Ok(schema) =
|
|
250
298
|
self.query_column_config_schema(view_config, session, col, Some(entry))
|
|
@@ -345,6 +393,8 @@ impl Renderer {
|
|
|
345
393
|
if let Ok(schema) =
|
|
346
394
|
self.query_column_config_schema(view_config, session, &col, Some(&cfg))
|
|
347
395
|
{
|
|
396
|
+
let active = schema.active_keys();
|
|
397
|
+
cfg.retain(|k, _| active.contains(k));
|
|
348
398
|
strip_default_values(&schema, &mut cfg);
|
|
349
399
|
}
|
|
350
400
|
|
|
@@ -468,13 +518,15 @@ impl Renderer {
|
|
|
468
518
|
/// the columns-config write paths (strip-on-write) and the
|
|
469
519
|
/// restore-prep snapshot (materialize-on-read).
|
|
470
520
|
///
|
|
471
|
-
/// Reads the cached `ColumnStats` (
|
|
472
|
-
/// `fetch_column_abs_max` task and cleared on `view_config_changed`)
|
|
521
|
+
/// Reads the cached `ColumnStats` (cleared on `view_config_changed`)
|
|
473
522
|
/// so plugins emit gradient defaults against the column's current
|
|
474
|
-
/// `abs_max` instead of falling back to `0`.
|
|
475
|
-
///
|
|
476
|
-
/// `include: true` fields
|
|
477
|
-
///
|
|
523
|
+
/// `abs_max` instead of falling back to `0`.
|
|
524
|
+
/// [`Self::all_columns_configs_materialized`] warms the cache on
|
|
525
|
+
/// demand before materializing `include: true` Number fields, so
|
|
526
|
+
/// the restore path always observes a real default; sync callers
|
|
527
|
+
/// (column-config strip-on-write) may still see a missing stats
|
|
528
|
+
/// pass-through and the plugin's `?? 0` fallback, but those writes
|
|
529
|
+
/// re-strip on the next render cycle.
|
|
478
530
|
fn query_column_config_schema(
|
|
479
531
|
&self,
|
|
480
532
|
view_config: &ViewConfig,
|
|
@@ -549,6 +601,8 @@ impl Renderer {
|
|
|
549
601
|
OptionalUpdate::Update(mut map) => {
|
|
550
602
|
let mut changed = false;
|
|
551
603
|
if let Some(s) = &schema {
|
|
604
|
+
let active = s.active_keys();
|
|
605
|
+
map.retain(|k, _| active.contains(k));
|
|
552
606
|
// Default-valued entries in a restore payload
|
|
553
607
|
// semantically reset the key — strip from the
|
|
554
608
|
// map AND clear any existing override in the
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
//! Cross-engine reset orchestration: reset session config, optionally clear
|
|
14
|
-
//! presentation columns config / theme,
|
|
15
|
-
//! redraw.
|
|
14
|
+
//! presentation columns config / theme, then delegate to `restore_and_render`
|
|
15
|
+
//! to switch back to the default plugin and redraw.
|
|
16
16
|
|
|
17
17
|
use futures::channel::oneshot;
|
|
18
18
|
use perspective_client::clone;
|
|
19
19
|
use perspective_js::utils::ApiFuture;
|
|
20
20
|
|
|
21
|
+
use super::restore_and_render;
|
|
22
|
+
use crate::config::{
|
|
23
|
+
ColumnConfigUpdate, OptionalUpdate, PluginConfigUpdate, PluginUpdate, ViewerConfigUpdate,
|
|
24
|
+
};
|
|
21
25
|
use crate::presentation::Presentation;
|
|
22
26
|
use crate::renderer::Renderer;
|
|
23
27
|
use crate::session::{ResetOptions, Session};
|
|
@@ -30,6 +34,12 @@ use crate::session::{ResetOptions, Session};
|
|
|
30
34
|
///
|
|
31
35
|
/// Optionally signals `sender` once the reset+redraw round-trip completes,
|
|
32
36
|
/// then emits `renderer.reset_changed`.
|
|
37
|
+
///
|
|
38
|
+
/// Delegates plugin selection + draw to [`restore_and_render`], whose
|
|
39
|
+
/// two-pass restore guarantees the default plugin sees materialized
|
|
40
|
+
/// `columns_config` / `plugin_config` on its first draw — fixing a race
|
|
41
|
+
/// where the raw post-reset bucket would reach the plugin before
|
|
42
|
+
/// stats-dependent `include: true` defaults were resolved.
|
|
33
43
|
pub fn reset_all(
|
|
34
44
|
session: &Session,
|
|
35
45
|
renderer: &Renderer,
|
|
@@ -47,32 +57,42 @@ pub fn reset_all(
|
|
|
47
57
|
..ResetOptions::default()
|
|
48
58
|
})
|
|
49
59
|
.await?;
|
|
50
|
-
let columns_config = if all {
|
|
51
|
-
renderer.reset_columns_configs();
|
|
52
|
-
renderer.reset_plugin_config();
|
|
53
|
-
// Mirror the per-plugin bucket clear on the event bus so
|
|
54
|
-
// `PluginTab` re-pulls (its props are interior-mutable
|
|
55
|
-
// handles whose identity doesn't change on the reset).
|
|
56
|
-
renderer
|
|
57
|
-
.plugin_config_changed
|
|
58
|
-
.emit(renderer.get_plugin_config());
|
|
59
|
-
None
|
|
60
|
-
} else {
|
|
61
|
-
Some(renderer.all_columns_configs())
|
|
62
|
-
};
|
|
63
60
|
|
|
64
|
-
renderer.reset(columns_config.as_ref()).await?;
|
|
65
61
|
presentation.reset_available_themes(None).await;
|
|
66
62
|
if all {
|
|
67
63
|
presentation.reset_theme().await?;
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
|
|
66
|
+
// For `all = true`, route the bucket clears through `restore_and_render`'s
|
|
67
|
+
// `update_*` paths as `SetDefault`. This guarantees the materialized
|
|
68
|
+
// restore fires even when the user is already on the default plugin
|
|
69
|
+
// (no plugin_swap signal), since `SetDefault` reports the bucket as
|
|
70
|
+
// `changed` when it was non-empty. The per-plugin bucket model means
|
|
71
|
+
// only the (post-swap) default plugin's bucket is cleared; other
|
|
72
|
+
// plugins' buckets persist with their per-plugin state.
|
|
73
|
+
let (columns_config, plugin_config) = if all {
|
|
74
|
+
(
|
|
75
|
+
ColumnConfigUpdate::SetDefault,
|
|
76
|
+
PluginConfigUpdate::SetDefault,
|
|
77
|
+
)
|
|
78
|
+
} else {
|
|
79
|
+
(OptionalUpdate::Missing, OptionalUpdate::Missing)
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let update = ViewerConfigUpdate {
|
|
83
|
+
plugin: PluginUpdate::SetDefault,
|
|
84
|
+
plugin_config,
|
|
85
|
+
columns_config,
|
|
86
|
+
..Default::default()
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
restore_and_render(&session, &renderer, &presentation, update, async { Ok(()) }).await?;
|
|
90
|
+
|
|
71
91
|
if let Some(sender) = sender {
|
|
72
92
|
sender.send(()).unwrap();
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
renderer.reset_changed.emit(());
|
|
76
|
-
|
|
96
|
+
Ok(())
|
|
77
97
|
})
|
|
78
98
|
}
|
|
@@ -86,6 +86,22 @@ pub fn restore_and_render(
|
|
|
86
86
|
let plugin_swapped = renderer.apply_pending_plugin()?;
|
|
87
87
|
let plugin = renderer.get_active_plugin()?;
|
|
88
88
|
|
|
89
|
+
// The previous call which acquired the lock errored, so skip this render
|
|
90
|
+
if let Some(error) = session.get_error() {
|
|
91
|
+
return Err(error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Validate + create the view BEFORE applying
|
|
95
|
+
// columns_config / plugin_config updates, so the
|
|
96
|
+
// strip-on-write and materialize passes see fresh
|
|
97
|
+
// `expression_schema` and `view_schema`, and the
|
|
98
|
+
// materialize warm step can call `View::get_min_max`
|
|
99
|
+
// against a view that knows about any new expression
|
|
100
|
+
// columns. Previously this happened after the strip,
|
|
101
|
+
// which silently dropped any `columns_config` entry
|
|
102
|
+
// keyed by a new expression column.
|
|
103
|
+
let view = session.validate().await?.create_view().await?;
|
|
104
|
+
|
|
89
105
|
// Apply incoming updates into the now-active plugin's
|
|
90
106
|
// bucket on `Renderer`. Per-plugin storage means no
|
|
91
107
|
// schema filter is needed before restore — foreign keys
|
|
@@ -96,9 +112,9 @@ pub fn restore_and_render(
|
|
|
96
112
|
let view_config_snapshot = session.get_view_config().clone();
|
|
97
113
|
let plugin_config_changed =
|
|
98
114
|
renderer.update_plugin_config(&view_config_snapshot, plugin_config);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
115
|
+
let columns_config_changed =
|
|
116
|
+
renderer.update_columns_configs(&view_config_snapshot, &session, columns_config);
|
|
117
|
+
let changed = plugin_config_changed || columns_config_changed;
|
|
102
118
|
|
|
103
119
|
// Force a materialized restore when the plugin just
|
|
104
120
|
// swapped — `commit_plugin_idx` already restored from the
|
|
@@ -109,24 +125,19 @@ pub fn restore_and_render(
|
|
|
109
125
|
let plugin_config_snapshot = renderer.get_plugin_config();
|
|
110
126
|
let plugin_update =
|
|
111
127
|
wasm_bindgen::JsValue::from_serde_ext(&plugin_config_snapshot).unwrap();
|
|
112
|
-
let columns_config =
|
|
113
|
-
|
|
128
|
+
let columns_config = renderer
|
|
129
|
+
.all_columns_configs_materialized(&view_config_snapshot, &session)
|
|
130
|
+
.await;
|
|
114
131
|
plugin.restore(&plugin_update, Some(&columns_config))?;
|
|
115
132
|
if plugin_config_changed {
|
|
116
133
|
renderer.plugin_config_changed.emit(plugin_config_snapshot);
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
136
|
|
|
120
|
-
// The previous call which acquired the lock errored, so skip this render
|
|
121
|
-
if let Some(error) = session.get_error() {
|
|
122
|
-
return Err(error);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let view = session.validate().await?.create_view().await;
|
|
126
137
|
if !presentation.is_visible() {
|
|
127
138
|
Ok(None)
|
|
128
139
|
} else {
|
|
129
|
-
view
|
|
140
|
+
Ok(view)
|
|
130
141
|
}
|
|
131
142
|
});
|
|
132
143
|
|
|
@@ -38,8 +38,9 @@ pub fn send_column_config(
|
|
|
38
38
|
clone!(session, renderer);
|
|
39
39
|
ApiFuture::spawn(async move {
|
|
40
40
|
let view_config_snapshot = session.get_view_config().clone();
|
|
41
|
-
let columns_configs =
|
|
42
|
-
|
|
41
|
+
let columns_configs = renderer
|
|
42
|
+
.all_columns_configs_materialized(&view_config_snapshot, &session)
|
|
43
|
+
.await;
|
|
43
44
|
let plugin_token =
|
|
44
45
|
wasm_bindgen::JsValue::from_serde_ext(&renderer.get_plugin_config()).unwrap();
|
|
45
46
|
renderer
|