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::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}