perspective_viewer/components/form/
debug.rs1use std::rc::Rc;
14
15use perspective_client::ExprValidationError;
16use perspective_js::utils::{ApiFuture, JsValueSerdeExt};
17use wasm_bindgen::prelude::*;
18use yew::prelude::*;
19
20use crate::components::form::code_editor::CodeEditor;
21use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard};
22use crate::presentation::*;
23use crate::renderer::*;
24use crate::session::*;
25use crate::utils::*;
26
27#[derive(Clone, PartialEq, Properties)]
28pub struct DebugPanelProps {
29 pub presentation: Presentation,
30 pub renderer: Renderer,
31 pub session: Session,
32
33 #[prop_or_default]
37 pub initial_width: f64,
38
39 #[prop_or_default]
42 pub on_auto_width: Callback<f64>,
43}
44
45#[function_component(DebugPanel)]
46pub fn debug_panel(props: &DebugPanelProps) -> Html {
47 let expr = use_state_eq(|| Rc::new("".to_string()));
48 let error = use_state_eq(|| Option::<ExprValidationError>::None);
49 let select_all = use_memo((), |()| PubSub::default());
50 let modified = use_state_eq(|| false);
51
52 let sizer = use_node_ref();
54 use_effect_with(expr.clone(), {
55 let sizer = sizer.clone();
56 let on_auto_width = props.on_auto_width.clone();
57 move |_| {
58 if let Some(elem) = sizer.cast::<web_sys::HtmlElement>() {
59 on_auto_width.emit(elem.get_bounding_client_rect().width());
60 }
61 }
62 });
63
64 use_effect_with((expr.setter(), props.clone()), {
65 clone!(error, modified);
66 move |(text, state)| {
67 state.set_text(text.clone());
68 error.set(None);
69 let sub1 = state
70 .renderer
71 .style_changed
72 .add_listener(state.reset_callback(
73 text.clone(),
74 error.setter(),
75 modified.setter(),
76 ));
77
78 let sub2 = state
79 .renderer
80 .reset_changed
81 .add_listener(state.reset_callback(
82 text.clone(),
83 error.setter(),
84 modified.setter(),
85 ));
86
87 let sub3 = state
88 .session
89 .view_config_changed
90 .add_listener(state.reset_callback(
91 text.clone(),
92 error.setter(),
93 modified.setter(),
94 ));
95
96 || {
97 drop(sub1);
98 drop(sub2);
99 drop(sub3);
100 }
101 }
102 });
103
104 let oninput = use_callback(expr.setter(), {
105 clone!(modified);
106 move |x, expr| {
107 modified.set(true);
108 expr.set(x)
109 }
110 });
111
112 let onsave = use_callback((expr.clone(), error.clone(), props.clone()), {
113 clone!(modified);
114 move |_, (text, error, props)| props.on_save(text, error, &modified)
115 });
116
117 let oncopy = use_callback(
118 (expr.clone(), select_all.callback()),
119 move |_, (text, select_all)| {
120 select_all.emit(());
121 let options = web_sys::BlobPropertyBag::new();
122 options.set_type("text/plain");
123 let blob_txt = (JsValue::from((***text).clone())).clone();
124 let blob_parts = js_sys::Array::from_iter([blob_txt].iter());
125 let blob = web_sys::Blob::new_with_str_sequence_and_options(&blob_parts, &options);
126 ApiFuture::spawn(copy_to_clipboard(
127 async move { Ok(blob?) },
128 MimeType::TextPlain,
129 ));
130 },
131 );
132
133 let onapply = use_callback((expr.clone(), error.clone(), props.clone()), {
134 clone!(modified);
135 move |_, (text, error, props)| props.on_save(text, error, &modified)
136 });
137
138 let onreset = use_callback((expr.setter(), error.clone(), props.clone()), {
139 clone!(modified);
140 move |_, (text, error, props)| {
141 props.set_text(text.clone());
142 error.set(None);
143 modified.set(false);
144 }
145 });
146
147 let onpaste = use_callback((expr.clone(), error.clone(), props.clone()), {
148 clone!(modified);
149 move |_, (text, error, props)| {
150 clone!(text, error, props, modified);
151 ApiFuture::spawn(async move {
152 if let Some(x) = paste_from_clipboard().await {
153 let x = Rc::new(x);
154 modified.set(true);
155 error.set(None);
156 text.set(x.clone());
157 props.on_save(&x, &error, &modified);
158 }
159
160 Ok(())
161 });
162 }
163 });
164
165 html! {
166 <>
167 <div id="debug-panel-overflow">
168 <div id="debug-panel" class="sidebar_column" ref={sizer}>
169 <div id="debug-panel-controls">
170 <button disabled={!*modified} onclick={onapply}>{ "Apply" }</button>
171 <button disabled={!*modified} onclick={onreset}>{ "Reset" }</button>
172 <button onclick={oncopy}>{ "Copy" }</button>
173 <button onclick={onpaste}>{ "Paste" }</button>
174 </div>
175 <div id="debug-panel-editor">
176 <CodeEditor
177 expr={&*expr}
178 disabled=false
179 {oninput}
180 {onsave}
181 select_all={select_all.subscriber()}
182 error={(*error).clone()}
183 />
184 </div>
185 <div
186 class="scroll-panel-auto-width"
187 style={format!("width:{}px", props.initial_width)}
188 />
189 </div>
190 </div>
191 </>
192 }
193}
194
195impl DebugPanelProps {
196 fn set_text(&self, setter: UseStateSetter<Rc<String>>) {
197 let props = self.clone();
198 ApiFuture::spawn(async move {
199 let config = crate::queries::get_viewer_config(
200 &props.session,
201 &props.renderer,
202 &props.presentation,
203 )
204 .await?;
205 let json = JsValue::from_serde_ext(&config)?;
206 let js_string =
207 js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?;
208
209 setter.set(Rc::new(js_string.as_string().unwrap()));
210 Ok(())
211 });
212 }
213
214 fn reset_callback(
215 &self,
216 text: UseStateSetter<Rc<String>>,
217 error: UseStateSetter<Option<ExprValidationError>>,
218 modified: UseStateSetter<bool>,
219 ) -> impl Fn(()) + use<> {
220 let props = self.clone();
221 move |_| {
222 error.set(None);
223 props.set_text(text.clone());
224 modified.set(false);
225 }
226 }
227
228 fn on_save(
229 &self,
230 text: &Rc<String>,
231 error: &UseStateHandle<Option<ExprValidationError>>,
232 modified: &UseStateHandle<bool>,
233 ) {
234 let props = self.clone();
235 clone!(text, error, modified);
236 ApiFuture::spawn(async move {
237 match serde_json::from_str(&text) {
238 Ok(config) => {
239 match crate::tasks::restore_and_render(
240 &props.session,
241 &props.renderer,
242 &props.presentation,
243 config,
244 async { Ok(()) },
245 )
246 .await
247 {
248 Ok(_) => {
249 modified.set(false);
250 },
251 Err(e) => {
252 modified.set(true);
253 error.set(Some(ExprValidationError {
254 error_message: JsValue::from(e).as_string().unwrap_or_else(|| {
255 "Failed to validate viewer config".to_owned()
256 }),
257 line: 0_u32,
258 column: 0,
259 }));
260 },
261 }
262 Ok(())
263 },
264 Err(err) => {
265 modified.set(true);
266 error.set(Some(ExprValidationError {
267 error_message: err.to_string(),
268 line: err.line() as u32 - 1,
269 column: err.column() as u32 - 1,
270 }));
271
272 Ok(())
273 },
274 }
275 });
276 }
277}