@perspective-dev/viewer 5.5.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/ExportMethod.d.ts +1 -1
- 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 +126 -35
- package/dist/wasm/perspective-viewer.js +320 -185
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
- package/package.json +33 -20
- 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/export_dropdown.rs +6 -0
- 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 +80 -21
- package/src/rust/components/status_indicator.rs +8 -1
- package/src/rust/components/viewer/filters.rs +15 -3
- package/src/rust/components/viewer/msg.rs +8 -0
- package/src/rust/components/viewer/panels.rs +44 -4
- 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 +18 -1
- package/src/rust/components/viewer.rs +47 -27
- package/src/rust/components/window_editor.rs +5 -4
- package/src/rust/config/column_config_schema.rs +38 -377
- package/src/rust/config/export_method.rs +24 -0
- 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 +189 -129
- 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 +35 -19
- 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 +931 -419
- 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 +72 -0
- package/src/rust/tasks/create_panel.rs +46 -21
- package/src/rust/tasks/derive.rs +312 -0
- package/src/rust/tasks/mod.rs +6 -2
- package/src/rust/tasks/pipeline.rs +152 -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 +40 -13
- 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/{tests/mod.rs → in_flight.rs} +62 -2
- package/src/rust/utils/local_fonts.rs +0 -36
- package/src/rust/utils/mod.rs +4 -3
- package/src/rust/utils/modal_position.rs +15 -21
- package/src/rust/utils/pending_effects.rs +6 -30
- 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/themes/monokai.css +1 -1
- package/src/themes/vaporwave.css +1 -1
- 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/ExportMethod.ts +1 -1
- 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/pubsub.rs +0 -95
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
|
@@ -20,6 +20,7 @@ use serde_json::Value;
|
|
|
20
20
|
use ts_rs::TS;
|
|
21
21
|
use wasm_bindgen::prelude::*;
|
|
22
22
|
|
|
23
|
+
use crate::config::TableRef;
|
|
23
24
|
use crate::renderer::ColumnConfigMap;
|
|
24
25
|
|
|
25
26
|
/// The state of an entire `custom_elements::PerspectiveViewerElement` component
|
|
@@ -30,18 +31,18 @@ use crate::renderer::ColumnConfigMap;
|
|
|
30
31
|
/// single-panel format flattens back to the legacy shape.
|
|
31
32
|
#[derive(Debug, Default, Serialize, PartialEq, TS)]
|
|
32
33
|
#[serde(deny_unknown_fields)]
|
|
33
|
-
pub struct ViewerConfig<V: TS = String> {
|
|
34
|
+
pub struct ViewerConfig<V: TS = String, T: TS = TableRef> {
|
|
34
35
|
pub settings: bool,
|
|
35
36
|
|
|
36
37
|
#[serde(flatten)]
|
|
37
|
-
pub panel: PanelViewerConfig<V>,
|
|
38
|
+
pub panel: PanelViewerConfig<V, T>,
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/// The per-panel state of a [`ViewerConfig`] — everything except the
|
|
41
42
|
/// element-level `settings` flag. This is the `panels` entry type of the
|
|
42
43
|
/// workspace config format.
|
|
43
44
|
#[derive(Debug, Default, Serialize, PartialEq, TS)]
|
|
44
|
-
pub struct PanelViewerConfig<V: TS = String> {
|
|
45
|
+
pub struct PanelViewerConfig<V: TS = String, T: TS = TableRef> {
|
|
45
46
|
/// The `@perspective-dev/viewer` version that wrote this config,
|
|
46
47
|
/// stamped on save and used to migrate older tokens. Callers do not
|
|
47
48
|
/// set it.
|
|
@@ -66,11 +67,11 @@ pub struct PanelViewerConfig<V: TS = String> {
|
|
|
66
67
|
/// the active plugin; see `get_style_schema`.
|
|
67
68
|
pub plugin_config: serde_json::Map<String, Value>,
|
|
68
69
|
|
|
69
|
-
///
|
|
70
|
-
/// `
|
|
71
|
-
///
|
|
72
|
-
///
|
|
73
|
-
pub table:
|
|
70
|
+
/// The `Table` this panel renders: its hosted name in a
|
|
71
|
+
/// [`WorkspaceConfig`] `panels` entry (`T = String`), and a
|
|
72
|
+
/// [`TableRef`] — a name, or a recursively inlined derived-table
|
|
73
|
+
/// definition — in the single-panel `save()` format.
|
|
74
|
+
pub table: T,
|
|
74
75
|
|
|
75
76
|
/// Selected theme NAME (e.g. `"Pro Dark"`) — not a CSS value. Valid
|
|
76
77
|
/// names are the Perspective themes loaded on the page, which
|
|
@@ -84,14 +85,40 @@ pub struct PanelViewerConfig<V: TS = String> {
|
|
|
84
85
|
pub view_config: ViewConfig,
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
impl<V: TS> Deref for ViewerConfig<V> {
|
|
88
|
-
type Target = PanelViewerConfig<V>;
|
|
88
|
+
impl<V: TS, T: TS> Deref for ViewerConfig<V, T> {
|
|
89
|
+
type Target = PanelViewerConfig<V, T>;
|
|
89
90
|
|
|
90
91
|
fn deref(&self) -> &Self::Target {
|
|
91
92
|
&self.panel
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
impl<V: TS, T: TS> PanelViewerConfig<V, T> {
|
|
97
|
+
/// The same config with the `table` slot mapped, which is how the
|
|
98
|
+
/// single-panel emitters convert a bound name to its inline form.
|
|
99
|
+
pub fn map_table<U: TS>(self, f: impl FnOnce(T) -> U) -> PanelViewerConfig<V, U> {
|
|
100
|
+
PanelViewerConfig {
|
|
101
|
+
version: self.version,
|
|
102
|
+
columns_config: self.columns_config,
|
|
103
|
+
plugin: self.plugin,
|
|
104
|
+
plugin_config: self.plugin_config,
|
|
105
|
+
table: f(self.table),
|
|
106
|
+
theme: self.theme,
|
|
107
|
+
title: self.title,
|
|
108
|
+
view_config: self.view_config,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
impl<V: TS, T: TS> ViewerConfig<V, T> {
|
|
114
|
+
pub fn map_table<U: TS>(self, f: impl FnOnce(T) -> U) -> ViewerConfig<V, U> {
|
|
115
|
+
ViewerConfig {
|
|
116
|
+
settings: self.settings,
|
|
117
|
+
panel: self.panel.map_table(f),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
pub static API_VERSION: LazyLock<&'static str> = LazyLock::new(|| {
|
|
96
123
|
#[derive(Deserialize)]
|
|
97
124
|
struct Package {
|
|
@@ -102,7 +129,7 @@ pub static API_VERSION: LazyLock<&'static str> = LazyLock::new(|| {
|
|
|
102
129
|
pkg.version
|
|
103
130
|
});
|
|
104
131
|
|
|
105
|
-
impl ViewerConfig {
|
|
132
|
+
impl<V: TS + Serialize, T: TS + Serialize> ViewerConfig<V, T> {
|
|
106
133
|
/// Encode a `ViewerConfig` to a `JsValue` in a supported type.
|
|
107
134
|
pub fn encode(&self) -> ApiResult<JsValue> {
|
|
108
135
|
Ok(JsValue::from_serde_ext(self)?)
|
|
@@ -150,10 +177,11 @@ pub struct ViewerConfigUpdate {
|
|
|
150
177
|
#[ts(optional)]
|
|
151
178
|
pub title: TitleUpdate,
|
|
152
179
|
|
|
153
|
-
///
|
|
154
|
-
///
|
|
155
|
-
///
|
|
156
|
-
///
|
|
180
|
+
/// The `Table` to render: a name as hosted on this panel's `Client`,
|
|
181
|
+
/// or an inline derived-table definition to materialize (reusing a
|
|
182
|
+
/// structurally identical derived `Table` when one exists). Rebinding
|
|
183
|
+
/// an existing panel keeps the rest of the config, so column names
|
|
184
|
+
/// that do not exist in the new table will fail validation.
|
|
157
185
|
#[serde(default)]
|
|
158
186
|
#[ts(as = "Option<_>")]
|
|
159
187
|
#[ts(optional)]
|
|
@@ -305,7 +333,7 @@ impl From<ViewerConfigInitial> for ViewerConfigUpdate {
|
|
|
305
333
|
version: up(value.version),
|
|
306
334
|
plugin: up(value.plugin),
|
|
307
335
|
title: up(value.title),
|
|
308
|
-
table: OptionalUpdate::Update(value.table),
|
|
336
|
+
table: OptionalUpdate::Update(value.table.into()),
|
|
309
337
|
theme: up(value.theme),
|
|
310
338
|
settings: OptionalUpdate::Missing,
|
|
311
339
|
plugin_config: up(value.plugin_config),
|
|
@@ -350,6 +378,8 @@ impl TryFrom<ViewerConfigUpdate> for ViewerConfigInitial {
|
|
|
350
378
|
return Err(ApiError::new(CREATE_REQUIRES_TABLE));
|
|
351
379
|
};
|
|
352
380
|
|
|
381
|
+
let table = table.name()?.to_owned();
|
|
382
|
+
|
|
353
383
|
Ok(Self {
|
|
354
384
|
table,
|
|
355
385
|
version: down(version),
|
|
@@ -393,7 +423,7 @@ pub type PluginUpdate = OptionalUpdate<String>;
|
|
|
393
423
|
pub type SettingsUpdate = OptionalUpdate<bool>;
|
|
394
424
|
pub type ThemeUpdate = OptionalUpdate<String>;
|
|
395
425
|
pub type TitleUpdate = OptionalUpdate<String>;
|
|
396
|
-
pub type TableUpdate = OptionalUpdate<
|
|
426
|
+
pub type TableUpdate = OptionalUpdate<TableRef>;
|
|
397
427
|
pub type VersionUpdate = OptionalUpdate<String>;
|
|
398
428
|
pub type ColumnConfigUpdate = OptionalUpdate<ColumnConfigMap>;
|
|
399
429
|
pub type PluginConfigUpdate = OptionalUpdate<serde_json::Map<String, Value>>;
|
|
@@ -12,11 +12,153 @@
|
|
|
12
12
|
|
|
13
13
|
use std::collections::BTreeMap;
|
|
14
14
|
|
|
15
|
-
use perspective_client::config::Filter;
|
|
15
|
+
use perspective_client::config::{Filter, ViewConfig};
|
|
16
16
|
|
|
17
17
|
use crate::config::{OptionalUpdate, PanelViewerConfig, ViewerConfigInitial};
|
|
18
18
|
use crate::utils::CssKind;
|
|
19
19
|
|
|
20
|
+
/// A derived `Table`'s definition: the `Table` its source `View` reads, and
|
|
21
|
+
/// the config of that `View`; generic over the `table` slot, which is a name
|
|
22
|
+
/// in the workspace `derived_tables` map and a recursive [`TableRef`] when
|
|
23
|
+
/// inlined in a single-panel config.
|
|
24
|
+
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
|
25
|
+
pub struct DerivedTableConfig<T: ts_rs::TS = String> {
|
|
26
|
+
/// The `Table` this derivation reads, which may itself be another
|
|
27
|
+
/// derived `Table`.
|
|
28
|
+
pub table: T,
|
|
29
|
+
|
|
30
|
+
#[serde(flatten)]
|
|
31
|
+
pub view_config: ViewConfig,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// What may occupy a `table` position in a single-panel config: a hosted
|
|
35
|
+
/// `Table`'s name, or an inline derived-table definition recursing through
|
|
36
|
+
/// its own `table` field until a concrete name.
|
|
37
|
+
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
|
|
38
|
+
#[serde(untagged)]
|
|
39
|
+
pub enum TableRef {
|
|
40
|
+
Name(String),
|
|
41
|
+
Derived(Box<DerivedTableConfig<TableRef>>),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Hand-written because the derived decl's recursion sits in a generic alias
|
|
45
|
+
/// instantiation (`DerivedTableConfig<TableRef>`), which TypeScript expands
|
|
46
|
+
/// eagerly and rejects as circular; recursion is legal only deferred through
|
|
47
|
+
/// an object-literal node.
|
|
48
|
+
impl ts_rs::TS for TableRef {
|
|
49
|
+
type OptionInnerType = Self;
|
|
50
|
+
type WithoutGenerics = Self;
|
|
51
|
+
|
|
52
|
+
fn docs() -> Option<String> {
|
|
53
|
+
Some(
|
|
54
|
+
"/**\n * What may occupy a `table` position in a single-panel config: a hosted\n * \
|
|
55
|
+
`Table`'s name, or an inline derived-table definition recursing through\n * its own \
|
|
56
|
+
`table` field until a concrete name.\n */\n"
|
|
57
|
+
.to_owned(),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn name() -> String {
|
|
62
|
+
"TableRef".to_owned()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn decl() -> String {
|
|
66
|
+
format!(
|
|
67
|
+
"type TableRef = string | (Omit<{}, \"table\"> & {{ table: TableRef }});",
|
|
68
|
+
<DerivedTableConfig as ts_rs::TS>::ident()
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn decl_concrete() -> String {
|
|
73
|
+
<Self as ts_rs::TS>::decl()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn inline() -> String {
|
|
77
|
+
format!(
|
|
78
|
+
"string | (Omit<{}, \"table\"> & {{ table: TableRef }})",
|
|
79
|
+
<DerivedTableConfig as ts_rs::TS>::ident()
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn inline_flattened() -> String {
|
|
84
|
+
panic!("`TableRef` cannot be flattened")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn visit_dependencies(visitor: &mut impl ts_rs::TypeVisitor)
|
|
88
|
+
where
|
|
89
|
+
Self: 'static,
|
|
90
|
+
{
|
|
91
|
+
visitor.visit::<DerivedTableConfig>();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn output_path() -> Option<std::path::PathBuf> {
|
|
95
|
+
Some(std::path::PathBuf::from("TableRef.ts"))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
impl Default for TableRef {
|
|
100
|
+
fn default() -> Self {
|
|
101
|
+
Self::Name(String::default())
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
impl From<String> for TableRef {
|
|
106
|
+
fn from(value: String) -> Self {
|
|
107
|
+
Self::Name(value)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl TableRef {
|
|
112
|
+
/// The concrete `Table` name, or an error for an inline definition which
|
|
113
|
+
/// a restore boundary has not resolved.
|
|
114
|
+
pub fn name(&self) -> Result<&str, perspective_js::utils::ApiError> {
|
|
115
|
+
match self {
|
|
116
|
+
Self::Name(name) => Ok(name),
|
|
117
|
+
Self::Derived(_) => Err(perspective_js::utils::ApiError::new(
|
|
118
|
+
"Unresolved inline derived table",
|
|
119
|
+
)),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
impl<'de> serde::Deserialize<'de> for TableRef {
|
|
125
|
+
/// Hand-written (not `untagged`) so a malformed inline definition
|
|
126
|
+
/// reports its own error instead of "data did not match any variant".
|
|
127
|
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
128
|
+
where
|
|
129
|
+
D: serde::Deserializer<'de>,
|
|
130
|
+
{
|
|
131
|
+
struct TableRefVisitor;
|
|
132
|
+
impl<'de> serde::de::Visitor<'de> for TableRefVisitor {
|
|
133
|
+
type Value = TableRef;
|
|
134
|
+
|
|
135
|
+
fn expecting(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
136
|
+
fmt.write_str("a table name or an inline derived table definition")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
140
|
+
where
|
|
141
|
+
E: serde::de::Error,
|
|
142
|
+
{
|
|
143
|
+
Ok(TableRef::Name(value.to_owned()))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
|
147
|
+
where
|
|
148
|
+
A: serde::de::MapAccess<'de>,
|
|
149
|
+
{
|
|
150
|
+
let config = <DerivedTableConfig<TableRef> as serde::Deserialize>::deserialize(
|
|
151
|
+
serde::de::value::MapAccessDeserializer::new(map),
|
|
152
|
+
)?;
|
|
153
|
+
|
|
154
|
+
Ok(TableRef::Derived(Box::new(config)))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
deserializer.deserialize_any(TableRefVisitor)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
20
162
|
/// The workspace config format (`{version, active?, layout, panels}`) —
|
|
21
163
|
/// the multi-panel counterpart of the single-panel [`ViewerConfig`] — as
|
|
22
164
|
/// emitted by [`PerspectiveViewerElement::save`].
|
|
@@ -39,7 +181,14 @@ pub struct WorkspaceConfig {
|
|
|
39
181
|
/// DETERMINISTIC (sorted) key order — a fresh `HashMap` per call
|
|
40
182
|
/// iterates in a per-instance random order, which made consecutive
|
|
41
183
|
/// `save()` outputs byte-unstable.
|
|
42
|
-
pub panels: BTreeMap<String, PanelViewerConfig
|
|
184
|
+
pub panels: BTreeMap<String, PanelViewerConfig<String, String>>,
|
|
185
|
+
|
|
186
|
+
/// Named derived `Table`s, each a `View` of its `parent` promoted to a
|
|
187
|
+
/// `Table`. A `BTreeMap` for the same determinism reason as `panels`.
|
|
188
|
+
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
|
189
|
+
#[ts(as = "Option<_>")]
|
|
190
|
+
#[ts(optional)]
|
|
191
|
+
pub derived_tables: BTreeMap<String, DerivedTableConfig>,
|
|
43
192
|
|
|
44
193
|
/// The element-level global (master/detail cross-) filters. A transient
|
|
45
194
|
/// overlay on every detail panel's view — persisted here, never in a
|
|
@@ -92,6 +241,13 @@ pub struct WorkspaceConfigUpdate {
|
|
|
92
241
|
#[ts(optional)]
|
|
93
242
|
pub panels: Option<BTreeMap<String, ViewerConfigInitial>>,
|
|
94
243
|
|
|
244
|
+
/// The complete replacement set of derived `Table`s, in any order (they
|
|
245
|
+
/// are built parent-first); absent keeps the current ones, `{}` deletes
|
|
246
|
+
/// them all.
|
|
247
|
+
#[serde(default)]
|
|
248
|
+
#[ts(optional)]
|
|
249
|
+
pub derived_tables: Option<BTreeMap<String, DerivedTableConfig>>,
|
|
250
|
+
|
|
95
251
|
/// The element-level cross-filter set applied to every detail panel as
|
|
96
252
|
/// one unattributed bucket, replacing the current set; `null` clears it.
|
|
97
253
|
#[serde(default)]
|
|
@@ -138,129 +294,3 @@ pub fn validate_palette(
|
|
|
138
294
|
})
|
|
139
295
|
.collect()
|
|
140
296
|
}
|
|
141
|
-
|
|
142
|
-
#[cfg(test)]
|
|
143
|
-
mod tests {
|
|
144
|
-
use serde_json::json;
|
|
145
|
-
|
|
146
|
-
use super::*;
|
|
147
|
-
|
|
148
|
-
#[test]
|
|
149
|
-
fn palette_serializes_only_when_present() {
|
|
150
|
-
let config = WorkspaceConfig {
|
|
151
|
-
version: "x".to_owned(),
|
|
152
|
-
active: None,
|
|
153
|
-
layout: None,
|
|
154
|
-
panels: BTreeMap::new(),
|
|
155
|
-
global_filters: vec![],
|
|
156
|
-
masters: vec![],
|
|
157
|
-
palette: BTreeMap::new(),
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
assert_eq!(
|
|
161
|
-
serde_json::to_value(&config).unwrap(),
|
|
162
|
-
json!({ "version": "x", "layout": null, "panels": {} })
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
let mut palette = BTreeMap::new();
|
|
166
|
-
palette.insert("--psp-user--color-hot".to_owned(), "#ff0000".to_owned());
|
|
167
|
-
|
|
168
|
-
let config = WorkspaceConfig { palette, ..config };
|
|
169
|
-
assert_eq!(
|
|
170
|
-
serde_json::to_value(&config).unwrap(),
|
|
171
|
-
json!({
|
|
172
|
-
"version": "x",
|
|
173
|
-
"layout": null,
|
|
174
|
-
"panels": {},
|
|
175
|
-
"palette": { "--psp-user--color-hot": "#ff0000" },
|
|
176
|
-
})
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
#[test]
|
|
181
|
-
fn update_absent_fields_are_missing_and_null_resets() {
|
|
182
|
-
let update: WorkspaceConfigUpdate = serde_json::from_value(json!({})).unwrap();
|
|
183
|
-
assert!(update.panels.is_none());
|
|
184
|
-
assert!(update.layout.is_none());
|
|
185
|
-
assert_eq!(update.active, OptionalUpdate::Missing);
|
|
186
|
-
assert_eq!(update.global_filters, OptionalUpdate::Missing);
|
|
187
|
-
assert_eq!(update.masters, OptionalUpdate::Missing);
|
|
188
|
-
assert_eq!(update.palette, OptionalUpdate::Missing);
|
|
189
|
-
|
|
190
|
-
let update: WorkspaceConfigUpdate = serde_json::from_value(json!({
|
|
191
|
-
"panels": null,
|
|
192
|
-
"active": null,
|
|
193
|
-
"global_filters": null,
|
|
194
|
-
"masters": null,
|
|
195
|
-
"palette": null,
|
|
196
|
-
}))
|
|
197
|
-
.unwrap();
|
|
198
|
-
|
|
199
|
-
assert!(update.panels.is_none());
|
|
200
|
-
assert_eq!(update.active, OptionalUpdate::SetDefault);
|
|
201
|
-
assert_eq!(update.global_filters, OptionalUpdate::SetDefault);
|
|
202
|
-
assert_eq!(update.masters, OptionalUpdate::SetDefault);
|
|
203
|
-
assert_eq!(update.palette, OptionalUpdate::SetDefault);
|
|
204
|
-
|
|
205
|
-
let update: WorkspaceConfigUpdate = serde_json::from_value(json!({
|
|
206
|
-
"panels": {},
|
|
207
|
-
"global_filters": [["Region", "==", "West"]],
|
|
208
|
-
"masters": ["a"],
|
|
209
|
-
}))
|
|
210
|
-
.unwrap();
|
|
211
|
-
|
|
212
|
-
assert_eq!(update.panels.map(|p| p.len()), Some(0));
|
|
213
|
-
assert_eq!(update.masters, OptionalUpdate::Update(vec!["a".to_owned()]));
|
|
214
|
-
assert!(matches!(update.global_filters, OptionalUpdate::Update(ref f) if f.len() == 1));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
#[test]
|
|
218
|
-
fn update_palette_defaults_empty_and_validates_by_prefix() {
|
|
219
|
-
let update: WorkspaceConfigUpdate =
|
|
220
|
-
serde_json::from_value(json!({ "panels": {} })).unwrap();
|
|
221
|
-
assert_eq!(update.palette, OptionalUpdate::Missing);
|
|
222
|
-
|
|
223
|
-
let update: WorkspaceConfigUpdate = serde_json::from_value(json!({
|
|
224
|
-
"panels": {},
|
|
225
|
-
"palette": {
|
|
226
|
-
"--psp-user--gradient-1": "linear-gradient(#000, #fff)",
|
|
227
|
-
"--psp-user--palette-warm": "linear-gradient(90deg, RGB(255,0,0), #ff0)",
|
|
228
|
-
"--psp-user--color-hot": "#F00",
|
|
229
|
-
},
|
|
230
|
-
}))
|
|
231
|
-
.unwrap();
|
|
232
|
-
|
|
233
|
-
let OptionalUpdate::Update(palette) = update.palette else {
|
|
234
|
-
panic!("palette missing");
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
let valid = validate_palette(palette).unwrap();
|
|
238
|
-
assert_eq!(
|
|
239
|
-
valid.get("--psp-user--gradient-1").unwrap(),
|
|
240
|
-
"linear-gradient(to right, #000000 0%, #ffffff 100%)"
|
|
241
|
-
);
|
|
242
|
-
assert_eq!(
|
|
243
|
-
valid.get("--psp-user--palette-warm").unwrap(),
|
|
244
|
-
"linear-gradient(to right, #ff0000, #ffff00)"
|
|
245
|
-
);
|
|
246
|
-
assert_eq!(valid.get("--psp-user--color-hot").unwrap(), "#ff0000");
|
|
247
|
-
|
|
248
|
-
let bad = |name: &str, value: &str| {
|
|
249
|
-
let mut map = BTreeMap::new();
|
|
250
|
-
map.insert(name.to_owned(), value.to_owned());
|
|
251
|
-
validate_palette(map).unwrap_err()
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
assert!(bad("--psp-user--other-1", "#ff0000").contains("--psp-user--other-1"));
|
|
255
|
-
assert!(bad("--psp-charts--gradient", "#ff0000").contains("must start with"));
|
|
256
|
-
assert!(
|
|
257
|
-
bad(
|
|
258
|
-
"--psp-user--palette-1",
|
|
259
|
-
"linear-gradient(#000 0%, #fff 100%)"
|
|
260
|
-
)
|
|
261
|
-
.contains("--psp-user--palette-1")
|
|
262
|
-
);
|
|
263
|
-
assert!(bad("--psp-user--gradient-1", "#ff0000").contains("linear-gradient"));
|
|
264
|
-
assert!(bad("--psp-user--color-1", "red").contains("red"));
|
|
265
|
-
}
|
|
266
|
-
}
|