perspective_viewer/components/
debug_panel.rs1use std::cell::Cell;
14use std::rc::Rc;
15
16use perspective_client::ExprValidationError;
17use perspective_js::utils::{ApiFuture, JsValueSerdeExt};
18use wasm_bindgen::prelude::*;
19use yew::prelude::*;
20
21use crate::components::code_editor::CodeEditor;
22use crate::config::*;
23use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard};
24use crate::presentation::*;
25use crate::queries::fetch_hosted_tables;
26use crate::renderer::*;
27use crate::session::*;
28use crate::utils::*;
29use crate::workspace::Workspace;
30
31#[derive(Clone, PartialEq, Properties)]
32pub struct DebugPanelProps {
33 pub presentation: Presentation,
34 pub renderer: Renderer,
35 pub session: Session,
36 pub workspace: Workspace,
37
38 #[prop_or_default]
42 pub initial_width: f64,
43
44 #[prop_or_default]
47 pub on_auto_width: Callback<f64>,
48}
49
50type Dirty = Rc<Cell<bool>>;
54
55#[function_component(DebugPanel)]
56pub fn debug_panel(props: &DebugPanelProps) -> Html {
57 let expr = use_state_eq(|| Rc::new("".to_string()));
58 let error = use_state_eq(|| Option::<ExprValidationError>::None);
59 let select_all = use_memo((), |()| PubSub::default());
60 let modified = use_state_eq(|| false);
61 let dirty: Dirty = use_memo((), |()| Cell::new(false));
62
63 let sizer = use_node_ref();
65 use_effect_with(expr.clone(), {
66 let sizer = sizer.clone();
67 let on_auto_width = props.on_auto_width.clone();
68 move |_| {
69 if let Some(elem) = sizer.cast::<web_sys::HtmlElement>() {
70 on_auto_width.emit(elem.get_bounding_client_rect().width());
71 }
72 }
73 });
74
75 use_effect_with((expr.setter(), props.clone()), {
76 clone!(error, modified, dirty);
77 move |(text, state)| {
78 state.set_text(text.clone());
79 error.set(None);
80 let sub1 = state
81 .renderer
82 .style_changed
83 .add_listener(state.reset_callback(
84 text.clone(),
85 error.setter(),
86 modified.setter(),
87 dirty.clone(),
88 ));
89
90 let sub2 = state
91 .renderer
92 .reset_changed
93 .add_listener(state.reset_callback(
94 text.clone(),
95 error.setter(),
96 modified.setter(),
97 dirty.clone(),
98 ));
99
100 let sub3 = state
101 .session
102 .view_config_changed
103 .add_listener(state.reset_callback(
104 text.clone(),
105 error.setter(),
106 modified.setter(),
107 dirty.clone(),
108 ));
109
110 || {
111 drop(sub1);
112 drop(sub2);
113 drop(sub3);
114 }
115 }
116 });
117
118 let oninput = use_callback(expr.setter(), {
119 clone!(modified, dirty);
120 move |x, expr| {
121 dirty.set(true);
122 modified.set(true);
123 expr.set(x)
124 }
125 });
126
127 let onsave = use_callback((expr.clone(), error.clone(), props.clone()), {
128 clone!(modified, dirty);
129 move |_, (text, error, props)| props.on_save(text, text.setter(), error, &modified, &dirty)
130 });
131
132 let oncopy = use_callback(
133 (expr.clone(), select_all.callback()),
134 move |_, (text, select_all)| {
135 select_all.emit(());
136 let options = web_sys::BlobPropertyBag::new();
137 options.set_type("text/plain");
138 let blob_txt = (JsValue::from((***text).clone())).clone();
139 let blob_parts = js_sys::Array::from_iter([blob_txt].iter());
140 let blob = web_sys::Blob::new_with_str_sequence_and_options(&blob_parts, &options);
141 ApiFuture::spawn(copy_to_clipboard(
142 async move { Ok(blob?) },
143 MimeType::TextPlain,
144 ));
145 },
146 );
147
148 let onapply = use_callback((expr.clone(), error.clone(), props.clone()), {
149 clone!(modified, dirty);
150 move |_, (text, error, props)| props.on_save(text, text.setter(), error, &modified, &dirty)
151 });
152
153 let onreset = use_callback((expr.setter(), error.clone(), props.clone()), {
154 clone!(modified, dirty);
155 move |_, (text, error, props)| {
156 dirty.set(false);
157 props.set_text(text.clone());
158 error.set(None);
159 modified.set(false);
160 }
161 });
162
163 let onpaste = use_callback((expr.clone(), error.clone(), props.clone()), {
164 clone!(modified, dirty);
165 move |_, (text, error, props)| {
166 clone!(text, error, props, modified, dirty);
167 ApiFuture::spawn(async move {
168 if let Some(x) = paste_from_clipboard().await {
169 let x = Rc::new(x);
170 dirty.set(true);
171 modified.set(true);
172 error.set(None);
173 text.set(x.clone());
174 props.on_save(&x, text.setter(), &error, &modified, &dirty);
175 }
176
177 Ok(())
178 });
179 }
180 });
181
182 html! {
183 <>
184 <div id="debug-panel-overflow">
185 <div id="debug-panel" class="sidebar_column" ref={sizer}>
186 <div id="debug-panel-controls">
187 <button id="debug-panel-apply" disabled={!*modified} onclick={onapply} />
188 <button id="debug-panel-reset" disabled={!*modified} onclick={onreset} />
189 <button id="debug-panel-copy" onclick={oncopy} />
190 <button id="debug-panel-paste" onclick={onpaste} />
191 </div>
192 <div id="debug-panel-editor">
193 <CodeEditor
194 expr={&*expr}
195 disabled=false
196 contained=true
197 {oninput}
198 {onsave}
199 select_all={select_all.subscriber()}
200 error={(*error).clone()}
201 />
202 if let Some(err) = &*error {
203 <div id="debug-panel-error" class="error">{ &err.error_message }</div>
204 }
205 </div>
206 <div
207 class="scroll-panel-auto-width"
208 style={format!("width:{}px", props.initial_width)}
209 />
210 </div>
211 </div>
212 </>
213 }
214}
215
216impl DebugPanelProps {
217 fn set_text(&self, setter: UseStateSetter<Rc<String>>) {
218 let props = self.clone();
219 ApiFuture::spawn(async move {
220 let config = crate::queries::get_viewer_config(
221 &props.session,
222 &props.renderer,
223 &props.presentation,
224 )
225 .await?;
226 let json = JsValue::from_serde_ext(&config)?;
227 let js_string =
228 js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?;
229
230 setter.set(Rc::new(js_string.as_string().unwrap()));
231 Ok(())
232 });
233 }
234
235 fn reset_callback(
236 &self,
237 text: UseStateSetter<Rc<String>>,
238 error: UseStateSetter<Option<ExprValidationError>>,
239 modified: UseStateSetter<bool>,
240 dirty: Dirty,
241 ) -> impl Fn(()) + use<> {
242 let props = self.clone();
243 move |_| {
244 if dirty.get() {
245 return;
246 }
247
248 error.set(None);
249 props.set_text(text.clone());
250 modified.set(false);
251 }
252 }
253
254 fn on_save(
257 &self,
258 source: &Rc<String>,
259 text: UseStateSetter<Rc<String>>,
260 error: &UseStateHandle<Option<ExprValidationError>>,
261 modified: &UseStateHandle<bool>,
262 dirty: &Dirty,
263 ) {
264 let props = self.clone();
265 clone!(source, error, modified, dirty);
266 ApiFuture::spawn(async move {
267 let fail = |message: String, (line, column): (u32, u32)| {
268 dirty.set(true);
269 modified.set(true);
270 error.set(Some(ExprValidationError {
271 error_message: message,
272 line,
273 column,
274 }));
275 };
276
277 let config: ViewerConfigUpdate = match serde_json::from_str(&source) {
278 Ok(config) => config,
279 Err(err) => {
280 let position = (err.line() as u32 - 1, err.column() as u32 - 1);
281 fail(err.to_string(), position);
282 return Ok(());
283 },
284 };
285
286 if let OptionalUpdate::Update(name) = &config.table {
287 let hosted = fetch_hosted_tables(&props.workspace).await;
288 if !hosted
289 .iter()
290 .any(|(_, names)| names.iter().any(|n| n == name))
291 {
292 fail(
293 format!("Unknown table \"{name}\""),
294 locate_key(&source, "table"),
295 );
296
297 return Ok(());
298 }
299 }
300
301 let active = props.workspace.active_renderer().as_ref() == Some(&props.renderer);
302 let result = crate::tasks::restore_panel(
303 &props.session,
304 &props.renderer,
305 &props.presentation,
306 &props.workspace,
307 crate::tasks::RestoreMode::Existing { active },
308 config,
309 crate::tasks::RestoreErrors::Suppress,
310 MissingTable::Error,
311 )
312 .await;
313
314 match result {
315 Ok(_) => {
316 dirty.set(false);
317 error.set(None);
318 modified.set(false);
319 props.set_text(text);
320 },
321 Err(e) => fail(format!("{e}"), (0, 0)),
322 }
323
324 Ok(())
325 });
326 }
327}
328
329fn locate_key(text: &str, key: &str) -> (u32, u32) {
332 let needle = format!("\"{key}\"");
333 text.lines()
334 .enumerate()
335 .find_map(|(line, content)| {
336 content
337 .find(&needle)
338 .map(|column| (line as u32, column as u32))
339 })
340 .unwrap_or((0, 0))
341}