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::PerspectiveProperties;
20use crate::custom_events::CustomEvents;
21use crate::model::*;
22use crate::renderer::*;
23use crate::session::*;
24use crate::utils::*;
25
26#[derive(PartialEq, Properties, PerspectiveProperties!)]
27pub struct StatusIndicatorProps {
28    pub custom_events: CustomEvents,
29    pub renderer: Renderer,
30    pub session: Session,
31}
32
33/// An indicator component which displays the current status of the perspective
34/// server as an icon. This indicator also functions as a button to invoke the
35/// reconnect callback when in an error state.
36#[function_component]
37pub fn StatusIndicator(props: &StatusIndicatorProps) -> Html {
38    let state = use_reducer_eq(|| {
39        if let Some(err) = props.session.get_error() {
40            StatusIconState::Errored(err.message(), err.stacktrace(), err.kind())
41        } else {
42            StatusIconState::Normal
43        }
44    });
45
46    use_effect_with(
47        (props.session.clone(), state.dispatcher()),
48        |(session, set_state)| {
49            let subs = [
50                session
51                    .table_errored
52                    .add_listener(set_state.callback(StatusIconStateAction::SetError)),
53                session
54                    .stats_changed
55                    .add_listener(set_state.callback(StatusIconStateAction::Load)),
56                session
57                    .view_config_changed
58                    .add_listener(set_state.callback(|_| StatusIconStateAction::Increment)),
59                session
60                    .view_created
61                    .add_listener(set_state.callback(|_| StatusIconStateAction::Decrement)),
62            ];
63
64            move || drop(subs)
65        },
66    );
67
68    let class_name = match (&*state, props.session.is_reconnect()) {
69        (StatusIconState::Errored(..), true) => "errored",
70        (StatusIconState::Errored(..), false) => "errored disabled",
71        (StatusIconState::Normal, _) => "connected",
72        (StatusIconState::Updating(_), _) => "updating",
73        (StatusIconState::Loading, _) => "loading",
74        (StatusIconState::Unititialized, _) => "uninitialized",
75    };
76
77    let onclick = use_async_callback(
78        (props.clone_state(), state.clone()),
79        async move |_: MouseEvent, (props, state)| {
80            match &**state {
81                StatusIconState::Errored(..) => {
82                    props.session.reconnect().await?;
83                    let cfg = ViewConfigUpdate::default();
84                    props.update_and_render(cfg)?.await?;
85                },
86                StatusIconState::Normal => {
87                    props
88                        .custom_events
89                        .dispatch_event("status-indicator-click", JsValue::UNDEFINED)?;
90                },
91                _ => {},
92            };
93
94            // if let StatusIconState::Errored(..) = &**state {
95            //     props.session.reconnect().await?;
96            //     let cfg = ViewConfigUpdate::default();
97            //     props.update_and_render(cfg)?.await?;
98            // }
99
100            Ok::<_, ApiError>(())
101        },
102    );
103
104    html! {
105        <>
106            <div class="section">
107                <div id="status_reconnect" class={class_name} {onclick}>
108                    <span id="status" class={class_name} />
109                    <span id="status_updating" class={class_name} />
110                </div>
111                if let StatusIconState::Errored(err, stack, kind) = &*state {
112                    <div class="error-dialog">
113                        <div class="error-dialog-message">{ format!("{} {}", kind, err) }</div>
114                        <div class="error-dialog-stack">{ stack }</div>
115                    </div>
116                }
117            </div>
118        </>
119    }
120}
121
122#[derive(Clone, Default, Debug, PartialEq)]
123enum StatusIconState {
124    Loading,
125    Updating(u32),
126    Errored(String, String, &'static str),
127    Normal,
128
129    #[default]
130    Unititialized,
131}
132
133#[derive(Clone, Debug)]
134enum StatusIconStateAction {
135    Increment,
136    Decrement,
137    Load(Option<ViewStats>),
138    SetError(ApiError),
139}
140
141impl Reducible for StatusIconState {
142    type Action = StatusIconStateAction;
143
144    fn reduce(self: std::rc::Rc<Self>, action: Self::Action) -> std::rc::Rc<Self> {
145        let new_status = match (&*self, action.clone()) {
146            (StatusIconState::Updating(x), StatusIconStateAction::Increment) => {
147                Self::Updating(x + 1)
148            },
149            (StatusIconState::Updating(x), StatusIconStateAction::Decrement) if *x > 1 => {
150                Self::Updating(x - 1)
151            },
152            (_, StatusIconStateAction::Load(stats)) => {
153                if stats.and_then(|x| x.num_table_cells).is_some() {
154                    StatusIconState::Normal
155                } else {
156                    Self::Loading
157                }
158            },
159            (_, StatusIconStateAction::SetError(e)) => {
160                Self::Errored(e.message(), e.stacktrace(), e.kind())
161            },
162            (
163                StatusIconState::Loading,
164                StatusIconStateAction::Increment | StatusIconStateAction::Decrement,
165            ) => StatusIconState::Loading,
166            (_, StatusIconStateAction::Increment) => Self::Updating(1),
167            (_, StatusIconStateAction::Decrement) => StatusIconState::Normal,
168        };
169
170        new_status.into()
171    }
172}
173
174#[extend::ext]
175impl<T: Reducible + 'static> UseReducerDispatcher<T> {
176    fn callback<U>(&self, action: impl Fn(U) -> T::Action + 'static) -> Callback<U> {
177        let dispatcher = self.clone();
178        Callback::from(move |event| dispatcher.dispatch(action(event)))
179    }
180}