perspective_viewer/components/
status_bar.rs1use perspective_client::config::Filter;
14use wasm_bindgen_futures::spawn_local;
15use web_sys::*;
16use yew::prelude::*;
17
18use super::status_indicator::StatusIndicator;
19use crate::components::containers::select::*;
20use crate::components::copy_dropdown::CopyDropDownMenu;
21use crate::components::export_dropdown::ExportDropDownMenu;
22use crate::components::global_filter_bar::GlobalFilterBar;
23use crate::components::portal::PortalModal;
24use crate::components::status_bar_counter::StatusBarRowsCounter;
25use crate::components::style::StyleSurface;
26use crate::config::*;
27use crate::js::*;
28use crate::presentation::{Presentation, PresentationProps};
29use crate::renderer::*;
30use crate::session::*;
31use crate::tasks::*;
32use crate::utils::*;
33use crate::workspace::Workspace;
34use crate::*;
35
36#[derive(Clone, Properties)]
37pub struct StatusBarProps {
38 pub id: String,
40
41 pub on_reset: Callback<bool>,
43
44 pub session_props: SessionProps,
48 pub presentation_props: PresentationProps,
49
50 pub is_settings_open: bool,
54
55 pub update_count: u32,
57
58 pub global_filters: Vec<Filter>,
61
62 pub on_remove_global_filter: Callback<usize>,
64
65 pub on_clear_global_filters: Callback<()>,
67
68 pub session: Session,
70 pub renderer: Renderer,
71 pub presentation: Presentation,
72
73 pub workspace: Workspace,
77}
78
79impl PartialEq for StatusBarProps {
80 fn eq(&self, other: &Self) -> bool {
81 self.id == other.id
82 && self.session_props == other.session_props
83 && self.presentation_props == other.presentation_props
84 && self.is_settings_open == other.is_settings_open
85 && self.update_count == other.update_count
86 && self.global_filters == other.global_filters
87 }
88}
89
90pub enum StatusBarMsg {
91 Reset(MouseEvent),
92 Export,
93 Copy,
94 CloseExport,
95 CloseCopy,
96 Eject,
97 SetTheme(String),
98 ResetTheme,
99 PointerEvent(web_sys::PointerEvent),
100}
101
102pub struct StatusBar {
104 copy_ref: NodeRef,
105 export_ref: NodeRef,
106 statusbar_ref: NodeRef,
107 copy_target: Option<HtmlElement>,
108 export_target: Option<HtmlElement>,
109}
110
111impl Component for StatusBar {
112 type Message = StatusBarMsg;
113 type Properties = StatusBarProps;
114
115 fn create(_ctx: &Context<Self>) -> Self {
116 Self {
117 copy_ref: NodeRef::default(),
118 export_ref: NodeRef::default(),
119 statusbar_ref: NodeRef::default(),
120 copy_target: None,
121 export_target: None,
122 }
123 }
124
125 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
126 match msg {
127 StatusBarMsg::Reset(event) => {
128 let all = event.shift_key();
129 ctx.props().on_reset.emit(all);
130 false
131 },
132 StatusBarMsg::ResetTheme => {
133 update_theme(
134 &ctx.props().renderer,
135 &ctx.props().presentation,
136 &ctx.props().workspace,
137 None,
138 );
139 true
140 },
141 StatusBarMsg::SetTheme(theme_name) => {
142 update_theme(
143 &ctx.props().renderer,
144 &ctx.props().presentation,
145 &ctx.props().workspace,
146 Some(theme_name),
147 );
148 false
149 },
150 StatusBarMsg::Export => {
151 self.export_target = self.export_ref.cast::<HtmlElement>();
152 true
153 },
154 StatusBarMsg::Copy => {
155 self.copy_target = self.copy_ref.cast::<HtmlElement>();
156 true
157 },
158 StatusBarMsg::CloseExport => {
159 self.export_target = None;
160 true
161 },
162 StatusBarMsg::CloseCopy => {
163 self.copy_target = None;
164 true
165 },
166 StatusBarMsg::Eject => {
167 ctx.props().presentation.on_eject.emit(());
168 false
169 },
170 StatusBarMsg::PointerEvent(event) => {
171 if event.target().map(JsValue::from)
172 == self.statusbar_ref.cast::<HtmlElement>().map(JsValue::from)
173 {
174 ctx.props().presentation.statusbar_pointer_event.emit(event);
175 }
176
177 false
178 },
179 }
180 }
181
182 fn view(&self, ctx: &Context<Self>) -> Html {
183 let Self::Properties {
184 renderer, session, ..
185 } = ctx.props();
186
187 let has_table = ctx.props().session_props.has_table.clone();
188 let is_errored = ctx.props().session_props.is_errored();
189 let is_settings_open = ctx.props().is_settings_open;
190 let title = &ctx.props().session_props.title;
191
192 let mut is_updating_class_name = classes!();
193 if !is_settings_open {
194 is_updating_class_name.push("settings-closed");
195 };
196
197 if !matches!(has_table, Some(TableLoadState::Loaded)) {
198 is_updating_class_name.push("updating");
199 }
200
201 let onclose = ctx.link().callback(|_| StatusBarMsg::Eject);
203 let onpointerdown = ctx.link().callback(StatusBarMsg::PointerEvent);
204 let onexport = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export);
205 let oncopy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy);
206 let onreset = ctx.link().callback(StatusBarMsg::Reset);
207
208 let extra_slot = ctx
213 .props()
214 .renderer
215 .slot_name()
216 .map(|id| format!("statusbar-extra-{id}"))
217 .unwrap_or_else(|| "statusbar-extra".to_owned());
218 let is_menu = matches!(has_table, Some(TableLoadState::Loaded)) && is_settings_open;
219 let is_title =
223 is_menu || ctx.props().presentation_props.is_workspace || title.is_some() || is_errored;
224
225 let is_settings =
226 !matches!(has_table, Some(TableLoadState::Loaded)) || is_errored || is_settings_open;
227
228 let on_copy_select = {
229 let props = ctx.props().clone();
230 let link = ctx.link().clone();
231 Callback::from(move |x: ExportFile| {
232 let props = props.clone();
233 let link = link.clone();
234 spawn_local(async move {
235 let mime = x.method.mimetype(x.is_chart);
236 let task = export_method_to_blob(
237 &props.session,
238 &props.renderer,
239 &props.presentation,
240 x.method,
241 );
242 let result = copy_to_clipboard(task, mime).await;
243 let r = (|| -> ApiResult<()> {
244 result?;
245 link.send_message(StatusBarMsg::CloseCopy);
246 Ok(())
247 })();
248 if let Err(e) = r {
249 web_sys::console::warn_1(&e.into());
250 }
251 })
252 })
253 };
254
255 let on_export_select = {
256 let props = ctx.props().clone();
257 let link = ctx.link().clone();
258 Callback::from(move |x: ExportFile| {
259 if !x.name.is_empty() {
260 clone!(props, link);
261 spawn_local(async move {
262 let val = export_method_to_blob(
263 &props.session,
264 &props.renderer,
265 &props.presentation,
266 x.method,
267 )
268 .await
269 .unwrap();
270 let is_chart = props.renderer.is_chart();
271 download(&x.as_filename(is_chart), &val).unwrap();
272 link.send_message(StatusBarMsg::CloseExport);
273 })
274 }
275 })
276 };
277
278 let on_close_copy = ctx.link().callback(|_| StatusBarMsg::CloseCopy);
279 let on_close_export = ctx.link().callback(|_| StatusBarMsg::CloseExport);
280
281 if is_settings {
282 html! {
283 <>
284 <div
285 ref={&self.statusbar_ref}
286 id={ctx.props().id.clone()}
287 class={is_updating_class_name}
288 {onpointerdown}
289 >
290 <StatusIndicator
291 {renderer}
292 {session}
293 update_count={ctx.props().update_count}
294 session_props={ctx.props().session_props.clone()}
295 />
296 if is_title {
297 <StatusBarRowsCounter stats={ctx.props().session_props.stats.clone()} />
298 }
299 if !ctx.props().global_filters.is_empty() {
302 <GlobalFilterBar
303 filters={ctx.props().global_filters.clone()}
304 on_remove={ctx.props().on_remove_global_filter.clone()}
305 on_clear={ctx.props().on_clear_global_filters.clone()}
306 />
307 }
308 <div id="spacer" />
309 if is_menu {
310 <div id="menu-bar" class="section">
311 <ThemeSelector
312 theme={ctx.props().presentation_props.selected_theme.clone()}
313 themes={ctx.props().presentation_props.available_themes.clone()}
314 on_change={ctx.link().callback(StatusBarMsg::SetTheme)}
315 on_reset={ctx.link().callback(|_| StatusBarMsg::ResetTheme)}
316 />
317 <div id="plugin-settings"><slot name={extra_slot} /></div>
318 <span class="hover-target">
319 <span id="reset" class="button" onmousedown={&onreset}>
320 <span class="icon shift-alt-icon" />
321 <span class="icon-label" />
322 </span>
323 </span>
324 <span
325 ref={&self.export_ref}
326 class="hover-target"
327 onmousedown={onexport}
328 >
329 <span id="export" class="button">
330 <span class="icon" />
331 <span class="icon-label" />
332 </span>
333 </span>
334 <span
335 ref={&self.copy_ref}
336 class="hover-target"
337 onmousedown={oncopy}
338 >
339 <span id="copy" class="button">
340 <span class="icon" />
341 <span class="icon-label" />
342 </span>
343 </span>
344 </div>
345 }
346 if !is_settings_open {
347 <div id="close_button" class="noselect" onmousedown={onclose}>
348 <span class="icon" />
349 </div>
350 }
351 </div>
352 <PortalModal
353 tag_name="perspective-copy-menu"
354 surface={StyleSurface::DropdownMenu}
355 target={self.copy_target.clone()}
356 own_focus=true
357 on_close={on_close_copy}
358 theme={ctx.props().presentation_props.selected_theme.clone().unwrap_or_default()}
359 >
360 <CopyDropDownMenu renderer={renderer.clone()} callback={on_copy_select} />
361 </PortalModal>
362 <PortalModal
363 tag_name="perspective-export-menu"
364 surface={StyleSurface::DropdownMenu}
365 target={self.export_target.clone()}
366 own_focus=true
367 on_close={on_close_export}
368 theme={ctx.props().presentation_props.selected_theme.clone().unwrap_or_default()}
369 >
370 <ExportDropDownMenu
371 renderer={renderer.clone()}
372 session={session.clone()}
373 callback={on_export_select}
374 />
375 </PortalModal>
376 </>
377 }
378 } else {
379 let class = classes!(is_updating_class_name, "floating");
383 html! {
384 <div id={ctx.props().id.clone()} {class}>
385 <div id="close_button" class="noselect" onmousedown={&onclose} />
386 </div>
387 }
388 }
389 }
390}
391
392#[derive(Properties, PartialEq)]
393struct ThemeSelectorProps {
394 pub theme: Option<String>,
395 pub themes: PtrEqRc<Vec<String>>,
396 pub on_reset: Callback<()>,
397 pub on_change: Callback<String>,
398}
399
400#[function_component]
401fn ThemeSelector(props: &ThemeSelectorProps) -> Html {
402 let is_first = props
403 .theme
404 .as_ref()
405 .and_then(|x| props.themes.first().map(|y| y == x))
406 .unwrap_or_default();
407
408 let values = use_memo(props.themes.clone(), |themes| {
409 themes
410 .iter()
411 .cloned()
412 .map(SelectItem::Option)
413 .collect::<Vec<_>>()
414 });
415
416 match &props.theme {
417 None => html! {},
418 Some(selected) => {
419 html! {
420 if values.len() > 1 {
421 <span class="hover-target">
422 <div
423 id="theme_icon"
424 class={if is_first {""} else {"modified"}}
425 tabindex="0"
426 onclick={props.on_reset.reform(|_| ())}
427 />
428 <span id="theme" class="button">
429 <span class="icon" />
430 <Select<String>
431 id="theme_selector"
432 class="invert"
433 {values}
434 selected={selected.to_owned()}
435 on_select={props.on_change.clone()}
436 />
437 </span>
438 </span>
439 }
440 }
441 },
442 }
443}