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