Skip to main content

below_view/
stats_view.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::cell::Ref;
16use std::cell::RefCell;
17use std::cell::RefMut;
18use std::collections::HashMap;
19use std::rc::Rc;
20
21use common::logutil::get_last_log_to_display;
22use common::logutil::CPMsgRecord;
23use cursive::event::Event;
24use cursive::event::EventResult;
25use cursive::event::EventTrigger;
26use cursive::utils::markup::StyledString;
27use cursive::view::Nameable;
28use cursive::view::Scrollable;
29use cursive::view::View;
30use cursive::view::ViewWrapper;
31use cursive::views::LinearLayout;
32use cursive::views::NamedView;
33use cursive::views::OnEventView;
34use cursive::views::Panel;
35use cursive::views::ResizedView;
36use cursive::views::ScrollView;
37use cursive::views::SelectView;
38use cursive::views::ViewRef;
39use cursive::Cursive;
40
41use crate::command_palette::CommandPalette;
42use crate::controllers::Controllers;
43use crate::tab_view::TabView;
44
45pub struct ColumnTitles {
46    pub titles: Vec<String>,
47    pub pinned_titles: usize, // the first `pinned_titles` titles are fixed
48}
49
50/// A trait that defines common state data querying or event handling.
51///
52/// This trait must be implemented by all view state. It will help to expose
53/// state data to the StatsView for common behavior. On the other hand, it force
54/// a view to have required data in order to fit itself inside the StatsView.
55pub trait StateCommon {
56    type ModelType;
57    type TagType: ToString;
58    type KeyType: Clone;
59
60    /// Expose filter data for StatsView to set fields in filter popup
61    fn get_filter_info(&self) -> &Option<(Self::TagType, String)>;
62
63    /// Gets the FieldId associated with given tab and column index
64    fn get_tag_from_tab_idx(&self, tab: &str, idx: usize) -> Self::TagType;
65
66    /// Returns true iff filtering is supported for column
67    fn is_filter_supported_from_tab_idx(&self, _tab: &str, _idx: usize) -> bool {
68        false
69    }
70    /// Set the filter (current column and filter string)
71    /// Return true on success, false on failure
72    fn set_filter_from_tab_idx(
73        &mut self,
74        _tab: &str,
75        _idx: usize,
76        _filter: Option<String>,
77    ) -> bool {
78        false
79    }
80
81    /// Set the sorting tag to common state
82    /// Return true on success, false if current tab doest support sorting.
83    fn set_sort_tag(&mut self, _tag: Self::TagType, _reverse: &mut bool) -> bool {
84        false
85    }
86    fn set_sort_string(&mut self, _selection: &str, _reverse: &mut bool) -> bool {
87        false
88    }
89    fn set_sort_tag_from_tab_idx(&mut self, _tab: &str, _idx: usize, _reverse: &mut bool) -> bool {
90        false
91    }
92
93    fn get_model(&self) -> Ref<Self::ModelType>;
94    fn get_model_mut(&self) -> RefMut<Self::ModelType>;
95    fn new(model: Rc<RefCell<Self::ModelType>>) -> Self;
96}
97
98/// ViewBridge defines how a ConcreteView will relate to StatsView
99pub trait ViewBridge {
100    type StateType: Default + StateCommon;
101    /// Return the name of the view, this function will help StatsView to
102    /// query view by name.
103    fn get_view_name() -> &'static str;
104
105    /// Return the column titles of the view
106    fn get_titles(&self) -> ColumnTitles;
107
108    /// The essential function that defines how a StatsView should fill
109    /// the data. This function will iterate through the data, apply filter and sorting,
110    /// return a Vec of (Stats String Line, Key) tuple.
111    /// # Arguments
112    /// * `state`: The concrete view state
113    /// * `offset`: Indicates how many columns we should pass after the first column when generating a line.
114    fn get_rows(
115        &mut self,
116        state: &Self::StateType,
117        offset: Option<usize>,
118    ) -> Vec<(
119        StyledString,
120        <<Self as ViewBridge>::StateType as StateCommon>::KeyType,
121    )>;
122
123    /// Optional callback called by on_select of inner SelectView and on
124    /// refresh for updating state based on selected key.
125    fn on_select_update_state(
126        _state: &mut Self::StateType,
127        _selected_key: Option<&<<Self as ViewBridge>::StateType as StateCommon>::KeyType>,
128    ) {
129    }
130
131    /// Optional callback called by on_select of inner SelectView for
132    /// updating command palette. Returns info String set on the palette.
133    fn on_select_update_cmd_palette(
134        _state: &Self::StateType,
135        _selected_key: &<<Self as ViewBridge>::StateType as StateCommon>::KeyType,
136        _current_tab: &str,
137        _selected_column: usize,
138    ) -> String {
139        "".to_owned()
140    }
141}
142
143/// StatsView is a view wrapper that wraps tabs, titles, and list of stats.
144///
145/// Terminology:
146/// `title`: A title here means a column name in the stats table. We call it title to align with
147///          below_derive's function `get_title_line`.
148/// `tab` or `topic`: A tab or topic is the content of "Tabs" defined in the module level description.
149///                   For example, "general", "cpu", "pressure", etc.
150///
151/// The `tab_titles_map` defines a hashmap between a tab name and a vector of its corresponding
152/// string titles. This will help StatsView to switch stats headline(columns name) when a user switching
153/// tabs. This hashmap will be automatically generated by `tab_view_map`.
154///
155/// `tab_view_map` defines a map relationship between a tab name and its concrete `V`
156/// data structure. The `V` here represents "view" which, in implementation, is a enum of
157/// "tab" data structure. And each "tab" defines stats data and will derive the BelowDecor
158/// to generate all display functions. Please check the implementation of V for more details.
159///
160/// `detailed_view` here is our cursive object. I wrapped it with OnEventView in order to
161/// let the concrete "view"s to define their customized handler. You can think the detailed_view
162/// will be something like this
163///
164/// OnEventView
165///    --> Panel
166///        --> LinearLayout::Vertical
167///          --> Child 0: A TabView that represent the topic tab
168///          --> child 1: ScrollView
169///            --> LinearLayout::Vertical
170///            --> child 0: A TabView that represent the title header tab
171///            --> child 1: A SelectView that represents the detail stats
172///          --> child 2: Command palette
173///
174/// `state` defines the state of a view. Filters, sorting orders will be defined here.
175pub struct StatsView<V: 'static + ViewBridge> {
176    tab_titles_map: HashMap<String, ColumnTitles>,
177    tab_view_map: HashMap<String, V>,
178    detailed_view: OnEventView<Panel<LinearLayout>>,
179    pub state: Rc<RefCell<V::StateType>>,
180    pub reverse_sort: bool,
181    pub event_controllers: Rc<RefCell<HashMap<Event, Controllers>>>,
182}
183
184impl<V: 'static + ViewBridge> ViewWrapper for StatsView<V> {
185    cursive::wrap_impl!(self.detailed_view: OnEventView<Panel<LinearLayout>>);
186
187    // We will handle common event in this wrapper. It will comsume the
188    // event if there's a match. Otherwise, it will pass the event to the
189    // concrete event handler.
190    fn wrap_on_event(&mut self, ch: Event) -> EventResult {
191        // Refresh event will be handled at root
192        if ch == Event::Refresh {
193            return EventResult::Ignored;
194        }
195
196        // if stats view is in cmd mode, pass all event to cmd_palette
197        let cmd_mode = self.get_cmd_palette().is_cmd_mode();
198        if cmd_mode {
199            return self.get_cmd_palette().on_event(ch);
200        }
201
202        let controller = self
203            .event_controllers
204            .borrow()
205            .get(&ch)
206            .unwrap_or(&Controllers::Unknown)
207            .clone();
208
209        // Unmapped event goes to the parent view.
210        if controller == Controllers::Unknown {
211            self.with_view_mut(|v| v.on_event(ch))
212                .unwrap_or(EventResult::Ignored)
213        } else {
214            controller.handle(self, &[]);
215            EventResult::with_cb(move |c| controller.callback::<V>(c, &[]))
216        }
217    }
218}
219
220impl<V: 'static + ViewBridge> StatsView<V> {
221    #[allow(unused)]
222    pub fn new(
223        name: &'static str,
224        tabs: Vec<String>,
225        tab_view_map: HashMap<String, V>,
226        select_view: SelectView<<V::StateType as StateCommon>::KeyType>,
227        state: V::StateType,
228        event_controllers: Rc<RefCell<HashMap<Event, Controllers>>>,
229        cmd_controllers: Rc<RefCell<HashMap<&'static str, Controllers>>>,
230    ) -> Self {
231        let mut tab_titles_map = HashMap::new();
232        for (tab, bridge) in &tab_view_map {
233            let mut titles = bridge.get_titles();
234            tab_titles_map.insert(tab.into(), titles);
235        }
236
237        let default_tab = tabs[0].clone();
238        let tab_titles = tab_titles_map
239            .get(&default_tab)
240            .expect("Failed to query default tab");
241
242        let select_view_with_cb =
243            select_view.on_select(|c, selected_key: &<V::StateType as StateCommon>::KeyType| {
244                c.call_on_name(V::get_view_name(), |view: &mut StatsView<V>| {
245                    V::on_select_update_state(&mut view.state.borrow_mut(), Some(selected_key));
246                    let mut cmd_palette = view.get_cmd_palette();
247                    let cur_tab = view.get_tab_view().get_cur_selected().to_string();
248                    let selected_column = view.get_title_view().current_selected;
249                    cmd_palette.set_info(V::on_select_update_cmd_palette(
250                        &view.state.borrow(),
251                        selected_key,
252                        &cur_tab,
253                        selected_column,
254                    ));
255                });
256            });
257
258        let detailed_view = OnEventView::new(Panel::new(
259            LinearLayout::vertical()
260                .child(
261                    TabView::new(tabs, "   ", 0 /* pinned titles */)
262                        .expect("Fail to construct tab")
263                        .with_name(format!("{}_tab", &name)),
264                )
265                .child(
266                    LinearLayout::vertical()
267                        .child(
268                            TabView::new(
269                                tab_titles.titles.clone(),
270                                " ",
271                                tab_titles.pinned_titles, /* pinned titles */
272                            )
273                            .expect("Fail to construct title")
274                            .with_name(format!("{}_title", &name)),
275                        )
276                        .child(ResizedView::with_full_screen(
277                            select_view_with_cb
278                                .with_name(format!("{}_detail", &name))
279                                .scrollable(),
280                        ))
281                        .scrollable()
282                        .scroll_x(true)
283                        .scroll_y(false),
284                )
285                .child(
286                    CommandPalette::new::<V>(name, "<root>", cmd_controllers)
287                        .with_name(format!("{}_cmd_palette", &name)),
288                ),
289        ));
290
291        Self {
292            tab_titles_map,
293            tab_view_map,
294            detailed_view,
295            state: Rc::new(RefCell::new(state)),
296            reverse_sort: true,
297            event_controllers,
298        }
299    }
300
301    // When a user switch tab, we need to reset the title state.
302    pub fn update_title(&mut self) {
303        let cur_tab = self.get_tab_view().get_cur_selected().to_string();
304        let mut title_view = self.get_title_view();
305        let tabs = self
306            .tab_titles_map
307            .get(&cur_tab)
308            .unwrap_or_else(|| panic!("Fail to query title from tab {}", cur_tab));
309        title_view.tabs.clone_from(&tabs.titles);
310        title_view.fixed_tabs = tabs.pinned_titles;
311        title_view.current_selected = 0;
312        title_view.current_offset_idx = 0;
313        title_view.cur_offset = 0;
314        title_view.total_length = title_view.tabs.iter().fold(0, |acc, x| acc + x.len() + 1);
315        title_view.cur_length = title_view.tabs[0].len();
316    }
317
318    // Expose the OnEventView API.
319    pub fn on_event<F, E>(mut self, trigger: E, cb: F) -> Self
320    where
321        E: Into<EventTrigger>,
322        F: 'static + Fn(&mut Cursive),
323    {
324        self.detailed_view.set_on_event(trigger, cb);
325        self
326    }
327
328    // A convenience function to get the topic tab view.
329    pub fn get_tab_view(&mut self) -> ViewRef<TabView> {
330        let tab_panel: &mut NamedView<TabView> = self
331            .detailed_view // OnEventView
332            .get_inner_mut() // PanelView
333            .get_inner_mut() // LinearLayout
334            .get_child_mut(0) // NamedView
335            .expect("Fail to get tab panel, StatsView may not properly init")
336            .downcast_mut()
337            .expect("Fail to downcast to panel, StatsView may not properly init");
338
339        tab_panel.get_mut()
340    }
341
342    // Helping method to downcast the scroll view.
343    fn get_scroll_view(&mut self) -> &mut ScrollView<LinearLayout> {
344        self.detailed_view // OnEventView
345            .get_inner_mut() // PanelView
346            .get_inner_mut() // LinearLayout
347            .get_child_mut(1) // ScrollView
348            .expect("Fail to get stats scrollable, StatsView may not properly init")
349            .downcast_mut()
350            .expect("Fail to downcast to stats scrollable, StatsView may not properly init")
351    }
352
353    // A convenience function to get the title tab view.
354    pub fn get_title_view(&mut self) -> ViewRef<TabView> {
355        let scroll_view = self.get_scroll_view();
356
357        let title_named: &mut NamedView<TabView> = scroll_view
358            .get_inner_mut() // LinearLayout
359            .get_child_mut(0) //NamedView
360            .expect("Fail to get title, StatsView may not properly init")
361            .downcast_mut()
362            .expect("Fail to downcast to title, StatsView may not properly init");
363
364        title_named.get_mut()
365    }
366
367    // A convenience function to get the scroll view of the list
368    pub fn get_list_scroll_view(
369        &mut self,
370    ) -> &mut ScrollView<NamedView<SelectView<<V::StateType as StateCommon>::KeyType>>> {
371        let scroll_view = self.get_scroll_view();
372
373        #[allow(clippy::type_complexity)]
374        let select_named: &mut ResizedView<
375            ScrollView<NamedView<SelectView<<V::StateType as StateCommon>::KeyType>>>,
376        > = scroll_view
377            .get_inner_mut() // LinearLayout
378            .get_child_mut(1) // ResizedView
379            .expect("Fail to get title, StatsView may not properly init")
380            .downcast_mut()
381            .expect("Fail to downcast to title, StatsView may not properly init");
382
383        select_named.get_inner_mut()
384    }
385
386    // A convenience function to get the detail stats SelectView
387    pub fn get_detail_view(
388        &mut self,
389    ) -> ViewRef<SelectView<<V::StateType as StateCommon>::KeyType>> {
390        self.get_list_scroll_view().get_inner_mut().get_mut()
391    }
392
393    // A convenience function to get the command palette
394    pub fn get_cmd_palette(&mut self) -> ViewRef<CommandPalette> {
395        let cmd_palette: &mut NamedView<CommandPalette> = self
396            .detailed_view // OnEventView
397            .get_inner_mut() // PanelView
398            .get_inner_mut() // LinearLayout
399            .get_child_mut(2) // NamedView
400            .expect("Fail to get cmd palette, StatsView may not properly init")
401            .downcast_mut()
402            .expect("Fail to downcast to cmd palette, StatsView may not properly init");
403
404        cmd_palette.get_mut()
405    }
406
407    // convenience function to get screen width
408    pub fn get_screen_width(&mut self) -> usize {
409        self.get_scroll_view().content_viewport().width()
410    }
411
412    pub fn set_horizontal_offset(&mut self, x: usize) {
413        let screen_width = self.get_screen_width();
414        if screen_width < x {
415            self.get_title_view()
416                .scroll_to_offset(x - screen_width, screen_width);
417        } else {
418            self.get_title_view().scroll_to_offset(0, screen_width);
419        }
420    }
421
422    // Function to refresh the view.
423    // A potential optimize here is put the model of the cursive view_state as Rc<RefCell>
424    // member of StatsView. In that case, we don't need to borrow the cursive object here.
425    pub fn refresh(&mut self, c: &mut Cursive) {
426        {
427            let cur_tab = self.get_tab_view().get_cur_selected().to_string();
428            let mut select_view = self.get_detail_view();
429
430            let pos = select_view.selected_id().unwrap_or(0);
431            select_view.clear();
432
433            let horizontal_offset = self.get_title_view().current_offset_idx;
434
435            let tab_detail = self
436                .tab_view_map
437                .get_mut(&cur_tab)
438                .unwrap_or_else(|| panic!("Fail to query data from tab {}", cur_tab));
439            select_view.add_all(tab_detail.get_rows(&self.state.borrow(), Some(horizontal_offset)));
440
441            // This will trigger on_select handler, but handler will not be able to
442            // find the current StatsView from cursive, presumably because we are
443            // holding on a mutable reference to self at this moment.
444            select_view.select_down(pos)(c);
445
446            let mut cmd_palette = self.get_cmd_palette();
447            if let Some(msg) = get_last_log_to_display() {
448                cmd_palette.set_alert(msg);
449            }
450
451            let selection = select_view.selection().map(|rc| rc.as_ref().clone());
452            let selected_column = self.get_title_view().current_selected;
453            V::on_select_update_state(&mut self.state.borrow_mut(), selection.as_ref());
454            // We should not override alert on refresh. Only selection should
455            // override alert.
456            if let (false, Some(selection)) = (cmd_palette.is_alerting(), selection) {
457                let info_msg = V::on_select_update_cmd_palette(
458                    &self.state.borrow(),
459                    &selection,
460                    &cur_tab,
461                    selected_column,
462                );
463                cmd_palette.set_info(info_msg);
464            }
465        }
466        self.get_list_scroll_view().scroll_to_important_area();
467    }
468
469    // Chaining call. Use for construction to get initial data.
470    pub fn feed_data(mut self, c: &mut Cursive) -> Self {
471        self.refresh(c);
472        self
473    }
474
475    /// Convenience function to get StatsView
476    pub fn get_view(c: &mut Cursive) -> ViewRef<Self> {
477        c.find_name::<Self>(V::get_view_name())
478            .expect("Fail to find view with name")
479    }
480
481    // Locates the view with its defined name and refresh it.
482    // This is a convenience function for refresh without need of anobject.
483    pub fn refresh_myself(c: &mut Cursive) {
484        Self::get_view(c).refresh(c)
485    }
486
487    pub fn set_alert(&mut self, msg: &str) {
488        self.get_cmd_palette()
489            .set_alert(CPMsgRecord::construct_msg(slog::Level::Warning, msg));
490    }
491
492    /// Convenience function to raise warning. Only to CommandPalette.
493    pub fn cp_warn(c: &mut Cursive, msg: &str) {
494        Self::get_view(c).set_alert(msg);
495    }
496
497    /// Convenience function to set filter to CommandPalette.
498    /// filter_info provides the column title (for the filtered field) and filter
499    pub fn cp_filter(c: &mut Cursive, filter_info: Option<(String, String)>) {
500        Self::get_view(c).get_cmd_palette().set_filter(filter_info);
501    }
502}