perspective_viewer/components/
plugin_selector.rs1use 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
159fn 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}