Skip to main content

perspective_viewer/components/form/
debug.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use 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}