perspective_viewer/components/
status_indicator.rs1use 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#[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 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}