1use std::collections::HashSet;
14use std::rc::Rc;
15
16use perspective_client::config::{
17 ColumnType, WindowAggregate, WindowFrame, WindowSort, WindowSortDir, WindowSpec,
18};
19use wasm_bindgen::JsCast;
20use web_sys::{DragEvent, HtmlInputElement, MouseEvent};
21use yew::prelude::*;
22
23use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
24use crate::components::column_selector::{ColumnSelectorColumnRow, InPlaceColumn, PivotColumn};
25use crate::components::containers::dragdrop_list::{
26 DragContext, DragDropList, DragDropListItemProps,
27};
28use crate::components::containers::select::{Select, SelectItem};
29use crate::presentation::Presentation;
30use crate::session::{Session, SessionMetadataRc};
31use crate::utils::{AddListener, DragEffect, DragTarget, Subscription};
32
33fn op_label(op: WindowAggregate) -> &'static str {
34 match op {
35 WindowAggregate::Sum => "sum",
36 WindowAggregate::Avg => "avg",
37 WindowAggregate::Count => "count",
38 WindowAggregate::Min => "min",
39 WindowAggregate::Max => "max",
40 WindowAggregate::Stddev => "stddev",
41 WindowAggregate::Var => "var",
42 WindowAggregate::First => "first",
43 WindowAggregate::Last => "last",
44 WindowAggregate::Lag => "lag",
45 WindowAggregate::Lead => "lead",
46 WindowAggregate::Diff => "diff",
47 WindowAggregate::Rate => "rate",
48 WindowAggregate::Ema => "ema",
49 }
50}
51
52fn op_from_label(label: &str) -> Option<WindowAggregate> {
53 Some(match label {
54 "sum" => WindowAggregate::Sum,
55 "avg" => WindowAggregate::Avg,
56 "count" => WindowAggregate::Count,
57 "min" => WindowAggregate::Min,
58 "max" => WindowAggregate::Max,
59 "stddev" => WindowAggregate::Stddev,
60 "var" => WindowAggregate::Var,
61 "first" => WindowAggregate::First,
62 "last" => WindowAggregate::Last,
63 "lag" => WindowAggregate::Lag,
64 "lead" => WindowAggregate::Lead,
65 "diff" => WindowAggregate::Diff,
66 "rate" => WindowAggregate::Rate,
67 "ema" => WindowAggregate::Ema,
68 _ => return None,
69 })
70}
71
72fn is_aggregating(op: WindowAggregate) -> bool {
73 matches!(
74 op,
75 WindowAggregate::Sum
76 | WindowAggregate::Avg
77 | WindowAggregate::Count
78 | WindowAggregate::Min
79 | WindowAggregate::Max
80 | WindowAggregate::Stddev
81 | WindowAggregate::Var
82 )
83}
84
85fn is_positional(op: WindowAggregate) -> bool {
86 matches!(
87 op,
88 WindowAggregate::Lag | WindowAggregate::Lead | WindowAggregate::Diff
89 )
90}
91
92fn is_orderable_for_range(ty: ColumnType) -> bool {
93 matches!(
94 ty,
95 ColumnType::Integer | ColumnType::Float | ColumnType::Date | ColumnType::Datetime
96 )
97}
98
99#[derive(Clone, Properties)]
100pub struct WindowEditorProps {
101 pub metadata: SessionMetadataRc,
102
103 pub initial: Option<WindowSpec>,
105
106 pub on_change: Callback<Option<WindowSpec>>,
110
111 pub reset_count: u8,
113
114 pub presentation: Presentation,
118
119 pub session: Session,
122
123 #[prop_or_default]
126 pub selected_theme: Option<String>,
127}
128
129impl PartialEq for WindowEditorProps {
130 fn eq(&self, other: &Self) -> bool {
131 self.metadata == other.metadata
132 && self.initial == other.initial
133 && self.reset_count == other.reset_count
134 && self.selected_theme == other.selected_theme
135 }
136}
137
138#[derive(Clone, PartialEq)]
146struct WindowDraft {
147 op: String,
148 source: String,
149 order_by: String,
150 order_desc: bool,
151 partition_by: Vec<String>,
152 frame_type: String,
153 frame_rows: u32,
154 frame_range: f64,
155 offset: u32,
156 alpha: f64,
157}
158
159impl Default for WindowDraft {
160 fn default() -> Self {
161 Self {
162 op: String::default(),
163 source: String::default(),
164 order_by: String::default(),
165 order_desc: false,
166 partition_by: vec![],
167 frame_type: String::default(),
168 frame_rows: 1,
169 frame_range: 1.0,
170 offset: 1,
171 alpha: 1.0,
172 }
173 }
174}
175
176impl WindowDraft {
177 fn from_spec(spec: &WindowSpec) -> Self {
178 let (frame_type, frame_rows, frame_range) = match spec.frame {
179 Some(WindowFrame::Rows(n)) => ("Rows", n, 1.0),
180 Some(WindowFrame::Range(x)) => ("Range", 1, x),
181 Some(WindowFrame::Cumulative) | None => ("Cumulative", 1, 1.0),
182 };
183
184 Self {
185 op: op_label(spec.aggregate).to_string(),
186 source: spec.column.clone(),
187 order_by: spec
188 .order_by
189 .as_ref()
190 .map(|x| x.0.clone())
191 .unwrap_or_default(),
192 order_desc: spec
193 .order_by
194 .as_ref()
195 .map(|x| x.1 == WindowSortDir::Desc)
196 .unwrap_or_default(),
197 partition_by: spec.partition_by.clone(),
198 frame_type: frame_type.to_string(),
199 frame_rows,
200 frame_range,
201 offset: spec.offset.unwrap_or(1),
202 alpha: spec.alpha.unwrap_or(1.0),
203 }
204 }
205
206 fn new_default() -> Self {
207 Self {
208 op: "sum".to_string(),
209 frame_type: "Cumulative".to_string(),
210 ..Self::default()
211 }
212 }
213
214 fn validate(&self, metadata: &SessionMetadataRc) -> Result<WindowSpec, String> {
215 let op = op_from_label(&self.op).ok_or("Unknown op")?;
216
217 let table_column = |col: &String| {
223 metadata
224 .get_table_columns()
225 .into_iter()
226 .flatten()
227 .any(|x| x == col)
228 };
229
230 if self.source.is_empty() {
231 return Err("Missing Column".to_string());
232 }
233
234 if !table_column(&self.source) {
235 return Err(format!(
237 "\"{}\" must be a table column to source a window",
238 self.source
239 ));
240 }
241
242 let source_ty = metadata
243 .get_column_table_type(&self.source)
244 .ok_or_else(|| format!("Unknown source column \"{}\"", self.source))?;
245
246 let available = metadata
250 .get_features()
251 .map(|x| x.get_window_aggregates(source_ty))
252 .unwrap_or_default();
253
254 if !available.contains(&op) {
255 return Err(format!(
256 "\"{}\" is not a supported window aggregate for this column",
257 op_label(op)
258 ));
259 }
260
261 let order_ty = if self.order_by.is_empty() {
266 if metadata
267 .get_features()
268 .map(|x| x.unordered)
269 .unwrap_or_default()
270 {
271 return Err("Missing Order By".to_string());
272 }
273
274 None
275 } else {
276 if !table_column(&self.order_by) {
277 return Err(format!(
279 "\"{}\" must be a table column to order by",
280 self.order_by
281 ));
282 }
283
284 Some(
285 metadata
286 .get_column_table_type(&self.order_by)
287 .ok_or_else(|| format!("Unknown order by column \"{}\"", self.order_by))?,
288 )
289 };
290
291 for col in self.partition_by.iter() {
292 if !table_column(col) {
294 return Err(format!(
295 "\"{}\" must be a table column to partition by",
296 col
297 ));
298 }
299 }
300
301 let frame = if is_aggregating(op) || op == WindowAggregate::Rate {
304 match self.frame_type.as_str() {
305 "Rows" | "Cumulative" if op == WindowAggregate::Rate => {
306 return Err("\"rate\" requires a range frame".to_string());
307 },
308 "Rows" => Some(WindowFrame::Rows(self.frame_rows)),
309 "Range" => {
310 let Some(order_ty) = order_ty else {
313 return Err("Range frames require an order by column".to_string());
314 };
315
316 if !is_orderable_for_range(order_ty) {
317 return Err("Range frames require a numeric, date or datetime order by \
318 column"
319 .to_string());
320 }
321
322 Some(WindowFrame::Range(self.frame_range))
323 },
324 _ => None,
325 }
326 } else {
327 None
328 };
329
330 let offset = (is_positional(op) && self.offset != 1).then_some(self.offset);
334 let alpha = (op == WindowAggregate::Ema).then_some(self.alpha);
335
336 let mut partition_by = self.partition_by.clone();
337 partition_by.retain(|col| !col.is_empty());
338
339 Ok(WindowSpec {
340 column: self.source.clone(),
341 aggregate: op,
342 partition_by,
343 order_by: (!self.order_by.is_empty()).then(|| {
344 WindowSort(
345 self.order_by.clone(),
346 if self.order_desc {
347 WindowSortDir::Desc
348 } else {
349 WindowSortDir::Asc
350 },
351 )
352 }),
353 frame,
354 offset,
355 alpha,
356 })
357 }
358}
359
360#[derive(Clone, Debug)]
361pub enum WindowEditorMsg {
362 SetOp(String),
363 ClearSource,
364 ClearOrderBy,
365 ToggleOrderDir,
366 RemovePartition(usize),
367 SetFrameType(String),
368 SetFrameRows(f64),
372 SetFrameRange(f64),
373 SetOffset(f64),
374 SetAlpha(f64),
375 Drop(String, DragTarget, Option<DragTarget>),
379 New(DragTarget, InPlaceColumn),
380 DragEnter(DragTarget, usize),
381 DragLeave(DragTarget),
382}
383
384struct WindowSourceContext;
389struct WindowOrderByContext;
390struct WindowPartitionByContext;
391
392impl DragContext<WindowEditorMsg> for WindowSourceContext {
393 fn close(_index: usize) -> WindowEditorMsg {
394 WindowEditorMsg::ClearSource
395 }
396
397 fn dragenter(index: usize) -> WindowEditorMsg {
398 WindowEditorMsg::DragEnter(DragTarget::WindowSource, index)
399 }
400
401 fn dragleave() -> WindowEditorMsg {
402 WindowEditorMsg::DragLeave(DragTarget::WindowSource)
403 }
404
405 fn create(col: InPlaceColumn) -> WindowEditorMsg {
406 WindowEditorMsg::New(DragTarget::WindowSource, col)
407 }
408
409 fn is_self_move(effect: DragTarget) -> bool {
410 effect == DragTarget::WindowSource
411 }
412}
413
414impl DragContext<WindowEditorMsg> for WindowOrderByContext {
415 fn close(_index: usize) -> WindowEditorMsg {
416 WindowEditorMsg::ClearOrderBy
417 }
418
419 fn dragenter(index: usize) -> WindowEditorMsg {
420 WindowEditorMsg::DragEnter(DragTarget::WindowOrderBy, index)
421 }
422
423 fn dragleave() -> WindowEditorMsg {
424 WindowEditorMsg::DragLeave(DragTarget::WindowOrderBy)
425 }
426
427 fn create(col: InPlaceColumn) -> WindowEditorMsg {
428 WindowEditorMsg::New(DragTarget::WindowOrderBy, col)
429 }
430
431 fn is_self_move(effect: DragTarget) -> bool {
432 effect == DragTarget::WindowOrderBy
433 }
434}
435
436impl DragContext<WindowEditorMsg> for WindowPartitionByContext {
437 fn close(index: usize) -> WindowEditorMsg {
438 WindowEditorMsg::RemovePartition(index)
439 }
440
441 fn dragenter(index: usize) -> WindowEditorMsg {
442 WindowEditorMsg::DragEnter(DragTarget::WindowPartitionBy, index)
443 }
444
445 fn dragleave() -> WindowEditorMsg {
446 WindowEditorMsg::DragLeave(DragTarget::WindowPartitionBy)
447 }
448
449 fn create(col: InPlaceColumn) -> WindowEditorMsg {
450 WindowEditorMsg::New(DragTarget::WindowPartitionBy, col)
451 }
452
453 fn is_self_move(effect: DragTarget) -> bool {
454 effect == DragTarget::WindowPartitionBy
455 }
456}
457
458#[derive(Clone, Properties)]
465pub struct WindowSlotColumnProps {
466 pub column: String,
467 pub column_type: Option<ColumnType>,
468
469 pub action: DragTarget,
471
472 pub presentation: Presentation,
473
474 #[prop_or_default]
475 pub aggregate: Option<Html>,
476
477 #[prop_or_default]
480 pub trailing: Html,
481}
482
483impl PartialEq for WindowSlotColumnProps {
484 fn eq(&self, other: &Self) -> bool {
485 self.column == other.column
486 && self.column_type == other.column_type
487 && self.action == other.action
488 && self.aggregate == other.aggregate
489 && self.trailing == other.trailing
490 }
491}
492
493impl DragDropListItemProps for WindowSlotColumnProps {
494 type Item = String;
495
496 fn get_item(&self) -> String {
497 self.column.clone()
498 }
499}
500
501pub struct WindowSlotColumn;
502
503impl Component for WindowSlotColumn {
504 type Message = ();
505 type Properties = WindowSlotColumnProps;
506
507 fn create(_ctx: &Context<Self>) -> Self {
508 Self
509 }
510
511 fn view(&self, ctx: &Context<Self>) -> Html {
512 let dragstart = Callback::from({
513 let column = ctx.props().column.clone();
514 let presentation = ctx.props().presentation.clone();
515 let action = ctx.props().action;
516 move |event: DragEvent| {
517 presentation.set_drag_image(&event).unwrap();
518 presentation.notify_drag_start(column.clone(), DragEffect::Move(action))
519 }
520 });
521
522 let dragend = Callback::from({
523 let presentation = ctx.props().presentation.clone();
524 move |_: DragEvent| presentation.notify_drag_end()
525 });
526
527 html! {
528 <div class="column-selector-column">
529 <ColumnSelectorColumnRow
530 name={ctx.props().column.clone()}
531 col_type={ctx.props().column_type}
532 aggregate={ctx.props().aggregate.clone()}
533 trailing={ctx.props().trailing.clone()}
534 wrapper_class={classes!["column-selector-column-title"]}
535 ondragstart={Some(dragstart)}
536 ondragend={Some(dragend)}
537 />
538 </div>
539 }
540 }
541}
542
543pub struct WindowEditor {
544 draft: WindowDraft,
545 error: Option<String>,
546 column_dropdown: ColumnDropDownElement,
547 _drop_sub: Subscription,
548}
549
550impl WindowEditor {
551 fn initialize(ctx: &Context<Self>) -> WindowDraft {
552 ctx.props()
553 .initial
554 .as_ref()
555 .map(WindowDraft::from_spec)
556 .unwrap_or_else(WindowDraft::new_default)
557 }
558
559 fn emit(&mut self, ctx: &Context<Self>) {
560 match self.draft.validate(&ctx.props().metadata) {
561 Ok(spec) => {
562 self.error = None;
563 ctx.props().on_change.emit(Some(spec));
564 },
565 Err(msg) => {
566 self.error = Some(msg);
567 ctx.props().on_change.emit(None);
568 },
569 }
570 }
571}
572
573impl Component for WindowEditor {
574 type Message = WindowEditorMsg;
575 type Properties = WindowEditorProps;
576
577 fn create(ctx: &Context<Self>) -> Self {
578 let _drop_sub = {
579 let link = ctx.link().clone();
580 ctx.props().presentation.drop_received.add_listener(
581 move |(column, target, effect, _index): (String, DragTarget, DragEffect, usize)| {
582 let staged_origin = match effect {
583 DragEffect::Move(origin) if origin.is_staged() => Some(origin),
584 _ => None,
585 };
586
587 if target.is_staged() || staged_origin.is_some() {
588 link.send_message(WindowEditorMsg::Drop(column, target, staged_origin));
589 }
590 },
591 )
592 };
593
594 let mut this = Self {
595 draft: Self::initialize(ctx),
596 error: None,
597 column_dropdown: ColumnDropDownElement::new(ctx.props().session.clone()),
598 _drop_sub,
599 };
600
601 this.emit(ctx);
602 this
603 }
604
605 fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
606 if ctx.props().reset_count != old_props.reset_count
607 || ctx.props().initial != old_props.initial
608 {
609 self.draft = Self::initialize(ctx);
610 self.emit(ctx);
611 true
612 } else {
613 false
614 }
615 }
616
617 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
618 match msg {
619 WindowEditorMsg::DragEnter(target, index) => {
620 return ctx.props().presentation.notify_drag_enter(target, index);
621 },
622 WindowEditorMsg::DragLeave(target) => {
623 ctx.props().presentation.notify_drag_leave(target);
624 return true;
625 },
626 WindowEditorMsg::New(target, InPlaceColumn::Column(column)) => {
627 ctx.link()
628 .send_message(WindowEditorMsg::Drop(column, target, None));
629 return false;
630 },
631 WindowEditorMsg::New(_, InPlaceColumn::Expression(_)) => {
632 return false;
635 },
636 WindowEditorMsg::Drop(column, target, origin) => {
637 match origin {
640 Some(DragTarget::WindowSource) => self.draft.source = String::default(),
641 Some(DragTarget::WindowOrderBy) => self.draft.order_by = String::default(),
642 Some(DragTarget::WindowPartitionBy) => {
643 self.draft.partition_by.retain(|x| x != &column)
644 },
645 _ => {},
646 }
647
648 let is_table_column = ctx
652 .props()
653 .metadata
654 .get_table_columns()
655 .into_iter()
656 .flatten()
657 .any(|x| *x == column);
658
659 match target {
660 _ if target.is_staged() && !is_table_column => {},
661 DragTarget::WindowSource => {
662 self.draft.source = column;
663
664 let available = ctx
668 .props()
669 .metadata
670 .get_column_table_type(&self.draft.source)
671 .and_then(|ty| {
672 ctx.props()
673 .metadata
674 .get_features()
675 .map(|x| x.get_window_aggregates(ty))
676 })
677 .unwrap_or_default();
678
679 if !op_from_label(&self.draft.op)
680 .map(|op| available.contains(&op))
681 .unwrap_or_default()
682 && let Some(first) = available.first()
683 {
684 self.draft.op = op_label(*first).to_string();
685 }
686 },
687 DragTarget::WindowOrderBy => self.draft.order_by = column,
688 DragTarget::WindowPartitionBy
689 if !column.is_empty() && !self.draft.partition_by.contains(&column) =>
690 {
691 self.draft.partition_by.push(column);
692 },
693 _ => {},
696 }
697 },
698 WindowEditorMsg::SetOp(op) => {
699 self.draft.op = op;
700
701 if op_from_label(&self.draft.op) == Some(WindowAggregate::Rate) {
705 self.draft.frame_type = "Range".to_string();
706 }
707 },
708 WindowEditorMsg::ClearSource => self.draft.source = String::default(),
709 WindowEditorMsg::ClearOrderBy => self.draft.order_by = String::default(),
710 WindowEditorMsg::ToggleOrderDir => self.draft.order_desc = !self.draft.order_desc,
711 WindowEditorMsg::RemovePartition(idx) => {
712 if idx < self.draft.partition_by.len() {
713 self.draft.partition_by.remove(idx);
714 }
715 },
716 WindowEditorMsg::SetFrameType(x) => self.draft.frame_type = x,
717 WindowEditorMsg::SetFrameRows(x) => {
718 self.draft.frame_rows = if x.is_finite() && x >= 0.0 {
719 x as u32
720 } else {
721 1
722 };
723 },
724 WindowEditorMsg::SetFrameRange(x) => {
725 self.draft.frame_range = if x.is_finite() && x > 0.0 { x } else { 1.0 };
726 },
727 WindowEditorMsg::SetOffset(x) => {
728 self.draft.offset = if x.is_finite() && x >= 0.0 {
729 x as u32
730 } else {
731 1
732 };
733 },
734 WindowEditorMsg::SetAlpha(x) => {
735 self.draft.alpha = if x.is_finite() && x > 0.0 && x <= 1.0 {
736 x
737 } else {
738 1.0
739 };
740 },
741 }
742
743 self.emit(ctx);
744 true
745 }
746
747 fn view(&self, ctx: &Context<Self>) -> Html {
748 let op = op_from_label(&self.draft.op);
749
750 let ops: Rc<Vec<SelectItem<String>>> = Rc::new(
755 ctx.props()
756 .metadata
757 .get_column_table_type(&self.draft.source)
758 .and_then(|ty| {
759 ctx.props()
760 .metadata
761 .get_features()
762 .map(|x| x.get_window_aggregates(ty))
763 })
764 .unwrap_or_default()
765 .into_iter()
766 .map(|x| SelectItem::Option(op_label(x).to_string()))
767 .collect(),
768 );
769
770 let on_number_input = |f: fn(f64) -> WindowEditorMsg| {
774 ctx.link().callback(move |event: InputEvent| {
775 let value = event
776 .target()
777 .and_then(|t| t.dyn_into::<HtmlInputElement>().ok())
778 .map(|x| x.value_as_number())
779 .unwrap_or(f64::NAN);
780 f(value)
781 })
782 };
783
784 let frame_types: Rc<Vec<SelectItem<String>>> = Rc::new(
788 if op == Some(WindowAggregate::Rate) {
789 vec!["Range"]
790 } else {
791 vec!["Rows", "Range", "Cumulative"]
792 }
793 .into_iter()
794 .map(|x| SelectItem::Option(x.to_string()))
795 .collect(),
796 );
797
798 let non_table: HashSet<String> = ctx
804 .props()
805 .metadata
806 .get_expression_columns()
807 .chain(ctx.props().metadata.get_window_columns())
808 .cloned()
809 .collect();
810
811 let is_invalid_drag = |target: DragTarget| {
812 ctx.props()
813 .presentation
814 .is_dragover(target)
815 .map(|(_, col)| non_table.contains(&col))
816 .unwrap_or_default()
817 };
818
819 let source_exclude: HashSet<String> = std::iter::once(self.draft.source.clone())
822 .chain(non_table.iter().cloned())
823 .collect();
824
825 let source_list = html! {
826 <DragDropList<WindowEditor, WindowSlotColumn, WindowSourceContext>
827 name="window-source"
828 parent={ctx.link().clone()}
829 presentation={ctx.props().presentation.clone()}
830 column_dropdown={self.column_dropdown.clone()}
831 exclude={source_exclude}
832 is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowSource)}
833 is_invalid={is_invalid_drag(DragTarget::WindowSource)}
834 single_slot=true
835 >
836 { for (!self.draft.source.is_empty()).then(|| yew::html_nested! {
837 <WindowSlotColumn
838 column={self.draft.source.clone()}
839 column_type={ctx.props().metadata.get_column_table_type(&self.draft.source)}
840 action={DragTarget::WindowSource}
841 presentation={ctx.props().presentation.clone()}
842 aggregate={html! {
843 <div class="aggregate-selector-wrapper">
844 <Select<String>
845 wrapper_class="aggregate-selector"
846 values={ops.clone()}
847 selected={self.draft.op.clone()}
848 on_select={ctx.link().callback(WindowEditorMsg::SetOp)}
849 />
850 </div>
851 }}
852 />
853 }) }
854 </DragDropList<WindowEditor, WindowSlotColumn, WindowSourceContext>>
855 };
856
857 let order_exclude: HashSet<String> = std::iter::once(self.draft.order_by.clone())
858 .chain(non_table.iter().cloned())
859 .collect();
860 let order_list = html! {
861 <DragDropList<WindowEditor, WindowSlotColumn, WindowOrderByContext>
862 name="window-order-by"
863 parent={ctx.link().clone()}
864 presentation={ctx.props().presentation.clone()}
865 column_dropdown={self.column_dropdown.clone()}
866 exclude={order_exclude}
867 is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowOrderBy)}
868 is_invalid={is_invalid_drag(DragTarget::WindowOrderBy)}
869 single_slot=true
870 >
871 { for (!self.draft.order_by.is_empty()).then(|| {
872 let dir = if self.draft.order_desc {
875 WindowSortDir::Desc
876 } else {
877 WindowSortDir::Asc
878 };
879
880 let onmousedown = ctx
881 .link()
882 .callback(|_: MouseEvent| WindowEditorMsg::ToggleOrderDir);
883
884 yew::html_nested! {
885 <WindowSlotColumn
886 column={self.draft.order_by.clone()}
887 column_type={ctx.props().metadata.get_column_table_type(&self.draft.order_by)}
888 action={DragTarget::WindowOrderBy}
889 presentation={ctx.props().presentation.clone()}
890 trailing={html! {
891 <span
892 class={format!("sort-icon {}", dir)}
893 {onmousedown}
894 />
895 }}
896 />
897 }
898 }) }
899 </DragDropList<WindowEditor, WindowSlotColumn, WindowOrderByContext>>
900 };
901
902 let partition_exclude: HashSet<String> = self
903 .draft
904 .partition_by
905 .iter()
906 .cloned()
907 .chain(non_table.iter().cloned())
908 .collect();
909 let partition_list = html! {
910 <DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>
911 name="window-partition-by"
912 parent={ctx.link().clone()}
913 presentation={ctx.props().presentation.clone()}
914 column_dropdown={self.column_dropdown.clone()}
915 exclude={partition_exclude}
916 is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowPartitionBy)}
917 is_invalid={is_invalid_drag(DragTarget::WindowPartitionBy)}
918 >
919 { for self.draft.partition_by.iter().map(|col| yew::html_nested! {
920 <PivotColumn
921 column={col.clone()}
922 column_type={ctx.props().metadata.get_column_table_type(col)}
923 action={DragTarget::WindowPartitionBy}
924 presentation={ctx.props().presentation.clone()}
925 />
926 }) }
927 </DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>>
928 };
929
930 let show_frame =
931 op.map(is_aggregating).unwrap_or_default() || op == Some(WindowAggregate::Rate);
932 let show_offset = op.map(is_positional).unwrap_or_default();
933 let show_alpha = op == Some(WindowAggregate::Ema);
934
935 html! {
936 <>
937 <div id="window-editor-slots">{ source_list }{ order_list }{ partition_list }</div>
938 <div id="window-editor-container">
939 if show_frame {
940 <div class="column-style-label"><label id="window-frame-label" /></div>
941 <div class="row">
942 <Select<String>
943 id="window-frame-type"
944 values={frame_types}
945 selected={self.draft.frame_type.clone()}
946 on_select={ctx.link().callback(WindowEditorMsg::SetFrameType)}
947 />
948 </div>
949 if self.draft.frame_type == "Rows" {
950 <div class="row">
951 <input
952 type="number"
953 class="parameter"
954 min="0"
955 value={self.draft.frame_rows.to_string()}
956 oninput={on_number_input(WindowEditorMsg::SetFrameRows)}
957 />
958 </div>
959 }
960 if self.draft.frame_type == "Range" {
961 <div class="row">
962 <input
963 type="number"
964 class="parameter"
965 min="0"
966 value={self.draft.frame_range.to_string()}
967 oninput={on_number_input(WindowEditorMsg::SetFrameRange)}
968 />
969 </div>
970 }
971 }
972 if show_offset {
973 <div class="column-style-label">
974 <label id="window-offset-label" class="indent" />
975 </div>
976 <div class="row">
977 <input
978 type="number"
979 class="parameter"
980 min="0"
981 value={self.draft.offset.to_string()}
982 oninput={on_number_input(WindowEditorMsg::SetOffset)}
983 />
984 </div>
985 }
986 if show_alpha {
987 <div class="column-style-label">
988 <label id="window-alpha-label" class="indent" />
989 </div>
990 <div class="row">
991 <input
992 type="number"
993 class="parameter"
994 step="0.05"
995 min="0"
996 max="1"
997 value={self.draft.alpha.to_string()}
998 oninput={on_number_input(WindowEditorMsg::SetAlpha)}
999 />
1000 </div>
1001 }
1002 if let Some(error) = &self.error {
1003 <div class="row window-editor-error">{ error.clone() }</div>
1004 }
1005 </div>
1006 <ColumnDropDownPortal
1007 element={self.column_dropdown.clone()}
1008 theme={ctx.props().selected_theme.clone().unwrap_or_default()}
1009 />
1010 </>
1011 }
1012 }
1013}