@perspective-dev/viewer 5.0.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/icons.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 +43 -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 +43 -1
- package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
- package/dist/wasm/perspective-viewer.d.ts +222 -26
- package/dist/wasm/perspective-viewer.js +378 -125
- 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 +9 -0
- package/src/css/column-settings-panel.css +67 -2
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +60 -4
- package/src/css/containers/context-menu.css +12 -6
- package/src/css/containers/tabs.css +2 -2
- package/src/css/form/code-editor.css +2 -3
- package/src/css/form/mirrored-textarea.css +81 -0
- package/src/css/viewer.css +336 -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/active_column.rs +21 -27
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +83 -8
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/inactive_column.rs +8 -1
- package/src/rust/components/column_selector.rs +27 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +110 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +6 -0
- package/src/rust/components/editable_header.rs +2 -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/main_panel.rs +5 -0
- package/src/rust/components/mod.rs +3 -0
- package/src/rust/components/panel_menu.rs +7 -0
- package/src/rust/components/panel_tab.rs +5 -0
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/settings_panel.rs +80 -2
- package/src/rust/components/viewer/msg.rs +2 -0
- package/src/rust/components/viewer/panels.rs +9 -14
- package/src/rust/components/viewer/render.rs +14 -0
- package/src/rust/components/viewer/settings.rs +33 -1
- package/src/rust/components/viewer.rs +2 -0
- package/src/rust/components/window_editor.rs +985 -0
- 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 +17 -1
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +8 -0
- package/src/rust/queries/columns_iter_set.rs +7 -1
- package/src/rust/queries/get_viewer_config.rs +5 -1
- package/src/rust/renderer/dispatch.rs +85 -58
- package/src/rust/renderer.rs +34 -1
- package/src/rust/session/column_defaults_update.rs +18 -0
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +64 -9
- package/src/rust/session/replace_expression_update.rs +2 -0
- package/src/rust/session.rs +7 -5
- package/src/rust/tasks/create_panel.rs +18 -8
- package/src/rust/tasks/edit_window.rs +155 -0
- package/src/rust/tasks/mod.rs +2 -0
- package/src/rust/tasks/restore_panel.rs +34 -17
- package/src/rust/tasks/sync_update_panels.rs +7 -23
- package/src/rust/utils/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/debounce.rs +76 -27
- package/src/rust/workspace.rs +112 -20
- package/src/svg/pin-icon.svg +5 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/de.css +23 -0
- package/src/themes/intl/es.css +23 -0
- package/src/themes/intl/fr.css +23 -0
- package/src/themes/intl/ja.css +23 -0
- package/src/themes/intl/pt.css +23 -0
- package/src/themes/intl/zh.css +23 -0
- package/src/themes/intl.css +26 -1
- 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 +48 -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 +48 -1
- package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +47 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
use std::collections::HashSet;
|
|
14
|
+
use std::rc::Rc;
|
|
15
|
+
|
|
16
|
+
use perspective_client::config::{ColumnType, WindowFrame, WindowSort, WindowSortDir, WindowSpec};
|
|
17
|
+
use wasm_bindgen::JsCast;
|
|
18
|
+
use web_sys::{DragEvent, HtmlInputElement, MouseEvent};
|
|
19
|
+
use yew::prelude::*;
|
|
20
|
+
|
|
21
|
+
use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
|
|
22
|
+
use crate::components::column_selector::{ColumnSelectorColumnRow, InPlaceColumn, PivotColumn};
|
|
23
|
+
use crate::components::containers::dragdrop_list::{
|
|
24
|
+
DragContext, DragDropList, DragDropListItemProps,
|
|
25
|
+
};
|
|
26
|
+
use crate::components::containers::select::{Select, SelectItem};
|
|
27
|
+
use crate::presentation::Presentation;
|
|
28
|
+
use crate::session::{Session, SessionMetadataRc};
|
|
29
|
+
use crate::utils::{AddListener, DragEffect, DragTarget, Subscription};
|
|
30
|
+
|
|
31
|
+
/// The declared capabilities of one window aggregate, for a `source` column
|
|
32
|
+
/// type. Which controls an aggregate needs is the data model's to state - the
|
|
33
|
+
/// editor cannot infer it from a name it has never seen.
|
|
34
|
+
fn op_spec(
|
|
35
|
+
metadata: &SessionMetadataRc,
|
|
36
|
+
source: &str,
|
|
37
|
+
op: &str,
|
|
38
|
+
) -> Option<perspective_client::proto::WindowAggregateArgs> {
|
|
39
|
+
let ty = metadata.get_column_table_type(source)?;
|
|
40
|
+
metadata.get_window_aggregate(ty, op)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The editor's frame-type labels, as the `frames` a declaration lists.
|
|
44
|
+
fn frame_label(frame: &str) -> &'static str {
|
|
45
|
+
match frame {
|
|
46
|
+
"rows" => "Rows",
|
|
47
|
+
"range" => "Range",
|
|
48
|
+
_ => "Cumulative",
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn frame_name(label: &str) -> &'static str {
|
|
53
|
+
match label {
|
|
54
|
+
"Rows" => "rows",
|
|
55
|
+
"Range" => "range",
|
|
56
|
+
_ => "cumulative",
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn is_orderable_for_range(ty: ColumnType) -> bool {
|
|
61
|
+
matches!(
|
|
62
|
+
ty,
|
|
63
|
+
ColumnType::Integer | ColumnType::Float | ColumnType::Date | ColumnType::Datetime
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[derive(Clone, Properties)]
|
|
68
|
+
pub struct WindowEditorProps {
|
|
69
|
+
pub metadata: SessionMetadataRc,
|
|
70
|
+
|
|
71
|
+
/// The saved spec under edit, or `None` for the "new column" drawer.
|
|
72
|
+
pub initial: Option<WindowSpec>,
|
|
73
|
+
|
|
74
|
+
/// Fires on every form mutation with the draft spec when it validates,
|
|
75
|
+
/// else `None`. The spec's `name` is a placeholder - the editable header
|
|
76
|
+
/// owns naming.
|
|
77
|
+
pub on_change: Callback<Option<WindowSpec>>,
|
|
78
|
+
|
|
79
|
+
/// Incremented by the parent to discard the draft.
|
|
80
|
+
pub reset_count: u8,
|
|
81
|
+
|
|
82
|
+
/// Drag/drop state - the editor's column slots are STAGED drop targets
|
|
83
|
+
/// ([`DragTarget::is_staged`]): drops mutate the draft only, and the
|
|
84
|
+
/// drag origin never self-removes.
|
|
85
|
+
pub presentation: Presentation,
|
|
86
|
+
|
|
87
|
+
/// Threaded for the slots' autocomplete dropdown
|
|
88
|
+
/// ([`ColumnDropDownElement`]).
|
|
89
|
+
pub session: Session,
|
|
90
|
+
|
|
91
|
+
/// Selected theme name, threaded for the autocomplete dropdown's
|
|
92
|
+
/// `PortalModal`.
|
|
93
|
+
#[prop_or_default]
|
|
94
|
+
pub selected_theme: Option<String>,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl PartialEq for WindowEditorProps {
|
|
98
|
+
fn eq(&self, other: &Self) -> bool {
|
|
99
|
+
self.metadata == other.metadata
|
|
100
|
+
&& self.initial == other.initial
|
|
101
|
+
&& self.reset_count == other.reset_count
|
|
102
|
+
&& self.selected_theme == other.selected_theme
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Input mirror of a draft [`WindowSpec`]. Free-text fields hold raw
|
|
107
|
+
/// strings so invalid intermediate input is representable; the NUMBER
|
|
108
|
+
/// fields are typed with default `1` and can never hold an invalid value -
|
|
109
|
+
/// like the Style tab's `NumberField`, a non-numeric or out-of-domain
|
|
110
|
+
/// input resets to the default at the keystroke. `validate` produces the
|
|
111
|
+
/// spec (or the error shown inline), applying the same rules as `View`
|
|
112
|
+
/// construction so a saveable draft cannot be rejected by the engine.
|
|
113
|
+
#[derive(Clone, PartialEq)]
|
|
114
|
+
struct WindowDraft {
|
|
115
|
+
op: String,
|
|
116
|
+
source: String,
|
|
117
|
+
order_by: String,
|
|
118
|
+
order_desc: bool,
|
|
119
|
+
partition_by: Vec<String>,
|
|
120
|
+
frame_type: String,
|
|
121
|
+
frame_rows: u32,
|
|
122
|
+
frame_range: f64,
|
|
123
|
+
offset: u32,
|
|
124
|
+
alpha: f64,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
impl Default for WindowDraft {
|
|
128
|
+
fn default() -> Self {
|
|
129
|
+
Self {
|
|
130
|
+
op: String::default(),
|
|
131
|
+
source: String::default(),
|
|
132
|
+
order_by: String::default(),
|
|
133
|
+
order_desc: false,
|
|
134
|
+
partition_by: vec![],
|
|
135
|
+
frame_type: String::default(),
|
|
136
|
+
frame_rows: 1,
|
|
137
|
+
frame_range: 1.0,
|
|
138
|
+
offset: 1,
|
|
139
|
+
alpha: 1.0,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
impl WindowDraft {
|
|
145
|
+
fn from_spec(spec: &WindowSpec) -> Self {
|
|
146
|
+
let (frame_type, frame_rows, frame_range) = match spec.frame {
|
|
147
|
+
Some(WindowFrame::Rows(n)) => ("Rows", n, 1.0),
|
|
148
|
+
Some(WindowFrame::Range(x)) => ("Range", 1, x),
|
|
149
|
+
Some(WindowFrame::Cumulative) | None => ("Cumulative", 1, 1.0),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
Self {
|
|
153
|
+
op: spec.aggregate.clone(),
|
|
154
|
+
source: spec.column.clone(),
|
|
155
|
+
order_by: spec
|
|
156
|
+
.order_by
|
|
157
|
+
.as_ref()
|
|
158
|
+
.map(|x| x.0.clone())
|
|
159
|
+
.unwrap_or_default(),
|
|
160
|
+
order_desc: spec
|
|
161
|
+
.order_by
|
|
162
|
+
.as_ref()
|
|
163
|
+
.map(|x| x.1 == WindowSortDir::Desc)
|
|
164
|
+
.unwrap_or_default(),
|
|
165
|
+
partition_by: spec.partition_by.clone(),
|
|
166
|
+
frame_type: frame_type.to_string(),
|
|
167
|
+
frame_rows,
|
|
168
|
+
frame_range,
|
|
169
|
+
offset: spec.offset.unwrap_or(1),
|
|
170
|
+
alpha: spec.alpha.unwrap_or(1.0),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn new_default() -> Self {
|
|
175
|
+
Self {
|
|
176
|
+
op: "sum".to_string(),
|
|
177
|
+
frame_type: "Cumulative".to_string(),
|
|
178
|
+
..Self::default()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn validate(&self, metadata: &SessionMetadataRc) -> Result<WindowSpec, String> {
|
|
183
|
+
let op = self.op.clone();
|
|
184
|
+
|
|
185
|
+
// Every slot takes true `Table` columns ONLY - expression aliases
|
|
186
|
+
// and other window columns would create dependency cycles (and
|
|
187
|
+
// force delete-blocking). The slots reject non-table drops with the
|
|
188
|
+
// invalid-X overlay, so these are backstops for API-authored specs
|
|
189
|
+
// opened in the editor.
|
|
190
|
+
let table_column = |col: &String| {
|
|
191
|
+
metadata
|
|
192
|
+
.get_table_columns()
|
|
193
|
+
.into_iter()
|
|
194
|
+
.flatten()
|
|
195
|
+
.any(|x| x == col)
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if self.source.is_empty() {
|
|
199
|
+
return Err("Missing Column".to_string());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if !table_column(&self.source) {
|
|
203
|
+
// TODO I should not be
|
|
204
|
+
return Err(format!(
|
|
205
|
+
"\"{}\" must be a table column to source a window",
|
|
206
|
+
self.source
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let source_ty = metadata
|
|
211
|
+
.get_column_table_type(&self.source)
|
|
212
|
+
.ok_or_else(|| format!("Unknown source column \"{}\"", self.source))?;
|
|
213
|
+
|
|
214
|
+
// Backstop for API-authored specs opened in the editor - the op
|
|
215
|
+
// menu only offers the feature-declared set, so this is
|
|
216
|
+
// unreachable from the UI. The declaration also supplies the
|
|
217
|
+
// controls this op takes, below.
|
|
218
|
+
let declared = metadata
|
|
219
|
+
.get_window_aggregate(source_ty, &op)
|
|
220
|
+
.ok_or_else(|| {
|
|
221
|
+
format!("\"{op}\" is not a supported window aggregate for this column")
|
|
222
|
+
})?;
|
|
223
|
+
|
|
224
|
+
// An EMPTY order slot is valid when the backend has a natural row
|
|
225
|
+
// order to fall back on (primary key order in the engine, `rowid`
|
|
226
|
+
// for SQL virtual servers); UNORDERED stores (`Features`) require
|
|
227
|
+
// an explicit order column.
|
|
228
|
+
let order_ty = if self.order_by.is_empty() {
|
|
229
|
+
if metadata
|
|
230
|
+
.get_features()
|
|
231
|
+
.map(|x| x.unordered)
|
|
232
|
+
.unwrap_or_default()
|
|
233
|
+
{
|
|
234
|
+
return Err("Missing Order By".to_string());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
None
|
|
238
|
+
} else {
|
|
239
|
+
if !table_column(&self.order_by) {
|
|
240
|
+
// TODO I should not be
|
|
241
|
+
return Err(format!(
|
|
242
|
+
"\"{}\" must be a table column to order by",
|
|
243
|
+
self.order_by
|
|
244
|
+
));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Some(
|
|
248
|
+
metadata
|
|
249
|
+
.get_column_table_type(&self.order_by)
|
|
250
|
+
.ok_or_else(|| format!("Unknown order by column \"{}\"", self.order_by))?,
|
|
251
|
+
)
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
for col in self.partition_by.iter() {
|
|
255
|
+
// TODO I should not be
|
|
256
|
+
if !table_column(col) {
|
|
257
|
+
return Err(format!(
|
|
258
|
+
"\"{}\" must be a table column to partition by",
|
|
259
|
+
col
|
|
260
|
+
));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The numeric fields are typed and input-clamped to their domains,
|
|
265
|
+
// so no parse or range errors are reachable here.
|
|
266
|
+
let frame = if declared.frames.is_empty() {
|
|
267
|
+
None
|
|
268
|
+
} else {
|
|
269
|
+
let chosen = frame_name(&self.frame_type);
|
|
270
|
+
if !declared.frames.iter().any(|x| x == chosen) {
|
|
271
|
+
return Err(format!("\"{op}\" does not support a {chosen} frame"));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
match self.frame_type.as_str() {
|
|
275
|
+
"Rows" => Some(WindowFrame::Rows(self.frame_rows)),
|
|
276
|
+
"Range" => {
|
|
277
|
+
// The natural-order fallback has no units, so `range`
|
|
278
|
+
// frames require an explicit order column.
|
|
279
|
+
let Some(order_ty) = order_ty else {
|
|
280
|
+
return Err("Range frames require an order by column".to_string());
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if !is_orderable_for_range(order_ty) {
|
|
284
|
+
return Err("Range frames require a numeric, date or datetime order by \
|
|
285
|
+
column"
|
|
286
|
+
.to_string());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
Some(WindowFrame::Range(self.frame_range))
|
|
290
|
+
},
|
|
291
|
+
_ => None,
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Emit `None` at the engine default so a spec saved without an
|
|
296
|
+
// explicit `offset` round-trips unchanged (the name-stripped
|
|
297
|
+
// change-detection baseline compares specs structurally).
|
|
298
|
+
let offset = (declared.offset && self.offset != 1).then_some(self.offset);
|
|
299
|
+
let alpha = declared.alpha.then_some(self.alpha);
|
|
300
|
+
|
|
301
|
+
let mut partition_by = self.partition_by.clone();
|
|
302
|
+
partition_by.retain(|col| !col.is_empty());
|
|
303
|
+
|
|
304
|
+
Ok(WindowSpec {
|
|
305
|
+
column: self.source.clone(),
|
|
306
|
+
aggregate: op,
|
|
307
|
+
partition_by,
|
|
308
|
+
order_by: (!self.order_by.is_empty()).then(|| {
|
|
309
|
+
WindowSort(
|
|
310
|
+
self.order_by.clone(),
|
|
311
|
+
if self.order_desc {
|
|
312
|
+
WindowSortDir::Desc
|
|
313
|
+
} else {
|
|
314
|
+
WindowSortDir::Asc
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
}),
|
|
318
|
+
frame,
|
|
319
|
+
offset,
|
|
320
|
+
alpha,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[derive(Clone, Debug)]
|
|
326
|
+
pub enum WindowEditorMsg {
|
|
327
|
+
SetOp(String),
|
|
328
|
+
ClearSource,
|
|
329
|
+
ClearOrderBy,
|
|
330
|
+
ToggleOrderDir,
|
|
331
|
+
RemovePartition(usize),
|
|
332
|
+
SetFrameType(String),
|
|
333
|
+
/// Number-field messages carry the input's `value_as_number` (`NaN` for
|
|
334
|
+
/// empty or unparseable text); the handlers clamp to each field's
|
|
335
|
+
/// domain, resetting to the default `1` on invalid input.
|
|
336
|
+
SetFrameRows(f64),
|
|
337
|
+
SetFrameRange(f64),
|
|
338
|
+
SetOffset(f64),
|
|
339
|
+
SetAlpha(f64),
|
|
340
|
+
/// A completed drop concerning this editor: a staged target to fill,
|
|
341
|
+
/// and/or the staged origin slot (`DragEffect::Move` from a slot pill)
|
|
342
|
+
/// to clear.
|
|
343
|
+
Drop(String, DragTarget, Option<DragTarget>),
|
|
344
|
+
New(DragTarget, InPlaceColumn),
|
|
345
|
+
DragEnter(DragTarget, usize),
|
|
346
|
+
DragLeave(DragTarget),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// [`DragContext`] bindings routing one [`DragDropList`] per staged target
|
|
350
|
+
/// into [`WindowEditorMsg`]s - the same composition the config selector
|
|
351
|
+
/// uses per zone, so the slots inherit its drop previews, autocomplete
|
|
352
|
+
/// (`EmptyColumn`) and pill styling.
|
|
353
|
+
struct WindowSourceContext;
|
|
354
|
+
struct WindowOrderByContext;
|
|
355
|
+
struct WindowPartitionByContext;
|
|
356
|
+
|
|
357
|
+
impl DragContext<WindowEditorMsg> for WindowSourceContext {
|
|
358
|
+
fn close(_index: usize) -> WindowEditorMsg {
|
|
359
|
+
WindowEditorMsg::ClearSource
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn dragenter(index: usize) -> WindowEditorMsg {
|
|
363
|
+
WindowEditorMsg::DragEnter(DragTarget::WindowSource, index)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fn dragleave() -> WindowEditorMsg {
|
|
367
|
+
WindowEditorMsg::DragLeave(DragTarget::WindowSource)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fn create(col: InPlaceColumn) -> WindowEditorMsg {
|
|
371
|
+
WindowEditorMsg::New(DragTarget::WindowSource, col)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fn is_self_move(effect: DragTarget) -> bool {
|
|
375
|
+
effect == DragTarget::WindowSource
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
impl DragContext<WindowEditorMsg> for WindowOrderByContext {
|
|
380
|
+
fn close(_index: usize) -> WindowEditorMsg {
|
|
381
|
+
WindowEditorMsg::ClearOrderBy
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn dragenter(index: usize) -> WindowEditorMsg {
|
|
385
|
+
WindowEditorMsg::DragEnter(DragTarget::WindowOrderBy, index)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fn dragleave() -> WindowEditorMsg {
|
|
389
|
+
WindowEditorMsg::DragLeave(DragTarget::WindowOrderBy)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fn create(col: InPlaceColumn) -> WindowEditorMsg {
|
|
393
|
+
WindowEditorMsg::New(DragTarget::WindowOrderBy, col)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fn is_self_move(effect: DragTarget) -> bool {
|
|
397
|
+
effect == DragTarget::WindowOrderBy
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
impl DragContext<WindowEditorMsg> for WindowPartitionByContext {
|
|
402
|
+
fn close(index: usize) -> WindowEditorMsg {
|
|
403
|
+
WindowEditorMsg::RemovePartition(index)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
fn dragenter(index: usize) -> WindowEditorMsg {
|
|
407
|
+
WindowEditorMsg::DragEnter(DragTarget::WindowPartitionBy, index)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fn dragleave() -> WindowEditorMsg {
|
|
411
|
+
WindowEditorMsg::DragLeave(DragTarget::WindowPartitionBy)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn create(col: InPlaceColumn) -> WindowEditorMsg {
|
|
415
|
+
WindowEditorMsg::New(DragTarget::WindowPartitionBy, col)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fn is_self_move(effect: DragTarget) -> bool {
|
|
419
|
+
effect == DragTarget::WindowPartitionBy
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/// A slot's filled state: the shared `ColumnSelectorColumnRow`, with the
|
|
424
|
+
/// window `op` selector bound into its aggregate slot for the SOURCE slot -
|
|
425
|
+
/// the ONLY op control in the UI. The pill is a drag ORIGIN with
|
|
426
|
+
/// `DragEffect::Move(action)` (the same shape as `PivotColumn`) - the open
|
|
427
|
+
/// [`WindowEditor`] consumes the staged origin from `drop_received` to
|
|
428
|
+
/// complete the move by clearing this slot in its draft.
|
|
429
|
+
#[derive(Clone, Properties)]
|
|
430
|
+
pub struct WindowSlotColumnProps {
|
|
431
|
+
pub column: String,
|
|
432
|
+
pub column_type: Option<ColumnType>,
|
|
433
|
+
|
|
434
|
+
/// The staged slot this pill occupies - its drag origin.
|
|
435
|
+
pub action: DragTarget,
|
|
436
|
+
|
|
437
|
+
pub presentation: Presentation,
|
|
438
|
+
|
|
439
|
+
#[prop_or_default]
|
|
440
|
+
pub aggregate: Option<Html>,
|
|
441
|
+
|
|
442
|
+
/// Trailing affordance in the row (the order slot's staged sort-dir
|
|
443
|
+
/// toggle).
|
|
444
|
+
#[prop_or_default]
|
|
445
|
+
pub trailing: Html,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
impl PartialEq for WindowSlotColumnProps {
|
|
449
|
+
fn eq(&self, other: &Self) -> bool {
|
|
450
|
+
self.column == other.column
|
|
451
|
+
&& self.column_type == other.column_type
|
|
452
|
+
&& self.action == other.action
|
|
453
|
+
&& self.aggregate == other.aggregate
|
|
454
|
+
&& self.trailing == other.trailing
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
impl DragDropListItemProps for WindowSlotColumnProps {
|
|
459
|
+
type Item = String;
|
|
460
|
+
|
|
461
|
+
fn get_item(&self) -> String {
|
|
462
|
+
self.column.clone()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
pub struct WindowSlotColumn;
|
|
467
|
+
|
|
468
|
+
impl Component for WindowSlotColumn {
|
|
469
|
+
type Message = ();
|
|
470
|
+
type Properties = WindowSlotColumnProps;
|
|
471
|
+
|
|
472
|
+
fn create(_ctx: &Context<Self>) -> Self {
|
|
473
|
+
Self
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
477
|
+
let dragstart = Callback::from({
|
|
478
|
+
let column = ctx.props().column.clone();
|
|
479
|
+
let presentation = ctx.props().presentation.clone();
|
|
480
|
+
let action = ctx.props().action;
|
|
481
|
+
move |event: DragEvent| {
|
|
482
|
+
presentation.set_drag_image(&event).unwrap();
|
|
483
|
+
presentation.notify_drag_start(column.clone(), DragEffect::Move(action))
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
let dragend = Callback::from({
|
|
488
|
+
let presentation = ctx.props().presentation.clone();
|
|
489
|
+
move |_: DragEvent| presentation.notify_drag_end()
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
html! {
|
|
493
|
+
<div class="column-selector-column">
|
|
494
|
+
<ColumnSelectorColumnRow
|
|
495
|
+
name={ctx.props().column.clone()}
|
|
496
|
+
col_type={ctx.props().column_type}
|
|
497
|
+
aggregate={ctx.props().aggregate.clone()}
|
|
498
|
+
trailing={ctx.props().trailing.clone()}
|
|
499
|
+
wrapper_class={classes!["column-selector-column-title"]}
|
|
500
|
+
ondragstart={Some(dragstart)}
|
|
501
|
+
ondragend={Some(dragend)}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
pub struct WindowEditor {
|
|
509
|
+
draft: WindowDraft,
|
|
510
|
+
error: Option<String>,
|
|
511
|
+
column_dropdown: ColumnDropDownElement,
|
|
512
|
+
_drop_sub: Subscription,
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
impl WindowEditor {
|
|
516
|
+
fn initialize(ctx: &Context<Self>) -> WindowDraft {
|
|
517
|
+
ctx.props()
|
|
518
|
+
.initial
|
|
519
|
+
.as_ref()
|
|
520
|
+
.map(WindowDraft::from_spec)
|
|
521
|
+
.unwrap_or_else(WindowDraft::new_default)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
fn emit(&mut self, ctx: &Context<Self>) {
|
|
525
|
+
match self.draft.validate(&ctx.props().metadata) {
|
|
526
|
+
Ok(spec) => {
|
|
527
|
+
self.error = None;
|
|
528
|
+
ctx.props().on_change.emit(Some(spec));
|
|
529
|
+
},
|
|
530
|
+
Err(msg) => {
|
|
531
|
+
self.error = Some(msg);
|
|
532
|
+
ctx.props().on_change.emit(None);
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
impl Component for WindowEditor {
|
|
539
|
+
type Message = WindowEditorMsg;
|
|
540
|
+
type Properties = WindowEditorProps;
|
|
541
|
+
|
|
542
|
+
fn create(ctx: &Context<Self>) -> Self {
|
|
543
|
+
let _drop_sub = {
|
|
544
|
+
let link = ctx.link().clone();
|
|
545
|
+
ctx.props().presentation.drop_received.add_listener(
|
|
546
|
+
move |(column, target, effect, _index): (String, DragTarget, DragEffect, usize)| {
|
|
547
|
+
let staged_origin = match effect {
|
|
548
|
+
DragEffect::Move(origin) if origin.is_staged() => Some(origin),
|
|
549
|
+
_ => None,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
if target.is_staged() || staged_origin.is_some() {
|
|
553
|
+
link.send_message(WindowEditorMsg::Drop(column, target, staged_origin));
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
)
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
let mut this = Self {
|
|
560
|
+
draft: Self::initialize(ctx),
|
|
561
|
+
error: None,
|
|
562
|
+
column_dropdown: ColumnDropDownElement::new(ctx.props().session.clone()),
|
|
563
|
+
_drop_sub,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
this.emit(ctx);
|
|
567
|
+
this
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
|
|
571
|
+
if ctx.props().reset_count != old_props.reset_count
|
|
572
|
+
|| ctx.props().initial != old_props.initial
|
|
573
|
+
{
|
|
574
|
+
self.draft = Self::initialize(ctx);
|
|
575
|
+
self.emit(ctx);
|
|
576
|
+
true
|
|
577
|
+
} else {
|
|
578
|
+
false
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
583
|
+
match msg {
|
|
584
|
+
WindowEditorMsg::DragEnter(target, index) => {
|
|
585
|
+
return ctx.props().presentation.notify_drag_enter(target, index);
|
|
586
|
+
},
|
|
587
|
+
WindowEditorMsg::DragLeave(target) => {
|
|
588
|
+
ctx.props().presentation.notify_drag_leave(target);
|
|
589
|
+
return true;
|
|
590
|
+
},
|
|
591
|
+
WindowEditorMsg::New(target, InPlaceColumn::Column(column)) => {
|
|
592
|
+
ctx.link()
|
|
593
|
+
.send_message(WindowEditorMsg::Drop(column, target, None));
|
|
594
|
+
return false;
|
|
595
|
+
},
|
|
596
|
+
WindowEditorMsg::New(_, InPlaceColumn::Expression(_)) => {
|
|
597
|
+
// In-place expression creation is a config-selector affordance;
|
|
598
|
+
// staged window slots take existing columns only.
|
|
599
|
+
return false;
|
|
600
|
+
},
|
|
601
|
+
WindowEditorMsg::Drop(column, target, origin) => {
|
|
602
|
+
// The origin slot clears FIRST so a self-move (origin ==
|
|
603
|
+
// target) re-inserts the same column and nets to no change.
|
|
604
|
+
match origin {
|
|
605
|
+
Some(DragTarget::WindowSource) => self.draft.source = String::default(),
|
|
606
|
+
Some(DragTarget::WindowOrderBy) => self.draft.order_by = String::default(),
|
|
607
|
+
Some(DragTarget::WindowPartitionBy) => {
|
|
608
|
+
self.draft.partition_by.retain(|x| x != &column)
|
|
609
|
+
},
|
|
610
|
+
_ => {},
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Slots take true `Table` columns ONLY - a staged drop of
|
|
614
|
+
// an expression or window column is rejected outright (the
|
|
615
|
+
// list already showed the invalid-X overlay during hover).
|
|
616
|
+
let is_table_column = ctx
|
|
617
|
+
.props()
|
|
618
|
+
.metadata
|
|
619
|
+
.get_table_columns()
|
|
620
|
+
.into_iter()
|
|
621
|
+
.flatten()
|
|
622
|
+
.any(|x| *x == column);
|
|
623
|
+
|
|
624
|
+
match target {
|
|
625
|
+
_ if target.is_staged() && !is_table_column => {},
|
|
626
|
+
DragTarget::WindowSource => {
|
|
627
|
+
self.draft.source = column;
|
|
628
|
+
|
|
629
|
+
// The op selector only offers the feature-declared
|
|
630
|
+
// set for the source's type, so an op orphaned by
|
|
631
|
+
// the new source coerces to that set's first entry.
|
|
632
|
+
let available = ctx
|
|
633
|
+
.props()
|
|
634
|
+
.metadata
|
|
635
|
+
.get_column_table_type(&self.draft.source)
|
|
636
|
+
.and_then(|ty| {
|
|
637
|
+
ctx.props()
|
|
638
|
+
.metadata
|
|
639
|
+
.get_features()
|
|
640
|
+
.map(|x| x.get_window_aggregates(ty))
|
|
641
|
+
})
|
|
642
|
+
.unwrap_or_default();
|
|
643
|
+
|
|
644
|
+
if !available.iter().any(|x| x.name == self.draft.op)
|
|
645
|
+
&& let Some(first) = available.first()
|
|
646
|
+
{
|
|
647
|
+
self.draft.op = first.name.clone();
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
DragTarget::WindowOrderBy => self.draft.order_by = column,
|
|
651
|
+
DragTarget::WindowPartitionBy
|
|
652
|
+
if !column.is_empty() && !self.draft.partition_by.contains(&column) =>
|
|
653
|
+
{
|
|
654
|
+
self.draft.partition_by.push(column);
|
|
655
|
+
},
|
|
656
|
+
// A committing target: the config zone handles its own
|
|
657
|
+
// insert; only the staged origin cleanup applies here.
|
|
658
|
+
_ => {},
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
WindowEditorMsg::SetOp(op) => {
|
|
662
|
+
self.draft.op = op;
|
|
663
|
+
|
|
664
|
+
// Keep the frame coherent as the op changes - an op that
|
|
665
|
+
// does not accept the current frame kind coerces to its
|
|
666
|
+
// first declared one (the dropdown omits the rest).
|
|
667
|
+
if let Some(declared) =
|
|
668
|
+
op_spec(&ctx.props().metadata, &self.draft.source, &self.draft.op)
|
|
669
|
+
&& !declared.frames.is_empty()
|
|
670
|
+
&& !declared
|
|
671
|
+
.frames
|
|
672
|
+
.iter()
|
|
673
|
+
.any(|x| x == frame_name(&self.draft.frame_type))
|
|
674
|
+
&& let Some(first) = declared.frames.first()
|
|
675
|
+
{
|
|
676
|
+
self.draft.frame_type = frame_label(first).to_string();
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
WindowEditorMsg::ClearSource => self.draft.source = String::default(),
|
|
680
|
+
WindowEditorMsg::ClearOrderBy => self.draft.order_by = String::default(),
|
|
681
|
+
WindowEditorMsg::ToggleOrderDir => self.draft.order_desc = !self.draft.order_desc,
|
|
682
|
+
WindowEditorMsg::RemovePartition(idx) => {
|
|
683
|
+
if idx < self.draft.partition_by.len() {
|
|
684
|
+
self.draft.partition_by.remove(idx);
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
WindowEditorMsg::SetFrameType(x) => self.draft.frame_type = x,
|
|
688
|
+
WindowEditorMsg::SetFrameRows(x) => {
|
|
689
|
+
self.draft.frame_rows = if x.is_finite() && x >= 0.0 {
|
|
690
|
+
x as u32
|
|
691
|
+
} else {
|
|
692
|
+
1
|
|
693
|
+
};
|
|
694
|
+
},
|
|
695
|
+
WindowEditorMsg::SetFrameRange(x) => {
|
|
696
|
+
self.draft.frame_range = if x.is_finite() && x > 0.0 { x } else { 1.0 };
|
|
697
|
+
},
|
|
698
|
+
WindowEditorMsg::SetOffset(x) => {
|
|
699
|
+
self.draft.offset = if x.is_finite() && x >= 0.0 {
|
|
700
|
+
x as u32
|
|
701
|
+
} else {
|
|
702
|
+
1
|
|
703
|
+
};
|
|
704
|
+
},
|
|
705
|
+
WindowEditorMsg::SetAlpha(x) => {
|
|
706
|
+
self.draft.alpha = if x.is_finite() && x > 0.0 && x <= 1.0 {
|
|
707
|
+
x
|
|
708
|
+
} else {
|
|
709
|
+
1.0
|
|
710
|
+
};
|
|
711
|
+
},
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
self.emit(ctx);
|
|
715
|
+
true
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
719
|
+
let declared = op_spec(&ctx.props().metadata, &self.draft.source, &self.draft.op);
|
|
720
|
+
|
|
721
|
+
// The op selector renders the FEATURE-DECLARED window aggregates
|
|
722
|
+
// for the source column's type, in the server's declared order -
|
|
723
|
+
// capability and type-validity both come from the data model
|
|
724
|
+
// (`GetFeaturesResp::window_aggregates`), not a hardcoded list.
|
|
725
|
+
let ops: Rc<Vec<SelectItem<String>>> = Rc::new(
|
|
726
|
+
ctx.props()
|
|
727
|
+
.metadata
|
|
728
|
+
.get_column_table_type(&self.draft.source)
|
|
729
|
+
.and_then(|ty| {
|
|
730
|
+
ctx.props()
|
|
731
|
+
.metadata
|
|
732
|
+
.get_features()
|
|
733
|
+
.map(|x| x.get_window_aggregates(ty))
|
|
734
|
+
})
|
|
735
|
+
.unwrap_or_default()
|
|
736
|
+
.into_iter()
|
|
737
|
+
.map(|x| SelectItem::Option(x.name.clone()))
|
|
738
|
+
.collect(),
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// Number inputs report `value_as_number` (`NaN` for empty/garbage),
|
|
742
|
+
// the Style tab `NumberField` idiom - the handlers reset invalid
|
|
743
|
+
// values to the default.
|
|
744
|
+
let on_number_input = |f: fn(f64) -> WindowEditorMsg| {
|
|
745
|
+
ctx.link().callback(move |event: InputEvent| {
|
|
746
|
+
let value = event
|
|
747
|
+
.target()
|
|
748
|
+
.and_then(|t| t.dyn_into::<HtmlInputElement>().ok())
|
|
749
|
+
.map(|x| x.value_as_number())
|
|
750
|
+
.unwrap_or(f64::NAN);
|
|
751
|
+
f(value)
|
|
752
|
+
})
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// Frame type as a dropdown; like the op selector, invalid choices
|
|
756
|
+
// are omitted rather than disabled - the declared `frames` are the
|
|
757
|
+
// menu, and `SetOp` already coerces the draft into them.
|
|
758
|
+
let frame_types: Rc<Vec<SelectItem<String>>> = Rc::new(
|
|
759
|
+
declared
|
|
760
|
+
.as_ref()
|
|
761
|
+
.map(|x| x.frames.clone())
|
|
762
|
+
.unwrap_or_default()
|
|
763
|
+
.iter()
|
|
764
|
+
.map(|x| SelectItem::Option(frame_label(x).to_string()))
|
|
765
|
+
.collect(),
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// Slots take true `Table` columns ONLY - expression aliases and
|
|
769
|
+
// other window columns would create dependency cycles (and force
|
|
770
|
+
// delete-blocking). They join every slot's `exclude` set (removing
|
|
771
|
+
// them from the autocomplete dropdowns), the drop handler rejects
|
|
772
|
+
// them, and an invalid hover shows the X overlay below.
|
|
773
|
+
let non_table: HashSet<String> = ctx
|
|
774
|
+
.props()
|
|
775
|
+
.metadata
|
|
776
|
+
.get_expression_columns()
|
|
777
|
+
.chain(ctx.props().metadata.get_window_columns())
|
|
778
|
+
.cloned()
|
|
779
|
+
.collect();
|
|
780
|
+
|
|
781
|
+
let is_invalid_drag = |target: DragTarget| {
|
|
782
|
+
ctx.props()
|
|
783
|
+
.presentation
|
|
784
|
+
.is_dragover(target)
|
|
785
|
+
.map(|(_, col)| non_table.contains(&col))
|
|
786
|
+
.unwrap_or_default()
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// The op selector renders in the source pill's aggregate-selector
|
|
790
|
+
// space - the ONLY place a window op control exists in the UI.
|
|
791
|
+
let source_exclude: HashSet<String> = std::iter::once(self.draft.source.clone())
|
|
792
|
+
.chain(non_table.iter().cloned())
|
|
793
|
+
.collect();
|
|
794
|
+
|
|
795
|
+
let source_list = html! {
|
|
796
|
+
<DragDropList<WindowEditor, WindowSlotColumn, WindowSourceContext>
|
|
797
|
+
name="window-source"
|
|
798
|
+
parent={ctx.link().clone()}
|
|
799
|
+
presentation={ctx.props().presentation.clone()}
|
|
800
|
+
column_dropdown={self.column_dropdown.clone()}
|
|
801
|
+
exclude={source_exclude}
|
|
802
|
+
is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowSource)}
|
|
803
|
+
is_invalid={is_invalid_drag(DragTarget::WindowSource)}
|
|
804
|
+
single_slot=true
|
|
805
|
+
>
|
|
806
|
+
{ for (!self.draft.source.is_empty()).then(|| yew::html_nested! {
|
|
807
|
+
<WindowSlotColumn
|
|
808
|
+
column={self.draft.source.clone()}
|
|
809
|
+
column_type={ctx.props().metadata.get_column_table_type(&self.draft.source)}
|
|
810
|
+
action={DragTarget::WindowSource}
|
|
811
|
+
presentation={ctx.props().presentation.clone()}
|
|
812
|
+
aggregate={html! {
|
|
813
|
+
<div class="aggregate-selector-wrapper">
|
|
814
|
+
<Select<String>
|
|
815
|
+
wrapper_class="aggregate-selector"
|
|
816
|
+
values={ops.clone()}
|
|
817
|
+
selected={self.draft.op.clone()}
|
|
818
|
+
on_select={ctx.link().callback(WindowEditorMsg::SetOp)}
|
|
819
|
+
/>
|
|
820
|
+
</div>
|
|
821
|
+
}}
|
|
822
|
+
/>
|
|
823
|
+
}) }
|
|
824
|
+
</DragDropList<WindowEditor, WindowSlotColumn, WindowSourceContext>>
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
let order_exclude: HashSet<String> = std::iter::once(self.draft.order_by.clone())
|
|
828
|
+
.chain(non_table.iter().cloned())
|
|
829
|
+
.collect();
|
|
830
|
+
let order_list = html! {
|
|
831
|
+
<DragDropList<WindowEditor, WindowSlotColumn, WindowOrderByContext>
|
|
832
|
+
name="window-order-by"
|
|
833
|
+
parent={ctx.link().clone()}
|
|
834
|
+
presentation={ctx.props().presentation.clone()}
|
|
835
|
+
column_dropdown={self.column_dropdown.clone()}
|
|
836
|
+
exclude={order_exclude}
|
|
837
|
+
is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowOrderBy)}
|
|
838
|
+
is_invalid={is_invalid_drag(DragTarget::WindowOrderBy)}
|
|
839
|
+
single_slot=true
|
|
840
|
+
>
|
|
841
|
+
{ for (!self.draft.order_by.is_empty()).then(|| {
|
|
842
|
+
// The sort pill's direction affordance, staged: the
|
|
843
|
+
// toggle mutates the draft only, committed by Save.
|
|
844
|
+
let dir = if self.draft.order_desc {
|
|
845
|
+
WindowSortDir::Desc
|
|
846
|
+
} else {
|
|
847
|
+
WindowSortDir::Asc
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
let onmousedown = ctx
|
|
851
|
+
.link()
|
|
852
|
+
.callback(|_: MouseEvent| WindowEditorMsg::ToggleOrderDir);
|
|
853
|
+
|
|
854
|
+
yew::html_nested! {
|
|
855
|
+
<WindowSlotColumn
|
|
856
|
+
column={self.draft.order_by.clone()}
|
|
857
|
+
column_type={ctx.props().metadata.get_column_table_type(&self.draft.order_by)}
|
|
858
|
+
action={DragTarget::WindowOrderBy}
|
|
859
|
+
presentation={ctx.props().presentation.clone()}
|
|
860
|
+
trailing={html! {
|
|
861
|
+
<span
|
|
862
|
+
class={format!("sort-icon {}", dir)}
|
|
863
|
+
{onmousedown}
|
|
864
|
+
/>
|
|
865
|
+
}}
|
|
866
|
+
/>
|
|
867
|
+
}
|
|
868
|
+
}) }
|
|
869
|
+
</DragDropList<WindowEditor, WindowSlotColumn, WindowOrderByContext>>
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
let partition_exclude: HashSet<String> = self
|
|
873
|
+
.draft
|
|
874
|
+
.partition_by
|
|
875
|
+
.iter()
|
|
876
|
+
.cloned()
|
|
877
|
+
.chain(non_table.iter().cloned())
|
|
878
|
+
.collect();
|
|
879
|
+
let partition_list = html! {
|
|
880
|
+
<DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>
|
|
881
|
+
name="window-partition-by"
|
|
882
|
+
parent={ctx.link().clone()}
|
|
883
|
+
presentation={ctx.props().presentation.clone()}
|
|
884
|
+
column_dropdown={self.column_dropdown.clone()}
|
|
885
|
+
exclude={partition_exclude}
|
|
886
|
+
is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowPartitionBy)}
|
|
887
|
+
is_invalid={is_invalid_drag(DragTarget::WindowPartitionBy)}
|
|
888
|
+
>
|
|
889
|
+
{ for self.draft.partition_by.iter().map(|col| yew::html_nested! {
|
|
890
|
+
<PivotColumn
|
|
891
|
+
column={col.clone()}
|
|
892
|
+
column_type={ctx.props().metadata.get_column_table_type(col)}
|
|
893
|
+
action={DragTarget::WindowPartitionBy}
|
|
894
|
+
presentation={ctx.props().presentation.clone()}
|
|
895
|
+
/>
|
|
896
|
+
}) }
|
|
897
|
+
</DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>>
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
let show_frame = declared
|
|
901
|
+
.as_ref()
|
|
902
|
+
.map(|x| !x.frames.is_empty())
|
|
903
|
+
.unwrap_or_default();
|
|
904
|
+
let show_offset = declared.as_ref().map(|x| x.offset).unwrap_or_default();
|
|
905
|
+
let show_alpha = declared.as_ref().map(|x| x.alpha).unwrap_or_default();
|
|
906
|
+
|
|
907
|
+
html! {
|
|
908
|
+
<>
|
|
909
|
+
<div id="window-editor-slots">{ source_list }{ order_list }{ partition_list }</div>
|
|
910
|
+
<div id="window-editor-container">
|
|
911
|
+
if show_frame {
|
|
912
|
+
<div class="column-style-label"><label id="window-frame-label" /></div>
|
|
913
|
+
<div class="row">
|
|
914
|
+
<Select<String>
|
|
915
|
+
id="window-frame-type"
|
|
916
|
+
values={frame_types}
|
|
917
|
+
selected={self.draft.frame_type.clone()}
|
|
918
|
+
on_select={ctx.link().callback(WindowEditorMsg::SetFrameType)}
|
|
919
|
+
/>
|
|
920
|
+
</div>
|
|
921
|
+
if self.draft.frame_type == "Rows" {
|
|
922
|
+
<div class="row">
|
|
923
|
+
<input
|
|
924
|
+
type="number"
|
|
925
|
+
class="parameter"
|
|
926
|
+
min="0"
|
|
927
|
+
value={self.draft.frame_rows.to_string()}
|
|
928
|
+
oninput={on_number_input(WindowEditorMsg::SetFrameRows)}
|
|
929
|
+
/>
|
|
930
|
+
</div>
|
|
931
|
+
}
|
|
932
|
+
if self.draft.frame_type == "Range" {
|
|
933
|
+
<div class="row">
|
|
934
|
+
<input
|
|
935
|
+
type="number"
|
|
936
|
+
class="parameter"
|
|
937
|
+
min="0"
|
|
938
|
+
value={self.draft.frame_range.to_string()}
|
|
939
|
+
oninput={on_number_input(WindowEditorMsg::SetFrameRange)}
|
|
940
|
+
/>
|
|
941
|
+
</div>
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if show_offset {
|
|
945
|
+
<div class="column-style-label">
|
|
946
|
+
<label id="window-offset-label" class="indent" />
|
|
947
|
+
</div>
|
|
948
|
+
<div class="row">
|
|
949
|
+
<input
|
|
950
|
+
type="number"
|
|
951
|
+
class="parameter"
|
|
952
|
+
min="0"
|
|
953
|
+
value={self.draft.offset.to_string()}
|
|
954
|
+
oninput={on_number_input(WindowEditorMsg::SetOffset)}
|
|
955
|
+
/>
|
|
956
|
+
</div>
|
|
957
|
+
}
|
|
958
|
+
if show_alpha {
|
|
959
|
+
<div class="column-style-label">
|
|
960
|
+
<label id="window-alpha-label" class="indent" />
|
|
961
|
+
</div>
|
|
962
|
+
<div class="row">
|
|
963
|
+
<input
|
|
964
|
+
type="number"
|
|
965
|
+
class="parameter"
|
|
966
|
+
step="0.05"
|
|
967
|
+
min="0"
|
|
968
|
+
max="1"
|
|
969
|
+
value={self.draft.alpha.to_string()}
|
|
970
|
+
oninput={on_number_input(WindowEditorMsg::SetAlpha)}
|
|
971
|
+
/>
|
|
972
|
+
</div>
|
|
973
|
+
}
|
|
974
|
+
if let Some(error) = &self.error {
|
|
975
|
+
<div class="row window-editor-error">{ error.clone() }</div>
|
|
976
|
+
}
|
|
977
|
+
</div>
|
|
978
|
+
<ColumnDropDownPortal
|
|
979
|
+
element={self.column_dropdown.clone()}
|
|
980
|
+
theme={ctx.props().selected_theme.clone().unwrap_or_default()}
|
|
981
|
+
/>
|
|
982
|
+
</>
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|