@perspective-dev/viewer 5.1.0 → 5.2.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/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.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/monokai.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/docs/perspective-docs.json +1 -0
- package/dist/esm/column-format.d.ts +19 -28
- package/dist/esm/perspective-viewer.d.ts +2 -0
- package/dist/esm/perspective-viewer.inline.js +8 -8
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/providers.d.ts +79 -0
- package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
- package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
- package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
- package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
- package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
- package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
- package/dist/esm/ts-rs/Notation.d.ts +17 -0
- package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -1
- package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
- package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
- package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
- package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
- package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
- package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/Unit.d.ts +1 -0
- package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
- package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +41 -1
- package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
- package/dist/wasm/perspective-viewer.d.ts +134 -25
- package/dist/wasm/perspective-viewer.js +367 -124
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
- package/package.json +2 -1
- package/src/css/_viewer_bundle.css +1 -0
- package/src/css/column-selector.css +8 -0
- package/src/css/config-selector.css +38 -3
- package/src/css/form/code-editor.css +2 -3
- package/src/css/form/mirrored-textarea.css +81 -0
- package/src/css/viewer.css +328 -0
- package/src/rust/agent/client/mod.rs +19 -0
- package/src/rust/agent/client/protocol.rs +320 -0
- package/src/rust/agent/client/transport.rs +287 -0
- package/src/rust/agent/client/turn.rs +174 -0
- package/src/rust/agent/config.rs +159 -0
- package/src/rust/agent/docs.rs +293 -0
- package/src/rust/agent/markdown/block.rs +509 -0
- package/src/rust/agent/markdown/entity.rs +52 -0
- package/src/rust/agent/markdown/events.rs +71 -0
- package/src/rust/agent/markdown/inline.rs +497 -0
- package/src/rust/agent/markdown/mod.rs +466 -0
- package/src/rust/agent/mod.rs +23 -0
- package/src/rust/agent/preamble.md +16 -0
- package/src/rust/agent/runtime.rs +201 -0
- package/src/rust/agent/slot.rs +236 -0
- package/src/rust/agent/tools.rs +1110 -0
- package/src/rust/components/chat_panel.rs +289 -0
- package/src/rust/components/column_selector/config_selector.rs +76 -7
- package/src/rust/components/column_selector.rs +7 -0
- package/src/rust/components/containers/dragdrop_list.rs +37 -0
- package/src/rust/components/form/code_editor.rs +14 -23
- package/src/rust/components/form/debug.rs +8 -5
- package/src/rust/components/form/mirrored_textarea.rs +165 -0
- package/src/rust/components/form/mod.rs +1 -0
- package/src/rust/components/mod.rs +2 -0
- package/src/rust/components/settings_panel.rs +80 -2
- package/src/rust/components/viewer/panels.rs +9 -14
- package/src/rust/components/viewer/render.rs +2 -0
- package/src/rust/components/window_editor.rs +75 -103
- package/src/rust/config/datetime_column_style/custom.rs +17 -0
- package/src/rust/config/datetime_column_style/simple.rs +10 -0
- package/src/rust/config/number_string_format.rs +33 -0
- package/src/rust/config/options.rs +18 -0
- package/src/rust/config/plugin_static_config.rs +120 -5
- package/src/rust/config/viewer_config.rs +220 -1
- package/src/rust/config/workspace_config.rs +7 -6
- package/src/rust/custom_elements/viewer.rs +209 -18
- package/src/rust/custom_events.rs +29 -0
- package/src/rust/lib.rs +13 -0
- package/src/rust/presentation.rs +8 -0
- package/src/rust/queries/get_viewer_config.rs +5 -1
- package/src/rust/session/column_defaults_update.rs +18 -0
- package/src/rust/session/metadata.rs +18 -20
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/tasks/create_panel.rs +18 -8
- package/src/rust/tasks/edit_window.rs +1 -1
- package/src/rust/tasks/restore_panel.rs +34 -17
- package/src/rust/tasks/sync_update_panels.rs +7 -23
- package/src/rust/workspace.rs +112 -20
- package/src/themes/intl/de.css +16 -0
- package/src/themes/intl/es.css +16 -0
- package/src/themes/intl/fr.css +16 -0
- package/src/themes/intl/ja.css +16 -0
- package/src/themes/intl/pt.css +16 -0
- package/src/themes/intl/zh.css +16 -0
- package/src/themes/intl.css +16 -0
- package/src/ts/column-format.ts +67 -46
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/providers.ts +96 -0
- package/src/ts/ts-rs/CompactDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyCode.ts +3 -0
- package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
- package/src/ts/ts-rs/CurrencySign.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
- package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
- package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
- package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
- package/src/ts/ts-rs/FormatMode.ts +3 -0
- package/src/ts/ts-rs/FormatUnit.ts +3 -0
- package/src/ts/ts-rs/Notation.ts +11 -0
- package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +47 -1
- package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
- package/src/ts/ts-rs/RestoreOptions.ts +20 -0
- package/src/ts/ts-rs/RoundingMode.ts +3 -0
- package/src/ts/ts-rs/RoundingPriority.ts +3 -0
- package/src/ts/ts-rs/SignDisplay.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
- package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
- package/src/ts/ts-rs/StringColorMode.ts +3 -0
- package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
- package/src/ts/ts-rs/Unit.ts +3 -0
- package/src/ts/ts-rs/UnitDisplay.ts +3 -0
- package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
- package/src/ts/ts-rs/UseGrouping.ts +3 -0
- package/src/ts/ts-rs/ViewerConfig.ts +47 -1
- package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
- package/src/ts/ts-rs/WindowSpec.ts +3 -2
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
- package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
- package/src/ts/ts-rs/WindowAggregate.ts +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
|
@@ -261,7 +261,36 @@ pub fn wire_element_events(
|
|
|
261
261
|
}
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
let layout_sub = workspace.layout_changed().add_listener({
|
|
265
|
+
clone!(elem);
|
|
266
|
+
move |panels: Vec<crate::workspace::PanelId>| {
|
|
267
|
+
let ids = panels
|
|
268
|
+
.iter()
|
|
269
|
+
.map(|id| JsValue::from_str(id.as_str()))
|
|
270
|
+
.collect::<js_sys::Array>();
|
|
271
|
+
|
|
272
|
+
let detail = js_sys::Object::new();
|
|
273
|
+
let _ = js_sys::Reflect::set(&detail, &JsValue::from_str("panels"), &ids);
|
|
274
|
+
dispatch_event(&elem, "layout-update", JsValue::from(detail)).unwrap();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
let active_panel_sub = workspace.active_changed().add_listener({
|
|
279
|
+
clone!(elem);
|
|
280
|
+
move |active: Option<crate::workspace::PanelId>| {
|
|
281
|
+
let panel = active
|
|
282
|
+
.map(|id| JsValue::from_str(id.as_str()))
|
|
283
|
+
.unwrap_or(JsValue::NULL);
|
|
284
|
+
|
|
285
|
+
let detail = js_sys::Object::new();
|
|
286
|
+
let _ = js_sys::Reflect::set(&detail, &JsValue::from_str("panel"), &panel);
|
|
287
|
+
dispatch_event(&elem, "active-panel-update", JsValue::from(detail)).unwrap();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
264
291
|
vec![
|
|
292
|
+
layout_sub,
|
|
293
|
+
active_panel_sub,
|
|
265
294
|
theme_sub,
|
|
266
295
|
before_settings_sub,
|
|
267
296
|
settings_sub,
|
package/src/rust/lib.rs
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
// `SESSION_CONFIG_COHERENCE_PLAN.md`) — deny, don't warn.
|
|
32
32
|
#![deny(clippy::await_holding_refcell_ref)]
|
|
33
33
|
|
|
34
|
+
#[cfg(feature = "llm-agent")]
|
|
35
|
+
mod agent;
|
|
34
36
|
pub mod components;
|
|
35
37
|
pub mod config;
|
|
36
38
|
pub mod custom_elements;
|
|
@@ -86,22 +88,33 @@ import type {
|
|
|
86
88
|
|
|
87
89
|
export type * from "../../src/ts/ts-rs/ViewerConfig.d.ts";
|
|
88
90
|
export type * from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
|
|
91
|
+
export type * from "../../src/ts/ts-rs/ViewerConfigInitial.d.ts";
|
|
89
92
|
export type * from "../../src/ts/ts-rs/PluginStaticConfig.d.ts";
|
|
90
93
|
export type * from "../../src/ts/ts-rs/WorkspaceConfig.d.ts";
|
|
91
94
|
export type * from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
|
|
92
95
|
export type * from "../../src/ts/ts-rs/ExportMethod.d.ts";
|
|
93
96
|
export type * from "../../src/ts/ts-rs/PanelOptions.d.ts";
|
|
97
|
+
export type * from "../../src/ts/ts-rs/RestoreOptions.d.ts";
|
|
94
98
|
export type * from "../../src/ts/ts-rs/ClientOptions.d.ts";
|
|
95
99
|
export type * from "../../src/ts/ts-rs/ExportOptions.d.ts";
|
|
96
100
|
export type * from "../../src/ts/ts-rs/GetTableOptions.d.ts";
|
|
97
101
|
export type * from "../../src/ts/ts-rs/GetClientOptions.d.ts";
|
|
102
|
+
export type * from "../../src/ts/ts-rs/CustomNumberFormatConfig.d.ts";
|
|
103
|
+
export type * from "../../src/ts/ts-rs/NumberFormatStyle.d.ts";
|
|
104
|
+
export type * from "../../src/ts/ts-rs/Notation.d.ts";
|
|
105
|
+
export type * from "../../src/ts/ts-rs/DatetimeFormatType.d.ts";
|
|
106
|
+
export type * from "../../src/ts/ts-rs/StringColorMode.d.ts";
|
|
107
|
+
export type * from "../../src/ts/ts-rs/DatetimeColorMode.d.ts";
|
|
108
|
+
export type * from "../../src/ts/ts-rs/FormatMode.d.ts";
|
|
98
109
|
import type {GetTableOptions} from "../../src/ts/ts-rs/GetTableOptions.d.ts";
|
|
99
110
|
import type {PanelOptions} from "../../src/ts/ts-rs/PanelOptions.d.ts";
|
|
111
|
+
import type {RestoreOptions} from "../../src/ts/ts-rs/RestoreOptions.d.ts";
|
|
100
112
|
import type {ExportOptions} from "../../src/ts/ts-rs/ExportOptions.d.ts";
|
|
101
113
|
import type {GetClientOptions} from "../../src/ts/ts-rs/GetClientOptions.d.ts";
|
|
102
114
|
import type {ClientOptions} from "../../src/ts/ts-rs/ClientOptions.d.ts";
|
|
103
115
|
import type {ViewerConfig} from "../../src/ts/ts-rs/ViewerConfig.d.ts";
|
|
104
116
|
import type {ViewerConfigUpdate} from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
|
|
117
|
+
import type {ViewerConfigInitial} from "../../src/ts/ts-rs/ViewerConfigInitial.d.ts";
|
|
105
118
|
import type {WorkspaceConfig} from "../../src/ts/ts-rs/WorkspaceConfig.d.ts";
|
|
106
119
|
import type {WorkspaceConfigUpdate} from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
|
|
107
120
|
"#;
|
package/src/rust/presentation.rs
CHANGED
|
@@ -63,6 +63,12 @@ impl DragState {
|
|
|
63
63
|
pub struct PresentationHandle {
|
|
64
64
|
viewer_elem: HtmlElement,
|
|
65
65
|
|
|
66
|
+
/// The embedded LLM agent's shared model (runtime + chat transcript) —
|
|
67
|
+
/// carried here so the settings sidebar can render the chat tab without
|
|
68
|
+
/// threading a new prop chain from the element.
|
|
69
|
+
#[cfg(feature = "llm-agent")]
|
|
70
|
+
pub agent: crate::agent::AgentSlot,
|
|
71
|
+
|
|
66
72
|
/// The available themes as detected in the browser environment or set
|
|
67
73
|
/// explicitly when CORS prevents detection — a MEMO of a document
|
|
68
74
|
/// external, not component state. `None` until first parsed (detection
|
|
@@ -148,6 +154,8 @@ impl Presentation {
|
|
|
148
154
|
pub fn new(elem: &HtmlElement) -> Self {
|
|
149
155
|
let theme = Self(Rc::new(PresentationHandle {
|
|
150
156
|
viewer_elem: elem.clone(),
|
|
157
|
+
#[cfg(feature = "llm-agent")]
|
|
158
|
+
agent: Default::default(),
|
|
151
159
|
themes: Default::default(),
|
|
152
160
|
theme_init: Default::default(),
|
|
153
161
|
is_workspace: Default::default(),
|
|
@@ -51,10 +51,14 @@ pub async fn get_viewer_config(
|
|
|
51
51
|
None => presentation.get_selected_theme_name().await,
|
|
52
52
|
};
|
|
53
53
|
let title = session.get_title();
|
|
54
|
+
// Placed ⇒ bound (or binding in flight): creation requires a `table`
|
|
55
|
+
// by type, so a panel with neither a bound nor pending table is an
|
|
56
|
+
// invariant violation, not a serializable state.
|
|
54
57
|
let table = session
|
|
55
58
|
.get_table()
|
|
56
59
|
.map(|x| x.get_name().to_owned())
|
|
57
|
-
.or_else(|| session.pending_table())
|
|
60
|
+
.or_else(|| session.pending_table())
|
|
61
|
+
.ok_or_else(|| ApiError::from("Panel has no `table`"))?;
|
|
58
62
|
let columns_config = renderer.all_columns_configs();
|
|
59
63
|
Ok(ViewerConfig {
|
|
60
64
|
settings,
|
|
@@ -52,6 +52,24 @@ pub impl ViewConfigUpdate {
|
|
|
52
52
|
self.group_rollup_mode
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
let split_rollup_features = metadata
|
|
57
|
+
.get_features()
|
|
58
|
+
.map(|x| x.get_split_rollup_modes())
|
|
59
|
+
.unwrap_or_default();
|
|
60
|
+
|
|
61
|
+
let split_rollups = config_static.get_split_rollups(&split_rollup_features);
|
|
62
|
+
if !split_rollups.contains(
|
|
63
|
+
self.split_rollup_mode
|
|
64
|
+
.as_ref()
|
|
65
|
+
.unwrap_or(&SplitRollupMode::Flat),
|
|
66
|
+
) {
|
|
67
|
+
self.split_rollup_mode = split_rollups.first().cloned();
|
|
68
|
+
tracing::debug!(
|
|
69
|
+
"Setting plugin-advised split rollup mode {:?}",
|
|
70
|
+
self.split_rollup_mode
|
|
71
|
+
);
|
|
72
|
+
}
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
/// Appends additional columns to the `columns` field of this
|
|
@@ -98,34 +98,20 @@ impl SessionMetadata {
|
|
|
98
98
|
|
|
99
99
|
/// Records the pre-aggregation types of the config's window columns, so
|
|
100
100
|
/// `get_column_table_type` resolves them like any other derived column.
|
|
101
|
-
/// Types mirror the engine's `t_window_engine::resolve_dtype` rules from
|
|
102
|
-
/// the window's op and its source's table type - independent of any
|
|
103
|
-
/// aggregation the view may also apply.
|
|
104
101
|
pub(super) fn update_windows(
|
|
105
102
|
&mut self,
|
|
106
103
|
windows: &perspective_client::config::Windows,
|
|
107
104
|
) -> ApiResult<()> {
|
|
108
|
-
use perspective_client::config::WindowAggregate;
|
|
109
105
|
let window_schema = windows
|
|
110
106
|
.iter()
|
|
111
107
|
.filter_map(|(name, w)| {
|
|
112
108
|
let source = self.get_column_table_type(&w.column)?;
|
|
113
|
-
let dtype =
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
| WindowAggregate::Ema
|
|
120
|
-
| WindowAggregate::Diff => ColumnType::Float,
|
|
121
|
-
WindowAggregate::Count => ColumnType::Integer,
|
|
122
|
-
WindowAggregate::Min
|
|
123
|
-
| WindowAggregate::Max
|
|
124
|
-
| WindowAggregate::First
|
|
125
|
-
| WindowAggregate::Last
|
|
126
|
-
| WindowAggregate::Lag
|
|
127
|
-
| WindowAggregate::Lead => source,
|
|
128
|
-
};
|
|
109
|
+
let dtype = self
|
|
110
|
+
.get_window_aggregate(source, &w.aggregate)
|
|
111
|
+
.and_then(|spec| spec.result_type)
|
|
112
|
+
.and_then(|ty| ColumnType::try_from(ty).ok())
|
|
113
|
+
.unwrap_or(source);
|
|
114
|
+
|
|
129
115
|
Some((name.clone(), dtype))
|
|
130
116
|
})
|
|
131
117
|
.collect();
|
|
@@ -133,6 +119,18 @@ impl SessionMetadata {
|
|
|
133
119
|
Ok(())
|
|
134
120
|
}
|
|
135
121
|
|
|
122
|
+
/// The declaration for one window aggregate over a `source` column type.
|
|
123
|
+
pub fn get_window_aggregate(
|
|
124
|
+
&self,
|
|
125
|
+
source: ColumnType,
|
|
126
|
+
name: &str,
|
|
127
|
+
) -> Option<perspective_client::proto::WindowAggregateArgs> {
|
|
128
|
+
self.get_features()?
|
|
129
|
+
.get_window_aggregates(source)
|
|
130
|
+
.into_iter()
|
|
131
|
+
.find(|x| x.name == name)
|
|
132
|
+
}
|
|
133
|
+
|
|
136
134
|
/// Whether `view_schema` has been populated by a prior successful
|
|
137
135
|
/// `create_view`. Used by `Session::create_view` to force a full
|
|
138
136
|
/// build on first run even when `is_clean` happens to be true, so
|
|
@@ -69,13 +69,23 @@ fn wire_panel_render_sub(session: &Session, renderer: &Renderer) -> Subscription
|
|
|
69
69
|
})
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/// Create a new independent panel (own `Session` + `Renderer` + id)
|
|
73
|
-
/// `
|
|
74
|
-
///
|
|
75
|
-
///
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
///
|
|
72
|
+
/// Create a new independent panel (own `Session` + `Renderer` + id) and
|
|
73
|
+
/// restore `update` into it. Shared by `addPanel`, `restore`'s
|
|
74
|
+
/// create-if-missing upsert, and `restorePanel`. `id` is the panel's id —
|
|
75
|
+
/// provided when restoring into a specific named slot, or `None` to
|
|
76
|
+
/// generate a fresh one. `theme` is stripped (element-level, not
|
|
77
|
+
/// per-panel) and `client` — or the element's default client when `None` —
|
|
78
|
+
/// is bound so the config's `table` resolves against it.
|
|
79
|
+
///
|
|
80
|
+
/// A `table` is OPTIONAL here: an update with none creates a deferred
|
|
81
|
+
/// panel that a later `load()` binds, which is the pre-existing
|
|
82
|
+
/// `restore`-then-`load` contract and the same state the reserved
|
|
83
|
+
/// `load()` path produces. The routes where a table-less panel WOULD be
|
|
84
|
+
/// permanently blank require one in their own argument types instead —
|
|
85
|
+
/// `addPanel`'s [`ViewerConfigInitial`], `WorkspaceConfigUpdate`'s
|
|
86
|
+
/// `panels` entries, and the agent's `add_panel` — so the guarantee sits
|
|
87
|
+
/// at the boundary that owns it rather than here, which would sweep
|
|
88
|
+
/// `restore`'s patch in with them.
|
|
79
89
|
pub(crate) async fn create_panel(
|
|
80
90
|
elem: &HtmlElement,
|
|
81
91
|
presentation: &Presentation,
|
|
@@ -128,9 +138,9 @@ pub(crate) async fn create_panel(
|
|
|
128
138
|
&renderer,
|
|
129
139
|
presentation,
|
|
130
140
|
workspace,
|
|
131
|
-
None,
|
|
132
141
|
RestoreMode::Fresh,
|
|
133
142
|
update,
|
|
143
|
+
crate::tasks::RestoreErrors::Publish,
|
|
134
144
|
)
|
|
135
145
|
.await;
|
|
136
146
|
|
|
@@ -10,15 +10,12 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
-
use futures::channel::oneshot::channel;
|
|
14
13
|
use perspective_client::clone;
|
|
15
14
|
use perspective_client::utils::PerspectiveResultExt;
|
|
16
15
|
|
|
17
|
-
use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg};
|
|
18
16
|
use crate::config::*;
|
|
19
17
|
use crate::presentation::Presentation;
|
|
20
18
|
use crate::renderer::Renderer;
|
|
21
|
-
use crate::root::Root;
|
|
22
19
|
use crate::session::{ResetOptions, Session};
|
|
23
20
|
use crate::tasks::*;
|
|
24
21
|
use crate::workspace::Workspace;
|
|
@@ -31,6 +28,20 @@ pub(crate) enum RestoreMode {
|
|
|
31
28
|
Fresh,
|
|
32
29
|
}
|
|
33
30
|
|
|
31
|
+
/// Where a failed restore's error goes. `Publish` (every user-facing path)
|
|
32
|
+
/// commits it to the session — the visible error UI and errored session
|
|
33
|
+
/// state. `Suppress` (the agent's `set_view_config` tool) returns it ONLY
|
|
34
|
+
/// to the caller: the model receives the error as a tool result and
|
|
35
|
+
/// self-corrects, so the transient failure is noise to the human user —
|
|
36
|
+
/// and the errored session state would otherwise BLOCK the corrected
|
|
37
|
+
/// retry, since error recovery below requires a `table` field the model's
|
|
38
|
+
/// patches don't carry.
|
|
39
|
+
#[derive(Clone, Copy)]
|
|
40
|
+
pub(crate) enum RestoreErrors {
|
|
41
|
+
Publish,
|
|
42
|
+
Suppress,
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
pub(crate) async fn bind_table_task(
|
|
35
46
|
session: &Session,
|
|
36
47
|
workspace: &Workspace,
|
|
@@ -57,9 +68,9 @@ pub(crate) async fn restore_panel(
|
|
|
57
68
|
renderer: &Renderer,
|
|
58
69
|
presentation: &Presentation,
|
|
59
70
|
workspace: &Workspace,
|
|
60
|
-
root: Option<&Root<PerspectiveViewer>>,
|
|
61
71
|
mode: RestoreMode,
|
|
62
72
|
mut update: ViewerConfigUpdate,
|
|
73
|
+
errors: RestoreErrors,
|
|
63
74
|
) -> ApiResult<()> {
|
|
64
75
|
let active = matches!(mode, RestoreMode::Existing { active: true });
|
|
65
76
|
let fresh = matches!(mode, RestoreMode::Fresh);
|
|
@@ -77,17 +88,17 @@ pub(crate) async fn restore_panel(
|
|
|
77
88
|
tracing::info!("Restoring {update}");
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
// NOTE: `update.settings` is deliberately NOT applied here. It is
|
|
92
|
+
// element-level chrome rather than panel state, so `restore()` — the
|
|
93
|
+
// only caller that can carry it — applies it before dispatching, and
|
|
94
|
+
// this pipeline stays per-panel. Applying it here reached only the
|
|
95
|
+
// `Existing { active: true }` mode, which is why a freshly created
|
|
96
|
+
// panel silently ignored it.
|
|
97
|
+
|
|
98
|
+
// Under `Suppress` the restore is TRANSACTIONAL, so snapshot the config
|
|
99
|
+
// it is about to overwrite — see the failure tail below for why.
|
|
100
|
+
let rollback =
|
|
101
|
+
matches!(errors, RestoreErrors::Suppress).then(|| session.get_view_config().clone());
|
|
91
102
|
|
|
92
103
|
let table_changed = !fresh
|
|
93
104
|
&& matches!(&update.table, OptionalUpdate::Update(name)
|
|
@@ -122,7 +133,6 @@ pub(crate) async fn restore_panel(
|
|
|
122
133
|
bind_table_task(&session, &workspace, name).await?;
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
receiver.await.unwrap_or_log();
|
|
126
136
|
Ok(())
|
|
127
137
|
}
|
|
128
138
|
},
|
|
@@ -130,7 +140,14 @@ pub(crate) async fn restore_panel(
|
|
|
130
140
|
.await;
|
|
131
141
|
|
|
132
142
|
if let Err(e) = &result {
|
|
133
|
-
|
|
143
|
+
match errors {
|
|
144
|
+
RestoreErrors::Publish => session.set_error(false, e.clone()).await?,
|
|
145
|
+
RestoreErrors::Suppress => {
|
|
146
|
+
if let Some(config) = rollback {
|
|
147
|
+
session.commit_view_config(config.into()).unwrap_or_log();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
result?;
|
|
@@ -55,20 +55,19 @@ pub fn sync_update_panels(
|
|
|
55
55
|
let mut fallback_fresh: Option<String> = None;
|
|
56
56
|
let mut active_fresh: Option<String> = None;
|
|
57
57
|
let mut contents = Vec::new();
|
|
58
|
+
// `panels` entries are `ViewerConfigInitial`s — `table` required by
|
|
59
|
+
// TYPE (a placed panel without a binding would be permanently blank),
|
|
60
|
+
// and no per-panel `settings` exists (element-level; carried by the
|
|
61
|
+
// top-level `active` field). An EMPTY `panels` map restores to the
|
|
62
|
+
// zero-panel empty stage — the former table-less fallback panel was
|
|
63
|
+
// exactly the blank-panel state this type exists to preclude.
|
|
58
64
|
for (saved_id, config) in panels {
|
|
59
|
-
if !matches!(config.settings, OptionalUpdate::Missing) {
|
|
60
|
-
#[rustfmt::skip]
|
|
61
|
-
tracing::warn!(
|
|
62
|
-
"`settings` on panel \"{saved_id}\" is ignored in a whole-element config; use the top-level `active` field"
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
65
|
let (fresh, session, renderer, config) = create_panel_model(
|
|
67
66
|
&this.elem,
|
|
68
67
|
&this.presentation,
|
|
69
68
|
&this.workspace,
|
|
70
69
|
None,
|
|
71
|
-
config,
|
|
70
|
+
config.into(),
|
|
72
71
|
None,
|
|
73
72
|
Placement::Placed,
|
|
74
73
|
);
|
|
@@ -82,21 +81,6 @@ pub fn sync_update_panels(
|
|
|
82
81
|
contents.push((fresh, session, renderer, config));
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
if contents.is_empty() {
|
|
86
|
-
let (fresh, session, renderer, config) = create_panel_model(
|
|
87
|
-
&this.elem,
|
|
88
|
-
&this.presentation,
|
|
89
|
-
&this.workspace,
|
|
90
|
-
None,
|
|
91
|
-
ViewerConfigUpdate::default(),
|
|
92
|
-
None,
|
|
93
|
-
Placement::Placed,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
fallback_fresh = Some(fresh.as_str().to_owned());
|
|
97
|
-
contents.push((fresh, session, renderer, config));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
84
|
let mut eject_tasks = Vec::new();
|
|
101
85
|
if let Some(panel) = this.workspace.take_reserved() {
|
|
102
86
|
eject_tasks.push(eject_panel(panel));
|
package/src/rust/workspace.rs
CHANGED
|
@@ -21,7 +21,7 @@ use perspective_client::config::Filter;
|
|
|
21
21
|
|
|
22
22
|
use crate::renderer::Renderer;
|
|
23
23
|
use crate::session::Session;
|
|
24
|
-
use crate::utils::{EffectLedger, PubSub, Subscription};
|
|
24
|
+
use crate::utils::{EffectLedger, PubSub, Subscription, spawn_owned};
|
|
25
25
|
|
|
26
26
|
/// A unique identifier for a [`Panel`] within a [`Workspace`].
|
|
27
27
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
@@ -243,6 +243,22 @@ struct WorkspaceData {
|
|
|
243
243
|
/// In-flight effects (public mutators + scheduled internal flows),
|
|
244
244
|
/// drained by `flush()` — see [`EffectLedger`].
|
|
245
245
|
effects: EffectLedger,
|
|
246
|
+
|
|
247
|
+
/// The PLACED panel set changed since the last emit. Mutation sites set
|
|
248
|
+
/// this and emit NOTHING; the coalescing flush task owns delivery (see
|
|
249
|
+
/// [`Workspace::schedule_layout_flush`]).
|
|
250
|
+
layout_dirty: bool,
|
|
251
|
+
layout_changed: Rc<PubSub<Vec<PanelId>>>,
|
|
252
|
+
|
|
253
|
+
/// The active panel changed since the last emit. A separate channel from
|
|
254
|
+
/// `layout_dirty`: "which panel is selected" and "which panels exist" are
|
|
255
|
+
/// distinct facts, and one event may not mean both.
|
|
256
|
+
active_dirty: bool,
|
|
257
|
+
active_changed: Rc<PubSub<Option<PanelId>>>,
|
|
258
|
+
|
|
259
|
+
/// A flush task is already queued — the flag that makes N mutations
|
|
260
|
+
/// within one operation schedule ONE task rather than N.
|
|
261
|
+
flush_scheduled: bool,
|
|
246
262
|
}
|
|
247
263
|
|
|
248
264
|
impl Default for Workspace {
|
|
@@ -269,6 +285,11 @@ impl Workspace {
|
|
|
269
285
|
staged_changed: Rc::new(PubSub::default()),
|
|
270
286
|
reserved: None,
|
|
271
287
|
effects: EffectLedger::default(),
|
|
288
|
+
layout_dirty: false,
|
|
289
|
+
layout_changed: Rc::new(PubSub::default()),
|
|
290
|
+
active_dirty: false,
|
|
291
|
+
active_changed: Rc::new(PubSub::default()),
|
|
292
|
+
flush_scheduled: false,
|
|
272
293
|
})))
|
|
273
294
|
}
|
|
274
295
|
|
|
@@ -277,6 +298,62 @@ impl Workspace {
|
|
|
277
298
|
self.0.borrow().effects.clone()
|
|
278
299
|
}
|
|
279
300
|
|
|
301
|
+
pub fn layout_changed(&self) -> Rc<PubSub<Vec<PanelId>>> {
|
|
302
|
+
self.0.borrow().layout_changed.clone()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
pub fn active_changed(&self) -> Rc<PubSub<Option<PanelId>>> {
|
|
306
|
+
self.0.borrow().active_changed.clone()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// Queue the coalescing layout-event flush, if anything is dirty and no
|
|
310
|
+
/// flush is already pending.
|
|
311
|
+
fn schedule_layout_flush(&self) {
|
|
312
|
+
let schedule = {
|
|
313
|
+
let mut data = self.0.borrow_mut();
|
|
314
|
+
let dirty = data.layout_dirty || data.active_dirty;
|
|
315
|
+
let queued = data.flush_scheduled;
|
|
316
|
+
data.flush_scheduled |= dirty;
|
|
317
|
+
dirty && !queued
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if !schedule {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let effects = self.effects();
|
|
325
|
+
let this = self.clone();
|
|
326
|
+
spawn_owned("workspace_layout_flush", async move {
|
|
327
|
+
effects.settle().await;
|
|
328
|
+
this.flush_layout_events();
|
|
329
|
+
Ok(())
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Emit whatever is dirty, outside any borrow.
|
|
334
|
+
fn flush_layout_events(&self) {
|
|
335
|
+
let (layout, active, panels, active_id, layout_pubsub, active_pubsub) = {
|
|
336
|
+
let mut data = self.0.borrow_mut();
|
|
337
|
+
data.flush_scheduled = false;
|
|
338
|
+
(
|
|
339
|
+
std::mem::take(&mut data.layout_dirty),
|
|
340
|
+
std::mem::take(&mut data.active_dirty),
|
|
341
|
+
data.panels.iter().map(|p| p.id.clone()).collect::<Vec<_>>(),
|
|
342
|
+
data.active.clone(),
|
|
343
|
+
data.layout_changed.clone(),
|
|
344
|
+
data.active_changed.clone(),
|
|
345
|
+
)
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
if layout {
|
|
349
|
+
layout_pubsub.emit(panels);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if active {
|
|
353
|
+
active_pubsub.emit(active_id);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
280
357
|
/// Stage a layout tree for `MainPanel` to apply at its next `rendered`
|
|
281
358
|
/// pass (see `WorkspaceData::pending_layout`).
|
|
282
359
|
pub fn set_pending_layout(&self, layout: crate::js::Layout) {
|
|
@@ -512,16 +589,22 @@ impl Workspace {
|
|
|
512
589
|
/// Append a [`Panel`]. When the element had zero panels, the inserted panel
|
|
513
590
|
/// becomes the active one (there is no other candidate).
|
|
514
591
|
pub fn insert_panel(&self, panel: Panel) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
data.active
|
|
592
|
+
{
|
|
593
|
+
let mut data = self.0.borrow_mut();
|
|
594
|
+
if data.active.is_none() {
|
|
595
|
+
data.active = Some(panel.id.clone());
|
|
596
|
+
data.active_dirty = true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
panel
|
|
600
|
+
.renderer
|
|
601
|
+
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
602
|
+
data.panels.push(panel);
|
|
603
|
+
data.layout_dirty = true;
|
|
604
|
+
Self::sync_solo_flags(&data);
|
|
518
605
|
}
|
|
519
606
|
|
|
520
|
-
|
|
521
|
-
.renderer
|
|
522
|
-
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
523
|
-
data.panels.push(panel);
|
|
524
|
-
Self::sync_solo_flags(&data);
|
|
607
|
+
self.schedule_layout_flush();
|
|
525
608
|
}
|
|
526
609
|
|
|
527
610
|
/// Hold `panel` in the reservation slot (see [`WorkspaceData::reserved`]):
|
|
@@ -585,8 +668,10 @@ impl Workspace {
|
|
|
585
668
|
|
|
586
669
|
if data.active.as_ref() == Some(id) {
|
|
587
670
|
data.active = None;
|
|
671
|
+
data.active_dirty = true;
|
|
588
672
|
}
|
|
589
673
|
|
|
674
|
+
data.layout_dirty |= removed.is_some();
|
|
590
675
|
Self::sync_solo_flags(&data);
|
|
591
676
|
(
|
|
592
677
|
removed,
|
|
@@ -605,25 +690,32 @@ impl Workspace {
|
|
|
605
690
|
staged_pubsub.emit(());
|
|
606
691
|
}
|
|
607
692
|
|
|
693
|
+
self.schedule_layout_flush();
|
|
608
694
|
removed
|
|
609
695
|
}
|
|
610
696
|
|
|
611
697
|
/// Set the active panel. Returns `false` (no-op) if `id` is not a known
|
|
612
698
|
/// panel.
|
|
613
699
|
pub fn set_active(&self, id: PanelId) -> bool {
|
|
614
|
-
let
|
|
615
|
-
|
|
616
|
-
data.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
700
|
+
let known = {
|
|
701
|
+
let mut data = self.0.borrow_mut();
|
|
702
|
+
if data.panels.iter().any(|p| p.id == id) {
|
|
703
|
+
data.active_dirty |= data.active.as_ref() != Some(&id);
|
|
704
|
+
data.active = Some(id);
|
|
705
|
+
for panel in data.panels.iter() {
|
|
706
|
+
panel
|
|
707
|
+
.renderer
|
|
708
|
+
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
true
|
|
712
|
+
} else {
|
|
713
|
+
false
|
|
621
714
|
}
|
|
715
|
+
};
|
|
622
716
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
false
|
|
626
|
-
}
|
|
717
|
+
self.schedule_layout_flush();
|
|
718
|
+
known
|
|
627
719
|
}
|
|
628
720
|
|
|
629
721
|
/// The default [`Client`], if one has been loaded.
|
package/src/themes/intl/de.css
CHANGED
|
@@ -118,6 +118,22 @@ perspective-dropdown {
|
|
|
118
118
|
--psp-label--query-tab--content: "Abfrage";
|
|
119
119
|
--psp-label--plugin-tab--content: "Plugin";
|
|
120
120
|
--psp-label--debug-tab--content: "JSON";
|
|
121
|
+
--psp-label--chat-tab--content: "Chat";
|
|
122
|
+
|
|
123
|
+
/* Chat panel */
|
|
124
|
+
--psp-label--chat-reasoning--content: "Denkprozess";
|
|
125
|
+
--psp-label--chat-send--content: "Senden";
|
|
126
|
+
--psp-label--chat-stop--content: "Stopp";
|
|
127
|
+
--psp-label--chat-placeholder--content: "Frage zu Ihren Daten…";
|
|
128
|
+
|
|
129
|
+
/* Plugin-declared pivot roles (PluginStaticConfig) */
|
|
130
|
+
--psp-label--role--x-axis--content: "X-Achse";
|
|
131
|
+
--psp-label--role--y-axis--content: "Y-Achse";
|
|
132
|
+
--psp-label--role--series--content: "Serien";
|
|
133
|
+
--psp-label--role--hierarchy--content: "Hierarchie";
|
|
134
|
+
--psp-label--role--facets--content: "Facetten";
|
|
135
|
+
--psp-label--role--row-groups--content: "Zeilengruppen";
|
|
136
|
+
--psp-label--role--column-groups--content: "Spaltengruppen";
|
|
121
137
|
|
|
122
138
|
/* Plugin settings */
|
|
123
139
|
--psp-label--edit-mode--content: "Bearbeitungsmodus";
|
package/src/themes/intl/es.css
CHANGED
|
@@ -118,6 +118,22 @@ perspective-dropdown {
|
|
|
118
118
|
--psp-label--query-tab--content: "Consulta";
|
|
119
119
|
--psp-label--plugin-tab--content: "Plugin";
|
|
120
120
|
--psp-label--debug-tab--content: "JSON";
|
|
121
|
+
--psp-label--chat-tab--content: "Chat";
|
|
122
|
+
|
|
123
|
+
/* Chat panel */
|
|
124
|
+
--psp-label--chat-reasoning--content: "Razonamiento";
|
|
125
|
+
--psp-label--chat-send--content: "Enviar";
|
|
126
|
+
--psp-label--chat-stop--content: "Detener";
|
|
127
|
+
--psp-label--chat-placeholder--content: "Pregunta sobre tus datos…";
|
|
128
|
+
|
|
129
|
+
/* Plugin-declared pivot roles (PluginStaticConfig) */
|
|
130
|
+
--psp-label--role--x-axis--content: "Eje X";
|
|
131
|
+
--psp-label--role--y-axis--content: "Eje Y";
|
|
132
|
+
--psp-label--role--series--content: "Series";
|
|
133
|
+
--psp-label--role--hierarchy--content: "Jerarquía";
|
|
134
|
+
--psp-label--role--facets--content: "Facetas";
|
|
135
|
+
--psp-label--role--row-groups--content: "Grupos de filas";
|
|
136
|
+
--psp-label--role--column-groups--content: "Grupos de columnas";
|
|
121
137
|
|
|
122
138
|
/* Plugin settings */
|
|
123
139
|
--psp-label--edit-mode--content: "Modo de edición";
|