perspective_viewer/components/
expression_editor.rs1use std::rc::Rc;
14
15use perspective_client::{ExprValidationError, clone};
16use yew::prelude::*;
17
18use super::form::code_editor::*;
19use super::style::LocalStyle;
20use crate::session::{Session, SessionMetadata, SessionMetadataRc};
21use crate::*;
22
23#[derive(Properties, PartialEq, Clone)]
24pub struct ExpressionEditorProps {
25 pub on_save: Callback<()>,
26 pub on_validate: Callback<bool>,
27 pub on_input: Callback<Rc<String>>,
28 pub alias: Option<String>,
29 pub disabled: bool,
30
31 #[prop_or_default]
32 pub reset_count: u8,
33
34 pub metadata: SessionMetadataRc,
36
37 #[prop_or_default]
39 pub selected_theme: Option<String>,
40
41 pub session: Session,
43}
44
45#[derive(Debug)]
46pub enum ExpressionEditorMsg {
47 SetExpr(Rc<String>),
48 ValidateComplete(Option<ExprValidationError>),
49}
50
51pub struct ExpressionEditor {
53 expr: Rc<String>,
54 error: Option<ExprValidationError>,
55 oninput: Callback<Rc<String>>,
56}
57
58impl Component for ExpressionEditor {
59 type Message = ExpressionEditorMsg;
60 type Properties = ExpressionEditorProps;
61
62 fn create(ctx: &Context<Self>) -> Self {
63 let oninput = ctx.link().callback(ExpressionEditorMsg::SetExpr);
64 let expr = initial_expr(&ctx.props().metadata, &ctx.props().alias);
65 ctx.link()
66 .send_message(Self::Message::SetExpr(expr.clone()));
67
68 Self {
69 error: None,
70 expr,
71 oninput,
72 }
73 }
74
75 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
76 match msg {
77 ExpressionEditorMsg::SetExpr(val) => {
78 ctx.props().on_input.emit(val.clone());
79 self.expr = val.clone();
80 clone!(ctx.props().session);
81 ctx.link().send_future(async move {
82 match session.validate_expr(&val).await {
83 Ok(x) => ExpressionEditorMsg::ValidateComplete(x),
84 Err(err) => {
85 web_sys::console::error_1(&format!("{err:?}").into());
86 ExpressionEditorMsg::ValidateComplete(None)
87 },
88 }
89 });
90
91 true
92 },
93 ExpressionEditorMsg::ValidateComplete(err) => {
94 self.error = err;
95 if self.error.is_none() {
96 maybe!({
97 let alias = ctx.props().alias.as_ref()?;
98 let session = &ctx.props().session;
99 let old = ctx.props().metadata.get_expression_by_alias(alias)?;
100 let is_edited = *self.expr != old;
101 session
102 .metadata_mut()
103 .set_edit_by_alias(alias, self.expr.to_string());
104
105 Some(is_edited)
106 });
107
108 ctx.props().on_validate.emit(true);
109 } else {
110 ctx.props().on_validate.emit(false);
111 }
112 true
113 },
114 }
115 }
116
117 fn view(&self, ctx: &Context<Self>) -> Html {
118 let disabled_class = ctx.props().disabled.then_some("disabled");
119 clone!(ctx.props().disabled);
120 html! {
121 <>
122 <LocalStyle href={css!("expression-editor")} />
123 <label class="item_title">{ "Expression" }</label>
124 <div id="editor-container" class={disabled_class}>
125 <CodeEditor
126 autofocus=true
127 expr={&self.expr}
128 autosuggest=true
129 error={self.error.clone().map(|x| x.into())}
130 {disabled}
131 oninput={self.oninput.clone()}
132 onsave={ctx.props().on_save.clone()}
133 theme={ctx.props().selected_theme.clone().unwrap_or_default()}
134 />
135 <div id="psp-expression-editor-meta">
136 <div class="error">
137 { &self.error.clone().map(|e| e.error_message).unwrap_or_default() }
138 </div>
139 </div>
140 </div>
141 </>
142 }
143 }
144
145 fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
146 if ctx.props().alias != old_props.alias
147 || ctx.props().reset_count != old_props.reset_count
148 || (ctx.props().alias.is_some() && ctx.props().metadata != old_props.metadata)
149 {
150 ctx.link()
151 .send_message(ExpressionEditorMsg::SetExpr(initial_expr(
152 &ctx.props().metadata,
153 &ctx.props().alias,
154 )));
155 false
156 } else {
157 true
158 }
159 }
160}
161
162fn initial_expr(metadata: &SessionMetadata, alias: &Option<String>) -> Rc<String> {
163 alias
164 .as_ref()
165 .and_then(|alias| metadata.get_expression_by_alias(alias))
166 .unwrap_or_default()
167 .into()
168}