@perspective-dev/viewer 4.0.0 → 4.1.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 +2 -2
- package/dist/cdn/perspective-viewer.js.map +4 -4
- 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/monokai.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/esm/extensions.d.ts +23 -2
- package/dist/esm/perspective-viewer.d.ts +2 -7
- package/dist/esm/perspective-viewer.inline.js +2 -2
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +2 -2
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/plugin.d.ts +1 -1
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +1 -0
- package/dist/wasm/perspective-viewer.d.ts +218 -46
- package/dist/wasm/perspective-viewer.js +1251 -762
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +38 -19
- package/package.json +1 -1
- package/src/less/containers/scroll-panel.less +0 -1
- package/src/less/plugin-selector.less +15 -5
- package/src/less/status-bar.less +75 -27
- package/src/less/viewer.less +140 -58
- package/src/rust/components/column_dropdown.rs +21 -21
- package/src/rust/components/column_selector/active_column.rs +131 -120
- package/src/rust/components/column_selector/add_expression_button.rs +5 -0
- package/src/rust/components/column_selector/aggregate_selector.rs +8 -4
- package/src/rust/components/column_selector/config_selector.rs +170 -161
- package/src/rust/components/column_selector/empty_column.rs +16 -11
- package/src/rust/components/column_selector/{expression_toolbar.rs → expr_edit_button.rs} +7 -0
- package/src/rust/components/column_selector/filter_column.rs +195 -194
- package/src/rust/components/column_selector/inactive_column.rs +82 -67
- package/src/rust/components/column_selector/pivot_column.rs +16 -11
- package/src/rust/components/column_selector/sort_column.rs +9 -7
- package/src/rust/components/column_selector.rs +42 -37
- package/src/rust/components/column_settings_sidebar/save_settings.rs +3 -1
- package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +58 -0
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +6 -6
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +2 -94
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +111 -0
- package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +3 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +23 -83
- package/src/rust/components/{column_settings_sidebar/sidebar.rs → column_settings_sidebar.rs} +198 -171
- package/src/rust/components/containers/dragdrop_list.rs +20 -20
- package/src/rust/components/containers/dropdown_menu.rs +4 -6
- package/src/rust/components/containers/mod.rs +1 -4
- package/src/rust/components/containers/scroll_panel.rs +80 -80
- package/src/rust/components/containers/scroll_panel_item.rs +36 -36
- package/src/rust/components/containers/select.rs +46 -44
- package/src/rust/components/containers/sidebar.rs +3 -19
- package/src/rust/components/{column_settings_sidebar/style_tab/symbol/symbol_config.rs → containers/sidebar_close_button.rs} +15 -9
- package/src/rust/components/containers/split_panel.rs +212 -200
- package/src/rust/components/containers/tab_list.rs +11 -11
- package/src/rust/components/copy_dropdown.rs +22 -25
- package/src/rust/components/datetime_column_style/custom.rs +19 -19
- package/src/rust/components/datetime_column_style/simple.rs +13 -14
- package/src/rust/components/datetime_column_style.rs +75 -76
- package/src/rust/components/editable_header.rs +18 -14
- package/src/rust/components/empty_row.rs +5 -5
- package/src/rust/components/export_dropdown.rs +42 -42
- package/src/rust/components/expression_editor.rs +25 -19
- package/src/rust/components/filter_dropdown.rs +22 -22
- package/src/rust/components/font_loader.rs +11 -9
- package/src/rust/components/form/code_editor.rs +106 -105
- package/src/rust/components/form/color_range_selector.rs +14 -12
- package/src/rust/components/form/color_selector.rs +3 -1
- package/src/rust/components/form/debug.rs +95 -94
- package/src/rust/components/form/highlight.rs +5 -3
- package/src/rust/components/form/mod.rs +3 -2
- package/src/rust/components/form/optional_field.rs +2 -2
- package/src/rust/components/form/{select_field.rs → select_enum_field.rs} +1 -46
- package/src/rust/components/form/select_value_field.rs +64 -0
- package/src/rust/components/function_dropdown.rs +21 -21
- package/src/rust/components/main_panel.rs +219 -0
- package/src/rust/components/mod.rs +6 -6
- package/src/rust/components/modal.rs +42 -42
- package/src/rust/components/number_column_style.rs +34 -88
- package/src/rust/components/plugin_selector.rs +22 -25
- package/src/rust/components/render_warning.rs +9 -6
- package/src/rust/components/settings_panel.rs +82 -0
- package/src/rust/components/status_bar.rs +250 -146
- package/src/rust/components/status_bar_counter.rs +26 -119
- package/src/rust/components/status_indicator.rs +95 -79
- package/src/rust/components/string_column_style.rs +45 -45
- package/src/rust/components/style/style_provider.rs +1 -15
- package/src/rust/components/style_controls/number_string_format/digits_section.rs +1 -1
- package/src/rust/components/style_controls/number_string_format/misc_section.rs +1 -1
- package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -1
- package/src/rust/components/style_controls/number_string_format.rs +45 -46
- package/src/rust/components/type_icon.rs +14 -11
- package/src/rust/components/viewer.rs +241 -384
- package/src/rust/config/columns_config.rs +2 -2
- package/src/rust/config/datetime_column_style.rs +1 -6
- package/src/rust/config/mod.rs +1 -0
- package/src/rust/config/number_column_style.rs +0 -6
- package/src/rust/config/number_string_format.rs +27 -4
- package/src/rust/config/viewer_config.rs +27 -167
- package/src/rust/custom_elements/copy_dropdown.rs +14 -6
- package/src/rust/custom_elements/export_dropdown.rs +15 -7
- package/src/rust/custom_elements/filter_dropdown.rs +4 -4
- package/src/rust/custom_elements/mod.rs +3 -0
- package/src/rust/custom_elements/viewer.rs +353 -161
- package/src/rust/custom_events.rs +55 -32
- package/src/rust/dragdrop.rs +4 -24
- package/src/rust/exprtk/cursor.rs +10 -1
- package/src/rust/exprtk/mod.rs +2 -0
- package/src/rust/exprtk/tokenize.rs +20 -3
- package/src/rust/js/clipboard.rs +2 -2
- package/src/rust/js/mimetype.rs +2 -7
- package/src/rust/js/mod.rs +0 -1
- package/src/rust/js/plugin.rs +7 -0
- package/src/rust/lib.rs +18 -5
- package/src/rust/model/column_locator.rs +82 -0
- package/src/rust/model/columns_iter_set.rs +1 -0
- package/src/rust/model/copy_export.rs +50 -14
- package/src/rust/model/edit_expression.rs +2 -5
- package/src/rust/model/eject.rs +41 -0
- package/src/rust/model/export_app.rs +3 -2
- package/src/rust/model/get_viewer_config.rs +4 -28
- package/src/rust/model/intersection_observer.rs +20 -8
- package/src/rust/model/mod.rs +11 -4
- package/src/rust/model/plugin_column_styles.rs +0 -31
- package/src/rust/model/reset_all.rs +38 -0
- package/src/rust/model/resize_observer.rs +34 -7
- package/src/rust/model/restore_and_render.rs +12 -7
- package/src/rust/{utils/scope.rs → model/send_plugin_config.rs} +32 -35
- package/src/rust/model/structural.rs +194 -23
- package/src/rust/model/update_and_render.rs +14 -4
- package/src/rust/{model/create_col.rs → presentation/column_locator.rs} +73 -42
- package/src/rust/{utils/wasm_abi.rs → presentation/sheets.rs} +54 -40
- package/src/rust/presentation.rs +60 -119
- package/src/rust/renderer/activate.rs +20 -5
- package/src/rust/renderer/limits.rs +0 -149
- package/src/rust/renderer/render_timer.rs +1 -1
- package/src/rust/renderer.rs +34 -18
- package/src/rust/root.rs +50 -0
- package/src/rust/session/column_defaults_update.rs +4 -4
- package/src/rust/session/drag_drop_update.rs +1 -1
- package/src/rust/session/metadata.rs +3 -17
- package/src/rust/session/replace_expression_update.rs +1 -2
- package/src/rust/session.rs +162 -82
- package/src/rust/utils/browser/blob.rs +16 -2
- package/src/rust/utils/browser/download.rs +1 -0
- package/src/rust/{components/column_settings_sidebar/mod.rs → utils/browser/dragdrop.rs} +14 -5
- package/src/rust/utils/browser/mod.rs +8 -4
- package/src/rust/utils/browser/selection.rs +5 -0
- package/src/rust/utils/custom_element.rs +28 -13
- package/src/rust/utils/datetime.rs +5 -0
- package/src/rust/utils/debounce.rs +7 -1
- package/src/rust/utils/hooks/use_async_callback.rs +7 -17
- package/src/rust/utils/mod.rs +28 -40
- package/src/rust/utils/number_format.rs +6 -5
- package/src/rust/utils/pubsub.rs +15 -10
- package/src/rust/utils/weak_scope.rs +11 -1
- package/src/svg/bookmark-icon.svg +4 -0
- package/src/svg/drag-handle copy.svg +10 -0
- package/src/svg/drawer-tab-hover.svg +5 -7
- package/src/svg/drawer-tab-invert-hover.svg +4 -8
- package/src/svg/drawer-tab-invert.svg +4 -7
- package/src/svg/drawer-tab.svg +4 -6
- package/src/svg/status_ok.svg +24 -24
- package/src/ts/extensions.ts +51 -3
- package/src/ts/perspective-viewer.ts +2 -14
- package/src/ts/plugin.ts +1 -1
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +1 -1
- package/src/rust/components/column_settings_sidebar/style_tab/column_style.rs +0 -177
- package/src/rust/components/containers/tests/mod.rs +0 -11
- package/src/rust/components/containers/tests/split_panel.rs +0 -91
- package/src/rust/js/testing.rs +0 -149
- package/src/rust/utils/tee.rs +0 -88
- /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline4.js +0 -0
- /package/src/rust/components/{style_controls.rs → style_controls/mod.rs} +0 -0
- /package/src/rust/{components/containers → config}/kvpair.rs +0 -0
|
@@ -16,6 +16,7 @@ use std::rc::Rc;
|
|
|
16
16
|
use chrono::{Datelike, NaiveDate, TimeZone, Utc};
|
|
17
17
|
use perspective_client::config::*;
|
|
18
18
|
use perspective_client::utils::PerspectiveResultExt;
|
|
19
|
+
use perspective_js::utils::ApiFuture;
|
|
19
20
|
use wasm_bindgen::JsCast;
|
|
20
21
|
use web_sys::*;
|
|
21
22
|
use yew::prelude::*;
|
|
@@ -29,30 +30,17 @@ use crate::dragdrop::*;
|
|
|
29
30
|
use crate::model::*;
|
|
30
31
|
use crate::renderer::*;
|
|
31
32
|
use crate::session::*;
|
|
32
|
-
use crate::utils
|
|
33
|
+
use crate::utils::*;
|
|
33
34
|
use crate::*;
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
pub struct FilterColumn {
|
|
37
|
-
input: String,
|
|
38
|
-
input_ref: NodeRef,
|
|
39
|
-
filter_ops: Rc<Vec<SelectItem<String>>>,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
#[derive(Debug)]
|
|
43
|
-
pub enum FilterColumnMsg {
|
|
44
|
-
FilterInput((usize, String), String),
|
|
45
|
-
Close,
|
|
46
|
-
FilterOpSelect(String),
|
|
47
|
-
FilterKeyDown(u32),
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
#[derive(Properties, Clone)]
|
|
36
|
+
#[derive(Clone, Properties, PerspectiveProperties!)]
|
|
51
37
|
pub struct FilterColumnProps {
|
|
52
38
|
pub filter: Filter,
|
|
53
39
|
pub idx: usize,
|
|
54
40
|
pub filter_dropdown: FilterDropDownElement,
|
|
55
41
|
pub on_keydown: Callback<String>,
|
|
42
|
+
|
|
43
|
+
// State
|
|
56
44
|
pub session: Session,
|
|
57
45
|
pub renderer: Renderer,
|
|
58
46
|
pub dragdrop: DragDrop,
|
|
@@ -64,8 +52,6 @@ impl PartialEq for FilterColumnProps {
|
|
|
64
52
|
}
|
|
65
53
|
}
|
|
66
54
|
|
|
67
|
-
derive_model!(Renderer, Session for FilterColumnProps);
|
|
68
|
-
|
|
69
55
|
impl DragDropListItemProps for FilterColumnProps {
|
|
70
56
|
type Item = Filter;
|
|
71
57
|
|
|
@@ -74,179 +60,42 @@ impl DragDropListItemProps for FilterColumnProps {
|
|
|
74
60
|
}
|
|
75
61
|
}
|
|
76
62
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|| self.filter.op() == "!="
|
|
84
|
-
|| self.filter.op() == "in"
|
|
85
|
-
|| self.filter.op() == "not in")
|
|
86
|
-
&& self.get_filter_type() == Some(ColumnType::String)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/// Get this filter's type, e.g. the type of the column.
|
|
90
|
-
fn get_filter_type(&self) -> Option<ColumnType> {
|
|
91
|
-
self.session
|
|
92
|
-
.metadata()
|
|
93
|
-
.get_column_table_type(self.filter.column())
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Get the string value, suitable for the `value` field of a `FilterColumns`'s
|
|
97
|
-
// `<input>`.
|
|
98
|
-
fn get_filter_input(&self) -> Option<String> {
|
|
99
|
-
let filter_type = self.get_filter_type()?;
|
|
100
|
-
match (&filter_type, &self.filter.term()) {
|
|
101
|
-
(ColumnType::Date, FilterTerm::Scalar(Scalar::Float(x))) => {
|
|
102
|
-
if *x > 0_f64 {
|
|
103
|
-
Some(
|
|
104
|
-
Utc.timestamp_opt(*x as i64 / 1000, (*x as u32 % 1000) * 1000)
|
|
105
|
-
.earliest()?
|
|
106
|
-
.format("%Y-%m-%d")
|
|
107
|
-
.to_string(),
|
|
108
|
-
)
|
|
109
|
-
} else {
|
|
110
|
-
None
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
(ColumnType::Datetime, FilterTerm::Scalar(Scalar::Float(x))) => {
|
|
114
|
-
posix_to_utc_str(*x).ok()
|
|
115
|
-
},
|
|
116
|
-
(ColumnType::Boolean, FilterTerm::Scalar(Scalar::Bool(x))) => {
|
|
117
|
-
Some((if *x { "true" } else { "false" }).to_owned())
|
|
118
|
-
},
|
|
119
|
-
(ColumnType::Boolean, _) => Some("true".to_owned()),
|
|
120
|
-
(_, x) => Some(format!("{x}")),
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/// Get the allowed `FilterOp`s for this filter.
|
|
125
|
-
fn get_filter_ops(&self, col_type: ColumnType) -> Option<Vec<String>> {
|
|
126
|
-
let metadata = self.session.metadata();
|
|
127
|
-
let features = metadata.get_features()?;
|
|
128
|
-
features
|
|
129
|
-
.filter_ops
|
|
130
|
-
.get(&(col_type as u32))
|
|
131
|
-
.map(|x| x.options.clone())
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/// Update the filter comparison operator.
|
|
135
|
-
///
|
|
136
|
-
/// # Arguments
|
|
137
|
-
/// - `op` The new `FilterOp`.
|
|
138
|
-
fn update_filter_op(&self, op: String) {
|
|
139
|
-
let mut filter = self.session.get_view_config().filter.clone();
|
|
140
|
-
let filter_column = &mut filter.get_mut(self.idx).expect("Filter on no column");
|
|
141
|
-
*filter_column.op_mut() = op;
|
|
142
|
-
let update = ViewConfigUpdate {
|
|
143
|
-
filter: Some(filter),
|
|
144
|
-
..ViewConfigUpdate::default()
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
self.update_and_render(update)
|
|
148
|
-
.map(ApiFuture::spawn)
|
|
149
|
-
.unwrap_or_log();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/// Update the filter value from the string input read from the DOM.
|
|
153
|
-
///
|
|
154
|
-
/// # Arguments
|
|
155
|
-
/// - `val` The new filter value.
|
|
156
|
-
fn update_filter_input(&self, val: String) {
|
|
157
|
-
let mut filter = self.session.get_view_config().filter.clone();
|
|
158
|
-
let filter_column = &mut filter.get_mut(self.idx).expect("Filter on no column");
|
|
159
|
-
|
|
160
|
-
// TODO This belongs in the Features API.
|
|
161
|
-
let filter_input = if filter_column.op() == "in" || filter_column.op() == "not in" {
|
|
162
|
-
Some(FilterTerm::Array(
|
|
163
|
-
val.split(',')
|
|
164
|
-
.map(|x| Scalar::String(x.trim().to_owned()))
|
|
165
|
-
.collect(),
|
|
166
|
-
))
|
|
167
|
-
} else {
|
|
168
|
-
match self.get_filter_type() {
|
|
169
|
-
Some(ColumnType::String) => Some(FilterTerm::Scalar(Scalar::String(val))),
|
|
170
|
-
Some(ColumnType::Integer) => {
|
|
171
|
-
if val.is_empty() {
|
|
172
|
-
None
|
|
173
|
-
} else if let Ok(num) = val.parse::<f64>() {
|
|
174
|
-
Some(FilterTerm::Scalar(Scalar::Float(num.floor())))
|
|
175
|
-
} else {
|
|
176
|
-
None
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
Some(ColumnType::Float) => {
|
|
180
|
-
if val.is_empty() {
|
|
181
|
-
None
|
|
182
|
-
} else if let Ok(num) = val.parse::<f64>() {
|
|
183
|
-
Some(FilterTerm::Scalar(Scalar::Float(num)))
|
|
184
|
-
} else {
|
|
185
|
-
None
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
Some(ColumnType::Date) => match NaiveDate::parse_from_str(&val, "%Y-%m-%d") {
|
|
189
|
-
Ok(ref posix) => Some(FilterTerm::Scalar(Scalar::String(format!(
|
|
190
|
-
"{:0>4}-{:0>2}-{:0>2}",
|
|
191
|
-
posix.year(),
|
|
192
|
-
posix.month(),
|
|
193
|
-
posix.day(),
|
|
194
|
-
)))),
|
|
195
|
-
_ => None,
|
|
196
|
-
},
|
|
197
|
-
Some(ColumnType::Datetime) => match str_to_utc_posix(&val) {
|
|
198
|
-
Ok(x) => Some(FilterTerm::Scalar(Scalar::Float(x))),
|
|
199
|
-
_ => None,
|
|
200
|
-
},
|
|
201
|
-
Some(ColumnType::Boolean) => Some(FilterTerm::Scalar(match val.as_str() {
|
|
202
|
-
"true" => Scalar::Bool(true),
|
|
203
|
-
_ => Scalar::Bool(false),
|
|
204
|
-
})),
|
|
205
|
-
|
|
206
|
-
// shouldn't be reachable ..
|
|
207
|
-
_ => None,
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
if let Some(input) = filter_input
|
|
212
|
-
&& &input != filter_column.term()
|
|
213
|
-
{
|
|
214
|
-
*filter_column.term_mut() = input;
|
|
215
|
-
let update = ViewConfigUpdate {
|
|
216
|
-
filter: Some(filter),
|
|
217
|
-
..ViewConfigUpdate::default()
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
self.update_and_render(update)
|
|
221
|
-
.map(ApiFuture::spawn)
|
|
222
|
-
.unwrap_or_log();
|
|
223
|
-
}
|
|
224
|
-
}
|
|
63
|
+
#[derive(Debug)]
|
|
64
|
+
pub enum FilterColumnMsg {
|
|
65
|
+
FilterInput((usize, String), String),
|
|
66
|
+
Close,
|
|
67
|
+
FilterOpSelect(String),
|
|
68
|
+
FilterKeyDown(u32),
|
|
225
69
|
}
|
|
226
70
|
|
|
227
|
-
|
|
71
|
+
/// A control for a single filter condition.
|
|
72
|
+
pub struct FilterColumn {
|
|
73
|
+
input: String,
|
|
74
|
+
input_ref: NodeRef,
|
|
75
|
+
filter_ops: Rc<Vec<SelectItem<String>>>,
|
|
76
|
+
}
|
|
228
77
|
|
|
229
78
|
impl Component for FilterColumn {
|
|
230
79
|
type Message = FilterColumnMsg;
|
|
231
80
|
type Properties = FilterColumnProps;
|
|
232
81
|
|
|
233
82
|
fn create(ctx: &Context<Self>) -> Self {
|
|
234
|
-
|
|
235
|
-
let
|
|
83
|
+
let input_ref = NodeRef::default();
|
|
84
|
+
let mut this = Self {
|
|
85
|
+
input: "".to_string(),
|
|
86
|
+
input_ref,
|
|
87
|
+
filter_ops: Rc::default(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let col_type = ctx.props().get_current_filter_type();
|
|
91
|
+
this.input = ctx
|
|
236
92
|
.props()
|
|
237
93
|
.get_filter_input()
|
|
238
94
|
.unwrap_or_else(|| "".to_owned());
|
|
239
|
-
let input_ref = NodeRef::default();
|
|
240
|
-
let col_type = ctx.props().get_filter_type();
|
|
241
|
-
if col_type == Some(ColumnType::Boolean) {
|
|
242
|
-
ctx.props().update_filter_input(input.clone());
|
|
243
|
-
}
|
|
244
95
|
|
|
245
|
-
|
|
96
|
+
this.filter_ops = Rc::new(
|
|
246
97
|
maybe! {
|
|
247
|
-
Some(ctx
|
|
248
|
-
.props()
|
|
249
|
-
.get_filter_ops(col_type?)?
|
|
98
|
+
Some(get_filter_ops(ctx.props().session(), col_type?)?
|
|
250
99
|
.into_iter()
|
|
251
100
|
.map(SelectItem::Option)
|
|
252
101
|
.collect::<Vec<_>>())
|
|
@@ -254,18 +103,18 @@ impl Component for FilterColumn {
|
|
|
254
103
|
.unwrap_or_default(),
|
|
255
104
|
);
|
|
256
105
|
|
|
257
|
-
|
|
258
|
-
input
|
|
259
|
-
input_ref,
|
|
260
|
-
filter_ops,
|
|
106
|
+
if col_type == Some(ColumnType::Boolean) {
|
|
107
|
+
ctx.props().update_filter_input(this.input.clone());
|
|
261
108
|
}
|
|
109
|
+
|
|
110
|
+
this
|
|
262
111
|
}
|
|
263
112
|
|
|
264
113
|
fn update(&mut self, ctx: &Context<Self>, msg: FilterColumnMsg) -> bool {
|
|
265
114
|
match msg {
|
|
266
115
|
FilterColumnMsg::FilterInput(column, input) => {
|
|
267
116
|
let target = self.input_ref.cast::<HtmlInputElement>().unwrap();
|
|
268
|
-
let input = if ctx.props().
|
|
117
|
+
let input = if ctx.props().get_current_filter_type() == Some(ColumnType::Boolean) {
|
|
269
118
|
if target.checked() {
|
|
270
119
|
"true".to_owned()
|
|
271
120
|
} else {
|
|
@@ -320,23 +169,21 @@ impl Component for FilterColumn {
|
|
|
320
169
|
},
|
|
321
170
|
FilterColumnMsg::FilterKeyDown(_) => false,
|
|
322
171
|
FilterColumnMsg::FilterOpSelect(op) => {
|
|
323
|
-
ctx.props().update_filter_op(op);
|
|
172
|
+
ctx.props().update_filter_op(ctx.props().idx, op);
|
|
324
173
|
true
|
|
325
174
|
},
|
|
326
175
|
}
|
|
327
176
|
}
|
|
328
177
|
|
|
329
178
|
fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
|
|
330
|
-
let col_type = ctx.props().
|
|
331
|
-
let old_col_type =
|
|
179
|
+
let col_type = ctx.props().get_current_filter_type();
|
|
180
|
+
let old_col_type = ctx.props().get_filter_type(&old.filter);
|
|
332
181
|
let mut changed = false;
|
|
333
182
|
if col_type != old_col_type {
|
|
334
183
|
changed = true;
|
|
335
184
|
self.filter_ops = Rc::new(
|
|
336
185
|
maybe! {
|
|
337
|
-
Some(ctx
|
|
338
|
-
.props()
|
|
339
|
-
.get_filter_ops(col_type?)?
|
|
186
|
+
Some(get_filter_ops(&ctx.props().session, col_type?)?
|
|
340
187
|
.into_iter()
|
|
341
188
|
.map(SelectItem::Option)
|
|
342
189
|
.collect::<Vec<_>>())
|
|
@@ -362,9 +209,7 @@ impl Component for FilterColumn {
|
|
|
362
209
|
.session
|
|
363
210
|
.metadata()
|
|
364
211
|
.get_column_table_type(&column);
|
|
365
|
-
|
|
366
212
|
let select = ctx.link().callback(FilterColumnMsg::FilterOpSelect);
|
|
367
|
-
|
|
368
213
|
let noderef = &self.input_ref;
|
|
369
214
|
let input = ctx.link().callback({
|
|
370
215
|
let column = column.clone();
|
|
@@ -505,7 +350,7 @@ impl Component for FilterColumn {
|
|
|
505
350
|
// <TypeIcon ty={ColumnType::String} />
|
|
506
351
|
<TypeIcon ty={final_col_type} />
|
|
507
352
|
<span class="column_name">{ filter.column().to_owned() }</span>
|
|
508
|
-
<
|
|
353
|
+
<Select<String>
|
|
509
354
|
class="filterop-selector"
|
|
510
355
|
is_autosize=true
|
|
511
356
|
values={self.filter_ops.clone()}
|
|
@@ -514,7 +359,9 @@ impl Component for FilterColumn {
|
|
|
514
359
|
/>
|
|
515
360
|
// TODO: Move this to the Features API.
|
|
516
361
|
if filter.op() != "is not null" && filter.op() != "is null" {
|
|
517
|
-
if col_type == Some(ColumnType::Boolean) {
|
|
362
|
+
if col_type == Some(ColumnType::Boolean) {
|
|
363
|
+
{ input_elem }
|
|
364
|
+
} else {
|
|
518
365
|
<label
|
|
519
366
|
class={format!("input-sizer {}", type_class)}
|
|
520
367
|
data-value={format!("{}", filter.term())}
|
|
@@ -528,3 +375,157 @@ impl Component for FilterColumn {
|
|
|
528
375
|
}
|
|
529
376
|
}
|
|
530
377
|
}
|
|
378
|
+
|
|
379
|
+
/// Get the allowed `FilterOp`s for this filter.
|
|
380
|
+
fn get_filter_ops(session: &Session, col_type: ColumnType) -> Option<Vec<String>> {
|
|
381
|
+
let metadata = session.metadata();
|
|
382
|
+
let features = metadata.get_features()?;
|
|
383
|
+
features
|
|
384
|
+
.filter_ops
|
|
385
|
+
.get(&(col_type as u32))
|
|
386
|
+
.map(|x| x.options.clone())
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
impl FilterColumnProps {
|
|
390
|
+
/// Does this filter item get a "suggestions" auto-complete modal?
|
|
391
|
+
fn is_suggestable(&self) -> bool {
|
|
392
|
+
// TODO This needs to be moved to Features API. Or ... we just do this
|
|
393
|
+
// all string column type filters, or otherwise "fix" this in the UI?
|
|
394
|
+
(self.filter.op() == "=="
|
|
395
|
+
|| self.filter.op() == "!="
|
|
396
|
+
|| self.filter.op() == "in"
|
|
397
|
+
|| self.filter.op() == "not in")
|
|
398
|
+
&& self.get_filter_type(&self.filter) == Some(ColumnType::String)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
fn get_current_filter_type(&self) -> Option<ColumnType> {
|
|
402
|
+
self.get_filter_type(&self.filter)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// Get this filter's type, e.g. the type of the column.
|
|
406
|
+
fn get_filter_type(&self, filter: &Filter) -> Option<ColumnType> {
|
|
407
|
+
self.session
|
|
408
|
+
.metadata()
|
|
409
|
+
.get_column_table_type(filter.column())
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Get the string value, suitable for the `value` field of a `FilterColumns`'s
|
|
413
|
+
// `<input>`.
|
|
414
|
+
fn get_filter_input(&self) -> Option<String> {
|
|
415
|
+
let filter_type = self.get_current_filter_type()?;
|
|
416
|
+
match (&filter_type, &self.filter.term()) {
|
|
417
|
+
(ColumnType::Date, FilterTerm::Scalar(Scalar::Float(x))) => {
|
|
418
|
+
if *x > 0_f64 {
|
|
419
|
+
Some(
|
|
420
|
+
Utc.timestamp_opt(*x as i64 / 1000, (*x as u32 % 1000) * 1000)
|
|
421
|
+
.earliest()?
|
|
422
|
+
.format("%Y-%m-%d")
|
|
423
|
+
.to_string(),
|
|
424
|
+
)
|
|
425
|
+
} else {
|
|
426
|
+
None
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
(ColumnType::Datetime, FilterTerm::Scalar(Scalar::Float(x))) => {
|
|
430
|
+
posix_to_utc_str(*x).ok()
|
|
431
|
+
},
|
|
432
|
+
(ColumnType::Boolean, FilterTerm::Scalar(Scalar::Bool(x))) => {
|
|
433
|
+
Some((if *x { "true" } else { "false" }).to_owned())
|
|
434
|
+
},
|
|
435
|
+
(ColumnType::Boolean, _) => Some("true".to_owned()),
|
|
436
|
+
(_, x) => Some(format!("{x}")),
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// Update the filter comparison operator.
|
|
441
|
+
///
|
|
442
|
+
/// # Arguments
|
|
443
|
+
/// - `op` The new `FilterOp`.
|
|
444
|
+
fn update_filter_op(&self, idx: usize, op: String) {
|
|
445
|
+
let mut filter = self.session.get_view_config().filter.clone();
|
|
446
|
+
let filter_column = &mut filter.get_mut(idx).expect("Filter on no column");
|
|
447
|
+
*filter_column.op_mut() = op;
|
|
448
|
+
let update = ViewConfigUpdate {
|
|
449
|
+
filter: Some(filter),
|
|
450
|
+
..ViewConfigUpdate::default()
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
self.update_and_render(update)
|
|
454
|
+
.map(ApiFuture::spawn)
|
|
455
|
+
.unwrap_or_log();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/// Update the filter value from the string input read from the DOM.
|
|
459
|
+
///
|
|
460
|
+
/// # Arguments
|
|
461
|
+
/// - `val` The new filter value.
|
|
462
|
+
fn update_filter_input(&self, val: String) {
|
|
463
|
+
let mut filters = self.session.get_view_config().filter.clone();
|
|
464
|
+
let filter_column = &mut filters.get_mut(self.idx).expect("Filter on no column");
|
|
465
|
+
|
|
466
|
+
// TODO This belongs in the Features API.
|
|
467
|
+
let filter_input = if filter_column.op() == "in" || filter_column.op() == "not in" {
|
|
468
|
+
Some(FilterTerm::Array(
|
|
469
|
+
val.split(',')
|
|
470
|
+
.map(|x| Scalar::String(x.trim().to_owned()))
|
|
471
|
+
.collect(),
|
|
472
|
+
))
|
|
473
|
+
} else {
|
|
474
|
+
match self.get_current_filter_type() {
|
|
475
|
+
Some(ColumnType::String) => Some(FilterTerm::Scalar(Scalar::String(val))),
|
|
476
|
+
Some(ColumnType::Integer) => {
|
|
477
|
+
if val.is_empty() {
|
|
478
|
+
None
|
|
479
|
+
} else if let Ok(num) = val.parse::<f64>() {
|
|
480
|
+
Some(FilterTerm::Scalar(Scalar::Float(num.floor())))
|
|
481
|
+
} else {
|
|
482
|
+
None
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
Some(ColumnType::Float) => {
|
|
486
|
+
if val.is_empty() {
|
|
487
|
+
None
|
|
488
|
+
} else if let Ok(num) = val.parse::<f64>() {
|
|
489
|
+
Some(FilterTerm::Scalar(Scalar::Float(num)))
|
|
490
|
+
} else {
|
|
491
|
+
None
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
Some(ColumnType::Date) => match NaiveDate::parse_from_str(&val, "%Y-%m-%d") {
|
|
495
|
+
Ok(ref posix) => Some(FilterTerm::Scalar(Scalar::String(format!(
|
|
496
|
+
"{:0>4}-{:0>2}-{:0>2}",
|
|
497
|
+
posix.year(),
|
|
498
|
+
posix.month(),
|
|
499
|
+
posix.day(),
|
|
500
|
+
)))),
|
|
501
|
+
_ => None,
|
|
502
|
+
},
|
|
503
|
+
Some(ColumnType::Datetime) => match str_to_utc_posix(&val) {
|
|
504
|
+
Ok(x) => Some(FilterTerm::Scalar(Scalar::Float(x))),
|
|
505
|
+
_ => None,
|
|
506
|
+
},
|
|
507
|
+
Some(ColumnType::Boolean) => Some(FilterTerm::Scalar(match val.as_str() {
|
|
508
|
+
"true" => Scalar::Bool(true),
|
|
509
|
+
_ => Scalar::Bool(false),
|
|
510
|
+
})),
|
|
511
|
+
|
|
512
|
+
// shouldn't be reachable ..
|
|
513
|
+
_ => None,
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
if let Some(input) = filter_input {
|
|
518
|
+
if &input != filter_column.term() {
|
|
519
|
+
*filter_column.term_mut() = input;
|
|
520
|
+
let update = ViewConfigUpdate {
|
|
521
|
+
filter: Some(filters),
|
|
522
|
+
..ViewConfigUpdate::default()
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
self.update_and_render(update)
|
|
526
|
+
.map(ApiFuture::spawn)
|
|
527
|
+
.unwrap_or_log();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
@@ -11,35 +11,50 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
use itertools::Itertools;
|
|
14
|
-
use perspective_client::config
|
|
14
|
+
use perspective_client::config::*;
|
|
15
15
|
use perspective_client::utils::PerspectiveResultExt;
|
|
16
|
+
use perspective_js::utils::ApiFuture;
|
|
16
17
|
use web_sys::*;
|
|
17
18
|
use yew::prelude::*;
|
|
18
19
|
|
|
19
|
-
use super::
|
|
20
|
+
use super::expr_edit_button::*;
|
|
20
21
|
use crate::components::type_icon::TypeIcon;
|
|
21
|
-
use crate::components::viewer::ColumnLocator;
|
|
22
22
|
use crate::dragdrop::*;
|
|
23
23
|
use crate::js::plugin::*;
|
|
24
24
|
use crate::model::*;
|
|
25
|
-
use crate::presentation::
|
|
25
|
+
use crate::presentation::ColumnLocator;
|
|
26
26
|
use crate::renderer::*;
|
|
27
27
|
use crate::session::*;
|
|
28
|
+
use crate::utils::*;
|
|
28
29
|
use crate::*;
|
|
29
30
|
|
|
30
|
-
#[derive(Properties,
|
|
31
|
+
#[derive(Clone, Properties, PerspectiveProperties!)]
|
|
31
32
|
pub struct InactiveColumnProps {
|
|
33
|
+
/// This column's index in its list.
|
|
32
34
|
pub idx: usize,
|
|
35
|
+
|
|
36
|
+
/// Is this column visible?
|
|
33
37
|
pub visible: bool,
|
|
38
|
+
|
|
39
|
+
/// Column name
|
|
34
40
|
pub name: String,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
pub renderer: Renderer,
|
|
38
|
-
pub presentation: Presentation,
|
|
41
|
+
|
|
42
|
+
/// Is the expression/config panel open for this column?
|
|
39
43
|
pub is_editing: bool,
|
|
44
|
+
|
|
45
|
+
/// `dragend` event`.
|
|
40
46
|
pub ondragend: Callback<()>,
|
|
47
|
+
|
|
48
|
+
/// Fires when this column's select button is sclicked.
|
|
41
49
|
pub onselect: Callback<()>,
|
|
50
|
+
|
|
51
|
+
/// Fires when this column's expression/config button is clicked.
|
|
42
52
|
pub on_open_expr_panel: Callback<ColumnLocator>,
|
|
53
|
+
|
|
54
|
+
// State
|
|
55
|
+
pub dragdrop: DragDrop,
|
|
56
|
+
pub session: Session,
|
|
57
|
+
pub renderer: Renderer,
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
impl PartialEq for InactiveColumnProps {
|
|
@@ -51,61 +66,6 @@ impl PartialEq for InactiveColumnProps {
|
|
|
51
66
|
}
|
|
52
67
|
}
|
|
53
68
|
|
|
54
|
-
derive_model!(Renderer, Session for InactiveColumnProps);
|
|
55
|
-
|
|
56
|
-
impl InactiveColumnProps {
|
|
57
|
-
/// Add a column to the active columns, which corresponds to the `columns`
|
|
58
|
-
/// field of the `JsPerspectiveViewConfig`.
|
|
59
|
-
///
|
|
60
|
-
/// # Arguments
|
|
61
|
-
/// - `name` The name of the column to de-activate, which is a unique ID
|
|
62
|
-
/// with respect to `columns`.
|
|
63
|
-
/// - `shift` whether to toggle or select this column.
|
|
64
|
-
pub fn activate_column(&self, name: String, shift: bool) {
|
|
65
|
-
let mut columns = self.session.get_view_config().columns.clone();
|
|
66
|
-
let max_cols = self
|
|
67
|
-
.renderer
|
|
68
|
-
.metadata()
|
|
69
|
-
.names
|
|
70
|
-
.as_ref()
|
|
71
|
-
.map_or(0, |x| x.len());
|
|
72
|
-
|
|
73
|
-
// Don't treat `None` at the end of the column list as columns, we'll refill
|
|
74
|
-
// these later
|
|
75
|
-
if let Some(last_filled) = columns.iter().rposition(|x| !x.is_none()) {
|
|
76
|
-
columns.truncate(last_filled + 1);
|
|
77
|
-
|
|
78
|
-
let mode = self.renderer.metadata().mode;
|
|
79
|
-
if (mode == ColumnSelectMode::Select) ^ shift {
|
|
80
|
-
columns.clear();
|
|
81
|
-
} else {
|
|
82
|
-
columns.retain(|x| x.as_ref() != Some(&name));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
columns.push(Some(name));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Do this outside the loop so errors dont just become noops
|
|
89
|
-
self.apply_columns(
|
|
90
|
-
columns
|
|
91
|
-
.into_iter()
|
|
92
|
-
.pad_using(max_cols, |_| None)
|
|
93
|
-
.collect::<Vec<_>>(),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
fn apply_columns(&self, columns: Vec<Option<String>>) {
|
|
98
|
-
let config = ViewConfigUpdate {
|
|
99
|
-
columns: Some(columns),
|
|
100
|
-
..ViewConfigUpdate::default()
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
self.update_and_render(config)
|
|
104
|
-
.map(ApiFuture::spawn)
|
|
105
|
-
.unwrap_or_log();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
69
|
pub enum InactiveColumnMsg {
|
|
110
70
|
ActivateColumn(bool),
|
|
111
71
|
MouseEnter(bool),
|
|
@@ -114,7 +74,6 @@ pub enum InactiveColumnMsg {
|
|
|
114
74
|
|
|
115
75
|
use InactiveColumnMsg::*;
|
|
116
76
|
|
|
117
|
-
#[derive(Default)]
|
|
118
77
|
pub struct InactiveColumn {
|
|
119
78
|
add_expression_ref: NodeRef,
|
|
120
79
|
mouseover: bool,
|
|
@@ -125,7 +84,10 @@ impl Component for InactiveColumn {
|
|
|
125
84
|
type Properties = InactiveColumnProps;
|
|
126
85
|
|
|
127
86
|
fn create(_ctx: &Context<Self>) -> Self {
|
|
128
|
-
Self
|
|
87
|
+
Self {
|
|
88
|
+
add_expression_ref: NodeRef::default(),
|
|
89
|
+
mouseover: false,
|
|
90
|
+
}
|
|
129
91
|
}
|
|
130
92
|
|
|
131
93
|
fn update(&mut self, ctx: &Context<Self>, msg: InactiveColumnMsg) -> bool {
|
|
@@ -195,9 +157,9 @@ impl Component for InactiveColumn {
|
|
|
195
157
|
<div {class} {onmouseover} {onmouseout} data-index={ctx.props().idx.to_string()}>
|
|
196
158
|
<span class={is_active_class} onmousedown={add_column} />
|
|
197
159
|
<div
|
|
160
|
+
ref={&self.add_expression_ref}
|
|
198
161
|
class="column-selector-draggable column-selector-column-title"
|
|
199
162
|
draggable="true"
|
|
200
|
-
ref={&self.add_expression_ref}
|
|
201
163
|
{ondragstart}
|
|
202
164
|
{ondragend}
|
|
203
165
|
>
|
|
@@ -219,3 +181,56 @@ impl Component for InactiveColumn {
|
|
|
219
181
|
}
|
|
220
182
|
}
|
|
221
183
|
}
|
|
184
|
+
|
|
185
|
+
impl InactiveColumnProps {
|
|
186
|
+
/// Add a column to the active columns, which corresponds to the `columns`
|
|
187
|
+
/// field of the `JsPerspectiveViewConfig`.
|
|
188
|
+
///
|
|
189
|
+
/// # Arguments
|
|
190
|
+
/// - `name` The name of the column to de-activate, which is a unique ID
|
|
191
|
+
/// with respect to `columns`.
|
|
192
|
+
/// - `shift` whether to toggle or select this column.
|
|
193
|
+
pub fn activate_column(&self, name: String, shift: bool) {
|
|
194
|
+
let mut columns = self.session.get_view_config().columns.clone();
|
|
195
|
+
let max_cols = self
|
|
196
|
+
.renderer
|
|
197
|
+
.metadata()
|
|
198
|
+
.names
|
|
199
|
+
.as_ref()
|
|
200
|
+
.map_or(0, |x| x.len());
|
|
201
|
+
|
|
202
|
+
// Don't treat `None` at the end of the column list as columns, we'll refill
|
|
203
|
+
// these later
|
|
204
|
+
if let Some(last_filled) = columns.iter().rposition(|x| !x.is_none()) {
|
|
205
|
+
columns.truncate(last_filled + 1);
|
|
206
|
+
|
|
207
|
+
let mode = self.renderer.metadata().mode;
|
|
208
|
+
if (mode == ColumnSelectMode::Select) ^ shift {
|
|
209
|
+
columns.clear();
|
|
210
|
+
} else {
|
|
211
|
+
columns.retain(|x| x.as_ref() != Some(&name));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
columns.push(Some(name));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Do this outside the loop so errors dont just become noops
|
|
218
|
+
self.apply_columns(
|
|
219
|
+
columns
|
|
220
|
+
.into_iter()
|
|
221
|
+
.pad_using(max_cols, |_| None)
|
|
222
|
+
.collect::<Vec<_>>(),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn apply_columns(&self, columns: Vec<Option<String>>) {
|
|
227
|
+
let config = ViewConfigUpdate {
|
|
228
|
+
columns: Some(columns),
|
|
229
|
+
..ViewConfigUpdate::default()
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
self.update_and_render(config)
|
|
233
|
+
.map(ApiFuture::spawn)
|
|
234
|
+
.unwrap_or_log();
|
|
235
|
+
}
|
|
236
|
+
}
|