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