@perspective-dev/viewer 5.5.0 → 5.6.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 (201) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
  43. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  44. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  45. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  46. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  47. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  50. package/dist/wasm/perspective-viewer.d.ts +126 -35
  51. package/dist/wasm/perspective-viewer.js +320 -185
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  54. package/package.json +33 -20
  55. package/src/css/column-selector.css +205 -50
  56. package/src/css/ui/containers/context-menu.css +12 -1
  57. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  58. package/src/css/ui/containers/scroll-panel.css +4 -1
  59. package/src/rust/agent/client/protocol.rs +0 -89
  60. package/src/rust/agent/markdown/mod.rs +0 -238
  61. package/src/rust/agent/preamble.md +1 -0
  62. package/src/rust/agent/tools.rs +2 -16
  63. package/src/rust/components/column_selector/active_column.rs +15 -5
  64. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  65. package/src/rust/components/column_selector/config_selector.rs +34 -6
  66. package/src/rust/components/column_selector/derived_control.rs +229 -0
  67. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  68. package/src/rust/components/column_selector/filter_column.rs +0 -2
  69. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  70. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  71. package/src/rust/components/column_selector/sort_column.rs +4 -3
  72. package/src/rust/components/column_selector.rs +21 -1
  73. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  74. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  75. package/src/rust/components/column_settings_sidebar.rs +25 -19
  76. package/src/rust/components/debug_panel.rs +38 -29
  77. package/src/rust/components/dragdrop_list.rs +8 -9
  78. package/src/rust/components/export_dropdown.rs +6 -0
  79. package/src/rust/components/main_panel/msg.rs +6 -4
  80. package/src/rust/components/main_panel/reconcile.rs +6 -1
  81. package/src/rust/components/main_panel/render.rs +70 -51
  82. package/src/rust/components/main_panel/update.rs +39 -1
  83. package/src/rust/components/main_panel.rs +97 -59
  84. package/src/rust/components/new_panel_menu.rs +69 -1
  85. package/src/rust/components/panel_menu.rs +63 -12
  86. package/src/rust/components/plugin_tab.rs +3 -2
  87. package/src/rust/components/render_warning.rs +2 -1
  88. package/src/rust/components/settings_panel.rs +4 -4
  89. package/src/rust/components/status_bar.rs +80 -21
  90. package/src/rust/components/status_indicator.rs +8 -1
  91. package/src/rust/components/viewer/filters.rs +15 -3
  92. package/src/rust/components/viewer/msg.rs +8 -0
  93. package/src/rust/components/viewer/panels.rs +44 -4
  94. package/src/rust/components/viewer/render.rs +1 -0
  95. package/src/rust/components/viewer/settings.rs +0 -22
  96. package/src/rust/components/viewer/snapshots.rs +2 -2
  97. package/src/rust/components/viewer/wiring.rs +18 -1
  98. package/src/rust/components/viewer.rs +47 -27
  99. package/src/rust/components/window_editor.rs +5 -4
  100. package/src/rust/config/column_config_schema.rs +38 -377
  101. package/src/rust/config/export_method.rs +24 -0
  102. package/src/rust/config/number_string_format.rs +0 -94
  103. package/src/rust/config/options.rs +3 -7
  104. package/src/rust/config/plugin_static_config.rs +0 -42
  105. package/src/rust/config/viewer_config.rs +47 -17
  106. package/src/rust/config/workspace_config.rs +158 -128
  107. package/src/rust/custom_elements/viewer.rs +189 -129
  108. package/src/rust/custom_events.rs +33 -18
  109. package/src/rust/derived.rs +266 -0
  110. package/src/rust/exprtk/tokenize.rs +0 -54
  111. package/src/rust/js/plugin.rs +1 -1
  112. package/src/rust/lib.rs +3 -0
  113. package/src/rust/presentation.rs +23 -4
  114. package/src/rust/queries/column_locator.rs +55 -48
  115. package/src/rust/queries/get_viewer_config.rs +1 -1
  116. package/src/rust/queries/plugin_column_styles.rs +22 -23
  117. package/src/rust/renderer/dispatch.rs +2 -0
  118. package/src/rust/renderer/plugin_config.rs +652 -487
  119. package/src/rust/renderer/props.rs +4 -0
  120. package/src/rust/renderer/registry.rs +2 -6
  121. package/src/rust/renderer/state.rs +205 -0
  122. package/src/rust/renderer.rs +201 -59
  123. package/src/rust/session/metadata.rs +35 -19
  124. package/src/rust/session/op_queue.rs +430 -0
  125. package/src/rust/session/panel_state.rs +327 -0
  126. package/src/rust/session/view_subscription.rs +42 -3
  127. package/src/rust/session.rs +931 -419
  128. package/src/rust/tasks/apply_global_filters.rs +49 -35
  129. package/src/rust/tasks/auto_pause.rs +22 -12
  130. package/src/rust/tasks/copy_export.rs +72 -0
  131. package/src/rust/tasks/create_panel.rs +46 -21
  132. package/src/rust/tasks/derive.rs +312 -0
  133. package/src/rust/tasks/mod.rs +6 -2
  134. package/src/rust/tasks/pipeline.rs +152 -89
  135. package/src/rust/tasks/reset_all.rs +31 -20
  136. package/src/rust/tasks/restore_derived.rs +200 -0
  137. package/src/rust/tasks/restore_panel.rs +153 -141
  138. package/src/rust/tasks/send_column_config.rs +56 -29
  139. package/src/rust/tasks/send_plugin_config.rs +52 -23
  140. package/src/rust/tasks/set_edit_mode.rs +33 -25
  141. package/src/rust/tasks/sync_update_panels.rs +40 -13
  142. package/src/rust/tasks/table_lifecycle.rs +70 -34
  143. package/src/rust/tasks/transactional_restore.rs +387 -0
  144. package/src/rust/tasks/update_theme.rs +45 -6
  145. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  146. package/src/rust/ui/containers/split_panel.rs +0 -41
  147. package/src/rust/ui/context_menu.rs +157 -18
  148. package/src/rust/ui/font_loader.rs +23 -35
  149. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  150. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  151. package/src/rust/ui/form/palette_selector.rs +0 -51
  152. package/src/rust/ui/portal.rs +4 -10
  153. package/src/rust/utils/browser/mod.rs +2 -3
  154. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  155. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  156. package/src/rust/utils/css.rs +0 -360
  157. package/src/rust/utils/{tests/mod.rs → in_flight.rs} +62 -2
  158. package/src/rust/utils/local_fonts.rs +0 -36
  159. package/src/rust/utils/mod.rs +4 -3
  160. package/src/rust/utils/modal_position.rs +15 -21
  161. package/src/rust/utils/pending_effects.rs +6 -30
  162. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  163. package/src/rust/utils/pubsub.rs +0 -1
  164. package/src/rust/workspace.rs +187 -140
  165. package/src/svg/derived.svg +6 -0
  166. package/src/themes/icons.css +1 -0
  167. package/src/themes/intl/ar.css +10 -0
  168. package/src/themes/intl/de.css +10 -0
  169. package/src/themes/intl/es.css +10 -0
  170. package/src/themes/intl/fr.css +10 -0
  171. package/src/themes/intl/ja.css +10 -0
  172. package/src/themes/intl/pt.css +10 -0
  173. package/src/themes/intl/zh.css +10 -0
  174. package/src/themes/intl.css +10 -0
  175. package/src/themes/monokai.css +1 -1
  176. package/src/themes/vaporwave.css +1 -1
  177. package/src/ts/extensions.ts +3 -36
  178. package/src/ts/perspective-viewer.ts +1 -1
  179. package/src/ts/plugin.ts +4 -1
  180. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  181. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  182. package/src/ts/ts-rs/ExportMethod.ts +1 -1
  183. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  184. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  185. package/src/ts/ts-rs/TableRef.ts +9 -0
  186. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  187. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  188. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  189. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  190. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  191. package/dist/esm/perspective-viewer.inline.js +0 -21
  192. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  193. package/src/rust/tasks/restore_and_render.rs +0 -107
  194. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  195. package/src/rust/utils/tests/pubsub.rs +0 -95
  196. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  197. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  198. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  199. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  200. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  201. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -17,7 +17,7 @@ use wasm_bindgen::prelude::*;
17
17
  use crate::components::viewer::PerspectiveViewerMsg;
18
18
  use crate::config::*;
19
19
  use crate::renderer::*;
20
- use crate::session::Session;
20
+ use crate::session::{Disposal, Session};
21
21
  use crate::tasks::*;
22
22
  use crate::workspace::PanelId;
23
23
  use crate::*;
@@ -34,6 +34,7 @@ extern "C" {
34
34
  pub type SyncUpdatePanelsResult = (
35
35
  Vec<(PanelId, Session, Renderer, ViewerConfigUpdate)>,
36
36
  std::vec::Vec<perspective_js::utils::ApiFuture<()>>,
37
+ Option<std::collections::BTreeMap<String, crate::config::DerivedTableConfig>>,
37
38
  );
38
39
 
39
40
  /// Synchronously apply a `WorkspaceConfigUpdate` to the element's models
@@ -47,6 +48,7 @@ pub fn sync_update_panels(
47
48
  active,
48
49
  layout,
49
50
  panels,
51
+ derived_tables,
50
52
  global_filters,
51
53
  masters,
52
54
  palette,
@@ -75,12 +77,28 @@ pub fn sync_update_panels(
75
77
  // zero-panel empty stage — the former table-less fallback panel was
76
78
  // exactly the blank-panel state this type exists to preclude.
77
79
  Some(panels) => {
80
+ // Eject the outgoing set FIRST so each saved id is free to be
81
+ // reused — panels keep their SAVED ids (`saveWorkspace` writes
82
+ // them back; `active`/`masters` and user configs address panels
83
+ // by these names across the round-trip).
84
+ if let Some(panel) = this.workspace.take_reserved() {
85
+ panel.renderer.retire_slot();
86
+ eject_tasks.push(eject_panel(panel, Disposal::Reject));
87
+ }
88
+
89
+ for old in &old_ids {
90
+ if let Some(panel) = this.workspace.remove_panel(old) {
91
+ panel.renderer.retire_slot();
92
+ eject_tasks.push(eject_panel(panel, Disposal::Reject));
93
+ }
94
+ }
95
+
78
96
  for (saved_id, config) in panels {
79
97
  let (fresh, session, renderer, config) = create_panel_model(
80
98
  &this.elem,
81
99
  &this.presentation,
82
100
  &this.workspace,
83
- None,
101
+ Some(PanelId::from(saved_id.as_str())),
84
102
  config.into(),
85
103
  None,
86
104
  Placement::Placed,
@@ -89,16 +107,6 @@ pub fn sync_update_panels(
89
107
  id_map.insert(saved_id, fresh.as_str().to_owned());
90
108
  contents.push((fresh, session, renderer, config));
91
109
  }
92
-
93
- if let Some(panel) = this.workspace.take_reserved() {
94
- eject_tasks.push(eject_panel(panel));
95
- }
96
-
97
- for old in &old_ids {
98
- if let Some(panel) = this.workspace.remove_panel(old) {
99
- eject_tasks.push(eject_panel(panel));
100
- }
101
- }
102
110
  },
103
111
  None => {
104
112
  for id in &old_ids {
@@ -119,6 +127,25 @@ pub fn sync_update_panels(
119
127
  if !replace {
120
128
  this.workspace.layout_staged().emit(());
121
129
  }
130
+ } else if replace && contents.len() > 1 {
131
+ // No saved tree: synthesize the equal-split default so a
132
+ // multi-panel replace still mounts in ONE layout commit —
133
+ // per-cell `insertPanel` commits are async, and a commit landing
134
+ // between two inserts drops the later one.
135
+ this.workspace
136
+ .set_pending_layout(crate::js::Layout::Split(crate::js::SplitLayout {
137
+ orientation: crate::js::Orientation::Horizontal,
138
+ sizes: vec![1.0 / contents.len() as f64; contents.len()],
139
+ children: contents
140
+ .iter()
141
+ .map(|(id, ..)| {
142
+ crate::js::Layout::Tab(crate::js::TabLayout {
143
+ tabs: vec![id.as_str().to_owned()],
144
+ selected: Some(0),
145
+ })
146
+ })
147
+ .collect(),
148
+ }));
122
149
  }
123
150
 
124
151
  // Phase 3 — the single visible commit
@@ -222,5 +249,5 @@ pub fn sync_update_panels(
222
249
  OptionalUpdate::Update(filters) => this.workspace.set_global_filters(filters),
223
250
  }
224
251
 
225
- Ok((contents, eject_tasks))
252
+ Ok((contents, eject_tasks, derived_tables))
226
253
  }
@@ -19,8 +19,8 @@ use perspective_js::utils::{ApiFuture, ApiResult, LocalPollLoop};
19
19
  use wasm_bindgen::JsValue;
20
20
 
21
21
  use super::pipeline::RunOrigin;
22
- use super::restore_and_render::restore_and_render;
23
- use crate::config::ViewerConfigUpdate;
22
+ use super::restore_panel::{Rebind, RestoreErrors, RestoreMode, restore_panel_step};
23
+ use crate::config::{OptionalUpdate, ViewerConfigUpdate};
24
24
  use crate::presentation::Presentation;
25
25
  use crate::utils::{AddListener, Subscription};
26
26
  use crate::workspace::{Panel, Workspace};
@@ -86,6 +86,10 @@ pub(crate) async fn sweep_table_bindings(
86
86
  workspace: &Workspace,
87
87
  presentation: &Presentation,
88
88
  ) -> ApiResult<()> {
89
+ if let Err(err) = super::resume_derived(workspace).await {
90
+ tracing::warn!("Failed to resume derived tables: {err}");
91
+ }
92
+
89
93
  for panel in workspace.panels() {
90
94
  if let Some(name) = panel.session.get_table().map(|t| t.get_name().to_owned())
91
95
  && let Some(client) = panel.session.get_client()
@@ -104,6 +108,7 @@ pub(crate) async fn sweep_table_bindings(
104
108
  }
105
109
  }
106
110
 
111
+ workspace.hosted_tables_changed().emit(());
107
112
  Ok(())
108
113
  }
109
114
 
@@ -113,52 +118,83 @@ pub(crate) async fn sweep_table_bindings(
113
118
  /// sweep's observation suspends its CURRENT state or no-ops.
114
119
  async fn suspend_panel(panel: &Panel) -> ApiResult<()> {
115
120
  clone!(panel.session, panel.renderer);
116
- renderer
117
- .clone()
118
- .render_task(|_guard| async move {
119
- if let Some(reset) = session.suspend_table() {
120
- reset.await?;
121
- if let Some(plugin) = renderer.active_plugin() {
122
- plugin.clear().await?;
123
- }
124
- }
121
+ let ticket = panel.session.submit(
122
+ crate::session::OpKind::Restore { update: None },
123
+ move |_ctx| {
124
+ Box::pin(async move {
125
+ renderer
126
+ .clone()
127
+ .render_task(|_guard| async move {
128
+ if let Some(reset) = session.suspend_table() {
129
+ reset.await?;
130
+ if let Some(plugin) = renderer.active_plugin() {
131
+ plugin.clear().await?;
132
+ }
133
+ }
134
+
135
+ Ok(())
136
+ })
137
+ .await?;
138
+
139
+ Ok(None)
140
+ })
141
+ },
142
+ );
125
143
 
126
- Ok(())
127
- })
128
- .await
144
+ ticket.settle().await
129
145
  }
130
146
 
131
- /// Complete a PENDING panel's bind through the shared restore pipeline: the
132
- /// committed view config is untouched, the pending name re-derived inside the
133
- /// locked run (capture-free), the client federated exactly as a `restore`
134
- /// would. A name still un-hosted (the sweep raced a delete) simply re-pends.
147
+ /// Complete a PENDING panel's bind, as the restore it is: the pending name
148
+ /// re-derived inside the op (capture-free), the client federated exactly as a
149
+ /// `restore` would, and the config and buckets committed as INTENT while the
150
+ /// table was away validated against the table that arrived.
135
151
  async fn bind_pending(
136
152
  panel: &Panel,
137
153
  workspace: &Workspace,
138
154
  presentation: &Presentation,
139
155
  ) -> ApiResult<()> {
140
- restore_and_render(
141
- &panel.session,
142
- &panel.renderer,
143
- presentation,
144
- RunOrigin::Internal,
145
- ViewerConfigUpdate::default(),
146
- {
147
- clone!(panel.session, workspace);
148
- async move {
156
+ clone!(panel.session, panel.renderer, workspace, presentation);
157
+ let ticket = panel.session.submit(
158
+ crate::session::OpKind::Restore { update: None },
159
+ move |ctx| {
160
+ Box::pin(async move {
149
161
  let Some(name) = session.pending_table() else {
150
- return Ok(());
162
+ return Ok(None);
163
+ };
164
+
165
+ let update = ViewerConfigUpdate {
166
+ table: OptionalUpdate::Update(name.into()),
167
+ plugin_config: OptionalUpdate::Update(renderer.committed_plugin_config()),
168
+ columns_config: OptionalUpdate::Update(renderer.committed_columns_configs()),
169
+ ..ViewerConfigUpdate::default()
151
170
  };
152
171
 
153
- super::restore_panel::bind_table_task(
172
+ let result = restore_panel_step(
173
+ &ctx,
154
174
  &session,
175
+ &renderer,
176
+ &presentation,
155
177
  &workspace,
156
- name,
178
+ RestoreMode::Existing { active: false },
179
+ Rebind::Complete,
180
+ RunOrigin::Internal,
181
+ update,
182
+ RestoreErrors::Publish,
157
183
  crate::session::MissingTable::Pend,
158
184
  )
159
- .await
160
- }
185
+ .await;
186
+
187
+ if let Err(e) = &result
188
+ && session.pending_table().is_some()
189
+ {
190
+ let _ = renderer.fail(e.clone());
191
+ }
192
+
193
+ result?;
194
+ Ok(None)
195
+ })
161
196
  },
162
- )
163
- .await
197
+ );
198
+
199
+ ticket.settle().await
164
200
  }
@@ -0,0 +1,387 @@
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
+ //! A restore as a TRANSACTION: [`prepare`] makes every check that can fail —
14
+ //! and waits on every round trip — without writing anything; [`commit`] then
15
+ //! replaces the panel's state in ONE swap and cannot fail.
16
+
17
+ use std::rc::Rc;
18
+
19
+ use perspective_client::config::ViewConfigUpdate;
20
+ use perspective_js::utils::*;
21
+
22
+ use super::pipeline::{Committed, RunCommit, RunOrigin, RunSpec, locked_run, run_locked};
23
+ use crate::config::{OptionalUpdate, SettingsUpdate, ThemeUpdate, ViewerConfigUpdate};
24
+ use crate::presentation::Presentation;
25
+ use crate::renderer::{
26
+ PluginRef, PreparedRenderer, Renderer, ValidatedColumnsConfig, ValidatedPluginConfig,
27
+ };
28
+ use crate::session::{
29
+ BindPlan, BindingEffects, OverlayClause, PreparedView, Session, ViewDefaults,
30
+ };
31
+
32
+ /// A restore that has passed every check and only awaits [`commit`].
33
+ pub(crate) struct Prepared {
34
+ state: PreparedState,
35
+ effects: Effects,
36
+ }
37
+
38
+ /// The panel state a [`Prepared`] restore commits.
39
+ struct PreparedState {
40
+ view: PreparedView,
41
+
42
+ /// The plugin selection to commit, when the restore ends on a plugin other
43
+ /// than the one selected (or selects the first).
44
+ plugin: Option<PluginRef>,
45
+
46
+ /// The name of the plugin the restore ends on — whose bucket the validated
47
+ /// bucket updates belong to.
48
+ target_name: String,
49
+ plugin_config: ValidatedPluginConfig,
50
+ columns_config: ValidatedColumnsConfig,
51
+ theme: Option<Option<String>>,
52
+ title: Option<Option<String>>,
53
+ }
54
+
55
+ /// What a restore does to the ELEMENT rather than the panel — applied once the
56
+ /// restore can no longer be rejected, as part of rendering it.
57
+ pub(crate) struct Effects {
58
+ settings: SettingsUpdate,
59
+ host_theme: ThemeUpdate,
60
+ }
61
+
62
+ /// Every fallible and asynchronous part of a restore.
63
+ pub(crate) async fn prepare(
64
+ session: &Session,
65
+ renderer: &Renderer,
66
+ presentation: &Presentation,
67
+ active: bool,
68
+ plan: BindPlan,
69
+ overlay: Option<Rc<Vec<OverlayClause>>>,
70
+ update: ViewerConfigUpdate,
71
+ ) -> ApiResult<Prepared> {
72
+ let ViewerConfigUpdate {
73
+ plugin,
74
+ plugin_config,
75
+ columns_config,
76
+ settings,
77
+ theme,
78
+ title,
79
+ view_config,
80
+ ..
81
+ } = update;
82
+
83
+ renderer.check_plugin_update(&plugin)?;
84
+ let named = renderer.resolve_plugin_update(&plugin);
85
+ let (target, swap_idx) = renderer.resolve_target(&plugin)?;
86
+ let rebinds = !matches!(plan, BindPlan::Keep);
87
+ let defaults = match &named {
88
+ Some((_, metadata)) => ViewDefaults::Swap(metadata),
89
+ None if rebinds && matches!(plugin, OptionalUpdate::Update(_)) => {
90
+ ViewDefaults::Swap(&target.static_config)
91
+ },
92
+ None => ViewDefaults::Rollup(&target.static_config),
93
+ };
94
+ let view = session
95
+ .prepare_view(plan, view_config, defaults, overlay)
96
+ .await?;
97
+ let (plugin_config, columns_config) = renderer.prepare_buckets(
98
+ &target,
99
+ session,
100
+ view.config(),
101
+ view.view_schema(),
102
+ rebinds,
103
+ plugin_config,
104
+ columns_config,
105
+ )?;
106
+
107
+ let panel_theme = match &theme {
108
+ OptionalUpdate::Update(theme) => Some(Some(theme.clone())),
109
+ OptionalUpdate::SetDefault => Some(presentation.get_default_theme_name().await),
110
+ OptionalUpdate::Missing if renderer.committed_theme().is_none() => {
111
+ Some(presentation.get_default_theme_name().await).filter(|x| x.is_some())
112
+ },
113
+ OptionalUpdate::Missing => None,
114
+ };
115
+
116
+ let title = match title {
117
+ OptionalUpdate::Update(title) => Some(Some(title).filter(|x| !x.is_empty())),
118
+ OptionalUpdate::SetDefault => Some(None),
119
+ OptionalUpdate::Missing => None,
120
+ };
121
+
122
+ Ok(Prepared {
123
+ state: PreparedState {
124
+ view,
125
+ plugin: swap_idx.map(|idx| renderer.plugin_ref(idx)).transpose()?,
126
+ target_name: target.static_config.name.clone(),
127
+ plugin_config,
128
+ columns_config,
129
+ theme: panel_theme,
130
+ title,
131
+ },
132
+ effects: Effects {
133
+ settings,
134
+ host_theme: if active {
135
+ theme
136
+ } else {
137
+ OptionalUpdate::Missing
138
+ },
139
+ },
140
+ })
141
+ }
142
+
143
+ /// A UI edit as the transaction it is: the view-config `update` — and the
144
+ /// plugin swap `plugin_idx`, when the control made one — validated against what
145
+ /// the ops AHEAD of it left committed, including the server's `describe`.
146
+ pub(crate) async fn prepare_edit(
147
+ session: &Session,
148
+ renderer: &Renderer,
149
+ update: ViewConfigUpdate,
150
+ plugin_idx: Option<usize>,
151
+ ) -> ApiResult<Prepared> {
152
+ let selected = renderer.committed_plugin_idx();
153
+ let (target, swap_idx) = match plugin_idx.filter(|idx| Some(*idx) != selected) {
154
+ Some(idx) => (renderer.target_at(idx)?, Some(idx)),
155
+ None => renderer.resolve_target(&OptionalUpdate::Missing)?,
156
+ };
157
+
158
+ let view = session
159
+ .prepare_view(BindPlan::Keep, update, ViewDefaults::AsGiven, None)
160
+ .await?
161
+ .projected();
162
+
163
+ edit_of(renderer, view, &target.static_config.name, swap_idx)
164
+ }
165
+
166
+ /// A new global-filter overlay as the transaction it is: the committed config
167
+ /// re-described with the clauses this panel's table honors.
168
+ pub(crate) async fn prepare_overlay(
169
+ session: &Session,
170
+ renderer: &Renderer,
171
+ overlay: Rc<Vec<OverlayClause>>,
172
+ ) -> ApiResult<Prepared> {
173
+ let (target, swap_idx) = renderer.resolve_target(&OptionalUpdate::Missing)?;
174
+ let view = session
175
+ .prepare_view(
176
+ BindPlan::Keep,
177
+ ViewConfigUpdate::default(),
178
+ ViewDefaults::AsGiven,
179
+ Some(overlay),
180
+ )
181
+ .await?;
182
+
183
+ edit_of(renderer, view, &target.static_config.name, swap_idx)
184
+ }
185
+
186
+ fn edit_of(
187
+ renderer: &Renderer,
188
+ view: PreparedView,
189
+ target_name: &str,
190
+ swap_idx: Option<usize>,
191
+ ) -> ApiResult<Prepared> {
192
+ Ok(Prepared {
193
+ state: PreparedState {
194
+ view,
195
+ plugin: swap_idx.map(|idx| renderer.plugin_ref(idx)).transpose()?,
196
+ target_name: target_name.to_owned(),
197
+ plugin_config: ValidatedPluginConfig::Missing,
198
+ columns_config: ValidatedColumnsConfig::Missing,
199
+ theme: None,
200
+ title: None,
201
+ },
202
+ effects: Effects {
203
+ settings: OptionalUpdate::Missing,
204
+ host_theme: OptionalUpdate::Missing,
205
+ },
206
+ })
207
+ }
208
+
209
+ /// Commit a [`prepare_edit`] NOW, in the op's step — so the write is the
210
+ /// drain's, and the render that follows may be detached.
211
+ pub(crate) fn commit_edit(session: &Session, renderer: &Renderer, prepared: Prepared) -> Committed {
212
+ let (committed, binding) = commit(session, renderer, prepared.state);
213
+ debug_assert!(!binding.rebound());
214
+ binding.forget();
215
+ committed
216
+ }
217
+
218
+ /// Commit and render a [`Prepared`] restore under a draw lock the caller
219
+ /// ALREADY holds (`load()`, which holds it across its payload's arrival).
220
+ pub(crate) async fn commit_and_render_locked(
221
+ guard: crate::utils::RenderGuard,
222
+ session: &Session,
223
+ renderer: &Renderer,
224
+ Prepared { state, .. }: Prepared,
225
+ ) -> ApiResult<()> {
226
+ let commit = {
227
+ let (session, renderer) = (session.clone(), renderer.clone());
228
+ Box::new(move || commit(&session, &renderer, state))
229
+ };
230
+
231
+ run_locked(guard, session.clone(), renderer.clone(), RunSpec {
232
+ origin: RunOrigin::Public,
233
+ commit: RunCommit::Deferred(commit),
234
+ presentation: None,
235
+ })
236
+ .await
237
+ }
238
+
239
+ /// Commit a [`Prepared`] restore to both engines, with no `await` between.
240
+ fn commit(
241
+ session: &Session,
242
+ renderer: &Renderer,
243
+ prepared: PreparedState,
244
+ ) -> (Committed, BindingEffects) {
245
+ let PreparedState {
246
+ view,
247
+ plugin,
248
+ target_name,
249
+ plugin_config,
250
+ columns_config,
251
+ theme,
252
+ title,
253
+ } = prepared;
254
+
255
+ let had_plugin = renderer.active_plugin().is_some();
256
+ let activate = plugin.is_some();
257
+ let binding = session.commit_view(view, title);
258
+ let changed = renderer.commit(PreparedRenderer {
259
+ plugin,
260
+ target_name,
261
+ plugin_config,
262
+ columns_config,
263
+ theme,
264
+ });
265
+
266
+ let committed = Committed {
267
+ activate,
268
+ plugin_swapped: activate && had_plugin,
269
+ plugin_config_changed: changed.plugin_config,
270
+ columns_config_changed: changed.columns_config,
271
+ };
272
+
273
+ (committed, binding)
274
+ }
275
+
276
+ /// A restore that leaves the table binding alone, for the host's own re-renders
277
+ /// (a reset, an auto-pause resume): to be called only from a running op's step.
278
+ pub(crate) async fn restore_in_place(
279
+ session: &Session,
280
+ renderer: &Renderer,
281
+ presentation: &Presentation,
282
+ origin: RunOrigin,
283
+ update: ViewerConfigUpdate,
284
+ ) -> ApiResult<()> {
285
+ let prepared = prepare(
286
+ session,
287
+ renderer,
288
+ presentation,
289
+ false,
290
+ BindPlan::Keep,
291
+ None,
292
+ update,
293
+ )
294
+ .await?;
295
+ commit_and_render(session, renderer, presentation, origin, prepared)
296
+ .await
297
+ .result
298
+ }
299
+
300
+ /// Whether a restore that failed had ALREADY committed — what tells a rejection
301
+ /// (the panel is as it was) from a failure to render valid state.
302
+ pub(crate) struct Outcome {
303
+ pub committed: bool,
304
+ pub result: ApiResult<()>,
305
+ }
306
+
307
+ /// Commit a [`Prepared`] restore and render it: element-level effects, then the
308
+ /// locked run, whose first act is the commit — under the draw lock, so no
309
+ /// render of the outgoing state can bind a `View` over the incoming table.
310
+ pub(crate) async fn commit_and_render(
311
+ session: &Session,
312
+ renderer: &Renderer,
313
+ presentation: &Presentation,
314
+ origin: RunOrigin,
315
+ Prepared { state, effects }: Prepared,
316
+ ) -> Outcome {
317
+ let Effects {
318
+ settings,
319
+ host_theme,
320
+ } = effects;
321
+
322
+ let did_commit = std::rc::Rc::new(std::cell::Cell::new(false));
323
+ let result = render(
324
+ session,
325
+ renderer,
326
+ presentation,
327
+ origin,
328
+ settings,
329
+ host_theme,
330
+ {
331
+ let (session, renderer, did_commit) =
332
+ (session.clone(), renderer.clone(), did_commit.clone());
333
+ Box::new(move || {
334
+ did_commit.set(true);
335
+ commit(&session, &renderer, state)
336
+ })
337
+ },
338
+ )
339
+ .await;
340
+
341
+ Outcome {
342
+ committed: did_commit.get(),
343
+ result,
344
+ }
345
+ }
346
+
347
+ #[allow(clippy::too_many_arguments)]
348
+ async fn render(
349
+ session: &Session,
350
+ renderer: &Renderer,
351
+ presentation: &Presentation,
352
+ origin: RunOrigin,
353
+ settings: SettingsUpdate,
354
+ host_theme: ThemeUpdate,
355
+ commit: Box<dyn FnOnce() -> (Committed, BindingEffects)>,
356
+ ) -> ApiResult<()> {
357
+ if let OptionalUpdate::Update(x) = settings {
358
+ presentation.set_settings_attribute(x);
359
+ presentation.set_settings_before_open(x);
360
+ }
361
+
362
+ match host_theme {
363
+ OptionalUpdate::SetDefault => {
364
+ if presentation.get_selected_theme_name().await.is_some() {
365
+ presentation.set_theme_name(None).await?;
366
+ }
367
+ },
368
+ OptionalUpdate::Update(x) => {
369
+ presentation.set_theme_name(Some(&x)).await?;
370
+ },
371
+ OptionalUpdate::Missing => {},
372
+ }
373
+
374
+ locked_run(session, renderer, RunSpec {
375
+ origin,
376
+ commit: RunCommit::Deferred(commit),
377
+ presentation: Some(presentation.clone()),
378
+ })
379
+ .await?;
380
+
381
+ if renderer.needs_restyle() {
382
+ renderer.restyle_all().await?;
383
+ }
384
+
385
+ presentation.publish_theme_config().await?;
386
+ Ok(())
387
+ }