pinakes-ui: integrate plugin registry into app navigation and routing

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I7c4593d93693bf08555a0b5f89a67aea6a6a6964
This commit is contained in:
raf 2026-03-10 00:02:31 +03:00
commit 6e442065b1
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 196 additions and 40 deletions

View file

@ -19,7 +19,7 @@
use std::collections::HashMap;
use dioxus::prelude::*;
use pinakes_plugin_api::UiPage;
use pinakes_plugin_api::{UiPage, UiWidget};
use crate::client::ApiClient;
@ -39,15 +39,17 @@ impl PluginPage {
}
}
/// Registry of all plugin-provided UI pages
/// Registry of all plugin-provided UI pages and widgets
///
/// This is typically stored as a context value in the Dioxus tree.
/// This is typically stored as a signal in the Dioxus tree.
#[derive(Debug, Clone)]
pub struct PluginRegistry {
/// API client for fetching pages from server
client: ApiClient,
/// Cached pages: (plugin_id, page_id) -> PluginPage
pages: HashMap<(String, String), PluginPage>,
/// Cached widgets: (plugin_id, widget_id) -> UiWidget
widgets: Vec<(String, UiWidget)>,
/// Last refresh timestamp
last_refresh: Option<chrono::DateTime<chrono::Utc>>,
}
@ -58,17 +60,15 @@ impl PluginRegistry {
Self {
client,
pages: HashMap::new(),
widgets: Vec::new(),
last_refresh: None,
}
}
/// Create a new registry with pre-loaded pages
pub fn with_pages(
client: ApiClient,
pages: Vec<(String, String, UiPage)>,
) -> Self {
pub fn with_pages(client: ApiClient, pages: Vec<(String, UiPage)>) -> Self {
let mut registry = Self::new(client);
for (plugin_id, _page_id, page) in pages {
for (plugin_id, page) in pages {
registry.register_page(plugin_id, page);
}
registry
@ -93,6 +93,16 @@ impl PluginRegistry {
.get(&(plugin_id.to_string(), page_id.to_string()))
}
/// Register a widget from a plugin
pub fn register_widget(&mut self, plugin_id: String, widget: UiWidget) {
self.widgets.push((plugin_id, widget));
}
/// Get all widgets (for use with WidgetContainer)
pub fn all_widgets(&self) -> Vec<(String, UiWidget)> {
self.widgets.clone()
}
/// Get all pages
pub fn all_pages(&self) -> Vec<&PluginPage> {
self.pages.values().collect()
@ -122,6 +132,7 @@ impl PluginRegistry {
match self.client.get_plugin_ui_pages().await {
Ok(pages) => {
self.pages.clear();
self.widgets.clear();
for (plugin_id, page) in pages {
self.register_page(plugin_id, page);
}
@ -245,4 +256,103 @@ mod tests {
assert_eq!(routes[0].1, "page1");
assert_eq!(routes[0].2, "/plugins/plugin1/page1");
}
#[test]
fn test_register_widget_and_all_widgets() {
let client = ApiClient::default();
let mut registry = PluginRegistry::new(client);
let widget: UiWidget = serde_json::from_value(serde_json::json!({
"id": "my-widget",
"target": "library_header",
"content": { "type": "badge", "text": "hello", "variant": "default" }
}))
.unwrap();
assert!(registry.all_widgets().is_empty());
registry.register_widget("test-plugin".to_string(), widget.clone());
let widgets = registry.all_widgets();
assert_eq!(widgets.len(), 1);
assert_eq!(widgets[0].0, "test-plugin");
assert_eq!(widgets[0].1.id, "my-widget");
}
#[test]
fn test_with_pages_builds_registry() {
let client = ApiClient::default();
let pages = vec![
("plugin1".to_string(), create_test_page("page1", "Page 1")),
("plugin2".to_string(), create_test_page("page2", "Page 2")),
];
let registry = PluginRegistry::with_pages(client, pages);
assert_eq!(registry.len(), 2);
assert!(registry.get_page("plugin1", "page1").is_some());
assert!(registry.get_page("plugin2", "page2").is_some());
}
#[test]
fn test_register_page_overwrites_same_key() {
let client = ApiClient::default();
let mut registry = PluginRegistry::new(client);
registry
.register_page("plugin1".to_string(), create_test_page("p", "Original"));
registry
.register_page("plugin1".to_string(), create_test_page("p", "Updated"));
assert_eq!(registry.len(), 1);
assert_eq!(
registry.get_page("plugin1", "p").unwrap().page.title,
"Updated"
);
}
#[test]
fn test_default_registry_is_empty() {
let registry = PluginRegistry::default();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert!(registry.last_refresh().is_none());
}
#[test]
fn test_all_pages_returns_references() {
let client = ApiClient::default();
let mut registry = PluginRegistry::new(client);
registry.register_page("p1".to_string(), create_test_page("a", "A"));
registry.register_page("p2".to_string(), create_test_page("b", "B"));
let pages = registry.all_pages();
assert_eq!(pages.len(), 2);
let titles: Vec<&str> =
pages.iter().map(|p| p.page.title.as_str()).collect();
assert!(titles.contains(&"A"));
assert!(titles.contains(&"B"));
}
#[test]
fn test_different_plugins_same_page_id_both_stored() {
let client = ApiClient::default();
let mut registry = PluginRegistry::new(client);
registry.register_page(
"plugin-a".to_string(),
create_test_page("home", "A Home"),
);
registry.register_page(
"plugin-b".to_string(),
create_test_page("home", "B Home"),
);
assert_eq!(registry.len(), 2);
assert_eq!(
registry.get_page("plugin-a", "home").unwrap().page.title,
"A Home"
);
assert_eq!(
registry.get_page("plugin-b", "home").unwrap().page.title,
"B Home"
);
}
}