Skip to main content

perspective_viewer/components/
plugin_selector.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::ApiFuture;
15use yew::prelude::*;
16
17use super::containers::select::*;
18use super::style::LocalStyle;
19use crate::config::*;
20use crate::js::*;
21use crate::model::*;
22use crate::presentation::Presentation;
23use crate::renderer::*;
24use crate::session::*;
25use crate::utils::*;
26use crate::{css, *};
27
28#[derive(Properties, PartialEq, PerspectiveProperties!)]
29pub struct PluginSelectorProps {
30    pub presentation: Presentation,
31    pub renderer: Renderer,
32    pub session: Session,
33}
34
35#[derive(Debug)]
36pub enum PluginSelectorMsg {
37    ComponentSelectPlugin(String),
38    RendererSelectPlugin(String),
39    OpenMenu,
40}
41
42use PluginSelectorMsg::*;
43
44pub struct PluginSelector {
45    options: Vec<SelectItem<String>>,
46    is_open: bool,
47    _plugin_sub: Subscription,
48}
49
50impl Component for PluginSelector {
51    type Message = PluginSelectorMsg;
52    type Properties = PluginSelectorProps;
53
54    fn create(ctx: &Context<Self>) -> Self {
55        let PluginSelectorProps { renderer, .. } = ctx.props();
56        let options = generate_plugin_optgroups(renderer);
57        let _plugin_sub = renderer.plugin_changed.add_listener({
58            let link = ctx.link().clone();
59            move |plugin: JsPerspectiveViewerPlugin| {
60                let name = plugin.name();
61                link.send_message(PluginSelectorMsg::RendererSelectPlugin(name))
62            }
63        });
64
65        Self {
66            options,
67            is_open: false,
68            _plugin_sub,
69        }
70    }
71
72    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
73        let PluginSelectorProps {
74            presentation,
75            renderer,
76            session,
77            ..
78        } = ctx.props();
79        match msg {
80            RendererSelectPlugin(_plugin_name) => true,
81            ComponentSelectPlugin(plugin_name) => {
82                if !session.is_errored() {
83                    let metadata =
84                        renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name));
85
86                    let prev_metadata = renderer.metadata();
87                    let requirements = metadata.as_ref().unwrap_or(&*prev_metadata);
88                    let rollup_features = session
89                        .metadata()
90                        .get_features()
91                        .map(|x| x.get_group_rollup_modes())
92                        .unwrap();
93
94                    let group_rollups = requirements.get_group_rollups(&rollup_features);
95                    let mut update = ViewConfigUpdate {
96                        group_rollup_mode: group_rollups.first().cloned(),
97                        ..ViewConfigUpdate::default()
98                    };
99
100                    session.set_update_column_defaults(&mut update, requirements);
101
102                    if let Ok(task) = ctx.props().update_and_render(update) {
103                        ApiFuture::spawn(task);
104                    }
105
106                    presentation.set_open_column_settings(None);
107                    self.is_open = false;
108                    false
109                } else {
110                    self.is_open = false;
111                    true
112                }
113            },
114            OpenMenu => {
115                self.is_open = !self.is_open;
116                true
117            },
118        }
119    }
120
121    fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
122        true
123    }
124
125    fn view(&self, ctx: &Context<Self>) -> Html {
126        let callback = ctx.link().callback(|_| OpenMenu);
127        let plugin_name = ctx.props().renderer.get_active_plugin().unwrap().name();
128        let plugin_name2 = plugin_name.clone();
129        let class = if self.is_open { "open" } else { "" };
130        let items = self.options.iter().map(|item| match item {
131            SelectItem::OptGroup(_cat, items) => html! {
132                items.iter().filter(|x| *x != &plugin_name2).map(|x| {
133                    let callback = ctx.link().callback(ComponentSelectPlugin);
134                    html! {
135                        <PluginSelect
136                            name={ x.to_owned() }
137                            on_click={ callback } />
138                    }
139                }).collect::<Html>()
140            },
141            SelectItem::Option(_item) => html! {},
142        });
143
144        html! {
145            <>
146                <LocalStyle href={css!("plugin-selector")} />
147                <div id="plugin_selector_container" {class}>
148                    <PluginSelect name={plugin_name} on_click={callback} />
149                    <div id="plugin_selector_border" />
150                    if self.is_open {
151                        <div class="plugin-selector-options">{ items.collect::<Html>() }</div>
152                    }
153                </div>
154            </>
155        }
156    }
157}
158
159/// Generate the opt groups for the plugin selector by collecting by category
160/// then sorting.
161fn generate_plugin_optgroups(renderer: &Renderer) -> Vec<SelectItem<String>> {
162    let mut options = renderer
163        .get_all_plugin_categories()
164        .into_iter()
165        .map(|(category, value)| SelectItem::OptGroup(category.into(), value))
166        .collect::<Vec<_>>();
167
168    options.sort_by_key(|x| x.name());
169    options
170}
171
172#[derive(Properties, PartialEq)]
173struct PluginSelectProps {
174    name: String,
175    on_click: Callback<String>,
176}
177
178#[function_component]
179fn PluginSelect(props: &PluginSelectProps) -> Html {
180    let name = props.name.clone();
181    let path: String = props
182        .name
183        .chars()
184        .map(|x| {
185            if x.is_alphanumeric() {
186                x.to_ascii_lowercase()
187            } else {
188                '-'
189            }
190        })
191        .collect();
192
193    html! {
194        <div
195            class="plugin-select-item"
196            data-plugin={name.clone()}
197            style={format!("--default-column-title:var(--plugin-name-{}--content, \"{}\")", path, props.name)}
198            onclick={props.on_click.reform(move |_| name.clone())}
199        >
200            <span class="plugin-select-item-name" />
201        </div>
202    }
203}