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