@perspective-dev/viewer 5.5.1 → 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 (194) 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/PanelViewerConfig.d.ts +7 -6
  43. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  44. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  45. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  46. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  47. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  48. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  49. package/dist/wasm/perspective-viewer.d.ts +115 -35
  50. package/dist/wasm/perspective-viewer.js +314 -190
  51. package/dist/wasm/perspective-viewer.wasm +0 -0
  52. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  53. package/package.json +31 -18
  54. package/src/css/column-selector.css +205 -50
  55. package/src/css/ui/containers/context-menu.css +12 -1
  56. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  57. package/src/css/ui/containers/scroll-panel.css +4 -1
  58. package/src/rust/agent/client/protocol.rs +0 -89
  59. package/src/rust/agent/markdown/mod.rs +0 -238
  60. package/src/rust/agent/preamble.md +1 -0
  61. package/src/rust/agent/tools.rs +2 -16
  62. package/src/rust/components/column_selector/active_column.rs +15 -5
  63. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  64. package/src/rust/components/column_selector/config_selector.rs +34 -6
  65. package/src/rust/components/column_selector/derived_control.rs +229 -0
  66. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  67. package/src/rust/components/column_selector/filter_column.rs +0 -2
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  69. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  70. package/src/rust/components/column_selector/sort_column.rs +4 -3
  71. package/src/rust/components/column_selector.rs +21 -1
  72. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  73. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  74. package/src/rust/components/column_settings_sidebar.rs +25 -19
  75. package/src/rust/components/debug_panel.rs +38 -29
  76. package/src/rust/components/dragdrop_list.rs +8 -9
  77. package/src/rust/components/main_panel/msg.rs +6 -4
  78. package/src/rust/components/main_panel/reconcile.rs +6 -1
  79. package/src/rust/components/main_panel/render.rs +70 -51
  80. package/src/rust/components/main_panel/update.rs +39 -1
  81. package/src/rust/components/main_panel.rs +97 -59
  82. package/src/rust/components/new_panel_menu.rs +69 -1
  83. package/src/rust/components/panel_menu.rs +63 -12
  84. package/src/rust/components/plugin_tab.rs +3 -2
  85. package/src/rust/components/render_warning.rs +2 -1
  86. package/src/rust/components/settings_panel.rs +4 -4
  87. package/src/rust/components/status_bar.rs +74 -24
  88. package/src/rust/components/status_indicator.rs +8 -1
  89. package/src/rust/components/viewer/filters.rs +13 -1
  90. package/src/rust/components/viewer/msg.rs +4 -0
  91. package/src/rust/components/viewer/panels.rs +41 -1
  92. package/src/rust/components/viewer/render.rs +1 -0
  93. package/src/rust/components/viewer/settings.rs +0 -22
  94. package/src/rust/components/viewer/snapshots.rs +2 -2
  95. package/src/rust/components/viewer/wiring.rs +14 -0
  96. package/src/rust/components/viewer.rs +45 -25
  97. package/src/rust/components/window_editor.rs +5 -4
  98. package/src/rust/config/column_config_schema.rs +38 -377
  99. package/src/rust/config/number_string_format.rs +0 -94
  100. package/src/rust/config/options.rs +3 -7
  101. package/src/rust/config/plugin_static_config.rs +0 -42
  102. package/src/rust/config/viewer_config.rs +47 -17
  103. package/src/rust/config/workspace_config.rs +158 -128
  104. package/src/rust/custom_elements/viewer.rs +187 -131
  105. package/src/rust/custom_events.rs +33 -18
  106. package/src/rust/derived.rs +266 -0
  107. package/src/rust/exprtk/tokenize.rs +0 -54
  108. package/src/rust/js/plugin.rs +1 -1
  109. package/src/rust/lib.rs +3 -0
  110. package/src/rust/presentation.rs +23 -4
  111. package/src/rust/queries/column_locator.rs +55 -48
  112. package/src/rust/queries/get_viewer_config.rs +1 -1
  113. package/src/rust/queries/plugin_column_styles.rs +22 -23
  114. package/src/rust/renderer/dispatch.rs +2 -0
  115. package/src/rust/renderer/plugin_config.rs +652 -487
  116. package/src/rust/renderer/props.rs +4 -0
  117. package/src/rust/renderer/registry.rs +2 -6
  118. package/src/rust/renderer/state.rs +205 -0
  119. package/src/rust/renderer.rs +201 -59
  120. package/src/rust/session/metadata.rs +30 -9
  121. package/src/rust/session/op_queue.rs +430 -0
  122. package/src/rust/session/panel_state.rs +327 -0
  123. package/src/rust/session/view_subscription.rs +42 -3
  124. package/src/rust/session.rs +900 -395
  125. package/src/rust/tasks/apply_global_filters.rs +49 -35
  126. package/src/rust/tasks/auto_pause.rs +22 -12
  127. package/src/rust/tasks/copy_export.rs +5 -0
  128. package/src/rust/tasks/create_panel.rs +34 -9
  129. package/src/rust/tasks/derive.rs +312 -0
  130. package/src/rust/tasks/mod.rs +6 -2
  131. package/src/rust/tasks/pipeline.rs +145 -89
  132. package/src/rust/tasks/reset_all.rs +31 -20
  133. package/src/rust/tasks/restore_derived.rs +200 -0
  134. package/src/rust/tasks/restore_panel.rs +153 -141
  135. package/src/rust/tasks/send_column_config.rs +56 -29
  136. package/src/rust/tasks/send_plugin_config.rs +52 -23
  137. package/src/rust/tasks/set_edit_mode.rs +33 -25
  138. package/src/rust/tasks/sync_update_panels.rs +39 -12
  139. package/src/rust/tasks/table_lifecycle.rs +70 -34
  140. package/src/rust/tasks/transactional_restore.rs +387 -0
  141. package/src/rust/tasks/update_theme.rs +45 -6
  142. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  143. package/src/rust/ui/containers/split_panel.rs +0 -41
  144. package/src/rust/ui/context_menu.rs +157 -18
  145. package/src/rust/ui/font_loader.rs +23 -35
  146. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  147. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  148. package/src/rust/ui/form/palette_selector.rs +0 -51
  149. package/src/rust/ui/portal.rs +4 -10
  150. package/src/rust/utils/browser/mod.rs +2 -3
  151. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  152. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  153. package/src/rust/utils/css.rs +0 -360
  154. package/src/rust/utils/local_fonts.rs +0 -36
  155. package/src/rust/utils/mod.rs +2 -3
  156. package/src/rust/utils/modal_position.rs +15 -21
  157. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  158. package/src/rust/utils/pubsub.rs +0 -1
  159. package/src/rust/workspace.rs +187 -140
  160. package/src/svg/derived.svg +6 -0
  161. package/src/themes/icons.css +1 -0
  162. package/src/themes/intl/ar.css +10 -0
  163. package/src/themes/intl/de.css +10 -0
  164. package/src/themes/intl/es.css +10 -0
  165. package/src/themes/intl/fr.css +10 -0
  166. package/src/themes/intl/ja.css +10 -0
  167. package/src/themes/intl/pt.css +10 -0
  168. package/src/themes/intl/zh.css +10 -0
  169. package/src/themes/intl.css +10 -0
  170. package/src/ts/extensions.ts +3 -36
  171. package/src/ts/perspective-viewer.ts +1 -1
  172. package/src/ts/plugin.ts +4 -1
  173. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  174. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  175. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  176. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  177. package/src/ts/ts-rs/TableRef.ts +9 -0
  178. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  179. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  180. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  181. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  182. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  183. package/dist/esm/perspective-viewer.inline.js +0 -21
  184. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  185. package/src/rust/tasks/restore_and_render.rs +0 -107
  186. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  187. package/src/rust/utils/tests/mod.rs +0 -14
  188. package/src/rust/utils/tests/pubsub.rs +0 -95
  189. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  190. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  191. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  192. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  193. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  194. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -10,50 +10,64 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use perspective_client::config::ViewConfigUpdate;
13
+ use std::rc::Rc;
14
14
 
15
- use super::apply_and_render;
16
- use crate::session::Session;
15
+ use futures::FutureExt;
16
+ use perspective_client::clone;
17
+ use perspective_js::utils::ApiFuture;
18
+
19
+ use super::pipeline::{RunCommit, render_run};
20
+ use super::transactional_restore::{commit_edit, prepare_overlay};
21
+ use crate::session::{OpKind, OverlayClause};
17
22
  use crate::utils::spawn_owned;
18
- use crate::workspace::{PanelId, Workspace};
23
+ use crate::workspace::{Panel, PanelId, Workspace};
19
24
 
20
- /// Stamp `session`'s transient global-filter overlay from the
21
- /// [`Workspace`]'s current set — EMPTY for master (filter-source) panels,
22
- /// the full set for details. Master immunity is enforced here and only
23
- /// here. Returns whether the overlay changed.
24
- ///
25
- /// Synchronous, so panel-creation paths can stamp BEFORE the panel's first
26
- /// locked bind — the initial render then picks the overlay up via its
27
- /// `ConfigSnapshot.effective`, with no second render and no unfiltered
28
- /// first paint.
29
- pub fn stamp_global_overlay(workspace: &Workspace, id: &PanelId, session: &Session) -> bool {
30
- let filters = if workspace.is_master(id) {
25
+ /// The element's global filter as broadcast to panel `id` — EMPTY for master
26
+ /// (filter-source) panels, the full set for details.
27
+ pub(crate) fn overlay_for(workspace: &Workspace, id: &PanelId) -> Rc<Vec<OverlayClause>> {
28
+ Rc::new(if workspace.is_master(id) {
31
29
  Vec::new()
32
30
  } else {
33
- workspace.global_filters()
34
- };
31
+ workspace.overlay()
32
+ })
33
+ }
34
+
35
+ /// Broadcast the [`Workspace`]'s current global filter to one panel, as an op
36
+ /// on ITS queue: the overlay is panel state, committed with the description of
37
+ /// the config it yields, like any other write.
38
+ pub fn broadcast_overlay(workspace: &Workspace, panel: &Panel) -> ApiFuture<()> {
39
+ let overlay = overlay_for(workspace, &panel.id);
40
+ clone!(panel.session, panel.renderer);
41
+ let ticket = panel.session.submit(OpKind::Overlay, move |_ctx| {
42
+ Box::pin(async move {
43
+ if *session.committed_overlay() == *overlay {
44
+ return Ok(None);
45
+ }
46
+
47
+ let bound = session.get_table().is_some();
48
+ let prepared = prepare_overlay(&session, &renderer, overlay).await?;
49
+ let committed = commit_edit(&session, &renderer, prepared);
50
+ Ok(if bound {
51
+ Some(render_run(session, renderer, RunCommit::Done(committed)).boxed_local())
52
+ } else {
53
+ None
54
+ })
55
+ })
56
+ });
35
57
 
36
- session.set_global_filter(filters)
58
+ ApiFuture::new(ticket.settle())
37
59
  }
38
60
 
39
- /// Re-stamp every panel's overlay and re-render those whose overlay CHANGED
40
- /// (details are usually non-active, so they need an explicit re-render
41
- /// rather than relying on a subscription; unchanged panels aren't touched
42
- /// at all).
61
+ /// Broadcast the global filter to every panel; panels whose overlay is
62
+ /// unchanged aren't touched at all.
43
63
  pub fn apply_global_filters(workspace: &Workspace) {
44
- for pid in workspace.panel_ids() {
45
- if let Some(panel) = workspace.panel(&pid)
46
- && stamp_global_overlay(workspace, &pid, &panel.session)
47
- {
48
- let effect = workspace.effects().guard();
49
- let session = panel.session.clone();
50
- let renderer = panel.renderer.clone();
51
- spawn_owned("apply-global-filters", async move {
52
- let _effect = effect;
53
- apply_and_render(&session, &renderer, ViewConfigUpdate::default())?.await?;
54
- Ok(())
55
- });
56
- }
64
+ for panel in workspace.panels() {
65
+ let effect = workspace.effects().guard();
66
+ let task = broadcast_overlay(workspace, &panel);
67
+ spawn_owned("apply-global-filters", async move {
68
+ let _effect = effect;
69
+ task.await
70
+ });
57
71
  }
58
72
  }
59
73
 
@@ -18,11 +18,12 @@ use wasm_bindgen::JsCast;
18
18
  use wasm_bindgen::prelude::*;
19
19
  use web_sys::*;
20
20
 
21
+ use super::transactional_restore::restore_in_place;
21
22
  use crate::config::ViewerConfigUpdate;
22
23
  use crate::js::*;
23
24
  use crate::presentation::Presentation;
24
25
  use crate::renderer::*;
25
- use crate::session::Session;
26
+ use crate::session::{OpKind, Session};
26
27
  use crate::utils::*;
27
28
  use crate::workspace::Workspace;
28
29
  use crate::*;
@@ -148,19 +149,28 @@ pub(crate) async fn set_panel_paused(
148
149
  ) -> ApiResult<()> {
149
150
  if visible {
150
151
  if session.set_pause(false) {
151
- let result = super::restore_and_render(
152
- session,
153
- renderer,
154
- presentation,
155
- super::RunOrigin::Internal,
156
- ViewerConfigUpdate::default(),
157
- async move { Ok(()) },
158
- )
159
- .await;
152
+ let ticket = session.submit(OpKind::Restore { update: None }, {
153
+ clone!(session, renderer, presentation);
154
+ move |_ctx| {
155
+ Box::pin(async move {
156
+ restore_in_place(
157
+ &session,
158
+ &renderer,
159
+ &presentation,
160
+ super::RunOrigin::Internal,
161
+ ViewerConfigUpdate::default(),
162
+ )
163
+ .await?;
164
+
165
+ Ok(None)
166
+ })
167
+ }
168
+ });
169
+
170
+ let result = ticket.settle().await;
160
171
 
161
172
  if let Err(e) = result.ignore_view_delete() {
162
- session.set_run_error(e.clone()).await?;
163
- return Err(e);
173
+ return renderer.fail(e);
164
174
  }
165
175
  }
166
176
  } else {
@@ -29,6 +29,7 @@ use crate::queries::{export_app, get_viewer_config};
29
29
  use crate::renderer::Renderer;
30
30
  use crate::session::Session;
31
31
  use crate::utils::*;
32
+ use crate::workspace::Workspace;
32
33
 
33
34
  fn tag_name_to_package(plugin: &JsPerspectiveViewerPlugin) -> String {
34
35
  let tag_name = plugin.unchecked_ref::<web_sys::HtmlElement>().tag_name();
@@ -118,6 +119,7 @@ pub async fn export_method_to_blob(
118
119
  session: &Session,
119
120
  renderer: &Renderer,
120
121
  presentation: &Presentation,
122
+ workspace: &Workspace,
121
123
  method: ExportMethod,
122
124
  ) -> ApiResult<web_sys::Blob> {
123
125
  let viewport = renderer.get_selection();
@@ -197,6 +199,7 @@ pub async fn export_method_to_blob(
197
199
  ExportMethod::JsonConfig => js_sys::JSON::stringify(
198
200
  &get_viewer_config(session, renderer, presentation)
199
201
  .await?
202
+ .map_table(|name| workspace.inline_table_ref(&name))
200
203
  .encode()?,
201
204
  )?
202
205
  .as_blob(),
@@ -208,6 +211,7 @@ pub async fn export_method_to_jsvalue(
208
211
  session: &Session,
209
212
  renderer: &Renderer,
210
213
  presentation: &Presentation,
214
+ workspace: &Workspace,
211
215
  method: ExportMethod,
212
216
  ) -> ApiResult<JsValue> {
213
217
  let viewport = renderer.get_selection();
@@ -286,6 +290,7 @@ pub async fn export_method_to_jsvalue(
286
290
  ExportMethod::Plugin => txt_as_jsvalue(session, renderer, viewport).await?.into(),
287
291
  ExportMethod::JsonConfig => get_viewer_config(session, renderer, presentation)
288
292
  .await?
293
+ .map_table(|name| workspace.inline_table_ref(&name))
289
294
  .encode()?,
290
295
  })
291
296
  }
@@ -31,11 +31,18 @@ use crate::*;
31
31
  pub(crate) fn wire_panel_subs(
32
32
  elem: &HtmlElement,
33
33
  presentation: &Presentation,
34
+ workspace: &Workspace,
34
35
  session: &Session,
35
36
  renderer: &Renderer,
36
37
  ) -> Vec<Subscription> {
37
38
  let mut subs = vec![wire_panel_render_sub(session, renderer)];
38
- subs.extend(wire_panel_events(elem, session, renderer, presentation));
39
+ subs.extend(wire_panel_events(
40
+ elem,
41
+ session,
42
+ renderer,
43
+ presentation,
44
+ workspace,
45
+ ));
39
46
  subs
40
47
  }
41
48
 
@@ -97,7 +104,6 @@ pub(crate) async fn create_panel(
97
104
  Placement::Staged,
98
105
  );
99
106
 
100
- stamp_global_overlay(workspace, &id, &session);
101
107
  notify.emit(());
102
108
 
103
109
  ApiFuture::spawn({
@@ -123,12 +129,12 @@ pub(crate) async fn create_panel(
123
129
  update,
124
130
  crate::tasks::RestoreErrors::Publish,
125
131
  missing,
132
+ None,
126
133
  )
127
134
  .await;
128
135
 
129
- // Promote on completion AND error — a failed restore's error state
130
- // must become visible too.
131
- if workspace.promote(&id) {
136
+ let discarded = discard_rejected(workspace, &id, &session, &result).await?;
137
+ if discarded || workspace.promote(&id) {
132
138
  notify.emit(());
133
139
  }
134
140
 
@@ -136,6 +142,26 @@ pub(crate) async fn create_panel(
136
142
  Ok(id)
137
143
  }
138
144
 
145
+ /// Remove the panel a rejected CREATING restore was for.
146
+ pub(crate) async fn discard_rejected(
147
+ workspace: &Workspace,
148
+ id: &PanelId,
149
+ session: &Session,
150
+ result: &ApiResult<()>,
151
+ ) -> ApiResult<bool> {
152
+ if result.is_ok() || session.get_table().is_some() || session.pending_table().is_some() {
153
+ return Ok(false);
154
+ }
155
+
156
+ match workspace.remove_panel(id) {
157
+ Some(panel) => {
158
+ eject_panel(panel, Disposal::Resolve).await?;
159
+ Ok(true)
160
+ },
161
+ None => Ok(false),
162
+ }
163
+ }
164
+
139
165
  /// Where [`create_panel_model`] registers the new panel model.
140
166
  pub(crate) enum Placement {
141
167
  /// Into the placed panel set at [`PanelPhase::Placed`] — a live, visible
@@ -173,7 +199,7 @@ pub(crate) fn create_panel_model(
173
199
  let renderer = Renderer::new(elem);
174
200
  let id = id.unwrap_or_else(|| workspace.generate_id());
175
201
  renderer.set_slot_name(id.as_str());
176
- let subs = wire_panel_subs(elem, presentation, &session, &renderer);
202
+ let subs = wire_panel_subs(elem, presentation, workspace, &session, &renderer);
177
203
  let panel = Panel::new(id.clone(), session.clone(), renderer.clone(), subs);
178
204
  match placement {
179
205
  Placement::Placed => workspace.insert_panel(panel, PanelPhase::Placed),
@@ -182,7 +208,7 @@ pub(crate) fn create_panel_model(
182
208
  }
183
209
 
184
210
  update.settings = OptionalUpdate::Missing;
185
- renderer.set_theme(match &update.theme {
211
+ renderer.commit_theme(match &update.theme {
186
212
  OptionalUpdate::Update(theme) => Some(theme.clone()),
187
213
  _ => presentation.active_theme_name_sync(),
188
214
  });
@@ -213,7 +239,6 @@ pub(crate) fn place_reserved(
213
239
  has_table: bool,
214
240
  ) -> Option<Panel> {
215
241
  let panel = workspace.claim_reserved(has_table)?;
216
- stamp_global_overlay(workspace, &panel.id, &panel.session);
217
242
  notify.emit(());
218
243
  Some(panel)
219
244
  }
@@ -223,7 +248,7 @@ pub(crate) fn place_reserved(
223
248
  /// Shared by the root's `ClosePanel` handler and `restoreWorkspace`'s
224
249
  /// batch replacement of the pre-existing panel set.
225
250
  pub(crate) fn eject_panel(panel: Panel, disposal: Disposal) -> ApiFuture<()> {
226
- panel.session.mark_disposed(disposal);
251
+ panel.session.dispose(disposal);
227
252
  ApiFuture::new(async move {
228
253
  panel.renderer.dispose().await?;
229
254
  let was_errored = panel.session.is_errored();
@@ -0,0 +1,312 @@
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
+ //! Create a derived `Table` from a panel's view, and release a panel back to
14
+ //! the `Table` its own derived one reads.
15
+
16
+ use perspective_client::config::{ViewConfig, ViewConfigUpdate};
17
+ use perspective_client::{Client, DeleteOptions, TableData, TableInitOptions};
18
+ use perspective_js::utils::*;
19
+
20
+ use crate::config::{DerivedTableConfig, OptionalUpdate, TableRef, ViewerConfigUpdate};
21
+ use crate::derived::{DerivedSource, DerivedTable, suggest_name};
22
+ use crate::presentation::Presentation;
23
+ use crate::queries::fetch_hosted_tables;
24
+ use crate::renderer::Renderer;
25
+ use crate::session::{MissingTable, Session, probe_table};
26
+ use crate::tasks::{RestoreErrors, RestoreMode, restore_panel};
27
+ use crate::utils::PtrEqRc;
28
+ use crate::workspace::Workspace;
29
+
30
+ /// Whether this panel's `Client` can derive a `Table` from a `View`.
31
+ pub fn can_derive(session: &Session) -> bool {
32
+ session
33
+ .metadata()
34
+ .get_features()
35
+ .is_some_and(|x| x.view_derivations)
36
+ }
37
+
38
+ /// Rewrite an inline derived-table definition in `update.table` to a concrete
39
+ /// hosted name, rebinding to a structurally identical live derivation where
40
+ /// one exists and building (or pending, per `missing`) the rest.
41
+ pub async fn resolve_table_ref(
42
+ workspace: &Workspace,
43
+ update: &mut ViewerConfigUpdate,
44
+ missing: MissingTable,
45
+ ) -> ApiResult<()> {
46
+ if !matches!(&update.table, OptionalUpdate::Update(TableRef::Derived(_))) {
47
+ return Ok(());
48
+ }
49
+
50
+ let OptionalUpdate::Update(table_ref) =
51
+ std::mem::replace(&mut update.table, OptionalUpdate::Missing)
52
+ else {
53
+ return Ok(());
54
+ };
55
+
56
+ let mut levels = vec![];
57
+ let mut cursor = table_ref;
58
+ let base = loop {
59
+ match cursor {
60
+ TableRef::Name(name) => break name,
61
+ TableRef::Derived(config) => {
62
+ levels.push(config.view_config);
63
+ cursor = config.table;
64
+ },
65
+ }
66
+ };
67
+
68
+ let mut pending = workspace.derived_pending();
69
+ let mut taken = fetch_hosted_tables(workspace)
70
+ .await
71
+ .into_iter()
72
+ .flat_map(|(_, tables)| tables)
73
+ .collect::<std::collections::BTreeSet<_>>();
74
+
75
+ taken.extend(pending.keys().cloned());
76
+ let mut parent = base;
77
+ for view_config in levels.into_iter().rev() {
78
+ let config = DerivedTableConfig {
79
+ table: parent.clone(),
80
+ view_config,
81
+ };
82
+
83
+ if let Some(existing) = workspace.derived_find_structural(&config) {
84
+ parent = existing;
85
+ continue;
86
+ }
87
+
88
+ if let Some(existing) = pending.iter().find(|(_, x)| **x == config) {
89
+ parent = existing.0.clone();
90
+ continue;
91
+ }
92
+
93
+ let name = suggest_name(
94
+ &taken,
95
+ &PtrEqRc::new(config.view_config.clone()),
96
+ &Some(config.table.clone()),
97
+ );
98
+
99
+ taken.insert(name.clone());
100
+ let built = match workspace
101
+ .resolve_client_for_table(&config.table, None)
102
+ .await
103
+ {
104
+ Some(client) => {
105
+ build_derived(
106
+ &client,
107
+ workspace,
108
+ &name,
109
+ &config.table,
110
+ &config.view_config,
111
+ )
112
+ .await?
113
+ },
114
+ None => false,
115
+ };
116
+
117
+ if !built {
118
+ if matches!(missing, MissingTable::Error) {
119
+ return Err(ApiError::new(format!("Unknown table \"{}\"", config.table)));
120
+ }
121
+
122
+ pending.insert(name.clone(), config);
123
+ }
124
+
125
+ parent = name;
126
+ }
127
+
128
+ workspace.set_derived_pending(pending);
129
+ update.table = OptionalUpdate::Update(TableRef::Name(parent));
130
+ Ok(())
131
+ }
132
+
133
+ /// Build a derived `Table` named `name` from `parent`'s `view_config`, and
134
+ /// register it on `workspace`.
135
+ pub async fn build_derived(
136
+ client: &Client,
137
+ workspace: &Workspace,
138
+ name: &str,
139
+ parent: &str,
140
+ view_config: &ViewConfig,
141
+ ) -> ApiResult<bool> {
142
+ let Some(parent_table) = probe_table(client, parent, MissingTable::Pend).await? else {
143
+ return Ok(false);
144
+ };
145
+
146
+ // An empty `columns` in a derived DEFINITION means "all columns" (the
147
+ // engine's absent-columns default), not a key-only view — `ViewConfig`'s
148
+ // serde default materializes `[]` for definitions that omit the key.
149
+ let mut update: ViewConfigUpdate = view_config.clone().into();
150
+ if update.columns.as_ref().is_none_or(|x| x.is_empty()) {
151
+ update.columns = None;
152
+ }
153
+
154
+ let view = parent_table.view(Some(update)).await?;
155
+ let derived = match client
156
+ .table(TableData::View(view.clone()), TableInitOptions {
157
+ name: Some(name.to_owned()),
158
+ ..TableInitOptions::default()
159
+ })
160
+ .await
161
+ {
162
+ Ok(derived) => derived,
163
+ Err(err) => {
164
+ view.delete().await?;
165
+ return Err(err.into());
166
+ },
167
+ };
168
+
169
+ workspace.register_derived(
170
+ name.to_owned(),
171
+ DerivedTable::new(
172
+ parent.to_owned(),
173
+ view_config.clone(),
174
+ DerivedSource::Restored(view),
175
+ derived,
176
+ ),
177
+ );
178
+
179
+ Ok(true)
180
+ }
181
+
182
+ /// Promote the panel's current `View` to a derived `Table` named `name`,
183
+ /// consuming that `View` and rebinding the panel to the result.
184
+ pub async fn create_derived(
185
+ session: &Session,
186
+ renderer: &Renderer,
187
+ presentation: &Presentation,
188
+ workspace: &Workspace,
189
+ name: String,
190
+ ) -> ApiResult<()> {
191
+ if !can_derive(session) {
192
+ return Err("This data source cannot derive a Table from a View".into());
193
+ }
194
+
195
+ let client = session.get_client().ok_or("`load()` has not been called")?;
196
+ let table = session.get_table().ok_or("`load()` has not been called")?;
197
+ let parent = table.get_name().to_owned();
198
+ let plugin = renderer.metadata().name.clone();
199
+ let sub = session
200
+ .take_view_sub()
201
+ .ok_or("Panel has no View to derive from")?;
202
+
203
+ let view_config = (*sub.get_view_config()).clone();
204
+ let derived = match client
205
+ .table(TableData::View(sub.get_view().clone()), TableInitOptions {
206
+ name: Some(name.clone()),
207
+ ..TableInitOptions::default()
208
+ })
209
+ .await
210
+ {
211
+ Ok(derived) => derived,
212
+ Err(err) => {
213
+ session.put_view_sub(sub);
214
+ return Err(err.into());
215
+ },
216
+ };
217
+
218
+ // The promoted `View` now feeds the derived `Table`, not this panel —
219
+ // silence its Session-bound stats/update channels, or every PARENT tick
220
+ // keeps writing the parent's stats over the panel's.
221
+ if let Err(err) = sub.quiesce().await {
222
+ session.put_view_sub(sub);
223
+ derived.delete(DeleteOptions::default()).await?;
224
+ return Err(err);
225
+ }
226
+
227
+ workspace.register_derived(
228
+ name.clone(),
229
+ DerivedTable::new(parent, view_config, DerivedSource::Panel(sub), derived),
230
+ );
231
+
232
+ let update = ViewerConfigUpdate {
233
+ table: OptionalUpdate::Update(name.clone().into()),
234
+ plugin: OptionalUpdate::Update(plugin),
235
+ ..ViewerConfigUpdate::default()
236
+ };
237
+
238
+ let restored = restore_panel(
239
+ session,
240
+ renderer,
241
+ presentation,
242
+ workspace,
243
+ RestoreMode::Existing { active: true },
244
+ update,
245
+ RestoreErrors::Publish,
246
+ MissingTable::Error,
247
+ None,
248
+ )
249
+ .await;
250
+
251
+ if restored.is_err()
252
+ && let Some(derived) = workspace.take_derived(&name)
253
+ {
254
+ derived.delete().await?;
255
+ }
256
+
257
+ restored
258
+ }
259
+
260
+ /// Rebind the panel to the `Table` its derived one reads, deleting that
261
+ /// derivation when `delete` and nothing else consumes it.
262
+ pub async fn release_derived(
263
+ session: &Session,
264
+ renderer: &Renderer,
265
+ presentation: &Presentation,
266
+ workspace: &Workspace,
267
+ delete: bool,
268
+ ) -> ApiResult<()> {
269
+ let table = session.get_table().ok_or("Panel has no table")?;
270
+ let name = table.get_name().to_owned();
271
+ let config = workspace
272
+ .derived_config(&name)
273
+ .ok_or("Panel is not bound to a derived table")?;
274
+
275
+ let update = ViewerConfigUpdate {
276
+ table: OptionalUpdate::Update(config.table.clone().into()),
277
+ view_config: config.view_config.into(),
278
+ ..ViewerConfigUpdate::default()
279
+ };
280
+
281
+ restore_panel(
282
+ session,
283
+ renderer,
284
+ presentation,
285
+ workspace,
286
+ RestoreMode::Existing { active: true },
287
+ update,
288
+ RestoreErrors::Publish,
289
+ MissingTable::Error,
290
+ None,
291
+ )
292
+ .await?;
293
+
294
+ if delete {
295
+ delete_derived(workspace, &name).await?;
296
+ }
297
+
298
+ Ok(())
299
+ }
300
+
301
+ /// Delete a derived `Table` that nothing consumes.
302
+ pub async fn delete_derived(workspace: &Workspace, name: &str) -> ApiResult<()> {
303
+ let consumers = workspace.derived_consumers(name);
304
+ if consumers > 0 {
305
+ return Err(format!("`{name}` is still read by {consumers} panels or tables").into());
306
+ }
307
+
308
+ match workspace.take_derived(name) {
309
+ Some(derived) => derived.delete().await,
310
+ None => Err(format!("`{name}` is not a derived table").into()),
311
+ }
312
+ }
@@ -25,6 +25,7 @@ mod apply_global_filters;
25
25
  mod auto_pause;
26
26
  mod copy_export;
27
27
  mod create_panel;
28
+ mod derive;
28
29
  mod dismiss_render_warning;
29
30
  mod edit_expression;
30
31
  mod edit_window;
@@ -33,13 +34,14 @@ mod pipeline;
33
34
  mod presize_panels;
34
35
  mod reset_all;
35
36
  mod resize_observer;
36
- mod restore_and_render;
37
+ mod restore_derived;
37
38
  mod restore_panel;
38
39
  mod send_column_config;
39
40
  mod send_plugin_config;
40
41
  mod set_edit_mode;
41
42
  mod sync_update_panels;
42
43
  mod table_lifecycle;
44
+ mod transactional_restore;
43
45
  mod update_theme;
44
46
  mod validate_expression;
45
47
 
@@ -57,6 +59,7 @@ pub use self::apply_global_filters::*;
57
59
  pub use self::auto_pause::*;
58
60
  pub use self::copy_export::*;
59
61
  pub(crate) use self::create_panel::*;
62
+ pub use self::derive::*;
60
63
  pub use self::dismiss_render_warning::*;
61
64
  pub use self::edit_expression::*;
62
65
  pub use self::edit_window::*;
@@ -65,12 +68,13 @@ pub use self::pipeline::*;
65
68
  pub use self::presize_panels::*;
66
69
  pub use self::reset_all::*;
67
70
  pub use self::resize_observer::*;
68
- pub use self::restore_and_render::*;
71
+ pub use self::restore_derived::*;
69
72
  pub(crate) use self::restore_panel::*;
70
73
  pub use self::send_column_config::*;
71
74
  pub use self::send_plugin_config::*;
72
75
  pub use self::set_edit_mode::*;
73
76
  pub(crate) use self::sync_update_panels::*;
74
77
  pub(crate) use self::table_lifecycle::*;
78
+ pub(crate) use self::transactional_restore::{commit_and_render_locked, prepare};
75
79
  pub use self::update_theme::*;
76
80
  pub use self::validate_expression::*;