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