Skip to main content

perspective_viewer/components/
status_indicator.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 perspective_client::config::ViewConfigUpdate;
14use perspective_js::utils::ApiError;
15use web_sys::*;
16use yew::prelude::*;
17
18use crate::renderer::Renderer;
19use crate::session::{Session, SessionProps, TableLoadState};
20use crate::utils::*;
21
22/// Value-prop version: no PubSub subscriptions, no reducer.
23/// The parent (`StatusBar`) re-renders this component whenever
24/// `session_props.error/has_table/stats` or `update_count` change (via
25/// root's `IncrementUpdateCount` / `DecrementUpdateCount` / `UpdateSession`
26/// messages).
27#[derive(PartialEq, Properties)]
28pub struct StatusIndicatorProps {
29    pub renderer: Renderer,
30    pub session: Session,
31    /// Number of in-flight renders (>0 → "updating" spinner).
32    pub update_count: u32,
33    /// Snapshot of session value props — read for `error`, `has_table`,
34    /// `stats` to derive the icon state.
35    pub session_props: SessionProps,
36}
37
38/// An indicator component which displays the current status of the perspective
39/// server as an icon. This indicator also functions as a button to invoke the
40/// reconnect callback when in an error state.
41#[function_component]
42pub fn StatusIndicator(props: &StatusIndicatorProps) -> Html {
43    let has_table_cells = props
44        .session_props
45        .stats
46        .as_ref()
47        .and_then(|s| s.num_table_cells)
48        .is_some();
49
50    let state = if let Some(err) = &props.session_props.error {
51        StatusIconState::Errored(
52            err.message(),
53            err.stacktrace(),
54            err.kind(),
55            err.is_reconnect(),
56        )
57    } else if !has_table_cells
58        && matches!(props.session_props.has_table, Some(TableLoadState::Loading))
59    {
60        StatusIconState::Loading
61    } else if props.update_count > 0 {
62        StatusIconState::Updating
63    } else if has_table_cells {
64        StatusIconState::Normal
65    } else {
66        StatusIconState::Uninitialized
67    };
68
69    let class_name = match &state {
70        StatusIconState::Errored(_, _, _, true) => "errored",
71        StatusIconState::Errored(_, _, _, false) => "errored disabled",
72        StatusIconState::Normal => "connected",
73        StatusIconState::Updating => "updating",
74        StatusIconState::Loading => "loading",
75        StatusIconState::Uninitialized => "uninitialized",
76    };
77
78    let onclick = use_async_callback(
79        (props.session.clone(), props.renderer.clone(), state.clone()),
80        async move |_: MouseEvent, (session, renderer, state)| {
81            match &state {
82                StatusIconState::Errored(..) => {
83                    session.reconnect().await?;
84                    let cfg = ViewConfigUpdate::default();
85                    session.update_view_config(cfg)?;
86                    renderer.apply_pending_plugin()?;
87                    renderer
88                        .draw(session.validate().await?.create_view())
89                        .await?;
90                },
91                StatusIconState::Normal => {
92                    session.status_indicator_clicked.emit(());
93                },
94                _ => {},
95            };
96
97            Ok::<_, ApiError>(())
98        },
99    );
100
101    html! {
102        <>
103            <div class="section">
104                <div id="status_reconnect" class={class_name} {onclick}>
105                    <span id="status" class={class_name} />
106                    <span id="status_updating" class={class_name} />
107                </div>
108                if let StatusIconState::Errored(err, stack, kind, _) = &state {
109                    <div class="error-dialog">
110                        <div class="error-dialog-message">{ format!("{} {}", kind, err) }</div>
111                        <div class="error-dialog-stack">{ stack }</div>
112                    </div>
113                }
114            </div>
115        </>
116    }
117}
118
119#[derive(Clone, Debug, PartialEq)]
120enum StatusIconState {
121    Loading,
122    Updating,
123    Errored(String, String, &'static str, bool),
124    Normal,
125    Uninitialized,
126}