Install plugins from Yaak plugin registry (#230)

This commit is contained in:
Gregory Schier
2025-06-23 08:55:38 -07:00
committed by GitHub
parent b5620fcdf3
commit cb7c44cc65
27 changed files with 421 additions and 218 deletions
@@ -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<PluginVersion>, };
export type PluginUpdatesResponse = { plugins: Array<PluginNameVersion>, };
@@ -1,5 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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<PluginVersion>, }; 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, };
+7 -7
View File
@@ -36,12 +36,13 @@ use yaak_models::models::{
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{ use yaak_plugins::events::{
BootResponse, CallHttpRequestActionRequest, FilterResponse, CallHttpRequestActionRequest, FilterResponse, GetHttpAuthenticationConfigResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive,
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, PluginWindowContext, RenderPurpose,
}; };
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_sse::sse::ServerSentEvent; use yaak_sse::sse::ServerSentEvent;
use yaak_templates::format::format_json; use yaak_templates::format::format_json;
@@ -1039,14 +1040,13 @@ async fn cmd_plugin_info<R: Runtime>(
id: &str, id: &str,
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
) -> YaakResult<BootResponse> { ) -> YaakResult<PluginMetadata> {
let plugin = app_handle.db().get_plugin(id)?; let plugin = app_handle.db().get_plugin(id)?;
Ok(plugin_manager Ok(plugin_manager
.get_plugin_by_dir(plugin.directory.as_str()) .get_plugin_by_dir(plugin.directory.as_str())
.await .await
.ok_or(GenericError("Failed to find plugin for info".to_string()))? .ok_or(GenericError("Failed to find plugin for info".to_string()))?
.info() .info())
.await)
} }
#[tauri::command] #[tauri::command]
+7 -7
View File
@@ -86,8 +86,8 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
environment.as_ref(), environment.as_ref(),
&cb, &cb,
) )
.await .await
.expect("Failed to render http request"); .expect("Failed to render http request");
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse { Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request, http_request,
})) }))
@@ -115,7 +115,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
&InternalEventPayload::ShowToastRequest(ShowToastRequest { &InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!( message: format!(
"Plugin error from {}: {}", "Plugin error from {}: {}",
plugin_handle.name().await, plugin_handle.info().name,
resp.error resp.error
), ),
color: Some(Color::Danger), color: Some(Color::Danger),
@@ -188,7 +188,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
cookie_jar, cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel &mut tokio::sync::watch::channel(false).1, // No-op cancel channel
) )
.await; .await;
let http_response = match result { let http_response = match result {
Ok(r) => r, Ok(r) => r,
@@ -257,17 +257,17 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
None None
} }
InternalEventPayload::SetKeyValueRequest(req) => { 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); app_handle.db().set_plugin_key_value(&name, &req.key, &req.value);
Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {})) Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {}))
} }
InternalEventPayload::GetKeyValueRequest(req) => { 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); let value = app_handle.db().get_plugin_key_value(&name, &req.key).map(|v| v.value);
Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value })) Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value }))
} }
InternalEventPayload::DeleteKeyValueRequest(req) => { 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(); let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key).unwrap();
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted })) Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
} }
+3 -11
View File
@@ -4,7 +4,6 @@ use log::{info, warn};
use std::collections::HashMap; use std::collections::HashMap;
use tauri::{AppHandle, Emitter, Manager, Runtime, Url}; use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
use yaak_plugins::api::get_plugin;
use yaak_plugins::events::{Color, ShowToastRequest}; use yaak_plugins::events::{Color, ShowToastRequest};
use yaak_plugins::install::download_and_install; use yaak_plugins::install::download_and_install;
@@ -23,14 +22,10 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
"install-plugin" => { "install-plugin" => {
let name = query_map.get("name").unwrap(); let name = query_map.get("name").unwrap();
let version = query_map.get("version").cloned(); let version = query_map.get("version").cloned();
let plugin_version = get_plugin(&app_handle, &name, version).await?;
_ = window.set_focus(); _ = window.set_focus();
let confirmed_install = app_handle let confirmed_install = app_handle
.dialog() .dialog()
.message(format!( .message(format!("Install plugin {name} {version:?}?",))
"Install plugin {}@{}?",
plugin_version.name, plugin_version.version
))
.kind(MessageDialogKind::Info) .kind(MessageDialogKind::Info)
.buttons(MessageDialogButtons::OkCustom("Install".to_string())) .buttons(MessageDialogButtons::OkCustom("Install".to_string()))
.blocking_show(); .blocking_show();
@@ -39,14 +34,11 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
return Ok(()); return Ok(());
} }
download_and_install(window, &plugin_version).await?; let pv = download_and_install(window, name, version).await?;
app_handle.emit( app_handle.emit(
"show_toast", "show_toast",
ShowToastRequest { ShowToastRequest {
message: format!( message: format!("Installed {name}@{}", pv.version),
"Installed {}@{}",
plugin_version.name, plugin_version.version
),
color: Some(Color::Success), color: Some(Color::Success),
icon: None, icon: None,
}, },
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::connection_or_tx::ConnectionOrTx; use crate::connection_or_tx::ConnectionOrTx;
use crate::error::Error::RowNotFound; use crate::error::Error::DBRowNotFound;
use crate::models::{AnyModel, UpsertModelInfo}; use crate::models::{AnyModel, UpsertModelInfo};
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource}; use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
use rusqlite::OptionalExtension; use rusqlite::OptionalExtension;
@@ -26,7 +26,7 @@ impl<'a> DbContext<'a> {
{ {
match self.find_optional::<M>(col, value) { match self.find_optional::<M>(col, value) {
Some(v) => Ok(v), Some(v) => Ok(v),
None => Err(RowNotFound), None => Err(DBRowNotFound(format!("{:?}", M::table_name()))),
} }
} }
+2 -2
View File
@@ -30,8 +30,8 @@ pub enum Error {
#[error("Multiple base environments for {0}. Delete duplicates before continuing.")] #[error("Multiple base environments for {0}. Delete duplicates before continuing.")]
MultipleBaseEnvironments(String), MultipleBaseEnvironments(String),
#[error("Row not found")] #[error("Database row not found: {0}")]
RowNotFound, DBRowNotFound(String),
#[error("unknown error")] #[error("unknown error")]
Unknown, Unknown,
+20 -20
View File
@@ -11,7 +11,7 @@ use sea_query::{IntoColumnRef, IntoIden, IntoTableRef, Order, SimpleExpr, enum_d
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt::Display; use std::fmt::{Debug, Display};
use std::str::FromStr; use std::str::FromStr;
use ts_rs::TS; use ts_rs::TS;
@@ -123,7 +123,7 @@ pub struct Settings {
} }
impl UpsertModelInfo for Settings { impl UpsertModelInfo for Settings {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
SettingsIden::Table SettingsIden::Table
} }
@@ -252,7 +252,7 @@ pub struct Workspace {
} }
impl UpsertModelInfo for Workspace { impl UpsertModelInfo for Workspace {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
WorkspaceIden::Table WorkspaceIden::Table
} }
@@ -355,7 +355,7 @@ pub struct WorkspaceMeta {
} }
impl UpsertModelInfo for WorkspaceMeta { impl UpsertModelInfo for WorkspaceMeta {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
WorkspaceMetaIden::Table WorkspaceMetaIden::Table
} }
@@ -456,7 +456,7 @@ pub struct CookieJar {
} }
impl UpsertModelInfo for CookieJar { impl UpsertModelInfo for CookieJar {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
CookieJarIden::Table CookieJarIden::Table
} }
@@ -535,7 +535,7 @@ pub struct Environment {
} }
impl UpsertModelInfo for Environment { impl UpsertModelInfo for Environment {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
EnvironmentIden::Table EnvironmentIden::Table
} }
@@ -655,7 +655,7 @@ pub struct Folder {
} }
impl UpsertModelInfo for Folder { impl UpsertModelInfo for Folder {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
FolderIden::Table FolderIden::Table
} }
@@ -786,7 +786,7 @@ pub struct HttpRequest {
} }
impl UpsertModelInfo for HttpRequest { impl UpsertModelInfo for HttpRequest {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
HttpRequestIden::Table HttpRequestIden::Table
} }
@@ -913,7 +913,7 @@ pub struct WebsocketConnection {
} }
impl UpsertModelInfo for WebsocketConnection { impl UpsertModelInfo for WebsocketConnection {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
WebsocketConnectionIden::Table WebsocketConnectionIden::Table
} }
@@ -1027,7 +1027,7 @@ pub struct WebsocketRequest {
} }
impl UpsertModelInfo for WebsocketRequest { impl UpsertModelInfo for WebsocketRequest {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
WebsocketRequestIden::Table WebsocketRequestIden::Table
} }
@@ -1152,7 +1152,7 @@ pub struct WebsocketEvent {
} }
impl UpsertModelInfo for WebsocketEvent { impl UpsertModelInfo for WebsocketEvent {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
WebsocketEventIden::Table WebsocketEventIden::Table
} }
@@ -1269,7 +1269,7 @@ pub struct HttpResponse {
} }
impl UpsertModelInfo for HttpResponse { impl UpsertModelInfo for HttpResponse {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
HttpResponseIden::Table HttpResponseIden::Table
} }
@@ -1377,7 +1377,7 @@ pub struct GraphQlIntrospection {
} }
impl UpsertModelInfo for GraphQlIntrospection { impl UpsertModelInfo for GraphQlIntrospection {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
GraphQlIntrospectionIden::Table GraphQlIntrospectionIden::Table
} }
@@ -1461,7 +1461,7 @@ pub struct GrpcRequest {
} }
impl UpsertModelInfo for GrpcRequest { impl UpsertModelInfo for GrpcRequest {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
GrpcRequestIden::Table GrpcRequestIden::Table
} }
@@ -1588,7 +1588,7 @@ pub struct GrpcConnection {
} }
impl UpsertModelInfo for GrpcConnection { impl UpsertModelInfo for GrpcConnection {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
GrpcConnectionIden::Table GrpcConnectionIden::Table
} }
@@ -1708,7 +1708,7 @@ pub struct GrpcEvent {
} }
impl UpsertModelInfo for GrpcEvent { impl UpsertModelInfo for GrpcEvent {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
GrpcEventIden::Table GrpcEventIden::Table
} }
@@ -1799,7 +1799,7 @@ pub struct Plugin {
} }
impl UpsertModelInfo for Plugin { impl UpsertModelInfo for Plugin {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
PluginIden::Table PluginIden::Table
} }
@@ -1881,7 +1881,7 @@ pub struct SyncState {
} }
impl UpsertModelInfo for SyncState { impl UpsertModelInfo for SyncState {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
SyncStateIden::Table SyncStateIden::Table
} }
@@ -1964,7 +1964,7 @@ pub struct KeyValue {
} }
impl UpsertModelInfo for KeyValue { impl UpsertModelInfo for KeyValue {
fn table_name() -> impl IntoTableRef { fn table_name() -> impl IntoTableRef + Debug {
KeyValueIden::Table KeyValueIden::Table
} }
@@ -2181,7 +2181,7 @@ impl AnyModel {
} }
pub trait UpsertModelInfo { pub trait UpsertModelInfo {
fn table_name() -> impl IntoTableRef; fn table_name() -> impl IntoTableRef + Debug;
fn id_column() -> impl IntoIden + Eq + Clone; fn id_column() -> impl IntoIden + Eq + Clone;
fn generate_id() -> String; fn generate_id() -> String;
fn order_by() -> (impl IntoColumnRef, Order); fn order_by() -> (impl IntoColumnRef, Order);
@@ -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<PluginVersion>, };
export type PluginUpdatesResponse = { plugins: Array<PluginNameVersion>, };
@@ -1,5 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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<PluginVersion>, }; 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, };
+1 -1
View File
@@ -1,4 +1,4 @@
const COMMANDS: &[&str] = &["search", "install"]; const COMMANDS: &[&str] = &["search", "install", "updates"];
fn main() { fn main() {
tauri_plugin::Builder::new(COMMANDS).build(); tauri_plugin::Builder::new(COMMANDS).build();
+7 -3
View File
@@ -1,5 +1,5 @@
import { invoke } from '@tauri-apps/api/core'; 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_models';
export * from './bindings/gen_events'; export * from './bindings/gen_events';
@@ -9,6 +9,10 @@ export async function searchPlugins(query: string) {
return invoke<PluginSearchResponse>('plugin:yaak-plugins|search', { query }); return invoke<PluginSearchResponse>('plugin:yaak-plugins|search', { query });
} }
export async function installPlugin(plugin: PluginVersion) { export async function installPlugin(name: string, version: string | null) {
return invoke<string>('plugin:yaak-plugins|install', { plugin }); return invoke<string>('plugin:yaak-plugins|install', { name, version });
}
export async function checkPluginUpdates() {
return invoke<PluginUpdatesResponse>('plugin:yaak-plugins|updates', {});
} }
@@ -1,3 +1,3 @@
[default] [default]
description = "Default permissions for the plugin" description = "Default permissions for the plugin"
permissions = ["allow-search", "allow-install"] permissions = ["allow-search", "allow-install", "allow-updates"]
+77
View File
@@ -1,10 +1,17 @@
use crate::error::Error::ApiErr;
use crate::commands::{PluginSearchResponse, PluginVersion}; use crate::commands::{PluginSearchResponse, PluginVersion};
use crate::error::Result; use crate::error::Result;
use crate::plugin_meta::get_plugin_meta;
use log::{info, warn};
use reqwest::{Response, Url}; use reqwest::{Response, Url};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use log::info; use log::info;
use tauri::{AppHandle, Runtime, is_dev}; use tauri::{AppHandle, Runtime, is_dev};
use ts_rs::TS;
use yaak_common::api_client::yaak_api_client; use yaak_common::api_client::yaak_api_client;
use yaak_models::query_manager::QueryManagerExt;
use crate::error::Error::ApiErr; use crate::error::Error::ApiErr;
pub async fn get_plugin<R: Runtime>( pub async fn get_plugin<R: Runtime>(
@@ -44,6 +51,38 @@ pub async fn download_plugin_archive<R: Runtime>(
Ok(resp) Ok(resp)
} }
pub async fn check_plugin_updates<R: Runtime>(
app_handle: &AppHandle<R>,
) -> Result<PluginUpdatesResponse> {
let name_versions: Vec<PluginNameVersion> = 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<R: Runtime>( pub async fn search_plugins<R: Runtime>(
app_handle: &AppHandle<R>, app_handle: &AppHandle<R>,
query: &str, query: &str,
@@ -65,3 +104,41 @@ fn base_url(path: &str) -> Url {
}; };
Url::from_str(&format!("{base_url}{path}")).unwrap() 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<String>,
pub name: String,
pub display_name: String,
pub homepage_url: Option<String>,
pub repository_url: Option<String>,
pub checksum: String,
pub readme: Option<String>,
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<PluginVersion>,
}
#[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<PluginNameVersion>,
}
+11 -27
View File
@@ -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::error::Result;
use crate::install::download_and_install; use crate::install::download_and_install;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Runtime, WebviewWindow, command}; use tauri::{AppHandle, Runtime, WebviewWindow, command};
use ts_rs::TS;
#[command] #[command]
pub(crate) async fn search<R: Runtime>( pub(crate) async fn search<R: Runtime>(
@@ -16,30 +16,14 @@ pub(crate) async fn search<R: Runtime>(
#[command] #[command]
pub(crate) async fn install<R: Runtime>( pub(crate) async fn install<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
plugin: PluginVersion, name: &str,
) -> Result<String> { version: Option<String>,
download_and_install(&window, &plugin).await ) -> Result<()> {
download_and_install(&window, name, version).await?;
Ok(())
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)] #[command]
#[serde(rename_all = "camelCase")] pub(crate) async fn updates<R: Runtime>(app_handle: AppHandle<R>) -> Result<PluginUpdatesResponse> {
#[ts(export, export_to = "gen_search.ts")] check_plugin_updates(&app_handle).await
pub struct PluginSearchResponse {
pub results: Vec<PluginVersion>,
}
#[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<String>,
pub name: String,
pub display_name: String,
pub homepage_url: Option<String>,
pub repository_url: Option<String>,
pub checksum: String,
pub readme: Option<String>,
pub yanked: bool,
} }
+14 -10
View File
@@ -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::checksum::compute_checksum;
use crate::commands::PluginVersion;
use crate::error::Error::PluginErr; use crate::error::Error::PluginErr;
use crate::error::Result; use crate::error::Result;
use crate::events::PluginWindowContext; use crate::events::PluginWindowContext;
use crate::manager::PluginManager; use crate::manager::PluginManager;
use chrono::Utc; use chrono::Utc;
use log::info; use log::info;
use std::fs::create_dir_all; use std::fs::{create_dir_all, remove_dir_all};
use std::io::Cursor; use std::io::Cursor;
use tauri::{Manager, Runtime, WebviewWindow}; use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::models::Plugin; use yaak_models::models::Plugin;
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{UpdateSource, generate_id}; use yaak_models::util::UpdateSource;
pub async fn download_and_install<R: Runtime>( pub async fn download_and_install<R: Runtime>(
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
plugin_version: &PluginVersion, name: &str,
) -> Result<String> { version: Option<String>,
) -> Result<PluginVersion> {
let plugin_manager = window.state::<PluginManager>();
let plugin_version = get_plugin(window.app_handle(), name, version).await?;
let resp = download_plugin_archive(window.app_handle(), &plugin_version).await?; let resp = download_plugin_archive(window.app_handle(), &plugin_version).await?;
let bytes = resp.bytes().await?; let bytes = resp.bytes().await?;
@@ -32,17 +34,19 @@ pub async fn download_and_install<R: Runtime>(
info!("Checksum matched {}", checksum); 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(); 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)?; create_dir_all(&plugin_dir)?;
zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?; zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?;
info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str); info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str);
let plugin_manager = window.state::<PluginManager>();
plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &plugin_dir_str).await?; plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &plugin_dir_str).await?;
let p = window.db().upsert_plugin( window.db().upsert_plugin(
&Plugin { &Plugin {
id: plugin_version.id.clone(), id: plugin_version.id.clone(),
checked_at: Some(Utc::now().naive_utc()), checked_at: Some(Utc::now().naive_utc()),
@@ -56,5 +60,5 @@ pub async fn download_and_install<R: Runtime>(
info!("Installed plugin {} to {}", plugin_version.id, plugin_dir_str); info!("Installed plugin {} to {}", plugin_version.id, plugin_dir_str);
Ok(p.id) Ok(plugin_version)
} }
+3 -2
View File
@@ -1,4 +1,4 @@
use crate::commands::{install, search}; use crate::commands::{install, search, updates};
use crate::manager::PluginManager; use crate::manager::PluginManager;
use log::info; use log::info;
use std::process::exit; use std::process::exit;
@@ -18,10 +18,11 @@ mod util;
mod checksum; mod checksum;
pub mod api; pub mod api;
pub mod install; pub mod install;
pub mod plugin_meta;
pub fn init<R: Runtime>() -> TauriPlugin<R> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-plugins") Builder::new("yaak-plugins")
.invoke_handler(generate_handler![search, install]) .invoke_handler(generate_handler![search, install, updates])
.setup(|app_handle, _| { .setup(|app_handle, _| {
let manager = PluginManager::new(app_handle.clone()); let manager = PluginManager::new(app_handle.clone());
app_handle.manage(manager.clone()); app_handle.manage(manager.clone());
+9 -9
View File
@@ -39,7 +39,7 @@ pub struct PluginManager {
kill_tx: tokio::sync::watch::Sender<bool>, kill_tx: tokio::sync::watch::Sender<bool>,
ws_service: Arc<PluginRuntimeServerWebsocket>, ws_service: Arc<PluginRuntimeServerWebsocket>,
vendored_plugin_dir: PathBuf, vendored_plugin_dir: PathBuf,
installed_plugin_dir: PathBuf, pub(crate) installed_plugin_dir: PathBuf,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -62,8 +62,11 @@ impl PluginManager {
.resolve("vendored/plugins", BaseDirectory::Resource) .resolve("vendored/plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource"); .expect("failed to resolve plugin directory resource");
let installed_plugin_dir = let installed_plugin_dir = app_handle
app_handle.path().app_data_dir().expect("failed to get app data dir"); .path()
.app_data_dir()
.expect("failed to get app data dir")
.join("installed-plugins");
let plugin_manager = PluginManager { let plugin_manager = PluginManager {
plugins: Default::default(), plugins: Default::default(),
@@ -209,7 +212,7 @@ impl PluginManager {
None => return Err(ClientNotInitializedErr), None => return Err(ClientNotInitializedErr),
Some(tx) => tx, 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 dir_path = Path::new(dir);
let is_vendored = dir_path.starts_with(self.vendored_plugin_dir.as_path()); 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()); let is_installed = dir_path.starts_with(self.installed_plugin_dir.as_path());
@@ -231,14 +234,11 @@ impl PluginManager {
// Add the new plugin // Add the new plugin
self.plugins.lock().await.push(plugin_handle.clone()); self.plugins.lock().await.push(plugin_handle.clone());
let resp = match event.payload { let _ = match event.payload {
InternalEventPayload::BootResponse(resp) => resp, InternalEventPayload::BootResponse(resp) => resp,
_ => return Err(UnknownEventErr), _ => return Err(UnknownEventErr),
}; };
// Set the boot response
plugin_handle.set_boot_response(&resp).await;
Ok(()) Ok(())
} }
@@ -317,7 +317,7 @@ impl PluginManager {
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> { pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
for plugin in self.plugins.lock().await.iter().cloned() { for plugin in self.plugins.lock().await.iter().cloned() {
let info = plugin.info().await; let info = plugin.info();
if info.name == name { if info.name == name {
return Some(plugin); return Some(plugin);
} }
+11 -19
View File
@@ -1,38 +1,35 @@
use crate::error::Result; 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 crate::util::gen_id;
use log::info; use log::info;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{Mutex, mpsc};
#[derive(Clone)] #[derive(Clone)]
pub struct PluginHandle { pub struct PluginHandle {
pub ref_id: String, pub ref_id: String,
pub dir: String, pub dir: String,
pub(crate) to_plugin_tx: Arc<Mutex<mpsc::Sender<InternalEvent>>>, pub(crate) to_plugin_tx: Arc<Mutex<mpsc::Sender<InternalEvent>>>,
pub(crate) boot_resp: Arc<Mutex<BootResponse>>, pub(crate) metadata: PluginMetadata,
} }
impl PluginHandle { impl PluginHandle {
pub fn new(dir: &str, tx: mpsc::Sender<InternalEvent>) -> Self { pub fn new(dir: &str, tx: mpsc::Sender<InternalEvent>) -> Result<Self> {
let ref_id = gen_id(); let ref_id = gen_id();
let metadata = get_plugin_meta(&Path::new(dir))?;
PluginHandle { Ok(PluginHandle {
ref_id: ref_id.clone(), ref_id: ref_id.clone(),
dir: dir.to_string(), dir: dir.to_string(),
to_plugin_tx: Arc::new(Mutex::new(tx)), to_plugin_tx: Arc::new(Mutex::new(tx)),
boot_resp: Arc::new(Mutex::new(BootResponse::default())), metadata,
} })
} }
pub async fn name(&self) -> String { pub fn info(&self) -> PluginMetadata {
self.boot_resp.lock().await.name.clone() self.metadata.clone()
}
pub async fn info(&self) -> BootResponse {
let resp = &*self.boot_resp.lock().await;
resp.clone()
} }
pub fn build_event_to_send( pub fn build_event_to_send(
@@ -72,9 +69,4 @@ impl PluginHandle {
self.to_plugin_tx.lock().await.send(event.to_owned()).await?; self.to_plugin_tx.lock().await.send(event.to_owned()).await?;
Ok(()) Ok(())
} }
pub async fn set_boot_response(&self, resp: &BootResponse) {
let mut boot_resp = self.boot_resp.lock().await;
*boot_resp = resp.clone();
}
} }
+64
View File
@@ -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<String>,
pub homepage_url: Option<String>,
pub repository_url: Option<String>,
}
pub(crate) fn get_plugin_meta(plugin_dir: &Path) -> Result<PluginMetadata> {
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<String>,
pub version: String,
pub repository: Option<RepositoryField>,
pub homepage: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum RepositoryField {
String(String),
Object { url: String },
}
+104 -63
View File
@@ -1,8 +1,13 @@
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { openUrl } from '@tauri-apps/plugin-opener'; import { openUrl } from '@tauri-apps/plugin-opener';
import type { Plugin } from '@yaakapp-internal/models'; import { Plugin, pluginsAtom } from '@yaakapp-internal/models';
import { pluginsAtom } from '@yaakapp-internal/models'; import {
import { installPlugin, PluginVersion, searchPlugins } from '@yaakapp-internal/plugins'; checkPluginUpdates,
installPlugin,
PluginVersion,
searchPlugins,
} from '@yaakapp-internal/plugins';
import { PluginUpdatesResponse } from '@yaakapp-internal/plugins/bindings/gen_api';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useDebouncedValue } from '../../hooks/useDebouncedValue'; import { useDebouncedValue } from '../../hooks/useDebouncedValue';
@@ -43,15 +48,8 @@ export function SettingsPlugins() {
<PluginSearch /> <PluginSearch />
</TabContent> </TabContent>
<TabContent value="installed"> <TabContent value="installed">
<InstalledPlugins /> <div className="h-full grid grid-rows-[minmax(0,1fr)_auto]">
<form <InstalledPlugins />
onSubmit={(e) => {
e.preventDefault();
if (directory == null) return;
createPlugin.mutate(directory);
setDirectory(null);
}}
>
<footer className="grid grid-cols-[minmax(0,1fr)_auto] -mx-4 py-2 px-4 border-t bg-surface-highlight border-border-subtle min-w-0"> <footer className="grid grid-cols-[minmax(0,1fr)_auto] -mx-4 py-2 px-4 border-t bg-surface-highlight border-border-subtle min-w-0">
<SelectFile <SelectFile
size="xs" size="xs"
@@ -62,7 +60,16 @@ export function SettingsPlugins() {
/> />
<HStack> <HStack>
{directory && ( {directory && (
<Button size="xs" type="submit" color="primary" className="ml-auto"> <Button
size="xs"
color="primary"
className="ml-auto"
onClick={() => {
if (directory == null) return;
createPlugin.mutate(directory);
setDirectory(null);
}}
>
Add Plugin Add Plugin
</Button> </Button>
)} )}
@@ -83,31 +90,61 @@ export function SettingsPlugins() {
/> />
</HStack> </HStack>
</footer> </footer>
</form> </div>
</TabContent> </TabContent>
</Tabs> </Tabs>
</div> </div>
); );
} }
function PluginInfo({ plugin }: { plugin: Plugin }) { function PluginTableRow({
plugin,
updates,
}: {
plugin: Plugin;
updates: PluginUpdatesResponse | null;
}) {
const pluginInfo = usePluginInfo(plugin.id); 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 ( return (
<tr className="group"> <TableRow>
<td className="py-2 select-text cursor-text w-full">{pluginInfo.data?.name}</td> <TableCell className="font-semibold">{pluginInfo.data.displayName}</TableCell>
<td className="py-2 select-text cursor-text text-right"> <TableCell>
<InlineCode>{pluginInfo.data?.version}</InlineCode> <InlineCode>{pluginInfo.data?.version}</InlineCode>
</td> </TableCell>
<td className="py-2 select-text cursor-text pl-2"> <TableCell className="w-full text-text-subtle">{pluginInfo.data.description}</TableCell>
<IconButton <TableCell>
size="sm" <HStack>
icon="trash" {latestVersion != null && (
title="Uninstall plugin" <Button
onClick={() => deletePlugin.mutate(plugin.id)} variant="border"
/> color="success"
</td> title={`Update to ${latestVersion}`}
</tr> size="xs"
isLoading={installPluginMutation.isPending}
onClick={() => installPluginMutation.mutate(pluginInfo.data.name)}
>
Update
</Button>
)}
<IconButton
size="sm"
icon="trash"
title="Uninstall plugin"
onClick={async () => {
uninstallPlugin.mutate({ pluginId: plugin.id, name: pluginInfo.data.displayName });
}}
/>
</HStack>
</TableCell>
</TableRow>
); );
} }
@@ -135,7 +172,7 @@ function PluginSearch() {
<EmptyStateText> <EmptyStateText>
<LoadingIcon size="xl" className="text-text-subtlest" /> <LoadingIcon size="xl" className="text-text-subtlest" />
</EmptyStateText> </EmptyStateText>
) : (results.data.results ?? []).length === 0 ? ( ) : (results.data.plugins ?? []).length === 0 ? (
<EmptyStateText>No plugins found</EmptyStateText> <EmptyStateText>No plugins found</EmptyStateText>
) : ( ) : (
<Table> <Table>
@@ -148,22 +185,20 @@ function PluginSearch() {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{results.data.results.map((plugin) => { {results.data.plugins.map((plugin) => (
return ( <TableRow key={plugin.id}>
<TableRow key={plugin.id}> <TableCell className="font-semibold">{plugin.displayName}</TableCell>
<TableCell className="font-semibold">{plugin.displayName}</TableCell> <TableCell>
<TableCell className="text-text-subtle"> <InlineCode>{plugin.version}</InlineCode>
<InlineCode>{plugin.version}</InlineCode> </TableCell>
</TableCell> <TableCell className="w-full text-text-subtle">
<TableCell className="w-full text-text-subtle"> {plugin.description ?? 'n/a'}
{plugin.description ?? 'n/a'} </TableCell>
</TableCell> <TableCell>
<TableCell> <InstallPluginButton plugin={plugin} />
<InstallPluginButton plugin={plugin} /> </TableCell>
</TableCell> </TableRow>
</TableRow> ))}
);
})}
</TableBody> </TableBody>
</Table> </Table>
)} )}
@@ -174,23 +209,23 @@ function PluginSearch() {
function InstallPluginButton({ plugin }: { plugin: PluginVersion }) { function InstallPluginButton({ plugin }: { plugin: PluginVersion }) {
const plugins = useAtomValue(pluginsAtom); const plugins = useAtomValue(pluginsAtom);
const deletePlugin = useUninstallPlugin(); const uninstallPlugin = useUninstallPlugin();
const installed = plugins?.some((p) => p.id === plugin.id); const installed = plugins?.some((p) => p.id === plugin.id);
const installPluginMutation = useMutation({ const installPluginMutation = useMutation({
mutationKey: ['install_plugin', plugin.id], mutationKey: ['install_plugin', plugin.id],
mutationFn: installPlugin, mutationFn: (pv: PluginVersion) => installPlugin(pv.name, null),
}); });
return ( return (
<Button <Button
size="xs" size="xs"
variant={installed ? 'solid' : 'border'} variant="border"
color={installed ? 'primary' : 'secondary'} color={installed ? 'secondary' : 'primary'}
className="ml-auto" className="ml-auto"
isLoading={installPluginMutation.isPending} isLoading={installPluginMutation.isPending}
onClick={async () => { onClick={async () => {
if (installed) { if (installed) {
deletePlugin.mutate(plugin.id); uninstallPlugin.mutate({ pluginId: plugin.id, name: plugin.displayName });
} else { } else {
installPluginMutation.mutate(plugin); installPluginMutation.mutate(plugin);
} }
@@ -203,6 +238,11 @@ function InstallPluginButton({ plugin }: { plugin: PluginVersion }) {
function InstalledPlugins() { function InstalledPlugins() {
const plugins = useAtomValue(pluginsAtom); const plugins = useAtomValue(pluginsAtom);
const updates = useQuery({
queryKey: ['plugin_updates'],
queryFn: () => checkPluginUpdates(),
});
return plugins.length === 0 ? ( return plugins.length === 0 ? (
<div className="pb-4"> <div className="pb-4">
<EmptyStateText className="text-center"> <EmptyStateText className="text-center">
@@ -212,19 +252,20 @@ function InstalledPlugins() {
</EmptyStateText> </EmptyStateText>
</div> </div>
) : ( ) : (
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight"> <Table>
<thead> <TableHead>
<tr> <TableRow>
<th className="py-2 text-left">Plugin</th> <TableHeaderCell>Name</TableHeaderCell>
<th className="py-2 text-right">Version</th> <TableHeaderCell>Version</TableHeaderCell>
<th></th> <TableHeaderCell>Description</TableHeaderCell>
</tr> <TableHeaderCell />
</thead> </TableRow>
</TableHead>
<tbody className="divide-y divide-surface-highlight"> <tbody className="divide-y divide-surface-highlight">
{plugins.map((p) => ( {plugins.map((p) => {
<PluginInfo key={p.id} plugin={p} /> return <PluginTableRow key={p.id} plugin={p} updates={updates.data ?? null} />;
))} })}
</tbody> </tbody>
</table> </Table>
); );
} }
+2 -1
View File
@@ -33,6 +33,7 @@ const icons = {
chevron_down: lucide.ChevronDownIcon, chevron_down: lucide.ChevronDownIcon,
chevron_right: lucide.ChevronRightIcon, chevron_right: lucide.ChevronRightIcon,
circle_alert: lucide.CircleAlertIcon, circle_alert: lucide.CircleAlertIcon,
circle_fading_arrow_up: lucide.CircleFadingArrowUpIcon,
clock: lucide.ClockIcon, clock: lucide.ClockIcon,
code: lucide.CodeIcon, code: lucide.CodeIcon,
columns_2: lucide.Columns2Icon, columns_2: lucide.Columns2Icon,
@@ -133,7 +134,7 @@ export const Icon = memo(function Icon({
title={title} title={title}
className={classNames( className={classNames(
className, className,
'flex-shrink-0 transform-cpu', 'flex-shrink-0',
size === 'xl' && 'h-6 w-6', size === 'xl' && 'h-6 w-6',
size === 'lg' && 'h-5 w-5', size === 'lg' && 'h-5 w-5',
size === 'md' && 'h-4 w-4', size === 'md' && 'h-4 w-4',
+18 -12
View File
@@ -6,6 +6,7 @@ import type { ButtonProps } from './Button';
import { Button } from './Button'; import { Button } from './Button';
import type { IconProps } from './Icon'; import type { IconProps } from './Icon';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LoadingIcon } from './LoadingIcon';
export type IconButtonProps = IconProps & export type IconButtonProps = IconProps &
ButtonProps & { ButtonProps & {
@@ -31,6 +32,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
iconSize, iconSize,
showBadge, showBadge,
iconColor, iconColor,
isLoading,
...props ...props
}: IconButtonProps, }: IconButtonProps,
ref, ref,
@@ -70,18 +72,22 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
<div className="w-2.5 h-2.5 bg-pink-500 rounded-full" /> <div className="w-2.5 h-2.5 bg-pink-500 rounded-full" />
</div> </div>
)} )}
<Icon {isLoading ? (
size={iconSize} <LoadingIcon size={iconSize} className={iconClassName} />
icon={confirmed ? 'check' : icon} ) : (
spin={spin} <Icon
color={iconColor} size={iconSize}
className={classNames( icon={confirmed ? 'check' : icon}
iconClassName, spin={spin}
'group-hover/button:text-text', color={iconColor}
confirmed && '!text-success', // Don't use Icon.color here because it won't override the hover color className={classNames(
props.disabled && 'opacity-70', iconClassName,
)} 'group-hover/button:text-text',
/> confirmed && '!text-success', // Don't use Icon.color here because it won't override the hover color
props.disabled && 'opacity-70',
)}
/>
)}
</Button> </Button>
); );
}); });
+1 -1
View File
@@ -53,7 +53,7 @@ export function TableHeaderCell({
children, children,
className, className,
}: { }: {
children: ReactNode; children?: ReactNode;
className?: string; className?: string;
}) { }) {
return ( return (
+12 -5
View File
@@ -1,16 +1,23 @@
import { useQuery } from '@tanstack/react-query'; 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 { queryClient } from '../lib/queryClient';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
function pluginInfoKey(id: string) { function pluginInfoKey(id: string, plugin: Plugin | null) {
return ['plugin_info', id]; return ['plugin_info', id, plugin?.updatedAt ?? 'n/a'];
} }
export function usePluginInfo(id: string) { 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({ return useQuery({
queryKey: pluginInfoKey(id), queryKey: pluginInfoKey(id, plugin ?? null),
queryFn: () => invokeCmd<BootResponse>('cmd_plugin_info', { id }), placeholderData: (prev) => prev, // Keep previous data on refetch
queryFn: () => invokeCmd<PluginMetadata>('cmd_plugin_info', { id }),
}); });
} }
-11
View File
@@ -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 });
},
});
}
+25
View File
@@ -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 <InlineCode>{name}</InlineCode>?
</>
),
});
if (confirmed) {
await invokeCmd('cmd_uninstall_plugin', { pluginId });
}
},
});
}