@perspective-dev/viewer 5.0.0 → 5.1.0
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 +6 -6
- 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 +9 -9
- 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/PanelViewerConfig.d.ts +2 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +2 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
- package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- package/dist/wasm/perspective-viewer.d.ts +103 -16
- package/dist/wasm/perspective-viewer.js +118 -108
- 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/css/column-selector.css +1 -0
- package/src/css/column-settings-panel.css +67 -2
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +22 -1
- package/src/css/containers/context-menu.css +12 -6
- package/src/css/containers/tabs.css +2 -2
- package/src/css/viewer.css +8 -0
- package/src/rust/components/column_selector/active_column.rs +21 -27
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +7 -1
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/inactive_column.rs +8 -1
- package/src/rust/components/column_selector.rs +20 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +73 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +6 -0
- package/src/rust/components/editable_header.rs +2 -0
- package/src/rust/components/main_panel.rs +5 -0
- package/src/rust/components/mod.rs +1 -0
- package/src/rust/components/panel_menu.rs +7 -0
- package/src/rust/components/panel_tab.rs +5 -0
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/viewer/msg.rs +2 -0
- package/src/rust/components/viewer/render.rs +12 -0
- package/src/rust/components/viewer/settings.rs +33 -1
- package/src/rust/components/viewer.rs +2 -0
- package/src/rust/components/window_editor.rs +1013 -0
- package/src/rust/lib.rs +4 -1
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/queries/columns_iter_set.rs +7 -1
- package/src/rust/renderer/dispatch.rs +85 -58
- package/src/rust/renderer.rs +34 -1
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +66 -9
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/session.rs +7 -5
- package/src/rust/tasks/edit_window.rs +155 -0
- package/src/rust/tasks/mod.rs +2 -0
- package/src/rust/utils/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/debounce.rs +76 -27
- package/src/svg/pin-icon.svg +5 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/de.css +7 -0
- package/src/themes/intl/es.css +7 -0
- package/src/themes/intl/fr.css +7 -0
- package/src/themes/intl/ja.css +7 -0
- package/src/themes/intl/pt.css +7 -0
- package/src/themes/intl/zh.css +7 -0
- package/src/themes/intl.css +10 -1
- package/src/ts/ts-rs/PanelViewerConfig.ts +2 -1
- package/src/ts/ts-rs/ViewerConfig.ts +2 -1
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
- package/src/ts/ts-rs/WindowAggregate.ts +3 -0
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +46 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline5.js +0 -0
package/src/rust/lib.rs
CHANGED
|
@@ -71,14 +71,17 @@ import type {
|
|
|
71
71
|
ColumnType,
|
|
72
72
|
TableInitOptions,
|
|
73
73
|
ColumnWindow,
|
|
74
|
-
ViewWindow,
|
|
74
|
+
ViewWindow,
|
|
75
75
|
TypedArrayWindow,
|
|
76
76
|
OnUpdateOptions,
|
|
77
77
|
JoinOptions,
|
|
78
78
|
UpdateOptions,
|
|
79
79
|
DeleteOptions,
|
|
80
|
+
ViewConfig,
|
|
80
81
|
ViewConfigUpdate,
|
|
81
82
|
SystemInfo,
|
|
83
|
+
Scalar,
|
|
84
|
+
Features,
|
|
82
85
|
} from "@perspective-dev/client";
|
|
83
86
|
|
|
84
87
|
export type * from "../../src/ts/ts-rs/ViewerConfig.d.ts";
|
|
@@ -19,6 +19,7 @@ use std::fmt::Display;
|
|
|
19
19
|
pub enum ColumnLocator {
|
|
20
20
|
Table(String),
|
|
21
21
|
Expression(String),
|
|
22
|
+
Window(String),
|
|
22
23
|
NewExpression,
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -28,7 +29,7 @@ impl ColumnLocator {
|
|
|
28
29
|
/// function will return None.
|
|
29
30
|
pub fn name(&self) -> Option<&String> {
|
|
30
31
|
match self {
|
|
31
|
-
Self::Table(s) | Self::Expression(s) => Some(s),
|
|
32
|
+
Self::Table(s) | Self::Expression(s) | Self::Window(s) => Some(s),
|
|
32
33
|
Self::NewExpression => None,
|
|
33
34
|
}
|
|
34
35
|
}
|
|
@@ -50,6 +51,19 @@ impl ColumnLocator {
|
|
|
50
51
|
pub fn is_new_expr(&self) -> bool {
|
|
51
52
|
matches!(self, ColumnLocator::NewExpression)
|
|
52
53
|
}
|
|
54
|
+
|
|
55
|
+
#[inline(always)]
|
|
56
|
+
pub fn is_saved_window(&self) -> bool {
|
|
57
|
+
matches!(self, ColumnLocator::Window(_))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[inline(always)]
|
|
61
|
+
pub fn is_window_editable(&self) -> bool {
|
|
62
|
+
matches!(
|
|
63
|
+
self,
|
|
64
|
+
ColumnLocator::Window(_) | ColumnLocator::NewExpression
|
|
65
|
+
)
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
|
@@ -57,6 +71,7 @@ impl ColumnLocator {
|
|
|
57
71
|
pub enum ColumnSettingsTab {
|
|
58
72
|
#[default]
|
|
59
73
|
Attributes,
|
|
74
|
+
Window,
|
|
60
75
|
Style,
|
|
61
76
|
}
|
|
62
77
|
|
|
@@ -102,7 +102,8 @@ impl<'a> ColumnsIteratorSet<'a> {
|
|
|
102
102
|
.get_table_columns()
|
|
103
103
|
.map(|x: &Vec<String>| x.len())
|
|
104
104
|
.unwrap_or_default()
|
|
105
|
-
+ self.config.expressions.len()
|
|
105
|
+
+ self.config.expressions.len()
|
|
106
|
+
+ self.config.windows.len();
|
|
106
107
|
|
|
107
108
|
match &self.is_dragover_column {
|
|
108
109
|
Some((to_index, from_column)) => {
|
|
@@ -238,6 +239,11 @@ impl<'a> ColumnsIteratorSet<'a> {
|
|
|
238
239
|
self.order_columns(self.metadata.get_expression_columns())
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
/// Generate an iterator for inactive window columns.
|
|
243
|
+
pub fn window(&'a self) -> impl Iterator<Item = OrderedColumn<'a>> {
|
|
244
|
+
self.order_columns(self.metadata.get_window_columns())
|
|
245
|
+
}
|
|
246
|
+
|
|
241
247
|
/// Generate an iterator for inactive columns, which also shows the columns
|
|
242
248
|
/// in sorted order by type, then name.
|
|
243
249
|
pub fn inactive(&'a self) -> impl Iterator<Item = OrderedColumn<'a>> {
|
|
@@ -25,7 +25,7 @@ use web_sys::*;
|
|
|
25
25
|
|
|
26
26
|
use super::activate::*;
|
|
27
27
|
use super::limits::*;
|
|
28
|
-
use super::{ContextPin, RenderContext, Renderer, SUBPIXEL_EPSILON};
|
|
28
|
+
use super::{ContextPin, GeometryCmd, RenderContext, Renderer, SUBPIXEL_EPSILON};
|
|
29
29
|
use crate::js::plugin::*;
|
|
30
30
|
use crate::session::FreshView;
|
|
31
31
|
use crate::utils::*;
|
|
@@ -133,19 +133,8 @@ impl Renderer {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
pub async fn resize(&self) -> ApiResult<()> {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
draw_mutex
|
|
139
|
-
.debounce_with(|_guard| async move {
|
|
140
|
-
set_timeout(timer.get_throttle()).await?;
|
|
141
|
-
let Some(jsplugin) = self.active_plugin() else {
|
|
142
|
-
return Ok(());
|
|
143
|
-
};
|
|
144
|
-
self.stamp_active(&jsplugin);
|
|
145
|
-
jsplugin.resize().await?;
|
|
146
|
-
Ok(())
|
|
147
|
-
})
|
|
148
|
-
.await
|
|
136
|
+
self.0.geometry_cmd.set(Some(GeometryCmd::Measure));
|
|
137
|
+
self.geometry_task().await.map(|_| ())
|
|
149
138
|
}
|
|
150
139
|
|
|
151
140
|
/// Public one-shot resize to explicit dimensions (`viewer.resize({
|
|
@@ -190,62 +179,100 @@ impl Renderer {
|
|
|
190
179
|
width: f64,
|
|
191
180
|
height: f64,
|
|
192
181
|
) -> ApiResult<Option<js_sys::Function>> {
|
|
193
|
-
|
|
194
|
-
|
|
182
|
+
self.0.geometry_cmd.set(Some(GeometryCmd::Presize {
|
|
183
|
+
translate,
|
|
184
|
+
width,
|
|
185
|
+
height,
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
self.geometry_task().await
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Run the LATEST queued [`GeometryCmd`] on the geometry debounce slot.
|
|
192
|
+
async fn geometry_task(&self) -> ApiResult<Option<js_sys::Function>> {
|
|
193
|
+
let timer = self.render_timer();
|
|
194
|
+
self.0
|
|
195
|
+
.geometry_slot
|
|
196
|
+
.clone()
|
|
195
197
|
.debounce_with(|_guard| async move {
|
|
196
|
-
let Some(
|
|
198
|
+
let Some(cmd) = self.0.geometry_cmd.take() else {
|
|
197
199
|
return Ok(None);
|
|
198
200
|
};
|
|
199
201
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
202
|
+
match cmd {
|
|
203
|
+
GeometryCmd::Measure => {
|
|
204
|
+
set_timeout(timer.get_throttle()).await?;
|
|
205
|
+
let Some(jsplugin) = self.active_plugin() else {
|
|
206
|
+
return Ok(None);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
self.stamp_active(&jsplugin);
|
|
210
|
+
jsplugin.resize().await?;
|
|
211
|
+
Ok(None)
|
|
212
|
+
},
|
|
213
|
+
GeometryCmd::Presize {
|
|
214
|
+
translate,
|
|
215
|
+
width,
|
|
216
|
+
height,
|
|
217
|
+
} => self.run_presize(translate, width, height).await,
|
|
211
218
|
}
|
|
219
|
+
})
|
|
220
|
+
.await
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async fn run_presize(
|
|
224
|
+
&self,
|
|
225
|
+
translate: Option<(f64, f64)>,
|
|
226
|
+
width: f64,
|
|
227
|
+
height: f64,
|
|
228
|
+
) -> ApiResult<Option<js_sys::Function>> {
|
|
229
|
+
let Some(plugin) = self.active_plugin() else {
|
|
230
|
+
return Ok(None);
|
|
231
|
+
};
|
|
212
232
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
self.stamp_active(&plugin);
|
|
234
|
+
self.clear_presize()?;
|
|
235
|
+
if plugin.capabilities().presize {
|
|
236
|
+
return plugin.presize(width, height).await;
|
|
237
|
+
}
|
|
217
238
|
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
|
|
240
|
+
let rect = main_panel.get_bounding_client_rect();
|
|
241
|
+
let size_changed = (height - rect.height()).abs() > SUBPIXEL_EPSILON
|
|
242
|
+
|| (width - rect.width()).abs() > SUBPIXEL_EPSILON;
|
|
220
243
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.style()
|
|
227
|
-
.set_property("height", &format!("{height}px"))?;
|
|
228
|
-
}
|
|
244
|
+
let (dx, dy) = translate.unwrap_or((0.0, 0.0));
|
|
245
|
+
let moved = dx.abs() > SUBPIXEL_EPSILON || dy.abs() > SUBPIXEL_EPSILON;
|
|
246
|
+
if !size_changed && !moved {
|
|
247
|
+
return Ok(None);
|
|
248
|
+
}
|
|
229
249
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
250
|
+
if size_changed {
|
|
251
|
+
main_panel
|
|
252
|
+
.style()
|
|
253
|
+
.set_property("width", &format!("{width}px"))?;
|
|
254
|
+
main_panel
|
|
255
|
+
.style()
|
|
256
|
+
.set_property("height", &format!("{height}px"))?;
|
|
257
|
+
}
|
|
235
258
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
259
|
+
if moved {
|
|
260
|
+
main_panel
|
|
261
|
+
.style()
|
|
262
|
+
.set_property("transform", &format!("translate({dx}px, {dy}px)"))?;
|
|
263
|
+
}
|
|
242
264
|
|
|
243
|
-
|
|
244
|
-
|
|
265
|
+
if size_changed {
|
|
266
|
+
let result = plugin.resize().await;
|
|
267
|
+
if translate.is_none() {
|
|
268
|
+
main_panel.style().set_property("width", "")?;
|
|
269
|
+
main_panel.style().set_property("height", "")?;
|
|
270
|
+
}
|
|
245
271
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
272
|
+
result?;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
Ok(None)
|
|
249
276
|
}
|
|
250
277
|
|
|
251
278
|
/// Remove the inline presize styles applied by [`Self::presize_with_box`].
|
package/src/rust/renderer.rs
CHANGED
|
@@ -53,6 +53,22 @@ use crate::utils::*;
|
|
|
53
53
|
/// presize paths.
|
|
54
54
|
pub(crate) const SUBPIXEL_EPSILON: f64 = 0.5;
|
|
55
55
|
|
|
56
|
+
/// One queued geometry task for the [`RendererData::geometry_slot`] runner.
|
|
57
|
+
/// `Measure` re-measures the plugin's live box (reactive resize); `Presize`
|
|
58
|
+
/// renders an anticipated box ahead of a layout commit. A later command
|
|
59
|
+
/// subsumes an earlier unconsumed one: `Measure` reflects whatever geometry
|
|
60
|
+
/// has committed by run time, and `Presize` anticipates the NEXT commit —
|
|
61
|
+
/// callers issue them in commit order.
|
|
62
|
+
#[derive(Clone, Copy)]
|
|
63
|
+
pub(crate) enum GeometryCmd {
|
|
64
|
+
Measure,
|
|
65
|
+
Presize {
|
|
66
|
+
translate: Option<(f64, f64)>,
|
|
67
|
+
width: f64,
|
|
68
|
+
height: f64,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
/// Everything a plugin may read back during its render, all belonging to
|
|
57
73
|
/// one snapshot (invariant I5). Built once per bound `View` by the render
|
|
58
74
|
/// pipeline and CACHED on the `Renderer`; pinning it for a run is an `Rc`
|
|
@@ -104,6 +120,20 @@ pub struct RendererData {
|
|
|
104
120
|
/// [`Renderer::presize_with_box`] presize calls.
|
|
105
121
|
presize_pending: Cell<u32>,
|
|
106
122
|
|
|
123
|
+
/// Debounce slot for geometry tasks (`resize` / presize), separate from
|
|
124
|
+
/// the default slot data-update redraws coalesce on. Invariant: a
|
|
125
|
+
/// geometry request must never resolve via a parked DATA task — a
|
|
126
|
+
/// pending update's redraw renders at the worker's current dimensions
|
|
127
|
+
/// and cannot subsume a size change (the drag-during-updates permanent
|
|
128
|
+
/// skew).
|
|
129
|
+
geometry_slot: DebounceSlot,
|
|
130
|
+
|
|
131
|
+
/// Latest-wins parameter cell for the geometry slot, consumed by the
|
|
132
|
+
/// runner AT RUN TIME under the lock. Parameters live here rather than
|
|
133
|
+
/// in runner captures so a parked runner can never act on a stale
|
|
134
|
+
/// target when later calls coalesced onto it.
|
|
135
|
+
geometry_cmd: Cell<Option<GeometryCmd>>,
|
|
136
|
+
|
|
107
137
|
/// Fires after every draw/update with the computed render limits.
|
|
108
138
|
pub on_render_limits_changed: RefCell<Option<Callback<RenderLimits>>>,
|
|
109
139
|
|
|
@@ -221,6 +251,7 @@ impl Deref for RendererData {
|
|
|
221
251
|
|
|
222
252
|
impl Renderer {
|
|
223
253
|
pub fn new(viewer_elem: &HtmlElement) -> Self {
|
|
254
|
+
let draw_lock = DebounceMutex::default();
|
|
224
255
|
Self(Rc::new(RendererData {
|
|
225
256
|
plugin_data: RefCell::new(RendererMutData {
|
|
226
257
|
viewer_elem: viewer_elem.clone(),
|
|
@@ -231,7 +262,9 @@ impl Renderer {
|
|
|
231
262
|
timer: MovingWindowRenderTimer::default(),
|
|
232
263
|
plugin_states: HashMap::default(),
|
|
233
264
|
}),
|
|
234
|
-
|
|
265
|
+
geometry_slot: draw_lock.slot(),
|
|
266
|
+
geometry_cmd: Cell::new(None),
|
|
267
|
+
draw_lock,
|
|
235
268
|
plugin_changed: Default::default(),
|
|
236
269
|
style_changed: Default::default(),
|
|
237
270
|
reset_changed: Default::default(),
|
|
@@ -96,9 +96,27 @@ pub impl ViewConfig {
|
|
|
96
96
|
config.filter.retain(|x| x.column() != column);
|
|
97
97
|
update.filter = Some(config.filter.clone());
|
|
98
98
|
},
|
|
99
|
+
DragEffect::Move(
|
|
100
|
+
DragTarget::WindowSource
|
|
101
|
+
| DragTarget::WindowOrderBy
|
|
102
|
+
| DragTarget::WindowPartitionBy,
|
|
103
|
+
) => {
|
|
104
|
+
// Staged origins (window-editor pills) have nothing to remove
|
|
105
|
+
// HERE - the editor's draft is not part of the view config
|
|
106
|
+
// this update mutates. The open `WindowEditor` observes the
|
|
107
|
+
// same `drop_received` event and clears the origin slot in
|
|
108
|
+
// its draft, completing the move.
|
|
109
|
+
},
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
match drop {
|
|
113
|
+
DragTarget::WindowSource
|
|
114
|
+
| DragTarget::WindowOrderBy
|
|
115
|
+
| DragTarget::WindowPartitionBy => {
|
|
116
|
+
// Unreachable: staged drops never build config updates
|
|
117
|
+
// (`DragTarget::is_staged` is excluded by every committing
|
|
118
|
+
// `drop_received` subscriber).
|
|
119
|
+
},
|
|
102
120
|
DragTarget::Active => {
|
|
103
121
|
if !is_swap_to_after_last {
|
|
104
122
|
if is_to_swap || is_from_required {
|
|
@@ -69,6 +69,7 @@ pub struct SessionMetadataState {
|
|
|
69
69
|
edit_port: f64,
|
|
70
70
|
view_schema: Option<HashMap<String, ColumnType>>,
|
|
71
71
|
expr_meta: Option<SessionViewExpressionMetadata>,
|
|
72
|
+
window_schema: HashMap<String, ColumnType>,
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
impl SessionMetadata {
|
|
@@ -95,6 +96,43 @@ impl SessionMetadata {
|
|
|
95
96
|
Ok(())
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/// Records the pre-aggregation types of the config's window columns, so
|
|
100
|
+
/// `get_column_table_type` resolves them like any other derived column.
|
|
101
|
+
/// Types mirror the engine's `t_window_engine::resolve_dtype` rules from
|
|
102
|
+
/// the window's op and its source's table type - independent of any
|
|
103
|
+
/// aggregation the view may also apply.
|
|
104
|
+
pub(super) fn update_windows(
|
|
105
|
+
&mut self,
|
|
106
|
+
windows: &perspective_client::config::Windows,
|
|
107
|
+
) -> ApiResult<()> {
|
|
108
|
+
use perspective_client::config::WindowAggregate;
|
|
109
|
+
let window_schema = windows
|
|
110
|
+
.iter()
|
|
111
|
+
.filter_map(|(name, w)| {
|
|
112
|
+
let source = self.get_column_table_type(&w.column)?;
|
|
113
|
+
let dtype = match w.aggregate {
|
|
114
|
+
WindowAggregate::Sum
|
|
115
|
+
| WindowAggregate::Avg
|
|
116
|
+
| WindowAggregate::Stddev
|
|
117
|
+
| WindowAggregate::Var
|
|
118
|
+
| WindowAggregate::Rate
|
|
119
|
+
| WindowAggregate::Ema
|
|
120
|
+
| WindowAggregate::Diff => ColumnType::Float,
|
|
121
|
+
WindowAggregate::Count => ColumnType::Integer,
|
|
122
|
+
WindowAggregate::Min
|
|
123
|
+
| WindowAggregate::Max
|
|
124
|
+
| WindowAggregate::First
|
|
125
|
+
| WindowAggregate::Last
|
|
126
|
+
| WindowAggregate::Lag
|
|
127
|
+
| WindowAggregate::Lead => source,
|
|
128
|
+
};
|
|
129
|
+
Some((name.clone(), dtype))
|
|
130
|
+
})
|
|
131
|
+
.collect();
|
|
132
|
+
self.as_mut().unwrap().window_schema = window_schema;
|
|
133
|
+
Ok(())
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
/// Whether `view_schema` has been populated by a prior successful
|
|
99
137
|
/// `create_view`. Used by `Session::create_view` to force a full
|
|
100
138
|
/// build on first run even when `is_clean` happens to be true, so
|
|
@@ -212,6 +250,19 @@ impl SessionMetadata {
|
|
|
212
250
|
self.as_ref().map(|meta| &meta.column_names)
|
|
213
251
|
}
|
|
214
252
|
|
|
253
|
+
pub fn is_column_window(&self, name: &str) -> bool {
|
|
254
|
+
self.as_ref()
|
|
255
|
+
.map(|meta| meta.window_schema.contains_key(name))
|
|
256
|
+
.unwrap_or_default()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
pub fn get_window_columns(&self) -> impl Iterator<Item = &'_ String> {
|
|
260
|
+
self.as_ref()
|
|
261
|
+
.map(|meta| meta.window_schema.keys())
|
|
262
|
+
.into_iter()
|
|
263
|
+
.flatten()
|
|
264
|
+
}
|
|
265
|
+
|
|
215
266
|
pub fn is_column_expression(&self, name: &str) -> bool {
|
|
216
267
|
let is_expr: Option<bool> = try {
|
|
217
268
|
self.as_ref()?
|
|
@@ -253,14 +304,18 @@ impl SessionMetadata {
|
|
|
253
304
|
pub fn get_column_table_type(&self, name: &str) -> Option<ColumnType> {
|
|
254
305
|
try {
|
|
255
306
|
let meta = self.as_ref()?;
|
|
256
|
-
meta.table_schema
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
307
|
+
meta.table_schema
|
|
308
|
+
.get(name)
|
|
309
|
+
.cloned()
|
|
310
|
+
.or_else(|| {
|
|
311
|
+
meta.expr_meta
|
|
312
|
+
.as_ref()?
|
|
313
|
+
.expressions
|
|
314
|
+
.expression_schema
|
|
315
|
+
.get(name)
|
|
316
|
+
.cloned()
|
|
317
|
+
})
|
|
318
|
+
.or_else(|| meta.window_schema.get(name).cloned())?
|
|
264
319
|
}
|
|
265
320
|
}
|
|
266
321
|
|
|
@@ -282,7 +337,9 @@ impl SessionMetadata {
|
|
|
282
337
|
/// expressions.
|
|
283
338
|
pub fn locator_name_or_default(&self, locator: &ColumnLocator) -> String {
|
|
284
339
|
match locator {
|
|
285
|
-
ColumnLocator::Table(s) | ColumnLocator::Expression(s)
|
|
340
|
+
ColumnLocator::Table(s) | ColumnLocator::Expression(s) | ColumnLocator::Window(s) => {
|
|
341
|
+
s.clone()
|
|
342
|
+
},
|
|
286
343
|
ColumnLocator::NewExpression => self.make_new_column_name(None),
|
|
287
344
|
}
|
|
288
345
|
}
|
package/src/rust/session.rs
CHANGED
|
@@ -806,11 +806,11 @@ impl Session {
|
|
|
806
806
|
}
|
|
807
807
|
|
|
808
808
|
/// SYNC name validation for a candidate config (I4): every referenced
|
|
809
|
-
/// column must be a table column or an expression
|
|
810
|
-
/// candidate itself (syntactic presence — server-side
|
|
811
|
-
/// run property, not a commit property). Skipped
|
|
812
|
-
/// yet: the config rides along until `load()`
|
|
813
|
-
/// surfaces any residual error on that run.
|
|
809
|
+
/// column must be a table column, or an expression or window column
|
|
810
|
+
/// present in the candidate itself (syntactic presence — server-side
|
|
811
|
+
/// compilability is a run property, not a commit property). Skipped
|
|
812
|
+
/// when no table is bound yet: the config rides along until `load()`
|
|
813
|
+
/// binds one, and the engine surfaces any residual error on that run.
|
|
814
814
|
fn validate_names(&self, config: &ViewConfig) -> ApiResult<()> {
|
|
815
815
|
let table_columns = self.all_columns();
|
|
816
816
|
if table_columns.is_empty() {
|
|
@@ -819,6 +819,7 @@ impl Session {
|
|
|
819
819
|
|
|
820
820
|
let mut allowed: HashSet<&str> = table_columns.iter().map(|x| x.as_str()).collect();
|
|
821
821
|
allowed.extend(config.expressions.0.keys().map(|x| x.as_str()));
|
|
822
|
+
allowed.extend(config.windows.keys().map(|x| x.as_str()));
|
|
822
823
|
let named = config
|
|
823
824
|
.columns
|
|
824
825
|
.iter()
|
|
@@ -1144,6 +1145,7 @@ impl Session {
|
|
|
1144
1145
|
let view = table.view(Some(view_config.into())).await?;
|
|
1145
1146
|
let view_schema = view.schema().await?;
|
|
1146
1147
|
self.metadata_mut().update_view_schema(&view_schema)?;
|
|
1148
|
+
self.metadata_mut().update_windows(&effective.windows)?;
|
|
1147
1149
|
let on_stats = Callback::from({
|
|
1148
1150
|
let this = self.clone();
|
|
1149
1151
|
move |stats| this.update_stats(stats)
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
use perspective_client::clone;
|
|
14
|
+
use perspective_client::config::{Filter, ViewConfigUpdate, WindowSpec};
|
|
15
|
+
|
|
16
|
+
use super::apply_and_render;
|
|
17
|
+
use crate::presentation::{ColumnLocator, ColumnSettingsTab, OpenColumnSettings, Presentation};
|
|
18
|
+
use crate::renderer::Renderer;
|
|
19
|
+
use crate::session::Session;
|
|
20
|
+
use crate::*;
|
|
21
|
+
|
|
22
|
+
fn reopen(presentation: &Presentation, name: String) {
|
|
23
|
+
presentation.set_open_column_settings(Some(OpenColumnSettings {
|
|
24
|
+
locator: Some(ColumnLocator::Window(name)),
|
|
25
|
+
tab: Some(ColumnSettingsTab::Window),
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Insert `spec` into the session's windows as `name` (replacing any spec
|
|
30
|
+
/// under the same key) and re-render. After the render completes, opens the
|
|
31
|
+
/// column-settings drawer on the new window column.
|
|
32
|
+
pub fn save_window(
|
|
33
|
+
session: &Session,
|
|
34
|
+
renderer: &Renderer,
|
|
35
|
+
presentation: &Presentation,
|
|
36
|
+
name: String,
|
|
37
|
+
spec: WindowSpec,
|
|
38
|
+
) -> ApiResult<()> {
|
|
39
|
+
let presentation = presentation.clone();
|
|
40
|
+
let task = {
|
|
41
|
+
let mut windows = session.get_view_config().windows.clone();
|
|
42
|
+
windows.insert(name.clone(), spec);
|
|
43
|
+
apply_and_render(session, renderer, ViewConfigUpdate {
|
|
44
|
+
windows: Some(windows),
|
|
45
|
+
..Default::default()
|
|
46
|
+
})
|
|
47
|
+
}?;
|
|
48
|
+
|
|
49
|
+
ApiFuture::spawn(async move {
|
|
50
|
+
task.await?;
|
|
51
|
+
reopen(&presentation, name);
|
|
52
|
+
Ok(())
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
Ok(())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Replace the window at `old_name` with `spec` and re-render. A rename
|
|
59
|
+
/// rewrites every config reference to the old name - `columns`, `group_by`,
|
|
60
|
+
/// `split_by`, `sort`, `filter` and the `aggregates` key - so usage follows
|
|
61
|
+
/// the column, as expression renames do.
|
|
62
|
+
pub fn update_window(
|
|
63
|
+
session: &Session,
|
|
64
|
+
renderer: &Renderer,
|
|
65
|
+
presentation: &Presentation,
|
|
66
|
+
old_name: String,
|
|
67
|
+
new_name: String,
|
|
68
|
+
spec: WindowSpec,
|
|
69
|
+
) {
|
|
70
|
+
clone!(session, renderer, presentation);
|
|
71
|
+
ApiFuture::spawn(async move {
|
|
72
|
+
let update = {
|
|
73
|
+
let config = session.get_view_config().clone();
|
|
74
|
+
let mut windows = config.windows.clone();
|
|
75
|
+
windows.remove(&old_name);
|
|
76
|
+
windows.insert(new_name.clone(), spec);
|
|
77
|
+
|
|
78
|
+
let rename = |col: &String| -> String {
|
|
79
|
+
if col == &old_name {
|
|
80
|
+
new_name.clone()
|
|
81
|
+
} else {
|
|
82
|
+
col.clone()
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let mut update = ViewConfigUpdate {
|
|
87
|
+
windows: Some(windows),
|
|
88
|
+
..Default::default()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if old_name != new_name {
|
|
92
|
+
update.columns = Some(
|
|
93
|
+
config
|
|
94
|
+
.columns
|
|
95
|
+
.iter()
|
|
96
|
+
.map(|col| col.as_ref().map(&rename))
|
|
97
|
+
.collect(),
|
|
98
|
+
);
|
|
99
|
+
update.group_by = Some(config.group_by.iter().map(&rename).collect());
|
|
100
|
+
update.split_by = Some(config.split_by.iter().map(&rename).collect());
|
|
101
|
+
update.sort = Some(
|
|
102
|
+
config
|
|
103
|
+
.sort
|
|
104
|
+
.iter()
|
|
105
|
+
.map(|sort| {
|
|
106
|
+
let mut sort = sort.clone();
|
|
107
|
+
sort.0 = rename(&sort.0);
|
|
108
|
+
sort
|
|
109
|
+
})
|
|
110
|
+
.collect(),
|
|
111
|
+
);
|
|
112
|
+
update.filter = Some(
|
|
113
|
+
config
|
|
114
|
+
.filter
|
|
115
|
+
.iter()
|
|
116
|
+
.map(|filter| {
|
|
117
|
+
if filter.column() == old_name {
|
|
118
|
+
Filter::new(new_name.as_str(), filter.op(), filter.term().clone())
|
|
119
|
+
} else {
|
|
120
|
+
filter.clone()
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.collect(),
|
|
124
|
+
);
|
|
125
|
+
update.aggregates = Some(
|
|
126
|
+
config
|
|
127
|
+
.aggregates
|
|
128
|
+
.iter()
|
|
129
|
+
.map(|(col, agg)| (rename(col), agg.clone()))
|
|
130
|
+
.collect(),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
update
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
apply_and_render(&session, &renderer, update)?.await?;
|
|
138
|
+
reopen(&presentation, new_name);
|
|
139
|
+
Ok(())
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Remove the window named `name` from the session and re-render.
|
|
144
|
+
pub fn delete_window(session: &Session, renderer: &Renderer, name: &str) -> ApiResult<()> {
|
|
145
|
+
let mut windows = session.get_view_config().windows.clone();
|
|
146
|
+
windows.remove(name);
|
|
147
|
+
let config = ViewConfigUpdate {
|
|
148
|
+
windows: Some(windows),
|
|
149
|
+
..ViewConfigUpdate::default()
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let task = apply_and_render(session, renderer, config)?;
|
|
153
|
+
ApiFuture::spawn(task);
|
|
154
|
+
Ok(())
|
|
155
|
+
}
|
package/src/rust/tasks/mod.rs
CHANGED
|
@@ -27,6 +27,7 @@ mod copy_export;
|
|
|
27
27
|
mod create_panel;
|
|
28
28
|
mod dismiss_render_warning;
|
|
29
29
|
mod edit_expression;
|
|
30
|
+
mod edit_window;
|
|
30
31
|
mod eject;
|
|
31
32
|
mod pipeline;
|
|
32
33
|
mod presize_panels;
|
|
@@ -58,6 +59,7 @@ pub use self::copy_export::*;
|
|
|
58
59
|
pub(crate) use self::create_panel::*;
|
|
59
60
|
pub use self::dismiss_render_warning::*;
|
|
60
61
|
pub use self::edit_expression::*;
|
|
62
|
+
pub use self::edit_window::*;
|
|
61
63
|
pub use self::eject::*;
|
|
62
64
|
pub use self::pipeline::*;
|
|
63
65
|
pub use self::presize_panels::*;
|