diff --git a/packages/plugin-runtime-types/src/bindings/gen_api.ts b/packages/plugin-runtime-types/src/bindings/gen_api.ts new file mode 100644 index 00000000..d8c88183 --- /dev/null +++ b/packages/plugin-runtime-types/src/bindings/gen_api.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginVersion } from "./gen_search.js"; + +export type PluginNameVersion = { name: string, version: string, }; + +export type PluginSearchResponse = { plugins: Array, }; + +export type PluginUpdatesResponse = { plugins: Array, }; diff --git a/packages/plugin-runtime-types/src/bindings/gen_search.ts b/packages/plugin-runtime-types/src/bindings/gen_search.ts index f0c56473..9dbe0103 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_search.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_search.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PluginSearchResponse = { results: Array, }; +export type PluginMetadata = { version: string, name: string, displayName: string, description: string | null, homepageUrl: string | null, repositoryUrl: string | null, }; -export type PluginVersion = { id: string, version: string, description: string | null, displayName: string, homepageUrl: string | null, repositoryUrl: string, checksum: string, readme: string | null, yanked: boolean, }; +export type PluginVersion = { id: string, version: string, description: string | null, name: string, displayName: string, homepageUrl: string | null, repositoryUrl: string | null, checksum: string, readme: string | null, yanked: boolean, }; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8adeee2c..8ba4d5ea 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -36,12 +36,13 @@ use yaak_models::models::{ use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; use yaak_plugins::events::{ - BootResponse, CallHttpRequestActionRequest, FilterResponse, - GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, - GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, - InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, + CallHttpRequestActionRequest, FilterResponse, GetHttpAuthenticationConfigResponse, + GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, + GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive, + PluginWindowContext, RenderPurpose, }; use yaak_plugins::manager::PluginManager; +use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_sse::sse::ServerSentEvent; use yaak_templates::format::format_json; @@ -1039,14 +1040,13 @@ async fn cmd_plugin_info( id: &str, app_handle: AppHandle, plugin_manager: State<'_, PluginManager>, -) -> YaakResult { +) -> YaakResult { let plugin = app_handle.db().get_plugin(id)?; Ok(plugin_manager .get_plugin_by_dir(plugin.directory.as_str()) .await .ok_or(GenericError("Failed to find plugin for info".to_string()))? - .info() - .await) + .info()) } #[tauri::command] diff --git a/src-tauri/src/plugin_events.rs b/src-tauri/src/plugin_events.rs index 9ebac1db..bcb136c4 100644 --- a/src-tauri/src/plugin_events.rs +++ b/src-tauri/src/plugin_events.rs @@ -86,8 +86,8 @@ pub(crate) async fn handle_plugin_event( environment.as_ref(), &cb, ) - .await - .expect("Failed to render http request"); + .await + .expect("Failed to render http request"); Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse { http_request, })) @@ -115,7 +115,7 @@ pub(crate) async fn handle_plugin_event( &InternalEventPayload::ShowToastRequest(ShowToastRequest { message: format!( "Plugin error from {}: {}", - plugin_handle.name().await, + plugin_handle.info().name, resp.error ), color: Some(Color::Danger), @@ -188,7 +188,7 @@ pub(crate) async fn handle_plugin_event( cookie_jar, &mut tokio::sync::watch::channel(false).1, // No-op cancel channel ) - .await; + .await; let http_response = match result { Ok(r) => r, @@ -257,17 +257,17 @@ pub(crate) async fn handle_plugin_event( None } InternalEventPayload::SetKeyValueRequest(req) => { - let name = plugin_handle.name().await; + let name = plugin_handle.info().name; app_handle.db().set_plugin_key_value(&name, &req.key, &req.value); Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {})) } InternalEventPayload::GetKeyValueRequest(req) => { - let name = plugin_handle.name().await; + let name = plugin_handle.info().name; let value = app_handle.db().get_plugin_key_value(&name, &req.key).map(|v| v.value); Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value })) } InternalEventPayload::DeleteKeyValueRequest(req) => { - let name = plugin_handle.name().await; + let name = plugin_handle.info().name; let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key).unwrap(); Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted })) } diff --git a/src-tauri/src/uri_scheme.rs b/src-tauri/src/uri_scheme.rs index eff52696..e3a201cb 100644 --- a/src-tauri/src/uri_scheme.rs +++ b/src-tauri/src/uri_scheme.rs @@ -4,7 +4,6 @@ use log::{info, warn}; use std::collections::HashMap; use tauri::{AppHandle, Emitter, Manager, Runtime, Url}; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; -use yaak_plugins::api::get_plugin; use yaak_plugins::events::{Color, ShowToastRequest}; use yaak_plugins::install::download_and_install; @@ -23,14 +22,10 @@ pub(crate) async fn handle_deep_link( "install-plugin" => { let name = query_map.get("name").unwrap(); let version = query_map.get("version").cloned(); - let plugin_version = get_plugin(&app_handle, &name, version).await?; _ = window.set_focus(); let confirmed_install = app_handle .dialog() - .message(format!( - "Install plugin {}@{}?", - plugin_version.name, plugin_version.version - )) + .message(format!("Install plugin {name} {version:?}?",)) .kind(MessageDialogKind::Info) .buttons(MessageDialogButtons::OkCustom("Install".to_string())) .blocking_show(); @@ -39,14 +34,11 @@ pub(crate) async fn handle_deep_link( return Ok(()); } - download_and_install(window, &plugin_version).await?; + let pv = download_and_install(window, name, version).await?; app_handle.emit( "show_toast", ShowToastRequest { - message: format!( - "Installed {}@{}", - plugin_version.name, plugin_version.version - ), + message: format!("Installed {name}@{}", pv.version), color: Some(Color::Success), icon: None, }, diff --git a/src-tauri/yaak-models/src/db_context.rs b/src-tauri/yaak-models/src/db_context.rs index f16000d5..04bb2552 100644 --- a/src-tauri/yaak-models/src/db_context.rs +++ b/src-tauri/yaak-models/src/db_context.rs @@ -1,5 +1,5 @@ use crate::connection_or_tx::ConnectionOrTx; -use crate::error::Error::RowNotFound; +use crate::error::Error::DBRowNotFound; use crate::models::{AnyModel, UpsertModelInfo}; use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource}; use rusqlite::OptionalExtension; @@ -26,7 +26,7 @@ impl<'a> DbContext<'a> { { match self.find_optional::(col, value) { Some(v) => Ok(v), - None => Err(RowNotFound), + None => Err(DBRowNotFound(format!("{:?}", M::table_name()))), } } diff --git a/src-tauri/yaak-models/src/error.rs b/src-tauri/yaak-models/src/error.rs index b7cc7050..d64ff9d2 100644 --- a/src-tauri/yaak-models/src/error.rs +++ b/src-tauri/yaak-models/src/error.rs @@ -30,8 +30,8 @@ pub enum Error { #[error("Multiple base environments for {0}. Delete duplicates before continuing.")] MultipleBaseEnvironments(String), - #[error("Row not found")] - RowNotFound, + #[error("Database row not found: {0}")] + DBRowNotFound(String), #[error("unknown error")] Unknown, diff --git a/src-tauri/yaak-models/src/models.rs b/src-tauri/yaak-models/src/models.rs index 4581646e..623fe815 100644 --- a/src-tauri/yaak-models/src/models.rs +++ b/src-tauri/yaak-models/src/models.rs @@ -11,7 +11,7 @@ use sea_query::{IntoColumnRef, IntoIden, IntoTableRef, Order, SimpleExpr, enum_d use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::collections::BTreeMap; -use std::fmt::Display; +use std::fmt::{Debug, Display}; use std::str::FromStr; use ts_rs::TS; @@ -123,7 +123,7 @@ pub struct Settings { } impl UpsertModelInfo for Settings { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { SettingsIden::Table } @@ -252,7 +252,7 @@ pub struct Workspace { } impl UpsertModelInfo for Workspace { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { WorkspaceIden::Table } @@ -355,7 +355,7 @@ pub struct WorkspaceMeta { } impl UpsertModelInfo for WorkspaceMeta { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { WorkspaceMetaIden::Table } @@ -456,7 +456,7 @@ pub struct CookieJar { } impl UpsertModelInfo for CookieJar { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { CookieJarIden::Table } @@ -535,7 +535,7 @@ pub struct Environment { } impl UpsertModelInfo for Environment { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { EnvironmentIden::Table } @@ -655,7 +655,7 @@ pub struct Folder { } impl UpsertModelInfo for Folder { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { FolderIden::Table } @@ -786,7 +786,7 @@ pub struct HttpRequest { } impl UpsertModelInfo for HttpRequest { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { HttpRequestIden::Table } @@ -913,7 +913,7 @@ pub struct WebsocketConnection { } impl UpsertModelInfo for WebsocketConnection { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { WebsocketConnectionIden::Table } @@ -1027,7 +1027,7 @@ pub struct WebsocketRequest { } impl UpsertModelInfo for WebsocketRequest { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { WebsocketRequestIden::Table } @@ -1152,7 +1152,7 @@ pub struct WebsocketEvent { } impl UpsertModelInfo for WebsocketEvent { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { WebsocketEventIden::Table } @@ -1269,7 +1269,7 @@ pub struct HttpResponse { } impl UpsertModelInfo for HttpResponse { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { HttpResponseIden::Table } @@ -1377,7 +1377,7 @@ pub struct GraphQlIntrospection { } impl UpsertModelInfo for GraphQlIntrospection { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { GraphQlIntrospectionIden::Table } @@ -1461,7 +1461,7 @@ pub struct GrpcRequest { } impl UpsertModelInfo for GrpcRequest { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { GrpcRequestIden::Table } @@ -1588,7 +1588,7 @@ pub struct GrpcConnection { } impl UpsertModelInfo for GrpcConnection { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { GrpcConnectionIden::Table } @@ -1708,7 +1708,7 @@ pub struct GrpcEvent { } impl UpsertModelInfo for GrpcEvent { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { GrpcEventIden::Table } @@ -1799,7 +1799,7 @@ pub struct Plugin { } impl UpsertModelInfo for Plugin { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { PluginIden::Table } @@ -1881,7 +1881,7 @@ pub struct SyncState { } impl UpsertModelInfo for SyncState { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { SyncStateIden::Table } @@ -1964,7 +1964,7 @@ pub struct KeyValue { } impl UpsertModelInfo for KeyValue { - fn table_name() -> impl IntoTableRef { + fn table_name() -> impl IntoTableRef + Debug { KeyValueIden::Table } @@ -2181,7 +2181,7 @@ impl AnyModel { } pub trait UpsertModelInfo { - fn table_name() -> impl IntoTableRef; + fn table_name() -> impl IntoTableRef + Debug; fn id_column() -> impl IntoIden + Eq + Clone; fn generate_id() -> String; fn order_by() -> (impl IntoColumnRef, Order); diff --git a/src-tauri/yaak-plugins/bindings/gen_api.ts b/src-tauri/yaak-plugins/bindings/gen_api.ts new file mode 100644 index 00000000..d8c88183 --- /dev/null +++ b/src-tauri/yaak-plugins/bindings/gen_api.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginVersion } from "./gen_search.js"; + +export type PluginNameVersion = { name: string, version: string, }; + +export type PluginSearchResponse = { plugins: Array, }; + +export type PluginUpdatesResponse = { plugins: Array, }; diff --git a/src-tauri/yaak-plugins/bindings/gen_search.ts b/src-tauri/yaak-plugins/bindings/gen_search.ts index f0c56473..9dbe0103 100644 --- a/src-tauri/yaak-plugins/bindings/gen_search.ts +++ b/src-tauri/yaak-plugins/bindings/gen_search.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PluginSearchResponse = { results: Array, }; +export type PluginMetadata = { version: string, name: string, displayName: string, description: string | null, homepageUrl: string | null, repositoryUrl: string | null, }; -export type PluginVersion = { id: string, version: string, description: string | null, displayName: string, homepageUrl: string | null, repositoryUrl: string, checksum: string, readme: string | null, yanked: boolean, }; +export type PluginVersion = { id: string, version: string, description: string | null, name: string, displayName: string, homepageUrl: string | null, repositoryUrl: string | null, checksum: string, readme: string | null, yanked: boolean, }; diff --git a/src-tauri/yaak-plugins/build.rs b/src-tauri/yaak-plugins/build.rs index 88527efa..7a8b73a3 100644 --- a/src-tauri/yaak-plugins/build.rs +++ b/src-tauri/yaak-plugins/build.rs @@ -1,4 +1,4 @@ -const COMMANDS: &[&str] = &["search", "install"]; +const COMMANDS: &[&str] = &["search", "install", "updates"]; fn main() { tauri_plugin::Builder::new(COMMANDS).build(); diff --git a/src-tauri/yaak-plugins/index.ts b/src-tauri/yaak-plugins/index.ts index 10ec0c89..2b147163 100644 --- a/src-tauri/yaak-plugins/index.ts +++ b/src-tauri/yaak-plugins/index.ts @@ -1,5 +1,5 @@ import { invoke } from '@tauri-apps/api/core'; -import { PluginSearchResponse, PluginVersion } from './bindings/gen_search'; +import { PluginSearchResponse, PluginUpdatesResponse } from './bindings/gen_api'; export * from './bindings/gen_models'; export * from './bindings/gen_events'; @@ -9,6 +9,10 @@ export async function searchPlugins(query: string) { return invoke('plugin:yaak-plugins|search', { query }); } -export async function installPlugin(plugin: PluginVersion) { - return invoke('plugin:yaak-plugins|install', { plugin }); +export async function installPlugin(name: string, version: string | null) { + return invoke('plugin:yaak-plugins|install', { name, version }); +} + +export async function checkPluginUpdates() { + return invoke('plugin:yaak-plugins|updates', {}); } diff --git a/src-tauri/yaak-plugins/permissions/default.toml b/src-tauri/yaak-plugins/permissions/default.toml index 054af4e3..7a3aea26 100644 --- a/src-tauri/yaak-plugins/permissions/default.toml +++ b/src-tauri/yaak-plugins/permissions/default.toml @@ -1,3 +1,3 @@ [default] description = "Default permissions for the plugin" -permissions = ["allow-search", "allow-install"] +permissions = ["allow-search", "allow-install", "allow-updates"] diff --git a/src-tauri/yaak-plugins/src/api.rs b/src-tauri/yaak-plugins/src/api.rs index 26e63463..e8aa95ab 100644 --- a/src-tauri/yaak-plugins/src/api.rs +++ b/src-tauri/yaak-plugins/src/api.rs @@ -1,10 +1,17 @@ +use crate::error::Error::ApiErr; use crate::commands::{PluginSearchResponse, PluginVersion}; use crate::error::Result; +use crate::plugin_meta::get_plugin_meta; +use log::{info, warn}; use reqwest::{Response, Url}; +use serde::{Deserialize, Serialize}; +use std::path::Path; use std::str::FromStr; use log::info; use tauri::{AppHandle, Runtime, is_dev}; +use ts_rs::TS; use yaak_common::api_client::yaak_api_client; +use yaak_models::query_manager::QueryManagerExt; use crate::error::Error::ApiErr; pub async fn get_plugin( @@ -44,6 +51,38 @@ pub async fn download_plugin_archive( Ok(resp) } +pub async fn check_plugin_updates( + app_handle: &AppHandle, +) -> Result { + let name_versions: Vec = app_handle + .db() + .list_plugins()? + .into_iter() + .filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) { + Ok(m) => Some(PluginNameVersion { + name: m.name, + version: m.version, + }), + Err(e) => { + warn!("Failed to get plugin metadata: {}", e); + None + } + }) + .collect(); + + let url = base_url("/updates"); + let body = serde_json::to_vec(&PluginUpdatesResponse { + plugins: name_versions, + })?; + let resp = yaak_api_client(app_handle)?.post(url.clone()).body(body).send().await?; + if !resp.status().is_success() { + return Err(ApiErr(format!("{} response to {}", resp.status(), url.to_string()))); + } + + let results: PluginUpdatesResponse = resp.json().await?; + Ok(results) +} + pub async fn search_plugins( app_handle: &AppHandle, query: &str, @@ -65,3 +104,41 @@ fn base_url(path: &str) -> Url { }; Url::from_str(&format!("{base_url}{path}")).unwrap() } + +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_search.ts")] +pub struct PluginVersion { + pub id: String, + pub version: String, + pub description: Option, + pub name: String, + pub display_name: String, + pub homepage_url: Option, + pub repository_url: Option, + pub checksum: String, + pub readme: Option, + pub yanked: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_api.ts")] +pub struct PluginSearchResponse { + pub plugins: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_api.ts")] +pub struct PluginNameVersion { + name: String, + version: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_api.ts")] +pub struct PluginUpdatesResponse { + pub plugins: Vec, +} diff --git a/src-tauri/yaak-plugins/src/commands.rs b/src-tauri/yaak-plugins/src/commands.rs index 460f3671..2b06660f 100644 --- a/src-tauri/yaak-plugins/src/commands.rs +++ b/src-tauri/yaak-plugins/src/commands.rs @@ -1,9 +1,9 @@ -use crate::api::search_plugins; +use crate::api::{ + PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates, search_plugins, +}; use crate::error::Result; use crate::install::download_and_install; -use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Runtime, WebviewWindow, command}; -use ts_rs::TS; #[command] pub(crate) async fn search( @@ -16,30 +16,14 @@ pub(crate) async fn search( #[command] pub(crate) async fn install( window: WebviewWindow, - plugin: PluginVersion, -) -> Result { - download_and_install(&window, &plugin).await + name: &str, + version: Option, +) -> Result<()> { + download_and_install(&window, name, version).await?; + Ok(()) } -#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] -#[serde(rename_all = "camelCase")] -#[ts(export, export_to = "gen_search.ts")] -pub struct PluginSearchResponse { - pub results: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] -#[serde(rename_all = "camelCase")] -#[ts(export, export_to = "gen_search.ts")] -pub struct PluginVersion { - pub id: String, - pub version: String, - pub description: Option, - pub name: String, - pub display_name: String, - pub homepage_url: Option, - pub repository_url: Option, - pub checksum: String, - pub readme: Option, - pub yanked: bool, +#[command] +pub(crate) async fn updates(app_handle: AppHandle) -> Result { + check_plugin_updates(&app_handle).await } diff --git a/src-tauri/yaak-plugins/src/install.rs b/src-tauri/yaak-plugins/src/install.rs index e1146263..105e9a08 100644 --- a/src-tauri/yaak-plugins/src/install.rs +++ b/src-tauri/yaak-plugins/src/install.rs @@ -1,23 +1,25 @@ -use crate::api::download_plugin_archive; +use crate::api::{PluginVersion, download_plugin_archive, get_plugin}; use crate::checksum::compute_checksum; -use crate::commands::PluginVersion; use crate::error::Error::PluginErr; use crate::error::Result; use crate::events::PluginWindowContext; use crate::manager::PluginManager; use chrono::Utc; use log::info; -use std::fs::create_dir_all; +use std::fs::{create_dir_all, remove_dir_all}; use std::io::Cursor; use tauri::{Manager, Runtime, WebviewWindow}; use yaak_models::models::Plugin; use yaak_models::query_manager::QueryManagerExt; -use yaak_models::util::{UpdateSource, generate_id}; +use yaak_models::util::UpdateSource; pub async fn download_and_install( window: &WebviewWindow, - plugin_version: &PluginVersion, -) -> Result { + name: &str, + version: Option, +) -> Result { + let plugin_manager = window.state::(); + let plugin_version = get_plugin(window.app_handle(), name, version).await?; let resp = download_plugin_archive(window.app_handle(), &plugin_version).await?; let bytes = resp.bytes().await?; @@ -32,17 +34,19 @@ pub async fn download_and_install( info!("Checksum matched {}", checksum); - let plugin_dir = window.path().app_data_dir()?.join("plugins").join(generate_id()); + let plugin_dir = plugin_manager.installed_plugin_dir.join(name); let plugin_dir_str = plugin_dir.to_str().unwrap().to_string(); + + // Re-create the plugin directory + let _ = remove_dir_all(&plugin_dir); create_dir_all(&plugin_dir)?; zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?; info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str); - let plugin_manager = window.state::(); plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &plugin_dir_str).await?; - let p = window.db().upsert_plugin( + window.db().upsert_plugin( &Plugin { id: plugin_version.id.clone(), checked_at: Some(Utc::now().naive_utc()), @@ -56,5 +60,5 @@ pub async fn download_and_install( info!("Installed plugin {} to {}", plugin_version.id, plugin_dir_str); - Ok(p.id) + Ok(plugin_version) } diff --git a/src-tauri/yaak-plugins/src/lib.rs b/src-tauri/yaak-plugins/src/lib.rs index 960b3dd5..abb821c7 100644 --- a/src-tauri/yaak-plugins/src/lib.rs +++ b/src-tauri/yaak-plugins/src/lib.rs @@ -1,4 +1,4 @@ -use crate::commands::{install, search}; +use crate::commands::{install, search, updates}; use crate::manager::PluginManager; use log::info; use std::process::exit; @@ -18,10 +18,11 @@ mod util; mod checksum; pub mod api; pub mod install; +pub mod plugin_meta; pub fn init() -> TauriPlugin { Builder::new("yaak-plugins") - .invoke_handler(generate_handler![search, install]) + .invoke_handler(generate_handler![search, install, updates]) .setup(|app_handle, _| { let manager = PluginManager::new(app_handle.clone()); app_handle.manage(manager.clone()); diff --git a/src-tauri/yaak-plugins/src/manager.rs b/src-tauri/yaak-plugins/src/manager.rs index 4bec172a..516ddc49 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -39,7 +39,7 @@ pub struct PluginManager { kill_tx: tokio::sync::watch::Sender, ws_service: Arc, vendored_plugin_dir: PathBuf, - installed_plugin_dir: PathBuf, + pub(crate) installed_plugin_dir: PathBuf, } #[derive(Clone)] @@ -62,8 +62,11 @@ impl PluginManager { .resolve("vendored/plugins", BaseDirectory::Resource) .expect("failed to resolve plugin directory resource"); - let installed_plugin_dir = - app_handle.path().app_data_dir().expect("failed to get app data dir"); + let installed_plugin_dir = app_handle + .path() + .app_data_dir() + .expect("failed to get app data dir") + .join("installed-plugins"); let plugin_manager = PluginManager { plugins: Default::default(), @@ -209,7 +212,7 @@ impl PluginManager { None => return Err(ClientNotInitializedErr), Some(tx) => tx, }; - let plugin_handle = PluginHandle::new(dir, tx.clone()); + let plugin_handle = PluginHandle::new(dir, tx.clone())?; let dir_path = Path::new(dir); let is_vendored = dir_path.starts_with(self.vendored_plugin_dir.as_path()); let is_installed = dir_path.starts_with(self.installed_plugin_dir.as_path()); @@ -231,14 +234,11 @@ impl PluginManager { // Add the new plugin self.plugins.lock().await.push(plugin_handle.clone()); - let resp = match event.payload { + let _ = match event.payload { InternalEventPayload::BootResponse(resp) => resp, _ => return Err(UnknownEventErr), }; - // Set the boot response - plugin_handle.set_boot_response(&resp).await; - Ok(()) } @@ -317,7 +317,7 @@ impl PluginManager { pub async fn get_plugin_by_name(&self, name: &str) -> Option { for plugin in self.plugins.lock().await.iter().cloned() { - let info = plugin.info().await; + let info = plugin.info(); if info.name == name { return Some(plugin); } diff --git a/src-tauri/yaak-plugins/src/plugin_handle.rs b/src-tauri/yaak-plugins/src/plugin_handle.rs index b3574570..6d6a9262 100644 --- a/src-tauri/yaak-plugins/src/plugin_handle.rs +++ b/src-tauri/yaak-plugins/src/plugin_handle.rs @@ -1,38 +1,35 @@ use crate::error::Result; -use crate::events::{BootResponse, InternalEvent, InternalEventPayload, PluginWindowContext}; +use crate::events::{InternalEvent, InternalEventPayload, PluginWindowContext}; +use crate::plugin_meta::{PluginMetadata, get_plugin_meta}; use crate::util::gen_id; use log::info; use std::path::Path; use std::sync::Arc; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{Mutex, mpsc}; #[derive(Clone)] pub struct PluginHandle { pub ref_id: String, pub dir: String, pub(crate) to_plugin_tx: Arc>>, - pub(crate) boot_resp: Arc>, + pub(crate) metadata: PluginMetadata, } impl PluginHandle { - pub fn new(dir: &str, tx: mpsc::Sender) -> Self { + pub fn new(dir: &str, tx: mpsc::Sender) -> Result { let ref_id = gen_id(); + let metadata = get_plugin_meta(&Path::new(dir))?; - PluginHandle { + Ok(PluginHandle { ref_id: ref_id.clone(), dir: dir.to_string(), to_plugin_tx: Arc::new(Mutex::new(tx)), - boot_resp: Arc::new(Mutex::new(BootResponse::default())), - } + metadata, + }) } - pub async fn name(&self) -> String { - self.boot_resp.lock().await.name.clone() - } - - pub async fn info(&self) -> BootResponse { - let resp = &*self.boot_resp.lock().await; - resp.clone() + pub fn info(&self) -> PluginMetadata { + self.metadata.clone() } pub fn build_event_to_send( @@ -72,9 +69,4 @@ impl PluginHandle { self.to_plugin_tx.lock().await.send(event.to_owned()).await?; Ok(()) } - - pub async fn set_boot_response(&self, resp: &BootResponse) { - let mut boot_resp = self.boot_resp.lock().await; - *boot_resp = resp.clone(); - } } diff --git a/src-tauri/yaak-plugins/src/plugin_meta.rs b/src-tauri/yaak-plugins/src/plugin_meta.rs new file mode 100644 index 00000000..5dd2549e --- /dev/null +++ b/src-tauri/yaak-plugins/src/plugin_meta.rs @@ -0,0 +1,64 @@ +use crate::error::Result; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; +use ts_rs::TS; + +#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "gen_search.ts")] +pub struct PluginMetadata { + pub version: String, + pub name: String, + pub display_name: String, + pub description: Option, + pub homepage_url: Option, + pub repository_url: Option, +} + +pub(crate) fn get_plugin_meta(plugin_dir: &Path) -> Result { + let package_json = fs::File::open(plugin_dir.join("package.json"))?; + let package_json: PackageJson = serde_json::from_reader(package_json)?; + + let display_name = match package_json.display_name { + None => { + let display_name = package_json.name.to_string(); + let display_name = display_name.split('/').last().unwrap_or(&package_json.name); + let display_name = display_name.strip_prefix("yaak-plugin-").unwrap_or(&display_name); + let display_name = display_name.strip_prefix("yaak-").unwrap_or(&display_name); + display_name.to_string() + } + Some(n) => n, + }; + + Ok(PluginMetadata { + version: package_json.version, + description: package_json.description, + name: package_json.name, + display_name, + homepage_url: package_json.homepage, + repository_url: match package_json.repository { + None => None, + Some(RepositoryField::Object { url }) => Some(url), + Some(RepositoryField::String(url)) => Some(url), + }, + }) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PackageJson { + pub name: String, + pub display_name: Option, + pub version: String, + pub repository: Option, + pub homepage: Option, + pub description: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum RepositoryField { + String(String), + Object { url: String }, +} diff --git a/src-web/components/Settings/SettingsPlugins.tsx b/src-web/components/Settings/SettingsPlugins.tsx index 8ffdcf3d..37b0d4be 100644 --- a/src-web/components/Settings/SettingsPlugins.tsx +++ b/src-web/components/Settings/SettingsPlugins.tsx @@ -1,8 +1,13 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { openUrl } from '@tauri-apps/plugin-opener'; -import type { Plugin } from '@yaakapp-internal/models'; -import { pluginsAtom } from '@yaakapp-internal/models'; -import { installPlugin, PluginVersion, searchPlugins } from '@yaakapp-internal/plugins'; +import { Plugin, pluginsAtom } from '@yaakapp-internal/models'; +import { + checkPluginUpdates, + installPlugin, + PluginVersion, + searchPlugins, +} from '@yaakapp-internal/plugins'; +import { PluginUpdatesResponse } from '@yaakapp-internal/plugins/bindings/gen_api'; import { useAtomValue } from 'jotai'; import React, { useState } from 'react'; import { useDebouncedValue } from '../../hooks/useDebouncedValue'; @@ -43,15 +48,8 @@ export function SettingsPlugins() { - -
{ - e.preventDefault(); - if (directory == null) return; - createPlugin.mutate(directory); - setDirectory(null); - }} - > +
+
{directory && ( - )} @@ -83,31 +90,61 @@ export function SettingsPlugins() { />
- +
); } -function PluginInfo({ plugin }: { plugin: Plugin }) { +function PluginTableRow({ + plugin, + updates, +}: { + plugin: Plugin; + updates: PluginUpdatesResponse | null; +}) { const pluginInfo = usePluginInfo(plugin.id); - const deletePlugin = useUninstallPlugin(); + const uninstallPlugin = useUninstallPlugin(); + const latestVersion = updates?.plugins.find((u) => u.name === pluginInfo.data?.name)?.version; + const installPluginMutation = useMutation({ + mutationKey: ['install_plugin', plugin.id], + mutationFn: (name: string) => installPlugin(name, null), + }); + if (pluginInfo.data == null) return null; + return ( - - {pluginInfo.data?.name} - + + {pluginInfo.data.displayName} + {pluginInfo.data?.version} - - - deletePlugin.mutate(plugin.id)} - /> - - + + {pluginInfo.data.description} + + + {latestVersion != null && ( + + )} + { + uninstallPlugin.mutate({ pluginId: plugin.id, name: pluginInfo.data.displayName }); + }} + /> + + + ); } @@ -135,7 +172,7 @@ function PluginSearch() { - ) : (results.data.results ?? []).length === 0 ? ( + ) : (results.data.plugins ?? []).length === 0 ? ( No plugins found ) : ( @@ -148,22 +185,20 @@ function PluginSearch() { - {results.data.results.map((plugin) => { - return ( - - {plugin.displayName} - - {plugin.version} - - - {plugin.description ?? 'n/a'} - - - - - - ); - })} + {results.data.plugins.map((plugin) => ( + + {plugin.displayName} + + {plugin.version} + + + {plugin.description ?? 'n/a'} + + + + + + ))}
)} @@ -174,23 +209,23 @@ function PluginSearch() { function InstallPluginButton({ plugin }: { plugin: PluginVersion }) { const plugins = useAtomValue(pluginsAtom); - const deletePlugin = useUninstallPlugin(); + const uninstallPlugin = useUninstallPlugin(); const installed = plugins?.some((p) => p.id === plugin.id); const installPluginMutation = useMutation({ mutationKey: ['install_plugin', plugin.id], - mutationFn: installPlugin, + mutationFn: (pv: PluginVersion) => installPlugin(pv.name, null), }); return ( ); }); diff --git a/src-web/components/core/Table.tsx b/src-web/components/core/Table.tsx index 55099a66..ff9e6f3c 100644 --- a/src-web/components/core/Table.tsx +++ b/src-web/components/core/Table.tsx @@ -53,7 +53,7 @@ export function TableHeaderCell({ children, className, }: { - children: ReactNode; + children?: ReactNode; className?: string; }) { return ( diff --git a/src-web/hooks/usePluginInfo.ts b/src-web/hooks/usePluginInfo.ts index 205e858c..0ae2a618 100644 --- a/src-web/hooks/usePluginInfo.ts +++ b/src-web/hooks/usePluginInfo.ts @@ -1,16 +1,23 @@ import { useQuery } from '@tanstack/react-query'; -import type { BootResponse } from '@yaakapp-internal/plugins'; +import type { Plugin } from '@yaakapp-internal/models'; +import { pluginsAtom } from '@yaakapp-internal/models'; +import type { PluginMetadata } from '@yaakapp-internal/plugins'; +import { useAtomValue } from 'jotai'; import { queryClient } from '../lib/queryClient'; import { invokeCmd } from '../lib/tauri'; -function pluginInfoKey(id: string) { - return ['plugin_info', id]; +function pluginInfoKey(id: string, plugin: Plugin | null) { + return ['plugin_info', id, plugin?.updatedAt ?? 'n/a']; } export function usePluginInfo(id: string) { + const plugins = useAtomValue(pluginsAtom); + // Get the plugin so we can refetch whenever it's updated + const plugin = plugins.find((p) => p.id === id); return useQuery({ - queryKey: pluginInfoKey(id), - queryFn: () => invokeCmd('cmd_plugin_info', { id }), + queryKey: pluginInfoKey(id, plugin ?? null), + placeholderData: (prev) => prev, // Keep previous data on refetch + queryFn: () => invokeCmd('cmd_plugin_info', { id }), }); } diff --git a/src-web/hooks/useUninstallPlugin.ts b/src-web/hooks/useUninstallPlugin.ts deleted file mode 100644 index de738e89..00000000 --- a/src-web/hooks/useUninstallPlugin.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { invokeCmd } from '../lib/tauri'; -import { useFastMutation } from './useFastMutation'; - -export function useUninstallPlugin() { - return useFastMutation({ - mutationKey: ['uninstall_plugin'], - mutationFn: async (pluginId: string) => { - return invokeCmd('cmd_uninstall_plugin', { pluginId }); - }, - }); -} diff --git a/src-web/hooks/useUninstallPlugin.tsx b/src-web/hooks/useUninstallPlugin.tsx new file mode 100644 index 00000000..ebbbc9aa --- /dev/null +++ b/src-web/hooks/useUninstallPlugin.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { InlineCode } from '../components/core/InlineCode'; +import { showConfirmDelete } from '../lib/confirm'; +import { invokeCmd } from '../lib/tauri'; +import { useFastMutation } from './useFastMutation'; + +export function useUninstallPlugin() { + return useFastMutation({ + mutationKey: ['uninstall_plugin'], + mutationFn: async ({ pluginId, name }: { pluginId: string; name: string }) => { + const confirmed = await showConfirmDelete({ + id: 'uninstall-plugin-' + name, + title: 'Uninstall Plugin', + description: ( + <> + Permanently uninstall {name}? + + ), + }); + if (confirmed) { + await invokeCmd('cmd_uninstall_plugin', { pluginId }); + } + }, + }); +}