@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,289 @@
|
|
|
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
|
+
//! The chat sidebar tab (feature `llm-agent`) — a pure VIEW over
|
|
14
|
+
//! [`AgentSlot`]: all conversation state lives on the slot so this component
|
|
15
|
+
//! can unmount (tab switch, settings close) without losing the transcript or
|
|
16
|
+
//! interrupting a running turn.
|
|
17
|
+
|
|
18
|
+
use yew::prelude::*;
|
|
19
|
+
|
|
20
|
+
use crate::agent::{AgentSlot, ChatEntry, render_markdown};
|
|
21
|
+
use crate::components::form::mirrored_textarea::MirroredTextarea;
|
|
22
|
+
|
|
23
|
+
#[derive(Properties, PartialEq)]
|
|
24
|
+
struct ChatReasoningProps {
|
|
25
|
+
reasoning: String,
|
|
26
|
+
open: bool,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// A reasoning-model's "thinking" text: plain-text collapsible block,
|
|
30
|
+
/// expanded while it is streaming (the only progress signal during a long
|
|
31
|
+
/// think) and collapsed once real content exists.
|
|
32
|
+
///
|
|
33
|
+
/// The body is capped and scrolls (`viewer.css`), and follows its own
|
|
34
|
+
/// tail as deltas append — but only while the reader is AT that tail.
|
|
35
|
+
/// Scrolling up to read something is a deliberate act, so it wins until
|
|
36
|
+
/// the reader returns to the bottom, exactly like the transcript.
|
|
37
|
+
#[function_component]
|
|
38
|
+
fn ChatReasoning(props: &ChatReasoningProps) -> Html {
|
|
39
|
+
let body_ref = use_node_ref();
|
|
40
|
+
let scroll_pinned = use_mut_ref(|| true);
|
|
41
|
+
let seen_len = use_mut_ref(|| None::<usize>);
|
|
42
|
+
let onscroll = {
|
|
43
|
+
let body_ref = body_ref.clone();
|
|
44
|
+
let scroll_pinned = scroll_pinned.clone();
|
|
45
|
+
Callback::from(move |_: Event| {
|
|
46
|
+
if let Some(elem) = body_ref.cast::<web_sys::Element>() {
|
|
47
|
+
let gap = elem.scroll_height() - elem.scroll_top() - elem.client_height();
|
|
48
|
+
*scroll_pinned.borrow_mut() = gap < 24;
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
let body_ref = body_ref.clone();
|
|
55
|
+
let scroll_pinned = scroll_pinned.clone();
|
|
56
|
+
let seen_len = seen_len.clone();
|
|
57
|
+
let len = props.reasoning.len();
|
|
58
|
+
use_effect(move || {
|
|
59
|
+
let grew = seen_len.replace(Some(len)).is_some_and(|seen| len > seen);
|
|
60
|
+
if grew
|
|
61
|
+
&& *scroll_pinned.borrow()
|
|
62
|
+
&& let Some(elem) = body_ref.cast::<web_sys::Element>()
|
|
63
|
+
{
|
|
64
|
+
elem.set_scroll_top(elem.scroll_height());
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
html! {
|
|
70
|
+
<details class="chat-reasoning" open={props.open}>
|
|
71
|
+
<summary />
|
|
72
|
+
<div class="chat-reasoning-body scrollable" ref={body_ref} {onscroll}>
|
|
73
|
+
{ &props.reasoning }
|
|
74
|
+
</div>
|
|
75
|
+
</details>
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// The prompt input, over the shared [`MirroredTextarea`] — so it grows
|
|
80
|
+
/// with its text instead of scrolling. This consumer wraps
|
|
81
|
+
/// (`white-space: pre-wrap` is the shared default), which confines growth
|
|
82
|
+
/// to the vertical axis; the expression editor's `pre` instead lets it
|
|
83
|
+
/// grow sideways too.
|
|
84
|
+
///
|
|
85
|
+
/// Split from [`ChatPanel`] because the mirror re-renders per keystroke:
|
|
86
|
+
/// the transcript re-parses markdown for every message it renders, which
|
|
87
|
+
/// must not happen per character.
|
|
88
|
+
#[derive(Properties, PartialEq)]
|
|
89
|
+
struct ChatInputProps {
|
|
90
|
+
agent: AgentSlot,
|
|
91
|
+
busy: bool,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[function_component]
|
|
95
|
+
fn ChatInput(props: &ChatInputProps) -> Html {
|
|
96
|
+
let input_ref = use_node_ref();
|
|
97
|
+
|
|
98
|
+
// Mirrors the textarea's value. The textarea is deliberately NOT
|
|
99
|
+
// value-bound — binding it fights the caret — so this is a shadow of
|
|
100
|
+
// the DOM value, written on `input` and cleared with it on submit.
|
|
101
|
+
let text = use_state_eq(String::new);
|
|
102
|
+
let submit = {
|
|
103
|
+
let agent = props.agent.clone();
|
|
104
|
+
let input_ref = input_ref.clone();
|
|
105
|
+
let text = text.clone();
|
|
106
|
+
Callback::from(move |()| {
|
|
107
|
+
if agent.is_busy() {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if let Some(elem) = input_ref.cast::<web_sys::HtmlTextAreaElement>() {
|
|
112
|
+
let prompt = elem.value().trim().to_owned();
|
|
113
|
+
if prompt.is_empty() {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
elem.set_value("");
|
|
118
|
+
text.set(String::new());
|
|
119
|
+
let agent = agent.clone();
|
|
120
|
+
wasm_bindgen_futures::spawn_local(async move {
|
|
121
|
+
// Failures land in the transcript; nothing to propagate.
|
|
122
|
+
let _ = agent.run_prompt(prompt).await;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
let onkeydown = {
|
|
129
|
+
let submit = submit.clone();
|
|
130
|
+
Callback::from(move |event: KeyboardEvent| {
|
|
131
|
+
if event.key() == "Enter" && !event.shift_key() {
|
|
132
|
+
event.prevent_default();
|
|
133
|
+
submit.emit(());
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// `input` fires after the value settles and covers paste, cut and
|
|
139
|
+
// Shift+Enter newlines, none of which `keydown` sees correctly.
|
|
140
|
+
let oninput = {
|
|
141
|
+
let input_ref = input_ref.clone();
|
|
142
|
+
let text = text.clone();
|
|
143
|
+
Callback::from(move |_: InputEvent| {
|
|
144
|
+
if let Some(elem) = input_ref.cast::<web_sys::HtmlTextAreaElement>() {
|
|
145
|
+
text.set(elem.value());
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
let onsend = submit.reform(|_: MouseEvent| ());
|
|
151
|
+
let onstop = {
|
|
152
|
+
let agent = props.agent.clone();
|
|
153
|
+
Callback::from(move |_: MouseEvent| agent.stop())
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
html! {
|
|
157
|
+
<div id="chat_input_row">
|
|
158
|
+
<div id="chat_input_scroll" class="scrollable">
|
|
159
|
+
<MirroredTextarea
|
|
160
|
+
id="chat_input"
|
|
161
|
+
class="chat-input-box"
|
|
162
|
+
mirror={html! { (*text).clone() }}
|
|
163
|
+
is_empty={text.is_empty()}
|
|
164
|
+
textarea_ref={input_ref}
|
|
165
|
+
placeholder="Ask about your data\u{2026}"
|
|
166
|
+
disabled={props.busy}
|
|
167
|
+
{onkeydown}
|
|
168
|
+
{oninput}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
if props.busy {
|
|
172
|
+
<button id="chat_stop_button" onclick={onstop} />
|
|
173
|
+
} else {
|
|
174
|
+
<button id="chat_send_button" onclick={onsend} />
|
|
175
|
+
}
|
|
176
|
+
</div>
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[derive(Properties, PartialEq)]
|
|
181
|
+
pub struct ChatPanelProps {
|
|
182
|
+
pub agent: AgentSlot,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[function_component]
|
|
186
|
+
pub fn ChatPanel(props: &ChatPanelProps) -> Html {
|
|
187
|
+
let update = use_force_update();
|
|
188
|
+
{
|
|
189
|
+
let agent = props.agent.clone();
|
|
190
|
+
use_effect_with((), move |_| {
|
|
191
|
+
let sub = agent
|
|
192
|
+
.on_update
|
|
193
|
+
.add_notify_listener(&Callback::from(move |_| update.force_update()));
|
|
194
|
+
|
|
195
|
+
move || drop(sub)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Autoscroll only while the user is pinned at the bottom — reading
|
|
200
|
+
// scrolled-back transcript must survive incoming streaming deltas.
|
|
201
|
+
// `onscroll` fires for programmatic scrolls too, so scrolling to the
|
|
202
|
+
// bottom re-pins consistently.
|
|
203
|
+
let log_ref = use_node_ref();
|
|
204
|
+
let scroll_pinned = use_mut_ref(|| true);
|
|
205
|
+
let onscroll = {
|
|
206
|
+
let log_ref = log_ref.clone();
|
|
207
|
+
let scroll_pinned = scroll_pinned.clone();
|
|
208
|
+
Callback::from(move |_: Event| {
|
|
209
|
+
if let Some(elem) = log_ref.cast::<web_sys::Element>() {
|
|
210
|
+
let gap = elem.scroll_height() - elem.scroll_top() - elem.client_height();
|
|
211
|
+
*scroll_pinned.borrow_mut() = gap < 30;
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
{
|
|
217
|
+
let log_ref = log_ref.clone();
|
|
218
|
+
let scroll_pinned = scroll_pinned.clone();
|
|
219
|
+
use_effect(move || {
|
|
220
|
+
if let Some(elem) = log_ref.cast::<web_sys::Element>()
|
|
221
|
+
&& *scroll_pinned.borrow()
|
|
222
|
+
{
|
|
223
|
+
elem.set_scroll_top(elem.scroll_height());
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let busy = props.agent.is_busy();
|
|
229
|
+
let entries = props
|
|
230
|
+
.agent
|
|
231
|
+
.transcript()
|
|
232
|
+
.into_iter()
|
|
233
|
+
.map(|entry| match entry {
|
|
234
|
+
ChatEntry::User(text) => html! {
|
|
235
|
+
<div class="chat-message chat-user">{ text.trim() }</div>
|
|
236
|
+
},
|
|
237
|
+
ChatEntry::Assistant { text, reasoning } => html! {
|
|
238
|
+
<div class="chat-message chat-assistant">
|
|
239
|
+
if let Some(reasoning) = reasoning { <ChatReasoning {reasoning} open=false /> }
|
|
240
|
+
{ render_markdown(&text) }
|
|
241
|
+
</div>
|
|
242
|
+
},
|
|
243
|
+
ChatEntry::Tool { name, args, error } => {
|
|
244
|
+
let class = if error.is_some() {
|
|
245
|
+
"chat-tool-chip chat-tool-chip-error"
|
|
246
|
+
} else {
|
|
247
|
+
"chat-tool-chip"
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let title = match &error {
|
|
251
|
+
Some(err) => format!("{err}\n\n{args}"),
|
|
252
|
+
None => args.clone(),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
html! { <div {class} {title}>{ name }</div> }
|
|
256
|
+
},
|
|
257
|
+
ChatEntry::Error(text) => html! { <div class="chat-message chat-error">{ text }</div> },
|
|
258
|
+
})
|
|
259
|
+
.collect::<Html>();
|
|
260
|
+
|
|
261
|
+
// The in-flight turn's streaming tail: reasoning renders auto-OPEN
|
|
262
|
+
// while it is the only signal, collapsing once answer text starts;
|
|
263
|
+
// no deltas yet (or between tool rounds) shows the typing dots.
|
|
264
|
+
let pending = props.agent.pending();
|
|
265
|
+
let tail = match &pending {
|
|
266
|
+
Some((text, reasoning)) if !text.is_empty() || !reasoning.is_empty() => html! {
|
|
267
|
+
<div class="chat-message chat-assistant chat-streaming">
|
|
268
|
+
if !reasoning.is_empty() {
|
|
269
|
+
<ChatReasoning reasoning={reasoning.clone()} open={text.is_empty()} />
|
|
270
|
+
}
|
|
271
|
+
if !text.is_empty() {
|
|
272
|
+
{ render_markdown(text) }
|
|
273
|
+
}
|
|
274
|
+
</div>
|
|
275
|
+
},
|
|
276
|
+
_ if busy => html! {
|
|
277
|
+
<div class="chat-message chat-assistant chat-pending"><span /><span /><span /></div>
|
|
278
|
+
},
|
|
279
|
+
_ => html! {},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
html! {
|
|
283
|
+
<div id="chat_panel">
|
|
284
|
+
<div id="chat_log" class="scrollable" ref={log_ref} {onscroll}>{ entries }{ tail }</div>
|
|
285
|
+
<ChatInput agent={props.agent.clone()} {busy} />
|
|
286
|
+
<div id="chat_badge">{ props.agent.label().unwrap_or_default() }</div>
|
|
287
|
+
</div>
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -18,10 +18,10 @@ use yew::prelude::*;
|
|
|
18
18
|
|
|
19
19
|
use super::InPlaceColumn;
|
|
20
20
|
use super::aggregate_selector::*;
|
|
21
|
+
use super::column_selector_column_row::ColumnSelectorColumnRow;
|
|
21
22
|
use super::expr_edit_button::*;
|
|
22
23
|
use crate::components::column_dropdown::ColumnDropDownElement;
|
|
23
24
|
use crate::components::column_selector::{EmptyColumn, InvalidColumn};
|
|
24
|
-
use crate::components::type_icon::TypeIcon;
|
|
25
25
|
use crate::config::ColumnSelectMode;
|
|
26
26
|
use crate::presentation::{ColumnLocator, Presentation};
|
|
27
27
|
use crate::queries::*;
|
|
@@ -66,6 +66,10 @@ pub struct ActiveColumnProps {
|
|
|
66
66
|
#[prop_or_default]
|
|
67
67
|
pub is_expression: bool,
|
|
68
68
|
|
|
69
|
+
/// Whether this column is a window column.
|
|
70
|
+
#[prop_or_default]
|
|
71
|
+
pub is_window: bool,
|
|
72
|
+
|
|
69
73
|
/// Whether the expression/config edit button should be shown. Computed
|
|
70
74
|
/// by the parent (`is_expression || can_render_column_styles`).
|
|
71
75
|
#[prop_or_default]
|
|
@@ -95,6 +99,7 @@ impl PartialEq for ActiveColumnProps {
|
|
|
95
99
|
&& self.is_aggregated == rhs.is_aggregated
|
|
96
100
|
&& self.is_editing == rhs.is_editing
|
|
97
101
|
&& self.is_expression == rhs.is_expression
|
|
102
|
+
&& self.is_window == rhs.is_window
|
|
98
103
|
&& self.show_edit_btn == rhs.show_edit_btn
|
|
99
104
|
&& self.col_type == rhs.col_type
|
|
100
105
|
&& self.metadata == rhs.metadata
|
|
@@ -204,10 +209,7 @@ impl Component for ActiveColumn {
|
|
|
204
209
|
Named(String),
|
|
205
210
|
}
|
|
206
211
|
|
|
207
|
-
let mut classes = classes![
|
|
208
|
-
if ctx.props().is_aggregated {
|
|
209
|
-
classes.push("show-aggregate");
|
|
210
|
-
};
|
|
212
|
+
let mut classes = classes![];
|
|
211
213
|
|
|
212
214
|
let mut outer_classes = classes!["column-selector-column"];
|
|
213
215
|
if self.mouseover {
|
|
@@ -344,6 +346,7 @@ impl Component for ActiveColumn {
|
|
|
344
346
|
.callback(|event: MouseEvent| MouseEnter(event.which() == 0));
|
|
345
347
|
|
|
346
348
|
let is_expression = ctx.props().is_expression;
|
|
349
|
+
let is_window = ctx.props().is_window;
|
|
347
350
|
let show_edit_btn = ctx.props().show_edit_btn;
|
|
348
351
|
let mut class = ctx.props().renderer.metadata().select_mode.css();
|
|
349
352
|
if is_required {
|
|
@@ -363,17 +366,14 @@ impl Component for ActiveColumn {
|
|
|
363
366
|
ondragenter={ondragenter.clone()}
|
|
364
367
|
>
|
|
365
368
|
<span {class} onmousedown={remove_column} />
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
{
|
|
371
|
-
{
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
<span class="drag-handle icon" />
|
|
375
|
-
<TypeIcon ty={col_type} />
|
|
376
|
-
if ctx.props().is_aggregated {
|
|
369
|
+
<ColumnSelectorColumnRow
|
|
370
|
+
name={name.clone()}
|
|
371
|
+
col_type={Some(col_type)}
|
|
372
|
+
wrapper_class={classes}
|
|
373
|
+
wrapper_ref={&self.add_expression_ref}
|
|
374
|
+
ondragstart={Some(ondragstart)}
|
|
375
|
+
ondragend={Some(ondragend.clone())}
|
|
376
|
+
aggregate={ctx.props().is_aggregated.then(|| html! {
|
|
377
377
|
<AggregateSelector
|
|
378
378
|
column={name.clone()}
|
|
379
379
|
aggregate={ctx.props().get_aggregate(&name)}
|
|
@@ -382,24 +382,18 @@ impl Component for ActiveColumn {
|
|
|
382
382
|
renderer={&ctx.props().renderer}
|
|
383
383
|
session={&ctx.props().session}
|
|
384
384
|
/>
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
class="column_name"
|
|
388
|
-
>
|
|
389
|
-
{ name.clone() }
|
|
390
|
-
</span>
|
|
391
|
-
if !ctx.props().is_aggregated {
|
|
392
|
-
<span class="column-selector--spacer" />
|
|
393
|
-
}
|
|
385
|
+
})}
|
|
386
|
+
trailing={html! {
|
|
394
387
|
<ExprEditButton
|
|
388
|
+
{is_window}
|
|
395
389
|
name={name.clone()}
|
|
396
390
|
on_open_expr_panel={&ctx.props().on_open_expr_panel}
|
|
397
391
|
{is_expression}
|
|
398
392
|
is_disabled={!show_edit_btn}
|
|
399
393
|
is_editing={ctx.props().is_editing}
|
|
400
394
|
/>
|
|
401
|
-
|
|
402
|
-
|
|
395
|
+
}}
|
|
396
|
+
/>
|
|
403
397
|
</div>
|
|
404
398
|
}
|
|
405
399
|
},
|
|
@@ -0,0 +1,73 @@
|
|
|
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 perspective_client::config::ColumnType;
|
|
14
|
+
use web_sys::DragEvent;
|
|
15
|
+
use yew::prelude::*;
|
|
16
|
+
|
|
17
|
+
use crate::components::type_icon::TypeIcon;
|
|
18
|
+
|
|
19
|
+
/// The draggable row of a `column-selector-column`
|
|
20
|
+
#[derive(Clone, PartialEq, Properties)]
|
|
21
|
+
pub struct ColumnSelectorColumnRowProps {
|
|
22
|
+
pub name: String,
|
|
23
|
+
|
|
24
|
+
#[prop_or_default]
|
|
25
|
+
pub col_type: Option<ColumnType>,
|
|
26
|
+
|
|
27
|
+
#[prop_or_default]
|
|
28
|
+
pub aggregate: Option<Html>,
|
|
29
|
+
|
|
30
|
+
/// Trailing affordance (e.g. the edit button in the active list).
|
|
31
|
+
#[prop_or_default]
|
|
32
|
+
pub trailing: Html,
|
|
33
|
+
|
|
34
|
+
#[prop_or_default]
|
|
35
|
+
pub wrapper_class: Classes,
|
|
36
|
+
|
|
37
|
+
#[prop_or_default]
|
|
38
|
+
pub wrapper_ref: NodeRef,
|
|
39
|
+
|
|
40
|
+
#[prop_or_default]
|
|
41
|
+
pub ondragstart: Option<Callback<DragEvent>>,
|
|
42
|
+
|
|
43
|
+
#[prop_or_default]
|
|
44
|
+
pub ondragend: Option<Callback<DragEvent>>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[function_component]
|
|
48
|
+
pub fn ColumnSelectorColumnRow(p: &ColumnSelectorColumnRowProps) -> Html {
|
|
49
|
+
let mut classes = classes!["column-selector-draggable"];
|
|
50
|
+
if p.aggregate.is_some() {
|
|
51
|
+
classes.push("show-aggregate");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
classes.extend(p.wrapper_class.clone());
|
|
55
|
+
html! {
|
|
56
|
+
<div
|
|
57
|
+
class={classes}
|
|
58
|
+
ref={&p.wrapper_ref}
|
|
59
|
+
draggable={p.ondragstart.is_some().then_some("true")}
|
|
60
|
+
ondragstart={p.ondragstart.clone()}
|
|
61
|
+
ondragend={p.ondragend.clone()}
|
|
62
|
+
>
|
|
63
|
+
<div class="column-selector-column-border">
|
|
64
|
+
<span class="drag-handle icon" />
|
|
65
|
+
<TypeIcon ty={p.col_type.unwrap_or(ColumnType::String)} />
|
|
66
|
+
if let Some(aggregate) = &p.aggregate { { aggregate.clone() } }
|
|
67
|
+
<span class="column_name">{ p.name.clone() }</span>
|
|
68
|
+
if p.aggregate.is_none() { <span class="column-selector--spacer" /> }
|
|
69
|
+
{ p.trailing.clone() }
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -24,6 +24,7 @@ use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPo
|
|
|
24
24
|
use crate::components::containers::dragdrop_list::*;
|
|
25
25
|
use crate::components::containers::select::{Select, SelectItem};
|
|
26
26
|
use crate::components::filter_dropdown::{FilterDropDownElement, FilterDropDownPortal};
|
|
27
|
+
use crate::config::PluginStaticConfig;
|
|
27
28
|
use crate::presentation::Presentation;
|
|
28
29
|
use crate::renderer::*;
|
|
29
30
|
use crate::session::drag_drop_update::*;
|
|
@@ -48,6 +49,14 @@ pub struct ConfigSelectorProps {
|
|
|
48
49
|
/// Session metadata snapshot — threaded from `SessionProps`.
|
|
49
50
|
pub metadata: SessionMetadataRc,
|
|
50
51
|
|
|
52
|
+
/// The ACTIVE plugin's declared contract, threaded as a VALUE prop.
|
|
53
|
+
/// Read from `renderer.metadata()` during render instead, the pivot
|
|
54
|
+
/// labels went stale: `renderer` is deliberately excluded from prop
|
|
55
|
+
/// equality, so switching Y Line back to Datagrid — same view config,
|
|
56
|
+
/// same named-slot count — re-rendered nothing and left the previous
|
|
57
|
+
/// plugin's role labels on screen until the panel was reopened.
|
|
58
|
+
pub plugin_static_config: Rc<PluginStaticConfig>,
|
|
59
|
+
|
|
51
60
|
/// Selected theme name, threaded for PortalModal consumers.
|
|
52
61
|
pub selected_theme: Option<String>,
|
|
53
62
|
|
|
@@ -63,6 +72,7 @@ impl PartialEq for ConfigSelectorProps {
|
|
|
63
72
|
&& self.drag_column == other.drag_column
|
|
64
73
|
&& self.metadata == other.metadata
|
|
65
74
|
&& self.selected_theme == other.selected_theme
|
|
75
|
+
&& self.plugin_static_config == other.plugin_static_config
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
78
|
|
|
@@ -76,6 +86,7 @@ pub enum ConfigSelectorMsg {
|
|
|
76
86
|
TransposePivots,
|
|
77
87
|
New(DragTarget, InPlaceColumn),
|
|
78
88
|
UpdateGroupRollupMode(GroupRollupMode),
|
|
89
|
+
UpdateSplitRollupMode(SplitRollupMode),
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
#[derive(Clone)]
|
|
@@ -156,6 +167,22 @@ impl Component for ConfigSelector {
|
|
|
156
167
|
|
|
157
168
|
false
|
|
158
169
|
},
|
|
170
|
+
ConfigSelectorMsg::UpdateSplitRollupMode(mode) => {
|
|
171
|
+
let config = ViewConfigUpdate {
|
|
172
|
+
split_rollup_mode: Some(mode),
|
|
173
|
+
..ViewConfigUpdate::default()
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
{
|
|
177
|
+
let session = ctx.props().session.clone();
|
|
178
|
+
let renderer = ctx.props().renderer.clone();
|
|
179
|
+
if let Ok(task) = apply_and_render(&session, &renderer, config) {
|
|
180
|
+
spawn_owned("config-selector", task);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
false
|
|
185
|
+
},
|
|
159
186
|
ConfigSelectorMsg::Close(index, DragTarget::GroupBy) => {
|
|
160
187
|
if ctx.props().view_config.group_rollup_mode == GroupRollupMode::Total {
|
|
161
188
|
let requirements = ctx.props().renderer.metadata();
|
|
@@ -235,7 +262,7 @@ impl Component for ConfigSelector {
|
|
|
235
262
|
},
|
|
236
263
|
ConfigSelectorMsg::Close(..) => false,
|
|
237
264
|
ConfigSelectorMsg::Drop(column, action, effect, index)
|
|
238
|
-
if action != DragTarget::Active =>
|
|
265
|
+
if action != DragTarget::Active && !action.is_staged() =>
|
|
239
266
|
{
|
|
240
267
|
let col_type = ctx
|
|
241
268
|
.props()
|
|
@@ -369,6 +396,12 @@ impl Component for ConfigSelector {
|
|
|
369
396
|
ctx.props().onselect.emit(());
|
|
370
397
|
false
|
|
371
398
|
},
|
|
399
|
+
ConfigSelectorMsg::New(
|
|
400
|
+
DragTarget::WindowSource
|
|
401
|
+
| DragTarget::WindowOrderBy
|
|
402
|
+
| DragTarget::WindowPartitionBy,
|
|
403
|
+
_,
|
|
404
|
+
) => false,
|
|
372
405
|
ConfigSelectorMsg::New(DragTarget::Filter, InPlaceColumn::Column(column)) => {
|
|
373
406
|
let mut view_config = (*ctx.props().view_config).clone();
|
|
374
407
|
let op = ctx.props().default_op(column.as_str()).unwrap_or_default();
|
|
@@ -525,6 +558,18 @@ impl Component for ConfigSelector {
|
|
|
525
558
|
..
|
|
526
559
|
} = ctx.props();
|
|
527
560
|
let config = &ctx.props().view_config;
|
|
561
|
+
|
|
562
|
+
// What the ACTIVE plugin says these pivots draw — the same
|
|
563
|
+
// declaration the agent reads through `list_plugins`, so the
|
|
564
|
+
// label a user sees and the role a model is told cannot drift.
|
|
565
|
+
let plugin_roles = {
|
|
566
|
+
let plugin = &ctx.props().plugin_static_config;
|
|
567
|
+
(
|
|
568
|
+
plugin.group_by_role.clone().map(AttrValue::from),
|
|
569
|
+
plugin.split_by_role.clone().map(AttrValue::from),
|
|
570
|
+
)
|
|
571
|
+
};
|
|
572
|
+
|
|
528
573
|
let transpose = ctx.link().callback(|_| ConfigSelectorMsg::TransposePivots);
|
|
529
574
|
let column_dropdown = self.column_dropdown.clone();
|
|
530
575
|
let mut class = classes!();
|
|
@@ -556,6 +601,17 @@ impl Component for ConfigSelector {
|
|
|
556
601
|
|
|
557
602
|
let group_rollups = requirements.get_group_rollups(&rollup_features);
|
|
558
603
|
|
|
604
|
+
let on_split_rollup_mode = ctx
|
|
605
|
+
.link()
|
|
606
|
+
.callback(ConfigSelectorMsg::UpdateSplitRollupMode);
|
|
607
|
+
|
|
608
|
+
let split_rollup_features = metadata
|
|
609
|
+
.get_features()
|
|
610
|
+
.map(|x| x.get_split_rollup_modes())
|
|
611
|
+
.unwrap();
|
|
612
|
+
|
|
613
|
+
let split_rollups = requirements.get_split_rollups(&split_rollup_features);
|
|
614
|
+
|
|
559
615
|
html! {
|
|
560
616
|
<>
|
|
561
617
|
<div slot="top_panel" id="top_panel" {class} ondragend={dragend}>
|
|
@@ -587,6 +643,7 @@ impl Component for ConfigSelector {
|
|
|
587
643
|
if features.group_by {
|
|
588
644
|
<GroupBySelector
|
|
589
645
|
name="group_by"
|
|
646
|
+
role_label={plugin_roles.0.clone()}
|
|
590
647
|
disabled={config.group_rollup_mode == GroupRollupMode::Total}
|
|
591
648
|
parent={ctx.link().clone()}
|
|
592
649
|
column_dropdown={column_dropdown.clone()}
|
|
@@ -609,18 +666,36 @@ impl Component for ConfigSelector {
|
|
|
609
666
|
</GroupBySelector>
|
|
610
667
|
}
|
|
611
668
|
if features.split_by {
|
|
612
|
-
if !config.split_by.is_empty() {
|
|
669
|
+
if !config.split_by.is_empty() || split_rollups.len() > 1 {
|
|
613
670
|
<div class="pivot_controls">
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
671
|
+
if split_rollups.len() > 1 {
|
|
672
|
+
<Select<SplitRollupMode>
|
|
673
|
+
id="split_rollup_mode_selector"
|
|
674
|
+
wrapper_class="split_rollup_wrapper"
|
|
675
|
+
is_autosize=true
|
|
676
|
+
values={Rc::new(
|
|
677
|
+
split_rollups
|
|
678
|
+
.iter()
|
|
679
|
+
.map(|x| SelectItem::Option(*x))
|
|
680
|
+
.collect(),
|
|
681
|
+
)}
|
|
682
|
+
selected={config.split_rollup_mode}
|
|
683
|
+
on_select={on_split_rollup_mode}
|
|
684
|
+
/>
|
|
685
|
+
}
|
|
686
|
+
if !config.split_by.is_empty() {
|
|
687
|
+
<span
|
|
688
|
+
id="transpose_button"
|
|
689
|
+
class="rrow centered"
|
|
690
|
+
title="Transpose Pivots"
|
|
691
|
+
onmousedown={transpose}
|
|
692
|
+
/>
|
|
693
|
+
}
|
|
620
694
|
</div>
|
|
621
695
|
}
|
|
622
696
|
<SplitBySelector
|
|
623
697
|
name="split_by"
|
|
698
|
+
role_label={plugin_roles.1.clone()}
|
|
624
699
|
parent={ctx.link().clone()}
|
|
625
700
|
column_dropdown={column_dropdown.clone()}
|
|
626
701
|
exclude={config.split_by.iter().cloned().collect::<HashSet<_>>()}
|
|
@@ -22,6 +22,10 @@ pub struct ExprEditButtonProps {
|
|
|
22
22
|
/// Is this an expression column?
|
|
23
23
|
pub is_expression: bool,
|
|
24
24
|
|
|
25
|
+
/// Is this a window column?
|
|
26
|
+
#[prop_or_default]
|
|
27
|
+
pub is_window: bool,
|
|
28
|
+
|
|
25
29
|
/// Fires when the config/expresison button is clicked.
|
|
26
30
|
pub on_open_expr_panel: Callback<ColumnLocator>,
|
|
27
31
|
|
|
@@ -38,7 +42,9 @@ pub struct ExprEditButtonProps {
|
|
|
38
42
|
#[function_component]
|
|
39
43
|
pub fn ExprEditButton(p: &ExprEditButtonProps) -> Html {
|
|
40
44
|
let onmousedown = yew::use_callback(p.clone(), |_, p| {
|
|
41
|
-
let name = if p.
|
|
45
|
+
let name = if p.is_window {
|
|
46
|
+
ColumnLocator::Window(p.name.clone())
|
|
47
|
+
} else if p.is_expression {
|
|
42
48
|
ColumnLocator::Expression(p.name.clone())
|
|
43
49
|
} else {
|
|
44
50
|
ColumnLocator::Table(p.name.clone())
|