@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
@@ -29,12 +29,23 @@ pub type HostedTables = Rc<Vec<(String, Vec<String>)>>;
29
29
  /// `(panel id, title)` per panel, in layout-insertion order.
30
30
  pub type PanelLabels = Rc<Vec<(String, Option<String>)>>;
31
31
 
32
+ /// Consumer count per derived `Table` name, which marks those rows and gates
33
+ /// their delete affordance.
34
+ pub type DerivedCounts = Rc<std::collections::BTreeMap<String, usize>>;
35
+
32
36
  #[derive(Properties, PartialEq)]
33
37
  pub struct NewPanelMenuProps {
34
38
  /// `None` while the hosted-table fetch is in flight.
35
39
  pub tables: Option<HostedTables>,
36
40
  pub panels: PanelLabels,
37
41
  pub callback: Callback<NewPanelPick>,
42
+
43
+ #[prop_or_default]
44
+ pub derived: DerivedCounts,
45
+
46
+ /// Delete a derived `Table`, offered only where nothing reads it.
47
+ #[prop_or_default]
48
+ pub on_delete: Option<Callback<String>>,
38
49
  }
39
50
 
40
51
  /// The "New" menu body shared by the status bar dropdown and the context
@@ -58,7 +69,48 @@ pub fn NewPanelMenu(props: &NewPanelMenuProps) -> Html {
58
69
  client: client.clone(),
59
70
  table: table.clone(),
60
71
  });
61
- html! { <span class="dropdown-menu-item" {onmousedown}>{ table }</span> }
72
+
73
+ let consumers = props.derived.get(table).copied();
74
+ let mut class = classes!("dropdown-menu-item");
75
+ if consumers.is_some() {
76
+ class.push("is-derived");
77
+ }
78
+
79
+ let on_delete = props
80
+ .on_delete
81
+ .clone()
82
+ .filter(|_| consumers == Some(0))
83
+ .map(|cb| {
84
+ let table = table.clone();
85
+ Callback::from(move |event: MouseEvent| {
86
+ event.stop_propagation();
87
+ cb.emit(table.clone());
88
+ })
89
+ });
90
+
91
+ html! {
92
+ <span {class} {onmousedown}>
93
+ { table }
94
+ if let Some(count) = consumers.filter(|x| *x > 0) {
95
+ <span class="derived_consumers">{ count }</span>
96
+ }
97
+ if let Some(on_delete) = on_delete {
98
+ <span
99
+ class="derived_delete"
100
+ title="Delete this derived table"
101
+ onmousedown={on_delete}
102
+ // The menu stays OPEN after a delete, so the
103
+ // CLICK half of the gesture must not bubble to
104
+ // the context-menu flyout's Submenu row, whose
105
+ // own `onclick` would fire "New" AND close the
106
+ // session.
107
+ onclick={Callback::from(|event: MouseEvent| {
108
+ event.stop_propagation();
109
+ })}
110
+ />
111
+ }
112
+ </span>
113
+ }
62
114
  });
63
115
 
64
116
  html! {
@@ -95,3 +147,19 @@ pub fn NewPanelMenu(props: &NewPanelMenuProps) -> Html {
95
147
  </div>
96
148
  }
97
149
  }
150
+
151
+ /// Consumer counts for every derived `Table` the workspace owns.
152
+ pub fn derived_counts(
153
+ workspace: &crate::workspace::Workspace,
154
+ ) -> crate::components::new_panel_menu::DerivedCounts {
155
+ std::rc::Rc::new(
156
+ workspace
157
+ .derived_names()
158
+ .into_iter()
159
+ .map(|name| {
160
+ let count = workspace.derived_consumers(&name);
161
+ (name, count)
162
+ })
163
+ .collect(),
164
+ )
165
+ }
@@ -35,13 +35,13 @@ use yew::prelude::*;
35
35
 
36
36
  use crate::components::copy_dropdown::CopyDropDownMenu;
37
37
  use crate::components::export_dropdown::ExportDropDownMenu;
38
- use crate::components::new_panel_menu::{HostedTables, NewPanelMenu, NewPanelPick};
38
+ use crate::components::new_panel_menu::{HostedTables, NewPanelMenu, NewPanelPick, derived_counts};
39
39
  use crate::components::style::StyleSurface;
40
40
  use crate::config::*;
41
41
  use crate::js::copy_to_clipboard;
42
42
  use crate::presentation::Presentation;
43
43
  use crate::queries::fetch_hosted_tables;
44
- use crate::tasks::export_method_to_blob;
44
+ use crate::tasks::{can_derive, delete_derived, export_method_to_blob};
45
45
  use crate::ui::{ContextMenu, ContextMenuEntry, ContextMenuItem, MODAL_SLOT, PortalModal};
46
46
  use crate::utils::*;
47
47
  use crate::workspace::{PanelId, Workspace};
@@ -59,6 +59,10 @@ pub enum PanelCommand {
59
59
  /// A fresh panel copied from the named panel.
60
60
  NewFromPanel(String),
61
61
  Duplicate,
62
+
63
+ /// Promote the panel's current `View` to a derived `Table` under a
64
+ /// generated name, and rebind the panel to it.
65
+ Derive,
62
66
  Reset,
63
67
  Maximize,
64
68
  Restore,
@@ -153,6 +157,10 @@ pub struct PanelMenu {
153
157
  /// loaded client, in registration order. `None` while the fetch spawned at
154
158
  /// menu open is still in flight.
155
159
  tables: Option<HostedTables>,
160
+
161
+ /// Keeps the [`Workspace::hosted_tables_changed`] listener attached for
162
+ /// the menu session, so the open listing tracks table creates/deletes.
163
+ _tables_sub: Subscription,
156
164
  }
157
165
 
158
166
  impl Component for PanelMenu {
@@ -160,13 +168,25 @@ impl Component for PanelMenu {
160
168
  type Properties = PanelMenuProps;
161
169
 
162
170
  fn create(ctx: &Context<Self>) -> Self {
163
- let workspace = ctx.props().workspace.clone();
164
- let link = ctx.link().clone();
165
- ApiFuture::spawn(async move {
166
- let tables = fetch_hosted_tables(&workspace).await;
167
- link.send_message(PanelMenuMsg::TablesLoaded(tables));
168
- Ok(())
169
- });
171
+ let fetch = {
172
+ let workspace = ctx.props().workspace.clone();
173
+ let link = ctx.link().clone();
174
+ move || {
175
+ clone!(workspace, link);
176
+ ApiFuture::spawn(async move {
177
+ let tables = fetch_hosted_tables(&workspace).await;
178
+ link.send_message(PanelMenuMsg::TablesLoaded(tables));
179
+ Ok(())
180
+ });
181
+ }
182
+ };
183
+
184
+ fetch();
185
+ let _tables_sub = ctx
186
+ .props()
187
+ .workspace
188
+ .hosted_tables_changed()
189
+ .add_listener(move |_| fetch());
170
190
 
171
191
  Self {
172
192
  anchor: session_anchor(
@@ -176,6 +196,7 @@ impl Component for PanelMenu {
176
196
  ),
177
197
  picker: None,
178
198
  tables: None,
199
+ _tables_sub,
179
200
  }
180
201
  }
181
202
 
@@ -251,6 +272,13 @@ impl PanelMenu {
251
272
  Some(panel_id) => {
252
273
  let can_close = ctx.props().workspace.len() > 1;
253
274
  let is_master = ctx.props().workspace.is_master(&PanelId::from(panel_id));
275
+ let panel = ctx.props().workspace.panel(&PanelId::from(panel_id));
276
+ let can_derive_view = panel.as_ref().is_some_and(|x| {
277
+ let config = x.session.get_view_config();
278
+ let is_pivoted = !config.group_by.is_empty() || !config.split_by.is_empty();
279
+ is_pivoted && can_derive(&x.session)
280
+ });
281
+
254
282
  vec![
255
283
  ContextMenuEntry::Submenu {
256
284
  label: "New".to_owned(),
@@ -258,6 +286,7 @@ impl PanelMenu {
258
286
  entries: vec![ContextMenuEntry::Custom(self.new_submenu_body(ctx))],
259
287
  },
260
288
  item("Duplicate", cmd(PanelCommand::Duplicate), false),
289
+ item("Derive Table", cmd(PanelCommand::Derive), !can_derive_view),
261
290
  item("Reset", cmd(PanelCommand::Reset), false),
262
291
  item(
263
292
  "Export",
@@ -335,7 +364,25 @@ impl PanelMenu {
335
364
  vec![cmd, PanelMenuMsg::MenuClosed]
336
365
  });
337
366
 
338
- html! { <NewPanelMenu tables={self.tables.clone()} {panels} {callback} /> }
367
+ let workspace = ctx.props().workspace.clone();
368
+ let on_delete = Callback::from(move |name: String| {
369
+ let workspace = workspace.clone();
370
+ let effect = workspace.effects().guard();
371
+ ApiFuture::spawn(async move {
372
+ let _effect = effect;
373
+ delete_derived(&workspace, &name).await
374
+ });
375
+ });
376
+
377
+ html! {
378
+ <NewPanelMenu
379
+ tables={self.tables.clone()}
380
+ {panels}
381
+ {callback}
382
+ derived={derived_counts(&ctx.props().workspace)}
383
+ {on_delete}
384
+ />
385
+ }
339
386
  }
340
387
 
341
388
  /// Export/Copy format-picker spawned in place of the context menu, anchored
@@ -364,18 +411,20 @@ impl PanelMenu {
364
411
  clone!(presentation);
365
412
  let session = panel.session.clone();
366
413
  let renderer = panel.renderer.clone();
414
+ let workspace = ctx.props().workspace.clone();
367
415
  let link = ctx.link().clone();
368
416
  Callback::from(move |file: ExportFile| {
369
417
  if file.name.is_empty() {
370
418
  return;
371
419
  }
372
420
 
373
- clone!(session, renderer, presentation, link);
421
+ clone!(session, renderer, presentation, workspace, link);
374
422
  ApiFuture::spawn(async move {
375
423
  let blob = export_method_to_blob(
376
424
  &session,
377
425
  &renderer,
378
426
  &presentation,
427
+ &workspace,
379
428
  file.method,
380
429
  )
381
430
  .await?;
@@ -400,14 +449,16 @@ impl PanelMenu {
400
449
  clone!(presentation);
401
450
  let session = panel.session.clone();
402
451
  let renderer = panel.renderer.clone();
452
+ let workspace = ctx.props().workspace.clone();
403
453
  let link = ctx.link().clone();
404
454
  Callback::from(move |file: ExportFile| {
405
- clone!(session, renderer, presentation, link);
455
+ clone!(session, renderer, presentation, workspace, link);
406
456
  ApiFuture::spawn(async move {
407
457
  let task = export_method_to_blob(
408
458
  &session,
409
459
  &renderer,
410
460
  &presentation,
461
+ &workspace,
411
462
  file.method,
412
463
  );
413
464
  copy_to_clipboard(task, file.method.mimetype(file.is_chart)).await?;
@@ -42,10 +42,10 @@ pub struct PluginTabProps {
42
42
  /// Active plugin's `plugin_config` bucket — threaded as a value
43
43
  /// snapshot from `RendererProps`. Changes on every mutation path
44
44
  /// that fires `plugin_config_changed` (in-tab edit,
45
- /// `restore_and_render` JSON paste, `reset_all` with `all=true`)
45
+ /// `restore` JSON paste, `reset_all` with `all=true`)
46
46
  /// AND on plugin switch (the active bucket is keyed by plugin
47
47
  /// name, so `to_props()` produces a fresh `Rc` after
48
- /// `commit_plugin_idx`). PluginTab is a pure function of this
48
+ /// a restore or edit commit). PluginTab is a pure function of this
49
49
  /// prop — no `Renderer::get_plugin_config()` reads against the
50
50
  /// interior-mutable handle.
51
51
  pub plugin_config: PtrEqRc<serde_json::Map<String, serde_json::Value>>,
@@ -256,6 +256,7 @@ fn render_leaf(
256
256
  max,
257
257
  step,
258
258
  include,
259
+ ..
259
260
  } => {
260
261
  let current = raw_config.get(&key).and_then(|v| v.as_f64());
261
262
  Some(html! {
@@ -13,6 +13,7 @@
13
13
  use yew::prelude::*;
14
14
 
15
15
  use crate::renderer::limits::RenderLimits;
16
+ use crate::utils::left_click;
16
17
 
17
18
  #[derive(Properties, PartialEq)]
18
19
  pub struct RenderWarningProps {
@@ -76,7 +77,7 @@ pub fn render_warning(props: &RenderWarningProps) -> Html {
76
77
  };
77
78
 
78
79
  let on_dismiss = props.on_dismiss.clone();
79
- let onclick = Callback::from(move |_: MouseEvent| on_dismiss.emit(()));
80
+ let onclick = left_click(Callback::from(move |_: MouseEvent| on_dismiss.emit(())));
80
81
  html! {
81
82
  <>
82
83
  <div
@@ -22,7 +22,7 @@ use super::plugin_tab::PluginTab;
22
22
  use crate::components::debug_panel::DebugPanel;
23
23
  use crate::config::{PluginStaticConfig, PluginUpdate};
24
24
  use crate::presentation::{ColumnLocator, ColumnSettingsTarget, OpenColumnSettings, Presentation};
25
- use crate::queries::classify_column;
25
+ use crate::queries::{classify_column, is_pivot_column};
26
26
  use crate::renderer::*;
27
27
  use crate::session::column_defaults_update::*;
28
28
  use crate::session::*;
@@ -166,8 +166,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
166
166
 
167
167
  let used = config.columns.iter().any(|maybe_col| {
168
168
  maybe_col.as_ref().map(|col| col == n).unwrap_or_default()
169
- }) || config.group_by.iter().any(|col| col == n)
170
- || config.split_by.iter().any(|col| col == n)
169
+ }) || is_pivot_column(n, config)
171
170
  || config.filter.iter().any(|col| col.column() == n)
172
171
  || config.sort.iter().any(|col| &col.0 == n);
173
172
  (used && props.renderer.can_render_column_styles()).then_some(locator)
@@ -193,7 +192,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
193
192
  let session_metadata = props.metadata.clone();
194
193
  let view_config = props.view_config.clone();
195
194
  Callback::from(move |plugin_name: String| {
196
- if session.is_errored() {
195
+ if session.is_errored() || renderer.failure().is_some() {
197
196
  return;
198
197
  }
199
198
  // Pure resolve — the swap itself is committed inside the locked
@@ -368,6 +367,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
368
367
  presentation={presentation.clone()}
369
368
  renderer={renderer.clone()}
370
369
  session={session.clone()}
370
+ workspace={props.workspace.clone()}
371
371
  initial_width={width}
372
372
  on_auto_width={on_auto_width.clone()}
373
373
  on_dimensions_reset={&props.on_dimensions_reset}
@@ -21,7 +21,9 @@ use super::status_indicator::StatusIndicator;
21
21
  use crate::components::copy_dropdown::CopyDropDownMenu;
22
22
  use crate::components::export_dropdown::ExportDropDownMenu;
23
23
  use crate::components::global_filter_bar::GlobalFilterBar;
24
- use crate::components::new_panel_menu::{HostedTables, NewPanelMenu, NewPanelPick, PanelLabels};
24
+ use crate::components::new_panel_menu::{
25
+ HostedTables, NewPanelMenu, NewPanelPick, PanelLabels, derived_counts,
26
+ };
25
27
  use crate::components::style::StyleSurface;
26
28
  use crate::config::*;
27
29
  use crate::js::*;
@@ -50,6 +52,9 @@ pub struct StatusBarProps {
50
52
  /// `has_table_cells`, `error` from session_props; `selected_theme`,
51
53
  /// `available_themes` from presentation_props.
52
54
  pub session_props: SessionProps,
55
+
56
+ /// The failure of the active panel's last draw.
57
+ pub render_error: Option<RenderError>,
53
58
  pub presentation_props: PresentationProps,
54
59
 
55
60
  /// Derived from root: `settings_open && has_table_loaded`. Used
@@ -106,6 +111,10 @@ pub enum StatusBarMsg {
106
111
 
107
112
  /// A pick in the "New" dropdown.
108
113
  NewPanelPick(NewPanelPick),
114
+
115
+ /// The hosted-table set changed: refresh the "New" dropdown's listing
116
+ /// in place, if it is open.
117
+ RefreshTables,
109
118
  Export,
110
119
  Copy,
111
120
  CloseExport,
@@ -128,16 +137,41 @@ pub struct StatusBar {
128
137
  /// The "New" dropdown's anchor and listings while it is open.
129
138
  new_menu: Option<(HtmlElement, HostedTables, PanelLabels)>,
130
139
 
140
+ /// Keeps the [`Workspace::hosted_tables_changed`] listener attached for
141
+ /// the component's lifetime.
142
+ _tables_sub: Subscription,
143
+
131
144
  /// Bumped per "New" click; a resolving fetch opens only if it is still
132
145
  /// the newest one.
133
146
  new_generation: u32,
134
147
  }
135
148
 
149
+ impl StatusBar {
150
+ /// Fetch the hosted-table listing and (re)open the "New" dropdown with
151
+ /// it, guarded by `new_generation` against stale resolutions.
152
+ fn spawn_tables_fetch(&mut self, ctx: &Context<Self>) {
153
+ self.new_generation = self.new_generation.wrapping_add(1);
154
+ let generation = self.new_generation;
155
+ let workspace = ctx.props().workspace.clone();
156
+ let link = ctx.link().clone();
157
+ ApiFuture::spawn(async move {
158
+ let tables = fetch_hosted_tables(&workspace).await;
159
+ link.send_message(StatusBarMsg::OpenNewPanel(generation, Rc::new(tables)));
160
+ Ok(())
161
+ });
162
+ }
163
+ }
164
+
136
165
  impl Component for StatusBar {
137
166
  type Message = StatusBarMsg;
138
167
  type Properties = StatusBarProps;
139
168
 
140
- fn create(_ctx: &Context<Self>) -> Self {
169
+ fn create(ctx: &Context<Self>) -> Self {
170
+ let _tables_sub = ctx.props().workspace.hosted_tables_changed().add_listener({
171
+ let link = ctx.link().clone();
172
+ move |_| link.send_message(StatusBarMsg::RefreshTables)
173
+ });
174
+
141
175
  Self {
142
176
  copy_ref: NodeRef::default(),
143
177
  export_ref: NodeRef::default(),
@@ -146,6 +180,7 @@ impl Component for StatusBar {
146
180
  copy_target: None,
147
181
  export_target: None,
148
182
  new_menu: None,
183
+ _tables_sub,
149
184
  new_generation: 0,
150
185
  }
151
186
  }
@@ -158,15 +193,13 @@ impl Component for StatusBar {
158
193
  false
159
194
  },
160
195
  StatusBarMsg::NewPanel => {
161
- self.new_generation = self.new_generation.wrapping_add(1);
162
- let generation = self.new_generation;
163
- let workspace = ctx.props().workspace.clone();
164
- let link = ctx.link().clone();
165
- ApiFuture::spawn(async move {
166
- let tables = fetch_hosted_tables(&workspace).await;
167
- link.send_message(StatusBarMsg::OpenNewPanel(generation, Rc::new(tables)));
168
- Ok(())
169
- });
196
+ self.spawn_tables_fetch(ctx);
197
+ false
198
+ },
199
+ StatusBarMsg::RefreshTables => {
200
+ if self.new_menu.is_some() {
201
+ self.spawn_tables_fetch(ctx);
202
+ }
170
203
 
171
204
  false
172
205
  },
@@ -198,15 +231,18 @@ impl Component for StatusBar {
198
231
  },
199
232
  StatusBarMsg::CloseNewPanel => {
200
233
  self.new_menu = None;
234
+ self.new_generation = self.new_generation.wrapping_add(1);
201
235
  true
202
236
  },
203
237
  StatusBarMsg::NewPanelPick(pick) => {
204
238
  ctx.props().on_new_panel.emit(pick);
205
239
  self.new_menu = None;
240
+ self.new_generation = self.new_generation.wrapping_add(1);
206
241
  true
207
242
  },
208
243
  StatusBarMsg::ResetTheme => {
209
244
  update_theme(
245
+ &ctx.props().session,
210
246
  &ctx.props().renderer,
211
247
  &ctx.props().presentation,
212
248
  &ctx.props().workspace,
@@ -216,6 +252,7 @@ impl Component for StatusBar {
216
252
  },
217
253
  StatusBarMsg::SetTheme(theme_name) => {
218
254
  update_theme(
255
+ &ctx.props().session,
219
256
  &ctx.props().renderer,
220
257
  &ctx.props().presentation,
221
258
  &ctx.props().workspace,
@@ -261,7 +298,8 @@ impl Component for StatusBar {
261
298
  } = ctx.props();
262
299
 
263
300
  let has_table = ctx.props().session_props.has_table.clone();
264
- let is_errored = ctx.props().session_props.is_errored();
301
+ let is_errored =
302
+ ctx.props().session_props.is_errored() || ctx.props().render_error.is_some();
265
303
  let is_settings_open = ctx.props().is_settings_open;
266
304
 
267
305
  let mut is_updating_class_name = classes!();
@@ -274,28 +312,35 @@ impl Component for StatusBar {
274
312
  }
275
313
 
276
314
  // TODO Memoizing these would reduce some vdom diffing later on
277
- let onclose = ctx.link().callback(|_| StatusBarMsg::Eject);
315
+ let onclose = left_click(ctx.link().callback(|_| StatusBarMsg::Eject));
278
316
  let onpointerdown = ctx.link().callback(StatusBarMsg::PointerEvent);
279
- let onexport = ctx.link().callback(|event: MouseEvent| {
317
+ let onexport = left_click(ctx.link().callback(|event: MouseEvent| {
280
318
  event.prevent_default();
281
319
  StatusBarMsg::Export
282
- });
283
- let oncopy = ctx.link().callback(|event: MouseEvent| {
320
+ }));
321
+ let oncopy = left_click(ctx.link().callback(|event: MouseEvent| {
284
322
  event.prevent_default();
285
323
  StatusBarMsg::Copy
286
- });
287
- let onreset = ctx.link().callback(StatusBarMsg::Reset);
288
- let onnew = ctx.link().callback(|event: MouseEvent| {
324
+ }));
325
+ let onreset = left_click(ctx.link().callback(StatusBarMsg::Reset));
326
+ let onnew = left_click(ctx.link().callback(|event: MouseEvent| {
289
327
  event.prevent_default();
290
328
  StatusBarMsg::NewPanel
291
- });
329
+ }));
292
330
  let on_new_select = ctx.link().callback(StatusBarMsg::NewPanelPick);
293
331
  let on_close_new = ctx.link().callback(|_| StatusBarMsg::CloseNewPanel);
332
+ let on_delete_derived = {
333
+ let workspace = ctx.props().workspace.clone();
334
+ Callback::from(move |name: String| {
335
+ let workspace = workspace.clone();
336
+ let effect = workspace.effects().guard();
337
+ ApiFuture::spawn(async move {
338
+ let _effect = effect;
339
+ delete_derived(&workspace, &name).await
340
+ });
341
+ })
342
+ };
294
343
 
295
- // Project only the *active* panel's plugin toolbar into the shared status
296
- // bar. Each panel's toolbar slots into `statusbar-extra-{its-panel-id}`
297
- // (see datagrid `toolbar.ts`); the active panel's id comes from the
298
- // active renderer this status bar is bound to.
299
344
  let extra_slot = ctx
300
345
  .props()
301
346
  .renderer
@@ -319,6 +364,7 @@ impl Component for StatusBar {
319
364
  &props.session,
320
365
  &props.renderer,
321
366
  &props.presentation,
367
+ &props.workspace,
322
368
  x.method,
323
369
  );
324
370
  let result = copy_to_clipboard(task, mime).await;
@@ -345,6 +391,7 @@ impl Component for StatusBar {
345
391
  &props.session,
346
392
  &props.renderer,
347
393
  &props.presentation,
394
+ &props.workspace,
348
395
  x.method,
349
396
  )
350
397
  .await
@@ -374,6 +421,7 @@ impl Component for StatusBar {
374
421
  {session}
375
422
  update_count={ctx.props().update_count}
376
423
  session_props={ctx.props().session_props.clone()}
424
+ render_error={ctx.props().render_error.clone()}
377
425
  />
378
426
  if is_panel_bar {
379
427
  <div id="panel-bar" class="section">
@@ -449,6 +497,8 @@ impl Component for StatusBar {
449
497
  tables={Some(tables.clone())}
450
498
  panels={panels.clone()}
451
499
  callback={on_new_select}
500
+ derived={derived_counts(&ctx.props().workspace)}
501
+ on_delete={on_delete_derived}
452
502
  />
453
503
  }
454
504
  </PortalModal>
@@ -15,7 +15,7 @@ use perspective_js::utils::ApiError;
15
15
  use web_sys::*;
16
16
  use yew::prelude::*;
17
17
 
18
- use crate::renderer::Renderer;
18
+ use crate::renderer::{RenderError, Renderer};
19
19
  use crate::session::{Session, SessionProps, TableLoadState};
20
20
  use crate::tasks::apply_and_render;
21
21
  use crate::utils::*;
@@ -32,6 +32,7 @@ pub struct StatusIndicatorProps {
32
32
  /// TODO(texodus): remove this
33
33
  pub update_count: u32,
34
34
  pub session_props: SessionProps,
35
+ pub render_error: Option<RenderError>,
35
36
  }
36
37
 
37
38
  /// An indicator component which displays the current status of the perspective
@@ -47,6 +48,8 @@ pub fn StatusIndicator(props: &StatusIndicatorProps) -> Html {
47
48
  err.kind(),
48
49
  err.is_reconnect(),
49
50
  )
51
+ } else if let Some(RenderError(err)) = &props.render_error {
52
+ StatusIconState::Errored(err.message(), err.stacktrace(), err.kind(), true)
50
53
  } else if !has_table_cells
51
54
  && matches!(props.session_props.has_table, Some(TableLoadState::Loading))
52
55
  {
@@ -76,6 +79,10 @@ pub fn StatusIndicator(props: &StatusIndicatorProps) -> Html {
76
79
  async move |_: MouseEvent, (session, renderer, state)| {
77
80
  match &state {
78
81
  StatusIconState::Errored(..) => {
82
+ if renderer.failure().is_some() {
83
+ session.reset_failed_config().await?;
84
+ }
85
+
79
86
  session.reconnect().await?;
80
87
  apply_and_render(session, renderer, ViewConfigUpdate::default())?.await?;
81
88
  },
@@ -23,6 +23,7 @@ use yew::prelude::*;
23
23
 
24
24
  use super::PerspectiveViewer;
25
25
  use super::msg::MasterSelection;
26
+ use crate::session::OverlayClause;
26
27
  use crate::tasks::*;
27
28
  use crate::workspace::PanelId;
28
29
 
@@ -107,7 +108,18 @@ impl PerspectiveViewer {
107
108
  },
108
109
  };
109
110
 
110
- workspace.set_contribution(&id, filters);
111
+ let metadata = workspace.panel(&id).map(|p| p.session.metadata());
112
+ let clauses = filters
113
+ .into_iter()
114
+ .map(|filter| OverlayClause {
115
+ column_type: metadata
116
+ .as_ref()
117
+ .and_then(|m| m.get_column_table_type(filter.column())),
118
+ filter,
119
+ })
120
+ .collect();
121
+
122
+ workspace.set_contribution(&id, clauses);
111
123
  apply_global_filters(workspace);
112
124
  false
113
125
  }
@@ -132,6 +132,10 @@ pub enum PerspectiveViewerMsg {
132
132
  /// panel appended to the layout.
133
133
  DuplicatePanel(String),
134
134
 
135
+ /// Promote the named panel's current `View` to a derived `Table` under a
136
+ /// generated name, rebinding the panel to it.
137
+ DerivePanel(String),
138
+
135
139
  /// New panel: a fresh (default-config) panel bound to the named panel's
136
140
  /// table (from the default client).
137
141
  NewPanel(String),