@perspective-dev/viewer 4.5.0 → 4.5.2
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.cdn.d.ts +3 -0
- package/dist/esm/perspective-viewer.inline.d.ts +3 -0
- 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/esm/ts-rs/OnUpdateData.d.ts +6 -0
- package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +4 -0
- package/dist/wasm/perspective-viewer.d.ts +15 -15
- package/dist/wasm/perspective-viewer.js +93 -88
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +15 -15
- package/package.json +1 -1
- package/src/{rust/components/style/local_style.rs → css/_column_dropdown.css} +14 -35
- package/src/css/_dropdown_menu.css +14 -0
- package/src/css/_filter_dropdown.css +14 -0
- package/src/css/_function_dropdown.css +14 -0
- package/src/css/_viewer_bundle.css +41 -0
- 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_dropdown.rs +3 -3
- package/src/rust/components/column_selector/active_column.rs +3 -0
- package/src/rust/components/column_selector/aggregate_selector.rs +0 -3
- package/src/rust/components/column_selector/config_selector.rs +0 -3
- package/src/rust/components/column_selector/empty_column.rs +0 -3
- package/src/rust/components/column_selector/filter_column.rs +0 -3
- package/src/rust/components/column_selector/inactive_column.rs +2 -1
- package/src/rust/components/column_selector/invalid_column.rs +0 -4
- package/src/rust/components/column_selector.rs +0 -3
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -0
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +0 -3
- package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +0 -3
- package/src/rust/components/column_settings_sidebar.rs +1 -3
- package/src/rust/components/containers/dragdrop_list.rs +27 -0
- package/src/rust/components/containers/dropdown_menu.rs +1 -3
- package/src/rust/components/containers/scroll_panel.rs +1 -3
- package/src/rust/components/containers/split_panel.rs +0 -4
- package/src/rust/components/containers/tab_list.rs +0 -3
- package/src/rust/components/datetime_column_style.rs +0 -3
- package/src/rust/components/editable_header.rs +15 -0
- package/src/rust/components/empty_row.rs +0 -3
- package/src/rust/components/expression_editor.rs +2 -3
- package/src/rust/components/filter_dropdown.rs +3 -3
- package/src/rust/components/font_loader.rs +2 -2
- package/src/rust/components/form/code_editor.rs +0 -3
- package/src/rust/components/form/debug.rs +0 -4
- package/src/rust/components/function_dropdown.rs +3 -3
- package/src/rust/components/number_series_style.rs +0 -3
- package/src/rust/components/plugin_selector.rs +0 -3
- package/src/rust/components/plugin_tab.rs +0 -5
- package/src/rust/components/portal.rs +5 -2
- package/src/rust/components/render_warning.rs +0 -3
- package/src/rust/components/status_bar.rs +4 -3
- package/src/rust/components/string_column_style.rs +0 -3
- package/src/rust/components/style/mod.rs +60 -27
- package/src/rust/components/style/style_provider.rs +49 -16
- package/src/rust/components/style_controls/number_string_format.rs +1 -8
- package/src/rust/components/type_icon.rs +1 -4
- package/src/rust/components/viewer.rs +79 -5
- package/src/rust/custom_elements/copy_dropdown.rs +6 -2
- package/src/rust/custom_elements/export_dropdown.rs +6 -2
- package/src/rust/lib.rs +0 -1
- package/src/rust/renderer.rs +85 -21
- package/src/rust/session/metadata.rs +11 -1
- package/src/rust/session.rs +14 -1
- 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/botanical.css +2 -0
- package/src/themes/dracula.css +2 -0
- package/src/themes/gruvbox-dark.css +2 -0
- 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/src/themes/monokai.css +2 -0
- package/src/themes/phosphor.css +2 -0
- package/src/themes/pro-dark.css +2 -0
- package/src/themes/vaporwave.css +2 -0
- package/tsconfig.json +1 -1
- package/src/rust/components/style/style_cache.rs +0 -136
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-93064d0d79855d97}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-93064d0d79855d97}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-93064d0d79855d97}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-93064d0d79855d97}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-39ab7da3ca157861 → perspective-viewer-93064d0d79855d97}/inline4.js +0 -0
|
@@ -14,9 +14,6 @@ use perspective_client::config::ColumnType;
|
|
|
14
14
|
use yew::html::IntoPropValue;
|
|
15
15
|
use yew::{Properties, classes, function_component, html};
|
|
16
16
|
|
|
17
|
-
use crate::components::style::LocalStyle;
|
|
18
|
-
use crate::css;
|
|
19
|
-
|
|
20
17
|
#[derive(PartialEq, Debug, Clone, Copy)]
|
|
21
18
|
pub enum TypeIconType {
|
|
22
19
|
Type(ColumnType),
|
|
@@ -31,7 +28,7 @@ pub struct TypeIconProps {
|
|
|
31
28
|
#[function_component(TypeIcon)]
|
|
32
29
|
pub fn type_icon(p: &TypeIconProps) -> yew::Html {
|
|
33
30
|
let class = classes!(p.ty.to_string(), "type-icon");
|
|
34
|
-
html! { <><
|
|
31
|
+
html! { <><span {class} /></> }
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
impl From<ColumnType> for TypeIconType {
|
|
@@ -14,17 +14,18 @@ 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;
|
|
21
23
|
use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
|
|
22
|
-
use super::style::
|
|
24
|
+
use super::style::StyleProvider;
|
|
23
25
|
use crate::components::column_settings_sidebar::ColumnSettingsPanel;
|
|
24
26
|
use crate::components::main_panel::MainPanel;
|
|
25
27
|
use crate::components::settings_panel::{SelectedTab, SettingsPanel};
|
|
26
28
|
use crate::config::*;
|
|
27
|
-
use crate::css;
|
|
28
29
|
use crate::js::JsPerspectiveViewerPlugin;
|
|
29
30
|
use crate::presentation::{
|
|
30
31
|
ColumnLocator, ColumnSettingsTab, DragDropProps, Presentation, PresentationProps,
|
|
@@ -121,6 +122,70 @@ pub struct PerspectiveViewer {
|
|
|
121
122
|
/// Counts in-flight renders (incremented on `view_config_changed`,
|
|
122
123
|
/// decremented on `view_created`). Threaded to `StatusIndicator`.
|
|
123
124
|
update_count: u32,
|
|
125
|
+
|
|
126
|
+
/// Window listeners that toggle the `.shift-active` class on the host
|
|
127
|
+
/// element while the Shift key is held, making Shift-modified affordances
|
|
128
|
+
/// (e.g. inactive column add, active column remove, status-bar reset)
|
|
129
|
+
/// visually discoverable. Stored so the closures outlive `create`.
|
|
130
|
+
_shift_listeners: ShiftListeners,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
struct ShiftListeners {
|
|
134
|
+
elem: web_sys::HtmlElement,
|
|
135
|
+
keydown: Closure<dyn FnMut(KeyboardEvent)>,
|
|
136
|
+
keyup: Closure<dyn FnMut(KeyboardEvent)>,
|
|
137
|
+
blur: Closure<dyn FnMut(FocusEvent)>,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
impl Drop for ShiftListeners {
|
|
141
|
+
fn drop(&mut self) {
|
|
142
|
+
let win = global::window();
|
|
143
|
+
let _ = win
|
|
144
|
+
.remove_event_listener_with_callback("keydown", self.keydown.as_ref().unchecked_ref());
|
|
145
|
+
let _ =
|
|
146
|
+
win.remove_event_listener_with_callback("keyup", self.keyup.as_ref().unchecked_ref());
|
|
147
|
+
let _ = win.remove_event_listener_with_callback("blur", self.blur.as_ref().unchecked_ref());
|
|
148
|
+
let _ = self.elem.class_list().remove_1("shift-active");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn install_shift_listeners(elem: web_sys::HtmlElement) -> ShiftListeners {
|
|
153
|
+
let keydown = {
|
|
154
|
+
let elem = elem.clone();
|
|
155
|
+
Closure::wrap(Box::new(move |event: KeyboardEvent| {
|
|
156
|
+
if event.key() == "Shift" {
|
|
157
|
+
let _ = elem.class_list().add_1("shift-active");
|
|
158
|
+
}
|
|
159
|
+
}) as Box<dyn FnMut(KeyboardEvent)>)
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let keyup = {
|
|
163
|
+
let elem = elem.clone();
|
|
164
|
+
Closure::wrap(Box::new(move |event: KeyboardEvent| {
|
|
165
|
+
if event.key() == "Shift" {
|
|
166
|
+
let _ = elem.class_list().remove_1("shift-active");
|
|
167
|
+
}
|
|
168
|
+
}) as Box<dyn FnMut(KeyboardEvent)>)
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
let blur = {
|
|
172
|
+
let elem = elem.clone();
|
|
173
|
+
Closure::wrap(Box::new(move |_: FocusEvent| {
|
|
174
|
+
let _ = elem.class_list().remove_1("shift-active");
|
|
175
|
+
}) as Box<dyn FnMut(FocusEvent)>)
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
let win = global::window();
|
|
179
|
+
let _ = win.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref());
|
|
180
|
+
let _ = win.add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref());
|
|
181
|
+
let _ = win.add_event_listener_with_callback("blur", blur.as_ref().unchecked_ref());
|
|
182
|
+
|
|
183
|
+
ShiftListeners {
|
|
184
|
+
elem,
|
|
185
|
+
keydown,
|
|
186
|
+
keyup,
|
|
187
|
+
blur,
|
|
188
|
+
}
|
|
124
189
|
}
|
|
125
190
|
|
|
126
191
|
impl Component for PerspectiveViewer {
|
|
@@ -160,6 +225,8 @@ impl Component for PerspectiveViewer {
|
|
|
160
225
|
});
|
|
161
226
|
}
|
|
162
227
|
|
|
228
|
+
let shift_listeners = install_shift_listeners(elem);
|
|
229
|
+
|
|
163
230
|
Self {
|
|
164
231
|
_subscriptions: subscriptions,
|
|
165
232
|
column_settings_panel_width_override: None,
|
|
@@ -178,6 +245,7 @@ impl Component for PerspectiveViewer {
|
|
|
178
245
|
presentation_props,
|
|
179
246
|
dragdrop_props: DragDropProps::default(),
|
|
180
247
|
update_count: 0,
|
|
248
|
+
_shift_listeners: shift_listeners,
|
|
181
249
|
}
|
|
182
250
|
}
|
|
183
251
|
|
|
@@ -539,7 +607,6 @@ impl Component for PerspectiveViewer {
|
|
|
539
607
|
|
|
540
608
|
html! {
|
|
541
609
|
<StyleProvider root={ctx.props().elem.clone()}>
|
|
542
|
-
<LocalStyle href={css!("viewer")} />
|
|
543
610
|
<div id="component_container">
|
|
544
611
|
if is_settings_open {
|
|
545
612
|
<SplitPanel
|
|
@@ -548,8 +615,15 @@ impl Component for PerspectiveViewer {
|
|
|
548
615
|
skip_empty=true
|
|
549
616
|
initial_size={self.settings_panel_width_override}
|
|
550
617
|
on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
|
|
551
|
-
on_resize={
|
|
552
|
-
|
|
618
|
+
on_resize={{
|
|
619
|
+
let size_cb = on_split_panel_resize.clone();
|
|
620
|
+
let resize_cb = resize_callback(&ctx.props().session, &ctx.props().renderer);
|
|
621
|
+
move |x| {
|
|
622
|
+
size_cb.emit(x);
|
|
623
|
+
resize_cb.emit(());
|
|
624
|
+
}
|
|
625
|
+
}}
|
|
626
|
+
on_resize_finished={resize_callback(&ctx.props().session, &ctx.props().renderer)}
|
|
553
627
|
>
|
|
554
628
|
{ settings_panel }
|
|
555
629
|
<div id="main_column_container">
|
|
@@ -22,7 +22,7 @@ use yew::prelude::*;
|
|
|
22
22
|
use super::viewer::PerspectiveViewerElement;
|
|
23
23
|
use crate::components::copy_dropdown::CopyDropDownMenu;
|
|
24
24
|
use crate::components::portal::PortalModal;
|
|
25
|
-
use crate::components::style::StyleProvider;
|
|
25
|
+
use crate::components::style::{StyleProvider, StyleSurface};
|
|
26
26
|
use crate::config::*;
|
|
27
27
|
use crate::js::*;
|
|
28
28
|
use crate::presentation::Presentation;
|
|
@@ -77,9 +77,13 @@ impl Component for CopyDropDownWrapper {
|
|
|
77
77
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
78
78
|
let on_close = ctx.link().callback(|_| CopyDropDownWrapperMsg::Close);
|
|
79
79
|
html! {
|
|
80
|
-
<StyleProvider
|
|
80
|
+
<StyleProvider
|
|
81
|
+
root={ctx.props().custom_element.clone()}
|
|
82
|
+
surface={StyleSurface::DropdownMenu}
|
|
83
|
+
>
|
|
81
84
|
<PortalModal
|
|
82
85
|
tag_name="perspective-copy-menu"
|
|
86
|
+
surface={StyleSurface::DropdownMenu}
|
|
83
87
|
target={self.target.clone()}
|
|
84
88
|
own_focus=true
|
|
85
89
|
{on_close}
|
|
@@ -22,7 +22,7 @@ use yew::prelude::*;
|
|
|
22
22
|
use super::viewer::PerspectiveViewerElement;
|
|
23
23
|
use crate::components::export_dropdown::ExportDropDownMenu;
|
|
24
24
|
use crate::components::portal::PortalModal;
|
|
25
|
-
use crate::components::style::StyleProvider;
|
|
25
|
+
use crate::components::style::{StyleProvider, StyleSurface};
|
|
26
26
|
use crate::config::*;
|
|
27
27
|
use crate::presentation::Presentation;
|
|
28
28
|
use crate::renderer::*;
|
|
@@ -77,9 +77,13 @@ impl Component for ExportDropDownWrapper {
|
|
|
77
77
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
78
78
|
let on_close = ctx.link().callback(|_| ExportDropDownWrapperMsg::Close);
|
|
79
79
|
html! {
|
|
80
|
-
<StyleProvider
|
|
80
|
+
<StyleProvider
|
|
81
|
+
root={ctx.props().custom_element.clone()}
|
|
82
|
+
surface={StyleSurface::DropdownMenu}
|
|
83
|
+
>
|
|
81
84
|
<PortalModal
|
|
82
85
|
tag_name="perspective-export-menu"
|
|
86
|
+
surface={StyleSurface::DropdownMenu}
|
|
83
87
|
target={self.target.clone()}
|
|
84
88
|
own_focus=true
|
|
85
89
|
{on_close}
|
package/src/rust/lib.rs
CHANGED
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,
|
|
@@ -492,6 +544,16 @@ impl Renderer {
|
|
|
492
544
|
.and_then(|idx| names.get(idx))
|
|
493
545
|
.map(|s| s.as_str());
|
|
494
546
|
|
|
547
|
+
// If the view schema itself hasn't been built yet (e.g. restore
|
|
548
|
+
// landed before `create_view` populated `view_schema`), refuse
|
|
549
|
+
// to answer rather than returning an empty schema. The strip
|
|
550
|
+
// pass in `update_columns_configs` treats `Err` as "leave the
|
|
551
|
+
// incoming cfg untouched"; an empty schema, by contrast, would
|
|
552
|
+
// cause `cfg.retain(|k, _| active.contains(k))` to drop every
|
|
553
|
+
// key and zero out user-supplied column config.
|
|
554
|
+
if !session.metadata().has_view_schema() {
|
|
555
|
+
return Err(JsValue::from("view_schema not initialized").into());
|
|
556
|
+
}
|
|
495
557
|
let Some(view_type) = session.metadata().get_column_view_type(column_name) else {
|
|
496
558
|
return Ok(ColumnConfigSchema { fields: vec![] });
|
|
497
559
|
};
|
|
@@ -549,6 +611,8 @@ impl Renderer {
|
|
|
549
611
|
OptionalUpdate::Update(mut map) => {
|
|
550
612
|
let mut changed = false;
|
|
551
613
|
if let Some(s) = &schema {
|
|
614
|
+
let active = s.active_keys();
|
|
615
|
+
map.retain(|k, _| active.contains(k));
|
|
552
616
|
// Default-valued entries in a restore payload
|
|
553
617
|
// semantically reset the key — strip from the
|
|
554
618
|
// map AND clear any existing override in the
|
|
@@ -986,7 +1050,7 @@ impl Renderer {
|
|
|
986
1050
|
}),
|
|
987
1051
|
];
|
|
988
1052
|
|
|
989
|
-
let (x, _, y) = select_all(tasks
|
|
1053
|
+
let (x, _, y) = select_all(tasks).await;
|
|
990
1054
|
x.ok_or_else(|| y.into_iter().next().unwrap())
|
|
991
1055
|
}
|
|
992
1056
|
|
|
@@ -994,8 +1058,8 @@ impl Renderer {
|
|
|
994
1058
|
async fn resize_with_explicit_dimensions(&self) -> TaskResult {
|
|
995
1059
|
let plugin = self.get_active_plugin()?;
|
|
996
1060
|
let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
|
|
997
|
-
let new_width = format!("{}px",
|
|
998
|
-
let new_height = format!("{}px",
|
|
1061
|
+
let new_width = format!("{}px", self.0.borrow().viewer_elem.client_width());
|
|
1062
|
+
let new_height = format!("{}px", self.0.borrow().viewer_elem.client_height());
|
|
999
1063
|
main_panel.style().set_property("width", &new_width)?;
|
|
1000
1064
|
main_panel.style().set_property("height", &new_height)?;
|
|
1001
1065
|
let result = plugin.resize().await;
|
|
@@ -95,6 +95,16 @@ impl SessionMetadata {
|
|
|
95
95
|
Ok(())
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/// Whether `view_schema` has been populated by a prior successful
|
|
99
|
+
/// `create_view`. Used by `Session::create_view` to force a full
|
|
100
|
+
/// build on first run even when `is_clean` happens to be true, so
|
|
101
|
+
/// that downstream consumers (`query_column_config_schema`,
|
|
102
|
+
/// strip-on-write in `update_columns_configs`) always see a real
|
|
103
|
+
/// view schema rather than `None`.
|
|
104
|
+
pub fn has_view_schema(&self) -> bool {
|
|
105
|
+
self.as_ref().and_then(|s| s.view_schema.as_ref()).is_some()
|
|
106
|
+
}
|
|
107
|
+
|
|
98
108
|
pub(super) fn update_expressions(
|
|
99
109
|
&mut self,
|
|
100
110
|
valid_recs: &perspective_client::ExprValidationResult,
|
|
@@ -302,7 +312,7 @@ impl SessionMetadata {
|
|
|
302
312
|
Some(
|
|
303
313
|
self.get_expression_columns()
|
|
304
314
|
.cloned()
|
|
305
|
-
.chain(self.get_table_columns()?.clone()
|
|
315
|
+
.chain(self.get_table_columns()?.clone())
|
|
306
316
|
.map(move |name| {
|
|
307
317
|
self.get_column_table_type(&name)
|
|
308
318
|
.map(|coltype| (name, coltype))
|
package/src/rust/session.rs
CHANGED
|
@@ -760,7 +760,20 @@ impl ValidSession<'_> {
|
|
|
760
760
|
/// `update()` subscription, consuming this `ValidSession<'_>` and returning
|
|
761
761
|
/// the original `&Session`.
|
|
762
762
|
pub async fn create_view(&self) -> Result<Option<View>, ApiError> {
|
|
763
|
-
|
|
763
|
+
// Always consume `is_clean` to preserve the "mark up-to-date"
|
|
764
|
+
// contract of `reset_clean`. Force a full build if no view
|
|
765
|
+
// schema has been built yet — `query_column_config_schema` (and
|
|
766
|
+
// downstream `update_columns_configs`) needs a populated
|
|
767
|
+
// `view_schema` to avoid destructively stripping every key from
|
|
768
|
+
// incoming `columns_config` payloads. Without this fallback, a
|
|
769
|
+
// first restore that lands with `is_clean = true` (e.g., when a
|
|
770
|
+
// prior validate already consumed the flag) would skip the
|
|
771
|
+
// view-schema-building inner block, leaving the schema empty
|
|
772
|
+
// and causing strip-on-write to silently zero out
|
|
773
|
+
// user-supplied column config.
|
|
774
|
+
let was_clean = self.0.reset_clean();
|
|
775
|
+
let needs_schema = !self.0.metadata().has_view_schema();
|
|
776
|
+
if (!was_clean && !self.0.borrow().is_paused) || needs_schema {
|
|
764
777
|
if !self.1 {
|
|
765
778
|
let config = self.0.borrow().config.clone();
|
|
766
779
|
if let Some(sub) = &mut self.0.borrow_mut().view_sub.as_mut() {
|
|
@@ -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
|
|
@@ -36,8 +36,9 @@ pub fn send_plugin_config(session: &Session, renderer: &Renderer, update: Column
|
|
|
36
36
|
let plugin_config = renderer.get_plugin_config();
|
|
37
37
|
let plugin_token = wasm_bindgen::JsValue::from_serde_ext(&plugin_config).unwrap();
|
|
38
38
|
let view_config_snapshot = session.get_view_config().clone();
|
|
39
|
-
let columns_configs =
|
|
40
|
-
|
|
39
|
+
let columns_configs = renderer
|
|
40
|
+
.all_columns_configs_materialized(&view_config_snapshot, &session)
|
|
41
|
+
.await;
|
|
41
42
|
renderer
|
|
42
43
|
.get_active_plugin()?
|
|
43
44
|
.restore(&plugin_token, Some(&columns_configs))?;
|
|
@@ -78,6 +78,13 @@ async fn update_and_render_inner(session: Session, renderer: Renderer) -> ApiRes
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
let plugin_swapped = renderer.apply_pending_plugin()?;
|
|
81
|
+
|
|
82
|
+
// Validate + create the view BEFORE the plugin-swap materialize
|
|
83
|
+
// so the schema query sees fresh `expression_schema` /
|
|
84
|
+
// `view_schema` and `resolve_abs_max` has a bound view that
|
|
85
|
+
// knows about any new expression columns.
|
|
86
|
+
let view = session.validate().await?.create_view().await?;
|
|
87
|
+
|
|
81
88
|
if plugin_swapped {
|
|
82
89
|
// `commit_plugin_idx` already restored the new plugin from its
|
|
83
90
|
// raw bucket; re-run with the materialized snapshot so any
|
|
@@ -91,13 +98,13 @@ async fn update_and_render_inner(session: Session, renderer: Renderer) -> ApiRes
|
|
|
91
98
|
let view_config_snapshot = session.get_view_config().clone();
|
|
92
99
|
let plugin_token = wasm_bindgen::JsValue::from_serde_ext(&renderer.get_plugin_config())
|
|
93
100
|
.unwrap_or(wasm_bindgen::JsValue::NULL);
|
|
94
|
-
let columns_config =
|
|
95
|
-
|
|
101
|
+
let columns_config = renderer
|
|
102
|
+
.all_columns_configs_materialized(&view_config_snapshot, &session)
|
|
103
|
+
.await;
|
|
96
104
|
renderer
|
|
97
105
|
.get_active_plugin()?
|
|
98
106
|
.restore(&plugin_token, Some(&columns_config))?;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
renderer.draw(view.create_view()).await
|
|
109
|
+
renderer.draw(async { Ok(view) }).await
|
|
103
110
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
<defs></defs>
|
|
3
3
|
<path
|
|
4
4
|
d="M 2 0 L 12 0 C 13.105 0 14 0.895 14 2 L 14 12 C 14 13.105 13.105 14 12 14 L 2 14 C 0.895 14 0 13.105 0 12 L 0 2 C 0 0.895 0.895 0 2 0 Z M 3.646 7.354 L 5.646 9.354 L 6 9.707 L 6.354 9.354 L 10.854 4.854 L 10.146 4.146 L 6 8.293 L 4.354 6.646 Z"
|
|
5
|
-
fill="#
|
|
5
|
+
fill="#000000"></path>
|
|
6
6
|
<path d="M4 7L6 9L10.5 4.5" style="visibility: hidden;"></path>
|
|
7
7
|
</svg>
|