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