pinakes-ui: add plugin data fetching
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ie28a544cf0df1a23b905e89b69db19c06a6a6964
This commit is contained in:
parent
5b204dceb5
commit
e46a8943cb
1 changed files with 273 additions and 0 deletions
273
crates/pinakes-ui/src/plugin_ui/data.rs
Normal file
273
crates/pinakes-ui/src/plugin_ui/data.rs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
//! Data fetching system for plugin UI pages
|
||||
//!
|
||||
//! Provides data fetching and caching for plugin data sources.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use pinakes_plugin_api::{DataSource, HttpMethod};
|
||||
|
||||
use crate::client::ApiClient;
|
||||
|
||||
/// Cached data for a plugin page
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PluginPageData {
|
||||
data: HashMap<String, serde_json::Value>,
|
||||
loading: HashMap<String, bool>,
|
||||
errors: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl PluginPageData {
|
||||
/// Get data for a specific source
|
||||
#[must_use]
|
||||
pub fn get(&self, source: &str) -> Option<&serde_json::Value> {
|
||||
self.data.get(source)
|
||||
}
|
||||
|
||||
/// Check if a source is currently loading
|
||||
#[must_use]
|
||||
pub fn is_loading(&self, source: &str) -> bool {
|
||||
self.loading.get(source).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get error for a specific source
|
||||
#[must_use]
|
||||
pub fn error(&self, source: &str) -> Option<&String> {
|
||||
self.errors.get(source)
|
||||
}
|
||||
|
||||
/// Check if there's data for a specific source
|
||||
#[must_use]
|
||||
pub fn has_data(&self, source: &str) -> bool {
|
||||
self.data.contains_key(source)
|
||||
}
|
||||
|
||||
/// Set data for a source
|
||||
pub fn set_data(&mut self, source: String, value: serde_json::Value) {
|
||||
self.data.insert(source, value);
|
||||
}
|
||||
|
||||
/// Set loading state for a source
|
||||
pub fn set_loading(&mut self, source: &str, loading: bool) {
|
||||
if loading {
|
||||
self.loading.insert(source.to_string(), true);
|
||||
self.errors.remove(source);
|
||||
} else {
|
||||
self.loading.remove(source);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set error for a source
|
||||
pub fn set_error(&mut self, source: String, error: String) {
|
||||
self.errors.insert(source, error);
|
||||
}
|
||||
|
||||
/// Clear all data
|
||||
pub fn clear(&mut self) {
|
||||
self.data.clear();
|
||||
self.loading.clear();
|
||||
self.errors.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch data from an endpoint
|
||||
async fn fetch_endpoint(
|
||||
client: &ApiClient,
|
||||
path: &str,
|
||||
method: HttpMethod,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let reqwest_method = match method {
|
||||
HttpMethod::Get => reqwest::Method::GET,
|
||||
HttpMethod::Post => reqwest::Method::POST,
|
||||
HttpMethod::Put => reqwest::Method::PUT,
|
||||
HttpMethod::Patch => reqwest::Method::PATCH,
|
||||
HttpMethod::Delete => reqwest::Method::DELETE,
|
||||
};
|
||||
|
||||
// Send request and parse response
|
||||
let response = client
|
||||
.raw_request(reqwest_method, path)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {e}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(format!("HTTP {status}: {body}"));
|
||||
}
|
||||
|
||||
response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse JSON: {e}"))
|
||||
}
|
||||
|
||||
/// Fetch all data sources for a page
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any data source fails to fetch
|
||||
pub async fn fetch_page_data(
|
||||
client: &ApiClient,
|
||||
data_sources: &HashMap<String, DataSource>,
|
||||
) -> Result<HashMap<String, serde_json::Value>, String> {
|
||||
let mut results = HashMap::new();
|
||||
|
||||
for (name, source) in data_sources {
|
||||
let value = match source {
|
||||
DataSource::Endpoint { path, method, .. } => {
|
||||
// Fetch from endpoint (ignoring params, poll_interval, transform for
|
||||
// now)
|
||||
fetch_endpoint(client, path, method.clone()).await?
|
||||
},
|
||||
DataSource::Static { value } => value.clone(),
|
||||
DataSource::Transform {
|
||||
source_name,
|
||||
expression,
|
||||
} => {
|
||||
// Get source data and apply transform
|
||||
let source_data = results
|
||||
.get(source_name)
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
// TODO: Actually evaluate expression against source_data
|
||||
// For now, return source_data unchanged
|
||||
let _ = expression;
|
||||
source_data
|
||||
},
|
||||
};
|
||||
results.insert(name.clone(), value);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Hook to fetch and cache plugin page data
|
||||
///
|
||||
/// Returns a signal containing the data state
|
||||
pub fn use_plugin_data(
|
||||
client: Signal<ApiClient>,
|
||||
data_sources: HashMap<String, DataSource>,
|
||||
) -> Signal<PluginPageData> {
|
||||
let mut data = use_signal(PluginPageData::default);
|
||||
|
||||
use_effect(move || {
|
||||
let sources = data_sources.clone();
|
||||
|
||||
spawn(async move {
|
||||
// Clear previous data
|
||||
data.write().clear();
|
||||
|
||||
// Mark all sources as loading
|
||||
for name in sources.keys() {
|
||||
data.write().set_loading(name, true);
|
||||
}
|
||||
|
||||
// Fetch data
|
||||
match fetch_page_data(&client.read(), &sources).await {
|
||||
Ok(results) => {
|
||||
for (name, value) in results {
|
||||
data.write().set_data(name, value);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
for name in sources.keys() {
|
||||
data.write().set_error(name.clone(), e.clone());
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
/// Get a value from JSON by path (dot notation)
|
||||
///
|
||||
/// Supports object keys and array indices
|
||||
#[must_use]
|
||||
pub fn get_json_path<'a>(
|
||||
value: &'a serde_json::Value,
|
||||
path: &str,
|
||||
) -> Option<&'a serde_json::Value> {
|
||||
let mut current = value;
|
||||
|
||||
for key in path.split('.') {
|
||||
match current {
|
||||
serde_json::Value::Object(map) => {
|
||||
current = map.get(key)?;
|
||||
},
|
||||
serde_json::Value::Array(arr) => {
|
||||
let idx = key.parse::<usize>().ok()?;
|
||||
current = arr.get(idx)?;
|
||||
},
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
Some(current)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_plugin_page_data() {
|
||||
let mut data = PluginPageData::default();
|
||||
|
||||
// Test empty state
|
||||
assert!(!data.has_data("test"));
|
||||
assert!(!data.is_loading("test"));
|
||||
assert!(data.error("test").is_none());
|
||||
|
||||
// Test setting data
|
||||
data.set_data("test".to_string(), serde_json::json!({"key": "value"}));
|
||||
assert!(data.has_data("test"));
|
||||
assert_eq!(data.get("test"), Some(&serde_json::json!({"key": "value"})));
|
||||
|
||||
// Test loading state
|
||||
data.set_loading("loading", true);
|
||||
assert!(data.is_loading("loading"));
|
||||
data.set_loading("loading", false);
|
||||
assert!(!data.is_loading("loading"));
|
||||
|
||||
// Test error state
|
||||
data.set_error("error".to_string(), "oops".to_string());
|
||||
assert_eq!(data.error("error"), Some(&"oops".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_json_path_object() {
|
||||
let data = serde_json::json!({
|
||||
"user": {
|
||||
"name": "John",
|
||||
"age": 30
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
get_json_path(&data, "user.name"),
|
||||
Some(&serde_json::Value::String("John".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_json_path_array() {
|
||||
let data = serde_json::json!({
|
||||
"items": ["a", "b", "c"]
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
get_json_path(&data, "items.1"),
|
||||
Some(&serde_json::Value::String("b".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_json_path_invalid() {
|
||||
let data = serde_json::json!({"foo": "bar"});
|
||||
assert!(get_json_path(&data, "nonexistent").is_none());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue