pinakes-ui: add plugin page registry
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ie83791e82c68f757173e5dc53a646b356a6a6964
This commit is contained in:
parent
901adcb2f0
commit
be4305f46e
3 changed files with 287 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ use tracing_subscriber::EnvFilter;
|
|||
mod app;
|
||||
mod client;
|
||||
mod components;
|
||||
mod plugin_ui;
|
||||
mod state;
|
||||
mod styles;
|
||||
|
||||
|
|
|
|||
38
crates/pinakes-ui/src/plugin_ui/mod.rs
Normal file
38
crates/pinakes-ui/src/plugin_ui/mod.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
//! Plugin UI system for Pinakes Desktop/Web UI
|
||||
//!
|
||||
//! This module provides a declarative UI plugin system that allows plugins
|
||||
//! to define custom pages without needing to compile against the UI crate.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! - [`registry`] - Plugin page registry and context provider
|
||||
//! - [`data`] - Data fetching and caching for plugin data sources
|
||||
//! - [`actions`] - Action execution system for plugin interactions
|
||||
//! - [`renderer`] - Schema-to-Dioxus rendering components
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Plugins define their UI as JSON schemas in the plugin manifest:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [[ui.pages]]
|
||||
//! id = "my-plugin-page"
|
||||
//! title = "My Plugin"
|
||||
//! route = "/plugins/my-plugin"
|
||||
//! icon = "cog"
|
||||
//!
|
||||
//! [ui.pages.layout]
|
||||
//! type = "container"
|
||||
//! # ... more layout definition
|
||||
//! ```
|
||||
//!
|
||||
//! The UI schema is fetched from the server and rendered using the
|
||||
//! components in this module.
|
||||
|
||||
pub mod actions;
|
||||
pub mod data;
|
||||
pub mod registry;
|
||||
pub mod renderer;
|
||||
|
||||
pub use registry::{PluginPage, PluginRegistry};
|
||||
pub use renderer::PluginViewRenderer;
|
||||
248
crates/pinakes-ui/src/plugin_ui/registry.rs
Normal file
248
crates/pinakes-ui/src/plugin_ui/registry.rs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
//! Plugin UI Registry
|
||||
//!
|
||||
//! Manages plugin-provided UI pages and provides hooks for accessing
|
||||
//! page definitions at runtime.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! // Initialize registry with API client
|
||||
//! let registry = PluginRegistry::new(api_client);
|
||||
//! registry.refresh().await?;
|
||||
//!
|
||||
//! // Access pages
|
||||
//! if let Some(page) = registry.get_page("my-plugin", "demo") {
|
||||
//! println!("Page: {}", page.page.title);
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use pinakes_plugin_api::UiPage;
|
||||
|
||||
use crate::client::ApiClient;
|
||||
|
||||
/// Information about a plugin-provided UI page
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginPage {
|
||||
/// Plugin ID that provides this page
|
||||
pub plugin_id: String,
|
||||
/// Page definition from schema
|
||||
pub page: UiPage,
|
||||
}
|
||||
|
||||
impl PluginPage {
|
||||
/// Full route including plugin prefix
|
||||
pub fn full_route(&self) -> String {
|
||||
format!("/plugins/{}/{}", self.plugin_id, self.page.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Registry of all plugin-provided UI pages
|
||||
///
|
||||
/// This is typically stored as a context value 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>,
|
||||
/// Last refresh timestamp
|
||||
last_refresh: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
impl PluginRegistry {
|
||||
/// Create a new empty registry
|
||||
pub fn new(client: ApiClient) -> Self {
|
||||
Self {
|
||||
client,
|
||||
pages: HashMap::new(),
|
||||
last_refresh: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new registry with pre-loaded pages
|
||||
pub fn with_pages(
|
||||
client: ApiClient,
|
||||
pages: Vec<(String, String, UiPage)>,
|
||||
) -> Self {
|
||||
let mut registry = Self::new(client);
|
||||
for (plugin_id, _page_id, page) in pages {
|
||||
registry.register_page(plugin_id, page);
|
||||
}
|
||||
registry
|
||||
}
|
||||
|
||||
/// Register a page from a plugin
|
||||
pub fn register_page(&mut self, plugin_id: String, page: UiPage) {
|
||||
let page_id = page.id.clone();
|
||||
self
|
||||
.pages
|
||||
.insert((plugin_id.clone(), page_id), PluginPage { plugin_id, page });
|
||||
}
|
||||
|
||||
/// Get a specific page by plugin ID and page ID
|
||||
pub fn get_page(
|
||||
&self,
|
||||
plugin_id: &str,
|
||||
page_id: &str,
|
||||
) -> Option<&PluginPage> {
|
||||
self
|
||||
.pages
|
||||
.get(&(plugin_id.to_string(), page_id.to_string()))
|
||||
}
|
||||
|
||||
/// Get all pages
|
||||
pub fn all_pages(&self) -> Vec<&PluginPage> {
|
||||
self.pages.values().collect()
|
||||
}
|
||||
|
||||
/// Get all page routes for navigation
|
||||
pub fn routes(&self) -> Vec<(String, String, String)> {
|
||||
self
|
||||
.pages
|
||||
.values()
|
||||
.map(|p| (p.plugin_id.clone(), p.page.id.clone(), p.full_route()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if any pages are registered
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pages.is_empty()
|
||||
}
|
||||
|
||||
/// Number of registered pages
|
||||
pub fn len(&self) -> usize {
|
||||
self.pages.len()
|
||||
}
|
||||
|
||||
/// Refresh pages from server
|
||||
pub async fn refresh(&mut self) -> Result<(), String> {
|
||||
match self.client.get_plugin_ui_pages().await {
|
||||
Ok(pages) => {
|
||||
self.pages.clear();
|
||||
for (plugin_id, page) in pages {
|
||||
self.register_page(plugin_id, page);
|
||||
}
|
||||
self.last_refresh = Some(chrono::Utc::now());
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => Err(format!("Failed to refresh plugin pages: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get last refresh time
|
||||
pub const fn last_refresh(&self) -> Option<chrono::DateTime<chrono::Utc>> {
|
||||
self.last_refresh
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PluginRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new(ApiClient::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pinakes_plugin_api::UiElement;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn create_test_page(id: &str, title: &str) -> UiPage {
|
||||
UiPage {
|
||||
id: id.to_string(),
|
||||
title: title.to_string(),
|
||||
route: format!("/plugins/test/{id}"),
|
||||
icon: None,
|
||||
root_element: UiElement::Container {
|
||||
children: vec![],
|
||||
gap: 16,
|
||||
padding: None,
|
||||
},
|
||||
data_sources: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registry_empty() {
|
||||
let client = ApiClient::default();
|
||||
let registry = PluginRegistry::new(client);
|
||||
assert!(registry.is_empty());
|
||||
assert_eq!(registry.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_and_get_page() {
|
||||
let client = ApiClient::default();
|
||||
let mut registry = PluginRegistry::new(client);
|
||||
let page = create_test_page("demo", "Demo Page");
|
||||
|
||||
registry.register_page("my-plugin".to_string(), page.clone());
|
||||
|
||||
assert!(!registry.is_empty());
|
||||
assert_eq!(registry.len(), 1);
|
||||
|
||||
let retrieved = registry.get_page("my-plugin", "demo");
|
||||
assert!(retrieved.is_some());
|
||||
assert_eq!(retrieved.unwrap().page.id, "demo");
|
||||
assert_eq!(retrieved.unwrap().page.title, "Demo Page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_page_not_found() {
|
||||
let client = ApiClient::default();
|
||||
let registry = PluginRegistry::new(client);
|
||||
|
||||
let result = registry.get_page("nonexistent", "page");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_page_full_route() {
|
||||
let client = ApiClient::default();
|
||||
let mut registry = PluginRegistry::new(client);
|
||||
let page = create_test_page("demo", "Demo Page");
|
||||
|
||||
registry.register_page("my-plugin".to_string(), page.clone());
|
||||
|
||||
let plugin_page = registry.get_page("my-plugin", "demo").unwrap();
|
||||
assert_eq!(plugin_page.full_route(), "/plugins/my-plugin/demo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_pages() {
|
||||
let client = ApiClient::default();
|
||||
let mut registry = PluginRegistry::new(client);
|
||||
|
||||
registry.register_page(
|
||||
"plugin1".to_string(),
|
||||
create_test_page("page1", "Page 1"),
|
||||
);
|
||||
registry.register_page(
|
||||
"plugin2".to_string(),
|
||||
create_test_page("page2", "Page 2"),
|
||||
);
|
||||
|
||||
let all = registry.all_pages();
|
||||
assert_eq!(all.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routes() {
|
||||
let client = ApiClient::default();
|
||||
let mut registry = PluginRegistry::new(client);
|
||||
|
||||
registry.register_page(
|
||||
"plugin1".to_string(),
|
||||
create_test_page("page1", "Page 1"),
|
||||
);
|
||||
|
||||
let routes = registry.routes();
|
||||
assert_eq!(routes.len(), 1);
|
||||
assert_eq!(routes[0].0, "plugin1");
|
||||
assert_eq!(routes[0].1, "page1");
|
||||
assert_eq!(routes[0].2, "/plugins/plugin1/page1");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue