@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.
Files changed (106) hide show
  1. package/dist/cdn/perspective-viewer.js +6 -6
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/esm/perspective-viewer.inline.js +9 -9
  24. package/dist/esm/perspective-viewer.inline.js.map +2 -2
  25. package/dist/esm/perspective-viewer.js +1 -1
  26. package/dist/esm/perspective-viewer.js.map +2 -2
  27. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +2 -0
  28. package/dist/esm/ts-rs/ViewerConfig.d.ts +2 -0
  29. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
  30. package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
  31. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  32. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  33. package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
  34. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  35. package/dist/wasm/perspective-viewer.d.ts +103 -16
  36. package/dist/wasm/perspective-viewer.js +118 -108
  37. package/dist/wasm/perspective-viewer.wasm +0 -0
  38. package/dist/wasm/perspective-viewer.wasm.d.ts +15 -15
  39. package/package.json +1 -1
  40. package/src/css/column-selector.css +1 -0
  41. package/src/css/column-settings-panel.css +67 -2
  42. package/src/css/column-style.css +4 -3
  43. package/src/css/config-selector.css +22 -1
  44. package/src/css/containers/context-menu.css +12 -6
  45. package/src/css/containers/tabs.css +2 -2
  46. package/src/css/viewer.css +8 -0
  47. package/src/rust/components/column_selector/active_column.rs +21 -27
  48. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  49. package/src/rust/components/column_selector/config_selector.rs +7 -1
  50. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  51. package/src/rust/components/column_selector/inactive_column.rs +8 -1
  52. package/src/rust/components/column_selector.rs +20 -3
  53. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  54. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  55. package/src/rust/components/column_settings_sidebar.rs +163 -16
  56. package/src/rust/components/containers/dragdrop_list.rs +73 -3
  57. package/src/rust/components/containers/sidebar.rs +99 -22
  58. package/src/rust/components/containers/split_panel.rs +6 -0
  59. package/src/rust/components/editable_header.rs +2 -0
  60. package/src/rust/components/main_panel.rs +5 -0
  61. package/src/rust/components/mod.rs +1 -0
  62. package/src/rust/components/panel_menu.rs +7 -0
  63. package/src/rust/components/panel_tab.rs +5 -0
  64. package/src/rust/components/portal.rs +7 -0
  65. package/src/rust/components/viewer/msg.rs +2 -0
  66. package/src/rust/components/viewer/render.rs +12 -0
  67. package/src/rust/components/viewer/settings.rs +33 -1
  68. package/src/rust/components/viewer.rs +2 -0
  69. package/src/rust/components/window_editor.rs +1013 -0
  70. package/src/rust/lib.rs +4 -1
  71. package/src/rust/presentation/column_locator.rs +16 -1
  72. package/src/rust/queries/columns_iter_set.rs +7 -1
  73. package/src/rust/renderer/dispatch.rs +85 -58
  74. package/src/rust/renderer.rs +34 -1
  75. package/src/rust/session/drag_drop_update.rs +18 -0
  76. package/src/rust/session/metadata.rs +66 -9
  77. package/src/rust/session/replace_expression_update.rs +1 -0
  78. package/src/rust/session.rs +7 -5
  79. package/src/rust/tasks/edit_window.rs +155 -0
  80. package/src/rust/tasks/mod.rs +2 -0
  81. package/src/rust/utils/browser/dragdrop.rs +12 -0
  82. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  83. package/src/rust/utils/debounce.rs +76 -27
  84. package/src/svg/pin-icon.svg +5 -0
  85. package/src/themes/icons.css +1 -0
  86. package/src/themes/intl/de.css +7 -0
  87. package/src/themes/intl/es.css +7 -0
  88. package/src/themes/intl/fr.css +7 -0
  89. package/src/themes/intl/ja.css +7 -0
  90. package/src/themes/intl/pt.css +7 -0
  91. package/src/themes/intl/zh.css +7 -0
  92. package/src/themes/intl.css +10 -1
  93. package/src/ts/ts-rs/PanelViewerConfig.ts +2 -1
  94. package/src/ts/ts-rs/ViewerConfig.ts +2 -1
  95. package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
  96. package/src/ts/ts-rs/WindowAggregate.ts +3 -0
  97. package/src/ts/ts-rs/WindowSort.ts +9 -0
  98. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  99. package/src/ts/ts-rs/WindowSpec.ts +46 -0
  100. package/src/ts/ts-rs/Windows.ts +10 -0
  101. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline0.js +0 -0
  102. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline1.js +0 -0
  103. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline2.js +0 -0
  104. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline3.js +0 -0
  105. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline4.js +0 -0
  106. /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
- let draw_mutex = self.draw_lock();
137
- let timer = self.render_timer();
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
- let draw_mutex = self.draw_lock();
194
- draw_mutex
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(plugin) = self.active_plugin() else {
198
+ let Some(cmd) = self.0.geometry_cmd.take() else {
197
199
  return Ok(None);
198
200
  };
199
201
 
200
- self.stamp_active(&plugin);
201
- self.clear_presize()?;
202
- let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
203
- let rect = main_panel.get_bounding_client_rect();
204
- let size_changed = (height - rect.height()).abs() > SUBPIXEL_EPSILON
205
- || (width - rect.width()).abs() > SUBPIXEL_EPSILON;
206
-
207
- let (dx, dy) = translate.unwrap_or((0.0, 0.0));
208
- let moved = dx.abs() > SUBPIXEL_EPSILON || dy.abs() > SUBPIXEL_EPSILON;
209
- if !size_changed && !moved {
210
- return Ok(None);
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
- if plugin.capabilities().presize {
214
- if size_changed {
215
- return plugin.presize(width, height).await;
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
- return Ok(None);
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
- if size_changed {
222
- main_panel
223
- .style()
224
- .set_property("width", &format!("{width}px"))?;
225
- main_panel
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
- if moved {
231
- main_panel
232
- .style()
233
- .set_property("transform", &format!("translate({dx}px, {dy}px)"))?;
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
- if size_changed {
237
- let result = plugin.resize().await;
238
- if translate.is_none() {
239
- main_panel.style().set_property("width", "")?;
240
- main_panel.style().set_property("height", "")?;
241
- }
259
+ if moved {
260
+ main_panel
261
+ .style()
262
+ .set_property("transform", &format!("translate({dx}px, {dy}px)"))?;
263
+ }
242
264
 
243
- result?;
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
- Ok(None)
247
- })
248
- .await
272
+ result?;
273
+ }
274
+
275
+ Ok(None)
249
276
  }
250
277
 
251
278
  /// Remove the inline presize styles applied by [`Self::presize_with_box`].
@@ -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
- draw_lock: Default::default(),
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.get(name).cloned().or_else(|| {
257
- meta.expr_meta
258
- .as_ref()?
259
- .expressions
260
- .expression_schema
261
- .get(name)
262
- .cloned()
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) => s.clone(),
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
  }
@@ -126,6 +126,7 @@ pub impl ViewConfig {
126
126
  filter_op: None,
127
127
  group_by_depth: None,
128
128
  group_rollup_mode: None,
129
+ windows: None,
129
130
  }
130
131
  }
131
132
  }
@@ -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 present in the
810
- /// candidate itself (syntactic presence — server-side compilability is a
811
- /// run property, not a commit property). Skipped when no table is bound
812
- /// yet: the config rides along until `load()` binds one, and the engine
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
+ }
@@ -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::*;