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