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