@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.
- package/README.md +82 -3
- package/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +3 -3
- package/dist/css/blueprint.css +1 -1
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/ar.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/ledger.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/css/velvet.css +1 -1
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/column-format.js +2 -0
- package/dist/esm/column-format.js.map +7 -0
- package/dist/esm/extensions.d.ts +3 -13
- package/dist/esm/perspective-viewer.d.ts +1 -1
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +3 -3
- package/dist/esm/plugin.d.ts +4 -1
- package/dist/esm/select-detail.d.ts +15 -0
- package/dist/esm/select-detail.js +2 -0
- package/dist/esm/select-detail.js.map +7 -0
- package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
- package/dist/esm/ts-rs/TableRef.d.ts +9 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
- package/dist/wasm/perspective-viewer.d.ts +115 -35
- package/dist/wasm/perspective-viewer.js +314 -190
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
- package/package.json +31 -18
- package/src/css/column-selector.css +205 -50
- package/src/css/ui/containers/context-menu.css +12 -1
- package/src/css/ui/containers/dropdown-menu-items.css +29 -0
- package/src/css/ui/containers/scroll-panel.css +4 -1
- package/src/rust/agent/client/protocol.rs +0 -89
- package/src/rust/agent/markdown/mod.rs +0 -238
- package/src/rust/agent/preamble.md +1 -0
- package/src/rust/agent/tools.rs +2 -16
- package/src/rust/components/column_selector/active_column.rs +15 -5
- package/src/rust/components/column_selector/add_expression_button.rs +9 -26
- package/src/rust/components/column_selector/config_selector.rs +34 -6
- package/src/rust/components/column_selector/derived_control.rs +229 -0
- package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
- package/src/rust/components/column_selector/filter_column.rs +0 -2
- package/src/rust/components/column_selector/inactive_column.rs +4 -5
- package/src/rust/components/column_selector/pivot_column.rs +21 -1
- package/src/rust/components/column_selector/sort_column.rs +4 -3
- package/src/rust/components/column_selector.rs +21 -1
- package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
- package/src/rust/components/column_settings_sidebar.rs +25 -19
- package/src/rust/components/debug_panel.rs +38 -29
- package/src/rust/components/dragdrop_list.rs +8 -9
- package/src/rust/components/main_panel/msg.rs +6 -4
- package/src/rust/components/main_panel/reconcile.rs +6 -1
- package/src/rust/components/main_panel/render.rs +70 -51
- package/src/rust/components/main_panel/update.rs +39 -1
- package/src/rust/components/main_panel.rs +97 -59
- package/src/rust/components/new_panel_menu.rs +69 -1
- package/src/rust/components/panel_menu.rs +63 -12
- package/src/rust/components/plugin_tab.rs +3 -2
- package/src/rust/components/render_warning.rs +2 -1
- package/src/rust/components/settings_panel.rs +4 -4
- package/src/rust/components/status_bar.rs +74 -24
- package/src/rust/components/status_indicator.rs +8 -1
- package/src/rust/components/viewer/filters.rs +13 -1
- package/src/rust/components/viewer/msg.rs +4 -0
- package/src/rust/components/viewer/panels.rs +41 -1
- package/src/rust/components/viewer/render.rs +1 -0
- package/src/rust/components/viewer/settings.rs +0 -22
- package/src/rust/components/viewer/snapshots.rs +2 -2
- package/src/rust/components/viewer/wiring.rs +14 -0
- package/src/rust/components/viewer.rs +45 -25
- package/src/rust/components/window_editor.rs +5 -4
- package/src/rust/config/column_config_schema.rs +38 -377
- package/src/rust/config/number_string_format.rs +0 -94
- package/src/rust/config/options.rs +3 -7
- package/src/rust/config/plugin_static_config.rs +0 -42
- package/src/rust/config/viewer_config.rs +47 -17
- package/src/rust/config/workspace_config.rs +158 -128
- package/src/rust/custom_elements/viewer.rs +187 -131
- package/src/rust/custom_events.rs +33 -18
- package/src/rust/derived.rs +266 -0
- package/src/rust/exprtk/tokenize.rs +0 -54
- package/src/rust/js/plugin.rs +1 -1
- package/src/rust/lib.rs +3 -0
- package/src/rust/presentation.rs +23 -4
- package/src/rust/queries/column_locator.rs +55 -48
- package/src/rust/queries/get_viewer_config.rs +1 -1
- package/src/rust/queries/plugin_column_styles.rs +22 -23
- package/src/rust/renderer/dispatch.rs +2 -0
- package/src/rust/renderer/plugin_config.rs +652 -487
- package/src/rust/renderer/props.rs +4 -0
- package/src/rust/renderer/registry.rs +2 -6
- package/src/rust/renderer/state.rs +205 -0
- package/src/rust/renderer.rs +201 -59
- package/src/rust/session/metadata.rs +30 -9
- package/src/rust/session/op_queue.rs +430 -0
- package/src/rust/session/panel_state.rs +327 -0
- package/src/rust/session/view_subscription.rs +42 -3
- package/src/rust/session.rs +900 -395
- package/src/rust/tasks/apply_global_filters.rs +49 -35
- package/src/rust/tasks/auto_pause.rs +22 -12
- package/src/rust/tasks/copy_export.rs +5 -0
- package/src/rust/tasks/create_panel.rs +34 -9
- package/src/rust/tasks/derive.rs +312 -0
- package/src/rust/tasks/mod.rs +6 -2
- package/src/rust/tasks/pipeline.rs +145 -89
- package/src/rust/tasks/reset_all.rs +31 -20
- package/src/rust/tasks/restore_derived.rs +200 -0
- package/src/rust/tasks/restore_panel.rs +153 -141
- package/src/rust/tasks/send_column_config.rs +56 -29
- package/src/rust/tasks/send_plugin_config.rs +52 -23
- package/src/rust/tasks/set_edit_mode.rs +33 -25
- package/src/rust/tasks/sync_update_panels.rs +39 -12
- package/src/rust/tasks/table_lifecycle.rs +70 -34
- package/src/rust/tasks/transactional_restore.rs +387 -0
- package/src/rust/tasks/update_theme.rs +45 -6
- package/src/rust/ui/containers/scroll_panel.rs +17 -10
- package/src/rust/ui/containers/split_panel.rs +0 -41
- package/src/rust/ui/context_menu.rs +157 -18
- package/src/rust/ui/font_loader.rs +23 -35
- package/src/rust/ui/form/mirrored_textarea.rs +0 -12
- package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
- package/src/rust/ui/form/palette_selector.rs +0 -51
- package/src/rust/ui/portal.rs +4 -10
- package/src/rust/utils/browser/mod.rs +2 -3
- package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
- package/src/rust/utils/browser/request_animation_frame.rs +27 -8
- package/src/rust/utils/css.rs +0 -360
- package/src/rust/utils/local_fonts.rs +0 -36
- package/src/rust/utils/mod.rs +2 -3
- package/src/rust/utils/modal_position.rs +15 -21
- package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
- package/src/rust/utils/pubsub.rs +0 -1
- package/src/rust/workspace.rs +187 -140
- package/src/svg/derived.svg +6 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/ar.css +10 -0
- package/src/themes/intl/de.css +10 -0
- package/src/themes/intl/es.css +10 -0
- package/src/themes/intl/fr.css +10 -0
- package/src/themes/intl/ja.css +10 -0
- package/src/themes/intl/pt.css +10 -0
- package/src/themes/intl/zh.css +10 -0
- package/src/themes/intl.css +10 -0
- package/src/ts/extensions.ts +3 -36
- package/src/ts/perspective-viewer.ts +1 -1
- package/src/ts/plugin.ts +4 -1
- package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
- package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
- package/src/ts/ts-rs/RestoreOptions.ts +3 -7
- package/src/ts/ts-rs/TableRef.ts +9 -0
- package/src/ts/ts-rs/ViewerConfig.ts +7 -6
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
- package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
- package/dist/esm/perspective-viewer.inline.d.ts +0 -3
- package/dist/esm/perspective-viewer.inline.js +0 -21
- package/dist/esm/perspective-viewer.inline.js.map +0 -7
- package/src/rust/tasks/restore_and_render.rs +0 -107
- package/src/rust/utils/browser/tests/debounce.rs +0 -163
- package/src/rust/utils/tests/mod.rs +0 -14
- package/src/rust/utils/tests/pubsub.rs +0 -95
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
- /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
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
/// `
|
|
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
|
-
///
|
|
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
|
-
}) ||
|
|
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::{
|
|
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(
|
|
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.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 =
|
|
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.
|
|
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),
|