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