diff --git a/packages/plugin-runtime-types/src/bindings/gen_events.ts b/packages/plugin-runtime-types/src/bindings/gen_events.ts index 1a82fb1b..001a58d0 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_events.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_events.ts @@ -372,7 +372,7 @@ export type ImportResponse = { resources: ImportResources, }; export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; -export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; +export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & BootResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; export type JsonPrimitive = string | number | boolean | null; diff --git a/packages/plugin-runtime-types/src/bindings/gen_search.ts b/packages/plugin-runtime-types/src/bindings/gen_search.ts new file mode 100644 index 00000000..f0c56473 --- /dev/null +++ b/packages/plugin-runtime-types/src/bindings/gen_search.ts @@ -0,0 +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 PluginVersion = { id: string, version: string, description: string | null, displayName: string, homepageUrl: string | null, repositoryUrl: string, checksum: string, readme: string | null, yanked: boolean, }; diff --git a/packages/plugin-runtime/src/PluginInstance.ts b/packages/plugin-runtime/src/PluginInstance.ts index 9120d4b9..2cf0921e 100644 --- a/packages/plugin-runtime/src/PluginInstance.ts +++ b/packages/plugin-runtime/src/PluginInstance.ts @@ -1,5 +1,6 @@ import { BootRequest, + BootResponse, DeleteKeyValueResponse, FindHttpResponsesResponse, FormInput, @@ -52,9 +53,22 @@ export class PluginInstance { // Reload plugin if the JS or package.json changes const windowContextNone: PluginWindowContext = { type: 'none' }; + + this.#mod = {}; + this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8')); + + const bootResponse: BootResponse = { + name: this.#pkg.name ?? 'unknown', + version: this.#pkg.version ?? '0.0.1', + }; + const fileChangeCallback = async () => { this.#importModule(); - return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null); + return this.#sendPayload( + windowContextNone, + { type: 'reload_response', ...bootResponse }, + null, + ); }; if (this.#workerData.bootRequest.watch) { @@ -62,12 +76,6 @@ export class PluginInstance { watchFile(this.#pathPkg(), fileChangeCallback); } - this.#mod = {}; - this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8')); - - // TODO: Re-implement this now that we're not using workers - // prefixStdout(`[plugin][${this.#pkg.name}] %s`); - this.#importModule(); } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6af8a024..2979c14f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -674,6 +674,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -926,6 +945,32 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -1080,6 +1125,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1198,6 +1249,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "deranged" version = "0.4.0" @@ -1326,6 +1383,15 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "document-features" version = "0.2.11" @@ -1593,6 +1659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -2920,6 +2987,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "liblzma" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libredox" version = "0.1.3" @@ -2956,6 +3043,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + [[package]] name = "libz-sys" version = "1.1.22" @@ -3712,6 +3808,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3823,6 +3929,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4557,9 +4673,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.19" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "async-compression", "base64 0.22.1", @@ -4577,13 +4693,11 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", @@ -4596,7 +4710,6 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", - "tokio-socks", "tokio-util", "tower 0.5.2", "tower-http", @@ -4704,6 +4817,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rust_decimal" version = "1.37.1" @@ -5731,6 +5855,26 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4976ac728ebc0487515aa956cfdf200abcc52b784e441493fc544bc6ce369c8" +dependencies = [ + "dunce", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.12", + "tracing", + "url", + "windows-registry", + "windows-result", +] + [[package]] name = "tauri-plugin-dialog" version = "2.2.2" @@ -5863,6 +6007,7 @@ dependencies = [ "serde", "serde_json", "tauri", + "tauri-plugin-deep-link", "thiserror 2.0.12", "tracing", "windows-sys 0.59.0", @@ -5898,7 +6043,7 @@ dependencies = [ "tokio", "url", "windows-sys 0.59.0", - "zip", + "zip 2.4.2", ] [[package]] @@ -6137,6 +6282,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -6211,18 +6365,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-socks" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" -dependencies = [ - "either", - "futures-util", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.17" @@ -6500,6 +6642,12 @@ dependencies = [ "petgraph", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" @@ -7644,6 +7792,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-clipboard-manager", + "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-log", @@ -7678,7 +7827,10 @@ name = "yaak-common" version = "0.1.0" dependencies = [ "regex", + "reqwest", + "serde", "tauri", + "thiserror 2.0.12", ] [[package]] @@ -7777,6 +7929,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.12", "ts-rs", + "yaak-common", "yaak-models", ] @@ -7823,16 +7976,21 @@ name = "yaak-plugins" version = "0.1.0" dependencies = [ "base64 0.22.1", + "chrono", "dunce", "futures-util", + "hex", "log", "md5", "path-slash", "rand 0.9.1", "regex", + "reqwest", "serde", "serde_json", + "sha2", "tauri", + "tauri-plugin", "tauri-plugin-shell", "thiserror 2.0.12", "tokio", @@ -7842,6 +8000,7 @@ dependencies = [ "yaak-crypto", "yaak-models", "yaak-templates", + "zip-extract", ] [[package]] @@ -8037,6 +8196,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] [[package]] name = "zerotrie" @@ -8086,6 +8259,89 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "zip" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap 2.9.0", + "liblzma", + "memchr", + "pbkdf2", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zip-extract" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aed5f10c571472911e37d8f7601a8dfba52b4f7f73a344015291b82ab292faf6" +dependencies = [ + "log", + "thiserror 2.0.12", + "zip 4.0.0", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.5.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f1527816..a4b16f1a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,9 +1,9 @@ [workspace] members = [ "yaak-crypto", + "yaak-fonts", "yaak-git", "yaak-grpc", - "yaak-fonts", "yaak-http", "yaak-license", "yaak-mac-window", @@ -40,7 +40,7 @@ tauri-build = { version = "2.2.0", features = [] } openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work [dependencies] -chrono = { version = "0.4.31", features = ["serde"] } +chrono = { workspace = true, features = ["serde"] } cookie = "0.18.1" encoding_rs = "0.8.35" eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" } @@ -56,27 +56,28 @@ serde_json = { workspace = true, features = ["raw_value"] } tauri = { workspace = true, features = ["devtools", "protocol-asset"] } tauri-plugin-clipboard-manager = "2.2.2" tauri-plugin-dialog = { workspace = true } -tauri-plugin-fs = "2.2.0" -tauri-plugin-log = { version = "2.3.1", features = ["colored"] } +tauri-plugin-fs = "2.3.0" +tauri-plugin-log = { version = "2.4.0", features = ["colored"] } tauri-plugin-opener = "2.2.6" tauri-plugin-os = "2.2.1" tauri-plugin-shell = { workspace = true } -tauri-plugin-single-instance = "2.2.2" -tauri-plugin-updater = "2.6.1" -tauri-plugin-window-state = "2.2.1" +tauri-plugin-deep-link = "2.3.0" +tauri-plugin-single-instance = { version = "2.2.4", features = ["deep-link"] } +tauri-plugin-updater = "2.7.1" +tauri-plugin-window-state = "2.2.2" thiserror = { workspace = true } tokio = { workspace = true, features = ["sync"] } tokio-stream = "0.1.17" uuid = "1.12.1" yaak-common = { workspace = true } yaak-crypto = { workspace = true } +yaak-fonts = { workspace = true } yaak-git = { path = "yaak-git" } yaak-grpc = { path = "yaak-grpc" } yaak-http = { workspace = true } yaak-license = { path = "yaak-license" } yaak-mac-window = { path = "yaak-mac-window" } yaak-models = { workspace = true } -yaak-fonts = { workspace = true } yaak-plugins = { workspace = true } yaak-sse = { workspace = true } yaak-sync = { workspace = true } @@ -84,7 +85,9 @@ yaak-templates = { workspace = true } yaak-ws = { path = "yaak-ws" } [workspace.dependencies] -reqwest = "0.12.19" +chrono = "0.4.41" +hex = "0.4.3" +reqwest = "0.12.20" serde = "1.0.219" serde_json = "1.0.140" tauri = "2.5.1" @@ -96,7 +99,9 @@ thiserror = "2.0.12" ts-rs = "11.0.1" rustls = { version = "0.23.27", default-features = false } rustls-platform-verifier = "0.6.0" +sha2 = "0.10.9" yaak-common = { path = "yaak-common" } +yaak-crypto = { path = "yaak-crypto" } yaak-fonts = { path = "yaak-fonts" } yaak-http = { path = "yaak-http" } yaak-models = { path = "yaak-models" } @@ -104,4 +109,3 @@ yaak-plugins = { path = "yaak-plugins" } yaak-sse = { path = "yaak-sse" } yaak-sync = { path = "yaak-sync" } yaak-templates = { path = "yaak-templates" } -yaak-crypto = { path = "yaak-crypto" } diff --git a/src-tauri/capabilities/capabilities.json b/src-tauri/capabilities/capabilities.json index 993acf6c..3eca9861 100644 --- a/src-tauri/capabilities/capabilities.json +++ b/src-tauri/capabilities/capabilities.json @@ -52,11 +52,12 @@ "opener:allow-reveal-item-in-dir", "shell:allow-open", "yaak-crypto:default", - "yaak-git:default", "yaak-fonts:default", + "yaak-git:default", "yaak-license:default", "yaak-mac-window:default", "yaak-models:default", + "yaak-plugins:default", "yaak-sync:default", "yaak-ws:default" ] diff --git a/src-tauri/macos/entitlements.plist b/src-tauri/macos/entitlements.plist index 67fd398e..7902d8a7 100644 --- a/src-tauri/macos/entitlements.plist +++ b/src-tauri/macos/entitlements.plist @@ -2,10 +2,6 @@ - - com.apple.security.cs.allow-unsigned-executable-memory - - diff --git a/src-tauri/src/history.rs b/src-tauri/src/history.rs index 1ae629c7..b1a2882e 100644 --- a/src-tauri/src/history.rs +++ b/src-tauri/src/history.rs @@ -43,18 +43,6 @@ pub async fn store_launch_history(app_handle: &AppHandle) -> Laun info } -pub fn get_os() -> &'static str { - if cfg!(target_os = "windows") { - "windows" - } else if cfg!(target_os = "macos") { - "macos" - } else if cfg!(target_os = "linux") { - "linux" - } else { - "unknown" - } -} - pub async fn get_num_launches(app_handle: &AppHandle) -> i32 { app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0) } diff --git a/src-tauri/src/import.rs b/src-tauri/src/import.rs new file mode 100644 index 00000000..5a118d93 --- /dev/null +++ b/src-tauri/src/import.rs @@ -0,0 +1,105 @@ +use crate::error::Result; +use log::info; +use std::collections::BTreeMap; +use std::fs::read_to_string; +use tauri::{Manager, Runtime, WebviewWindow}; +use yaak_models::models::{ + Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace, +}; +use yaak_models::query_manager::QueryManagerExt; +use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt}; +use yaak_plugins::manager::PluginManager; + +pub(crate) async fn import_data( + window: &WebviewWindow, + file_path: &str, +) -> Result { + let plugin_manager = window.state::(); + let file = + read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); + let file_contents = file.as_str(); + let import_result = plugin_manager.import_data(window, file_contents).await?; + + let mut id_map: BTreeMap = BTreeMap::new(); + + let resources = import_result.resources; + + let workspaces: Vec = resources + .workspaces + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); + v + }) + .collect(); + + let environments: Vec = resources + .environments + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); + v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); + v + }) + .collect(); + + let folders: Vec = resources + .folders + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); + v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); + v.folder_id = maybe_gen_id_opt::(v.folder_id, &mut id_map); + v + }) + .collect(); + + let http_requests: Vec = resources + .http_requests + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); + v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); + v.folder_id = maybe_gen_id_opt::(v.folder_id, &mut id_map); + v + }) + .collect(); + + let grpc_requests: Vec = resources + .grpc_requests + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); + v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); + v.folder_id = maybe_gen_id_opt::(v.folder_id, &mut id_map); + v + }) + .collect(); + + let websocket_requests: Vec = resources + .websocket_requests + .into_iter() + .map(|mut v| { + v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); + v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); + v.folder_id = maybe_gen_id_opt::(v.folder_id, &mut id_map); + v + }) + .collect(); + + info!("Importing data"); + + let upserted = window.with_tx(|tx| { + tx.batch_upsert( + workspaces, + environments, + folders, + http_requests, + grpc_requests, + websocket_requests, + &UpdateSource::Import, + ) + })?; + + Ok(upserted) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9f9dffbf..8adeee2c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,14 +3,15 @@ use crate::encoding::read_response_body; use crate::error::Error::GenericError; use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request}; use crate::http_request::send_http_request; +use crate::import::import_data; use crate::notifications::YaakNotifier; use crate::render::{render_grpc_request, render_template}; use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater}; -use crate::uri_scheme::handle_uri_scheme; +use crate::uri_scheme::handle_deep_link; use error::Result as YaakResult; use eventsource_client::{EventParser, SSE}; use log::{debug, error, info, warn}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::fs::{File, create_dir_all}; use std::path::PathBuf; use std::str::FromStr; @@ -19,24 +20,21 @@ use std::{fs, panic}; use tauri::{AppHandle, Emitter, RunEvent, State, WebviewWindow, is_dev}; use tauri::{Listener, Runtime}; use tauri::{Manager, WindowEvent}; +use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_log::fern::colors::ColoredLevelConfig; use tauri_plugin_log::{Builder, Target, TargetKind}; use tauri_plugin_window_state::{AppHandleExt, StateFlags}; -use tokio::fs::read_to_string; use tokio::sync::Mutex; use tokio::task::block_in_place; use yaak_common::window::WorkspaceWindowTrait; use yaak_grpc::manager::{DynamicMessage, GrpcHandle}; use yaak_grpc::{Code, ServiceDefinition, deserialize_message, serialize_message}; use yaak_models::models::{ - CookieJar, Environment, Folder, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, - GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, WebsocketRequest, Workspace, - WorkspaceMeta, + CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, + GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, Plugin, Workspace, WorkspaceMeta, }; use yaak_models::query_manager::QueryManagerExt; -use yaak_models::util::{ - BatchUpsertResult, UpdateSource, get_workspace_export_resources, maybe_gen_id, maybe_gen_id_opt, -}; +use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; use yaak_plugins::events::{ BootResponse, CallHttpRequestActionRequest, FilterResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, @@ -55,6 +53,7 @@ mod error; mod grpc; mod history; mod http_request; +mod import; mod notifications; mod plugin_events; mod render; @@ -778,98 +777,9 @@ async fn cmd_get_sse_events(file_path: &str) -> YaakResult> #[tauri::command] async fn cmd_import_data( window: WebviewWindow, - app_handle: AppHandle, - plugin_manager: State<'_, PluginManager>, file_path: &str, ) -> YaakResult { - let file = read_to_string(file_path) - .await - .unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); - let file_contents = file.as_str(); - let import_result = plugin_manager.import_data(&window, file_contents).await?; - - let mut id_map: BTreeMap = BTreeMap::new(); - - let resources = import_result.resources; - - let workspaces: Vec = resources - .workspaces - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); - v - }) - .collect(); - - let environments: Vec = resources - .environments - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); - v - }) - .collect(); - - let folders: Vec = resources - .folders - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); - v.folder_id = maybe_gen_id_opt::(v.folder_id, &mut id_map); - v - }) - .collect(); - - let http_requests: Vec = resources - .http_requests - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); - v.folder_id = maybe_gen_id_opt::(v.folder_id, &mut id_map); - v - }) - .collect(); - - let grpc_requests: Vec = resources - .grpc_requests - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); - v.folder_id = maybe_gen_id_opt::(v.folder_id, &mut id_map); - v - }) - .collect(); - - let websocket_requests: Vec = resources - .websocket_requests - .into_iter() - .map(|mut v| { - v.id = maybe_gen_id::(v.id.as_str(), &mut id_map); - v.workspace_id = maybe_gen_id::(v.workspace_id.as_str(), &mut id_map); - v.folder_id = maybe_gen_id_opt::(v.folder_id, &mut id_map); - v - }) - .collect(); - - info!("Importing data"); - - let upserted = app_handle.with_tx(|tx| { - tx.batch_upsert( - workspaces, - environments, - folders, - http_requests, - grpc_requests, - websocket_requests, - &UpdateSource::Import, - ) - })?; - - Ok(upserted) + import_data(&window, file_path).await } #[tauri::command] @@ -1066,7 +976,7 @@ async fn cmd_install_plugin( app_handle: AppHandle, window: WebviewWindow, ) -> YaakResult { - plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &directory, true).await?; + plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &directory).await?; Ok(app_handle.db().upsert_plugin( &Plugin { @@ -1255,6 +1165,7 @@ pub fn run() { .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_window_state::Builder::default().build()) + .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::default().build()) .plugin(tauri_plugin_dialog::init()) @@ -1272,6 +1183,21 @@ pub fn run() { builder .setup(|app| { + { + let app_handle = app.app_handle().clone(); + app.deep_link().on_open_url(move |event| { + info!("Handling deep link open"); + let app_handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + for url in event.urls() { + if let Err(e) = handle_deep_link(&app_handle, &url).await { + warn!("Failed to handle deep link {}: {e:?}", url.to_string()); + }; + } + }); + }); + }; + let app_data_dir = app.path().app_data_dir().unwrap(); create_dir_all(app_data_dir.clone()).expect("Problem creating App directory!"); @@ -1332,7 +1258,6 @@ pub fn run() { crate::commands::cmd_secure_template, crate::commands::cmd_show_workspace_key, ]) - .register_uri_scheme_protocol("yaak", handle_uri_scheme) .build(tauri::generate_context!()) .expect("error while running tauri application") .run(|app_handle, event| { diff --git a/src-tauri/src/notifications.rs b/src-tauri/src/notifications.rs index 29fa1bc6..88b28577 100644 --- a/src-tauri/src/notifications.rs +++ b/src-tauri/src/notifications.rs @@ -1,13 +1,14 @@ use std::time::SystemTime; use crate::error::Result; -use crate::history::{get_num_launches, get_os}; +use crate::history::get_num_launches; use chrono::{DateTime, Duration, Utc}; use log::debug; use reqwest::Method; use serde::{Deserialize, Serialize}; use serde_json::Value; use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow}; +use yaak_common::platform::get_os; use yaak_license::{LicenseCheckStatus, check_license}; use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::UpdateSource; diff --git a/src-tauri/src/plugin_events.rs b/src-tauri/src/plugin_events.rs index 9da13249..9ebac1db 100644 --- a/src-tauri/src/plugin_events.rs +++ b/src-tauri/src/plugin_events.rs @@ -126,7 +126,7 @@ pub(crate) async fn handle_plugin_event( Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await; None } - InternalEventPayload::ReloadResponse(_) => { + InternalEventPayload::ReloadResponse(r) => { let plugins = app_handle.db().list_plugins().unwrap(); for plugin in plugins { if plugin.directory != plugin_handle.dir { @@ -142,7 +142,7 @@ pub(crate) async fn handle_plugin_event( let toast_event = plugin_handle.build_event_to_send( &window_context, &InternalEventPayload::ShowToastRequest(ShowToastRequest { - message: format!("Reloaded plugin {}", plugin_handle.dir), + message: format!("Reloaded plugin {}@{}", r.name, r.version), icon: Some(Icon::Info), ..Default::default() }), diff --git a/src-tauri/src/uri_scheme.rs b/src-tauri/src/uri_scheme.rs index a9a932dd..eff52696 100644 --- a/src-tauri/src/uri_scheme.rs +++ b/src-tauri/src/uri_scheme.rs @@ -1,25 +1,74 @@ +use crate::error::Result; +use crate::import::import_data; use log::{info, warn}; -use tauri::{Manager, Runtime, UriSchemeContext}; +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; -pub(crate) fn handle_uri_scheme( - a: UriSchemeContext, - req: http::Request>, -) -> http::Response> { - println!("------------- Yaak URI scheme invoked!"); - let uri = req.uri(); - let window = a - .app_handle() - .get_webview_window(a.webview_label()) - .expect("Failed to get webview window for URI scheme event"); - info!("Yaak URI scheme invoked with {uri:?} {window:?}"); +pub(crate) async fn handle_deep_link( + app_handle: &AppHandle, + url: &Url, +) -> Result<()> { + let command = url.domain().unwrap_or_default(); + info!("Yaak URI scheme invoked {}?{}", command, url.query().unwrap_or_default()); - let path = uri.path(); - if path == "/data/import" { - warn!("TODO: import data") - } else if path == "/plugins/install" { - warn!("TODO: install plugin") + let query_map: HashMap = url.query_pairs().into_owned().collect(); + let windows = app_handle.webview_windows(); + let (_, window) = windows.iter().next().unwrap(); + + match command { + "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 + )) + .kind(MessageDialogKind::Info) + .buttons(MessageDialogButtons::OkCustom("Install".to_string())) + .blocking_show(); + if !confirmed_install { + // Cancelled installation + return Ok(()); + } + + download_and_install(window, &plugin_version).await?; + app_handle.emit( + "show_toast", + ShowToastRequest { + message: format!( + "Installed {}@{}", + plugin_version.name, plugin_version.version + ), + color: Some(Color::Success), + icon: None, + }, + )?; + } + "import-data" => { + let file_path = query_map.get("path").unwrap(); + let results = import_data(window, file_path).await?; + _ = window.set_focus(); + window.emit( + "show_toast", + ShowToastRequest { + message: format!("Imported data for {} workspaces", results.workspaces.len()), + color: Some(Color::Success), + icon: None, + }, + )?; + } + _ => { + warn!("Unknown deep link command: {command}"); + } } - let msg = format!("No handler found for {path}"); - tauri::http::Response::builder().status(404).body(msg.as_bytes().to_vec()).unwrap() + Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 16a28fac..c7a98979 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -23,7 +23,6 @@ }, "plugins": { "deep-link": { - "mobile": [], "desktop": { "schemes": [ "yaak" diff --git a/src-tauri/yaak-common/Cargo.toml b/src-tauri/yaak-common/Cargo.toml index 1186bf98..be033751 100644 --- a/src-tauri/yaak-common/Cargo.toml +++ b/src-tauri/yaak-common/Cargo.toml @@ -6,4 +6,7 @@ publish = false [dependencies] tauri = { workspace = true } +reqwest = { workspace = true, features = ["system-proxy", "gzip"] } +thiserror = { workspace = true } regex = "1.11.0" +serde = { workspace = true, features = ["derive"] } diff --git a/src-tauri/yaak-common/src/api_client.rs b/src-tauri/yaak-common/src/api_client.rs new file mode 100644 index 00000000..79c40595 --- /dev/null +++ b/src-tauri/yaak-common/src/api_client.rs @@ -0,0 +1,23 @@ +use crate::error::Result; +use crate::platform::get_ua_platform; +use reqwest::Client; +use std::time::Duration; +use tauri::http::{HeaderMap, HeaderValue}; +use tauri::{AppHandle, Runtime}; + +pub fn yaak_api_client(app_handle: &AppHandle) -> Result { + let platform = get_ua_platform(); + let version = app_handle.package_info().version.clone(); + let ua = format!("Yaak/{version} ({platform})"); + let mut default_headers = HeaderMap::new(); + default_headers.insert("Accept", HeaderValue::from_str("application/json").unwrap()); + + let client = reqwest::ClientBuilder::new() + .timeout(Duration::from_secs(20)) + .default_headers(default_headers) + .gzip(true) + .user_agent(ua) + .build()?; + + Ok(client) +} diff --git a/src-tauri/yaak-common/src/error.rs b/src-tauri/yaak-common/src/error.rs new file mode 100644 index 00000000..46a9c103 --- /dev/null +++ b/src-tauri/yaak-common/src/error.rs @@ -0,0 +1,19 @@ +use serde::{Serialize, Serializer}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +pub type Result = std::result::Result; diff --git a/src-tauri/yaak-common/src/lib.rs b/src-tauri/yaak-common/src/lib.rs index d42d7dbe..6229a01f 100644 --- a/src-tauri/yaak-common/src/lib.rs +++ b/src-tauri/yaak-common/src/lib.rs @@ -1 +1,4 @@ -pub mod window; \ No newline at end of file +pub mod window; +pub mod platform; +pub mod api_client; +pub mod error; \ No newline at end of file diff --git a/src-tauri/yaak-common/src/platform.rs b/src-tauri/yaak-common/src/platform.rs new file mode 100644 index 00000000..4e1f2587 --- /dev/null +++ b/src-tauri/yaak-common/src/platform.rs @@ -0,0 +1,23 @@ +pub fn get_os() -> &'static str { + if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "macos") { + "macos" + } else if cfg!(target_os = "linux") { + "linux" + } else { + "unknown" + } +} + +pub fn get_ua_platform() -> &'static str { + if cfg!(target_os = "windows") { + "Win" + } else if cfg!(target_os = "macos") { + "Mac" + } else if cfg!(target_os = "linux") { + "Linux" + } else { + "Unknown" + } +} diff --git a/src-tauri/yaak-crypto/Cargo.toml b/src-tauri/yaak-crypto/Cargo.toml index 0203c011..9f838038 100644 --- a/src-tauri/yaak-crypto/Cargo.toml +++ b/src-tauri/yaak-crypto/Cargo.toml @@ -13,7 +13,7 @@ keyring = { version = "4.0.0-rc.1" } log = "0.4.26" serde = { workspace = true, features = ["derive"] } tauri = { workspace = true } -thiserror = "2.0.12" +thiserror = { workspace = true } yaak-models = { workspace = true } [build-dependencies] diff --git a/src-tauri/yaak-git/Cargo.toml b/src-tauri/yaak-git/Cargo.toml index 0fe6f69e..7698382e 100644 --- a/src-tauri/yaak-git/Cargo.toml +++ b/src-tauri/yaak-git/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" publish = false [dependencies] -chrono = { version = "0.4.38", features = ["serde"] } +chrono = { workspace = true, features = ["serde"] } git2 = { version = "0.20.0", features = ["vendored-libgit2", "vendored-openssl"] } log = "0.4.22" serde = { workspace = true, features = ["derive"] } diff --git a/src-tauri/yaak-license/Cargo.toml b/src-tauri/yaak-license/Cargo.toml index 67d62cb0..38dc136a 100644 --- a/src-tauri/yaak-license/Cargo.toml +++ b/src-tauri/yaak-license/Cargo.toml @@ -15,6 +15,7 @@ tauri = { workspace = true } thiserror = { workspace = true } ts-rs = { workspace = true } yaak-models = { workspace = true } +yaak-common = { workspace = true } [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } diff --git a/src-tauri/yaak-license/src/lib.rs b/src-tauri/yaak-license/src/lib.rs index dd8fd540..03b76bfc 100644 --- a/src-tauri/yaak-license/src/lib.rs +++ b/src-tauri/yaak-license/src/lib.rs @@ -16,15 +16,3 @@ pub fn init() -> TauriPlugin { .invoke_handler(generate_handler![check, activate, deactivate]) .build() } - -pub(crate) fn get_os() -> &'static str { - if cfg!(target_os = "windows") { - "windows" - } else if cfg!(target_os = "macos") { - "macos" - } else if cfg!(target_os = "linux") { - "linux" - } else { - "unknown" - } -} diff --git a/src-tauri/yaak-license/src/license.rs b/src-tauri/yaak-license/src/license.rs index 9e22f858..48c40635 100644 --- a/src-tauri/yaak-license/src/license.rs +++ b/src-tauri/yaak-license/src/license.rs @@ -7,6 +7,7 @@ use std::ops::Add; use std::time::Duration; use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev}; use ts_rs::TS; +use yaak_common::platform::get_os; use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::UpdateSource; @@ -68,7 +69,7 @@ pub async fn activate_license( let client = reqwest::Client::new(); let payload = ActivateLicenseRequestPayload { license_key: license_key.to_string(), - app_platform: crate::get_os().to_string(), + app_platform: get_os().to_string(), app_version: window.app_handle().package_info().version.to_string(), }; let response = client.post(build_url("/licenses/activate")).json(&payload).send().await?; @@ -107,7 +108,7 @@ pub async fn deactivate_license(window: &WebviewWindow) -> Result let client = reqwest::Client::new(); let path = format!("/licenses/activations/{}/deactivate", activation_id); let payload = DeactivateLicenseRequestPayload { - app_platform: crate::get_os().to_string(), + app_platform: get_os().to_string(), app_version: window.app_handle().package_info().version.to_string(), }; let response = client.post(build_url(&path)).json(&payload).send().await?; @@ -149,7 +150,7 @@ pub enum LicenseCheckStatus { pub async fn check_license(window: &WebviewWindow) -> Result { let payload = CheckActivationRequestPayload { - app_platform: crate::get_os().to_string(), + app_platform: get_os().to_string(), app_version: window.package_info().version.to_string(), }; let activation_id = get_activation_id(window.app_handle()).await; diff --git a/src-tauri/yaak-models/Cargo.toml b/src-tauri/yaak-models/Cargo.toml index b69bb5e5..ba26760d 100644 --- a/src-tauri/yaak-models/Cargo.toml +++ b/src-tauri/yaak-models/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] chrono = { version = "0.4.38", features = ["serde"] } -hex = "0.4.3" +hex = { workspace = true } include_dir = "0.7" log = "0.4.22" nanoid = "0.4.0" @@ -18,10 +18,10 @@ sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] } sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -sha2 = "0.10.9" -tauri = { workspace = true} +sha2 = { workspace = true } +tauri = { workspace = true } tauri-plugin-dialog = { workspace = true } -thiserror = "2.0.11" +thiserror = { workspace = true } tokio = { workspace = true } ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] } diff --git a/src-tauri/yaak-models/bindings/gen_models.ts b/src-tauri/yaak-models/bindings/gen_models.ts index 7da8e425..c36185b2 100644 --- a/src-tauri/yaak-models/bindings/gen_models.ts +++ b/src-tauri/yaak-models/bindings/gen_models.ts @@ -58,7 +58,7 @@ export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, }; -export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, bypass: string, } | { "type": "disabled" }; +export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" }; export type ProxySettingAuth = { user: string, password: string, }; diff --git a/src-tauri/yaak-plugins/Cargo.toml b/src-tauri/yaak-plugins/Cargo.toml index f34d7630..3d877ca6 100644 --- a/src-tauri/yaak-plugins/Cargo.toml +++ b/src-tauri/yaak-plugins/Cargo.toml @@ -1,10 +1,12 @@ [package] name = "yaak-plugins" +links = "yaak-plugins" version = "0.1.0" edition = "2024" publish = false [dependencies] +base64 = "0.22.1" dunce = "1.0.4" futures-util = "0.3.30" log = "0.4.21" @@ -12,16 +14,23 @@ md5 = "0.7.0" path-slash = "0.2.1" rand = "0.9.0" regex = "1.10.6" +reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tauri = { workspace = true } tauri-plugin-shell = { workspace = true } -thiserror = "2.0.7" +thiserror = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] } tokio-tungstenite = "0.26.1" ts-rs = { workspace = true, features = ["import-esm"] } +sha2 = { workspace = true } +yaak-common = { workspace = true } +yaak-crypto = { workspace = true } yaak-models = { workspace = true } yaak-templates = { workspace = true } -yaak-crypto = { workspace = true } -yaak-common = { workspace = true } -base64 = "0.22.1" +zip-extract = "0.4.0" +chrono = { workspace = true } +hex = { workspace = true } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } diff --git a/src-tauri/yaak-plugins/bindings/gen_events.ts b/src-tauri/yaak-plugins/bindings/gen_events.ts index 1a82fb1b..001a58d0 100644 --- a/src-tauri/yaak-plugins/bindings/gen_events.ts +++ b/src-tauri/yaak-plugins/bindings/gen_events.ts @@ -372,7 +372,7 @@ export type ImportResponse = { resources: ImportResources, }; export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; -export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; +export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & BootResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; export type JsonPrimitive = string | number | boolean | null; diff --git a/src-tauri/yaak-plugins/bindings/gen_search.ts b/src-tauri/yaak-plugins/bindings/gen_search.ts new file mode 100644 index 00000000..f0c56473 --- /dev/null +++ b/src-tauri/yaak-plugins/bindings/gen_search.ts @@ -0,0 +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 PluginVersion = { id: string, version: string, description: string | null, displayName: string, homepageUrl: string | null, repositoryUrl: string, checksum: string, readme: string | null, yanked: boolean, }; diff --git a/src-tauri/yaak-plugins/build.rs b/src-tauri/yaak-plugins/build.rs new file mode 100644 index 00000000..88527efa --- /dev/null +++ b/src-tauri/yaak-plugins/build.rs @@ -0,0 +1,5 @@ +const COMMANDS: &[&str] = &["search", "install"]; + +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 2a389fc3..10ec0c89 100644 --- a/src-tauri/yaak-plugins/index.ts +++ b/src-tauri/yaak-plugins/index.ts @@ -1,2 +1,14 @@ +import { invoke } from '@tauri-apps/api/core'; +import { PluginSearchResponse, PluginVersion } from './bindings/gen_search'; + export * from './bindings/gen_models'; export * from './bindings/gen_events'; +export * from './bindings/gen_search'; + +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 }); +} diff --git a/src-tauri/yaak-plugins/permissions/default.toml b/src-tauri/yaak-plugins/permissions/default.toml new file mode 100644 index 00000000..054af4e3 --- /dev/null +++ b/src-tauri/yaak-plugins/permissions/default.toml @@ -0,0 +1,3 @@ +[default] +description = "Default permissions for the plugin" +permissions = ["allow-search", "allow-install"] diff --git a/src-tauri/yaak-plugins/src/api.rs b/src-tauri/yaak-plugins/src/api.rs new file mode 100644 index 00000000..26e63463 --- /dev/null +++ b/src-tauri/yaak-plugins/src/api.rs @@ -0,0 +1,67 @@ +use crate::commands::{PluginSearchResponse, PluginVersion}; +use crate::error::Result; +use reqwest::{Response, Url}; +use std::str::FromStr; +use log::info; +use tauri::{AppHandle, Runtime, is_dev}; +use yaak_common::api_client::yaak_api_client; +use crate::error::Error::ApiErr; + +pub async fn get_plugin( + app_handle: &AppHandle, + name: &str, + version: Option, +) -> Result { + info!("Getting plugin: {name} {version:?}"); + let mut url = base_url(&format!("/{name}")); + if let Some(version) = version { + let mut query_pairs = url.query_pairs_mut(); + query_pairs.append_pair("version", &version); + }; + let resp = yaak_api_client(app_handle)?.get(url.clone()).send().await?; + if !resp.status().is_success() { + return Err(ApiErr(format!("{} response to {}", resp.status(), url.to_string()))); + } + Ok(resp.json().await?) +} + +pub async fn download_plugin_archive( + app_handle: &AppHandle, + plugin_version: &PluginVersion, +) -> Result { + let name = plugin_version.name.clone(); + let version = plugin_version.version.clone(); + info!("Downloading plugin: {name} {version}"); + let mut url = base_url(&format!("/{}/download", name)); + { + let mut query_pairs = url.query_pairs_mut(); + query_pairs.append_pair("version", &version); + }; + let resp = yaak_api_client(app_handle)?.get(url.clone()).send().await?; + if !resp.status().is_success() { + return Err(ApiErr(format!("{} response to {}", resp.status(), url.to_string()))); + } + Ok(resp) +} + +pub async fn search_plugins( + app_handle: &AppHandle, + query: &str, +) -> Result { + let mut url = base_url("/search"); + { + let mut query_pairs = url.query_pairs_mut(); + query_pairs.append_pair("query", query); + }; + let resp = yaak_api_client(app_handle)?.get(url).send().await?; + Ok(resp.json().await?) +} + +fn base_url(path: &str) -> Url { + let base_url = if is_dev() { + "http://localhost:9444/api/v1/plugins" + } else { + "https://api.yaak.app/api/v1/plugins" + }; + Url::from_str(&format!("{base_url}{path}")).unwrap() +} diff --git a/src-tauri/yaak-plugins/src/checksum.rs b/src-tauri/yaak-plugins/src/checksum.rs new file mode 100644 index 00000000..6b525c77 --- /dev/null +++ b/src-tauri/yaak-plugins/src/checksum.rs @@ -0,0 +1,8 @@ +use sha2::{Digest, Sha256}; + +pub(crate) fn compute_checksum(bytes: impl AsRef<[u8]>) -> String { + let mut hasher = Sha256::new(); + hasher.update(&bytes); + let hash = hasher.finalize(); + hex::encode(hash) +} diff --git a/src-tauri/yaak-plugins/src/commands.rs b/src-tauri/yaak-plugins/src/commands.rs new file mode 100644 index 00000000..460f3671 --- /dev/null +++ b/src-tauri/yaak-plugins/src/commands.rs @@ -0,0 +1,45 @@ +use crate::api::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( + app_handle: AppHandle, + query: &str, +) -> Result { + search_plugins(&app_handle, query).await +} + +#[command] +pub(crate) async fn install( + window: WebviewWindow, + plugin: PluginVersion, +) -> Result { + download_and_install(&window, &plugin).await +} + +#[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, +} diff --git a/src-tauri/yaak-plugins/src/error.rs b/src-tauri/yaak-plugins/src/error.rs index 2996425a..64c515ec 100644 --- a/src-tauri/yaak-plugins/src/error.rs +++ b/src-tauri/yaak-plugins/src/error.rs @@ -1,4 +1,5 @@ use crate::events::InternalEvent; +use serde::{Serialize, Serializer}; use thiserror::Error; use tokio::io; use tokio::sync::mpsc::error::SendError; @@ -8,9 +9,12 @@ pub enum Error { #[error(transparent)] CryptoErr(#[from] yaak_crypto::error::Error), + #[error(transparent)] + DbErr(#[from] yaak_models::error::Error), + #[error(transparent)] TemplateErr(#[from] yaak_templates::error::Error), - + #[error("IO error: {0}")] IoErr(#[from] io::Error), @@ -23,8 +27,17 @@ pub enum Error { #[error("Grpc send error: {0}")] GrpcSendErr(#[from] SendError), + #[error("Failed to send request: {0}")] + RequestError(#[from] reqwest::Error), + #[error("JSON error: {0}")] JsonErr(#[from] serde_json::Error), + + #[error("API Error: {0}")] + ApiErr(String), + + #[error(transparent)] + CommonError(#[from] yaak_common::error::Error), #[error("Timeout elapsed: {0}")] TimeoutElapsed(#[from] tokio::time::error::Elapsed), @@ -38,6 +51,9 @@ pub enum Error { #[error("Plugin error: {0}")] PluginErr(String), + #[error("zip error: {0}")] + ZipError(#[from] zip_extract::ZipExtractError), + #[error("Client not initialized error")] ClientNotInitializedErr, @@ -45,4 +61,13 @@ pub enum Error { UnknownEventErr, } +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + pub type Result = std::result::Result; diff --git a/src-tauri/yaak-plugins/src/events.rs b/src-tauri/yaak-plugins/src/events.rs index c9786cfb..edd28046 100644 --- a/src-tauri/yaak-plugins/src/events.rs +++ b/src-tauri/yaak-plugins/src/events.rs @@ -67,7 +67,7 @@ pub enum InternalEventPayload { BootResponse(BootResponse), ReloadRequest(EmptyPayload), - ReloadResponse(EmptyPayload), + ReloadResponse(BootResponse), TerminateRequest, TerminateResponse, diff --git a/src-tauri/yaak-plugins/src/install.rs b/src-tauri/yaak-plugins/src/install.rs new file mode 100644 index 00000000..e1146263 --- /dev/null +++ b/src-tauri/yaak-plugins/src/install.rs @@ -0,0 +1,60 @@ +use crate::api::download_plugin_archive; +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::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}; + +pub async fn download_and_install( + window: &WebviewWindow, + plugin_version: &PluginVersion, +) -> Result { + let resp = download_plugin_archive(window.app_handle(), &plugin_version).await?; + let bytes = resp.bytes().await?; + + let checksum = compute_checksum(&bytes); + if checksum != plugin_version.checksum { + return Err(PluginErr(format!( + "Checksum mismatch {}b {checksum} != {}", + bytes.len(), + plugin_version.checksum + ))); + } + + info!("Checksum matched {}", checksum); + + let plugin_dir = window.path().app_data_dir()?.join("plugins").join(generate_id()); + let plugin_dir_str = plugin_dir.to_str().unwrap().to_string(); + 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( + &Plugin { + id: plugin_version.id.clone(), + checked_at: Some(Utc::now().naive_utc()), + directory: plugin_dir_str.clone(), + enabled: true, + url: None, + ..Default::default() + }, + &UpdateSource::Background, + )?; + + info!("Installed plugin {} to {}", plugin_version.id, plugin_dir_str); + + Ok(p.id) +} diff --git a/src-tauri/yaak-plugins/src/lib.rs b/src-tauri/yaak-plugins/src/lib.rs index 6976732b..960b3dd5 100644 --- a/src-tauri/yaak-plugins/src/lib.rs +++ b/src-tauri/yaak-plugins/src/lib.rs @@ -1,9 +1,11 @@ +use crate::commands::{install, search}; use crate::manager::PluginManager; use log::info; use std::process::exit; use tauri::plugin::{Builder, TauriPlugin}; -use tauri::{Manager, RunEvent, Runtime, State}; +use tauri::{Manager, RunEvent, Runtime, State, generate_handler}; +mod commands; pub mod error; pub mod events; pub mod manager; @@ -13,9 +15,13 @@ pub mod plugin_handle; mod server_ws; pub mod template_callback; mod util; +mod checksum; +pub mod api; +pub mod install; pub fn init() -> TauriPlugin { Builder::new("yaak-plugins") + .invoke_handler(generate_handler![search, install]) .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 0f431a09..4bec172a 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -18,7 +18,7 @@ use crate::server_ws::PluginRuntimeServerWebsocket; use log::{error, info, warn}; use std::collections::HashMap; use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use tauri::path::BaseDirectory; @@ -38,12 +38,13 @@ pub struct PluginManager { plugins: Arc>>, kill_tx: tokio::sync::watch::Sender, ws_service: Arc, + vendored_plugin_dir: PathBuf, + installed_plugin_dir: PathBuf, } #[derive(Clone)] struct PluginCandidate { dir: String, - watch: bool, } impl PluginManager { @@ -56,11 +57,21 @@ impl PluginManager { let ws_service = PluginRuntimeServerWebsocket::new(events_tx, client_disconnect_tx, client_connect_tx); + let vendored_plugin_dir = app_handle + .path() + .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 plugin_manager = PluginManager { plugins: Default::default(), subscribers: Default::default(), ws_service: Arc::new(ws_service.clone()), kill_tx: kill_server_tx, + vendored_plugin_dir, + installed_plugin_dir, }; // Forward events to subscribers @@ -135,18 +146,13 @@ impl PluginManager { &self, app_handle: &AppHandle, ) -> Vec { - let bundled_plugins_dir = &app_handle - .path() - .resolve("vendored/plugins", BaseDirectory::Resource) - .expect("failed to resolve plugin directory resource"); - let plugins_dir = if is_dev() { // Use plugins directly for easy development env::current_dir() .map(|cwd| cwd.join("../plugins").canonicalize().unwrap()) - .unwrap_or_else(|_| bundled_plugins_dir.clone()) + .unwrap_or_else(|_| self.vendored_plugin_dir.to_path_buf()) } else { - bundled_plugins_dir.clone() + self.vendored_plugin_dir.to_path_buf() }; info!("Loading bundled plugins from {plugins_dir:?}"); @@ -155,13 +161,7 @@ impl PluginManager { .await .expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str()) .iter() - .map(|d| { - let is_vendored = plugins_dir.starts_with(bundled_plugins_dir); - PluginCandidate { - dir: d.into(), - watch: !is_vendored, - } - }) + .map(|d| PluginCandidate { dir: d.into() }) .collect(); let plugins = app_handle.db().list_plugins().unwrap_or_default(); @@ -169,7 +169,6 @@ impl PluginManager { .iter() .map(|p| PluginCandidate { dir: p.directory.to_owned(), - watch: true, }) .collect(); @@ -203,7 +202,6 @@ impl PluginManager { &self, window_context: &PluginWindowContext, dir: &str, - watch: bool, ) -> Result<()> { info!("Adding plugin by dir {dir}"); let maybe_tx = self.ws_service.app_to_plugin_events_tx.lock().await; @@ -212,6 +210,9 @@ impl PluginManager { Some(tx) => tx, }; 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()); // Boot the plugin let event = timeout( @@ -221,7 +222,7 @@ impl PluginManager { &plugin_handle, &InternalEventPayload::BootRequest(BootRequest { dir: dir.to_string(), - watch, + watch: !is_vendored && !is_installed, }), ), ) @@ -256,10 +257,7 @@ impl PluginManager { continue; } } - if let Err(e) = self - .add_plugin_by_dir(window_context, candidate.dir.as_str(), candidate.watch) - .await - { + if let Err(e) = self.add_plugin_by_dir(window_context, candidate.dir.as_str()).await { warn!("Failed to add plugin {} {e:?}", candidate.dir); } } diff --git a/src-tauri/yaak-sync/Cargo.toml b/src-tauri/yaak-sync/Cargo.toml index a5a7afd6..a843fc0a 100644 --- a/src-tauri/yaak-sync/Cargo.toml +++ b/src-tauri/yaak-sync/Cargo.toml @@ -6,8 +6,8 @@ edition = "2024" publish = false [dependencies] -chrono = { version = "0.4.38", features = ["serde"] } -hex = "0.4.3" +chrono = { workspace = true, features = ["serde"] } +hex = { workspace = true } log = "0.4.22" notify = "8.0.0" serde = { workspace = true, features = ["derive"] } diff --git a/src-tauri/yaak-ws/Cargo.toml b/src-tauri/yaak-ws/Cargo.toml index 0501c742..cbc06abb 100644 --- a/src-tauri/yaak-ws/Cargo.toml +++ b/src-tauri/yaak-ws/Cargo.toml @@ -12,7 +12,7 @@ md5 = "0.7.0" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tauri = { workspace = true } -thiserror = "2.0.11" +thiserror = { workspace = true } tokio = { workspace = true, features = ["macros", "time", "test-util"] } tokio-tungstenite = { version = "0.26.2", default-features = false, features = ["rustls-tls-native-roots", "connect"] } yaak-http = { workspace = true } diff --git a/src-web/components/GrpcRequestPane.tsx b/src-web/components/GrpcRequestPane.tsx index 0d89533f..7feb5e58 100644 --- a/src-web/components/GrpcRequestPane.tsx +++ b/src-web/components/GrpcRequestPane.tsx @@ -269,7 +269,7 @@ export function GrpcRequestPane({ label="Request" onChangeValue={setActiveTab} tabs={tabs} - tabListClassName="mt-2 !mb-1.5" + tabListClassName="mt-1 !mb-1.5" > diff --git a/src-web/components/HttpResponsePane.tsx b/src-web/components/HttpResponsePane.tsx index 79ed88b0..348435d3 100644 --- a/src-web/components/HttpResponsePane.tsx +++ b/src-web/components/HttpResponsePane.tsx @@ -153,7 +153,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) { tabs={tabs} label="Response" className="ml-3 mr-3 mb-3" - tabListClassName="mt-1.5" + tabListClassName="mt-0.5" > diff --git a/src-web/components/ImportDataDialog.tsx b/src-web/components/ImportDataDialog.tsx index aea80d2b..932db03e 100644 --- a/src-web/components/ImportDataDialog.tsx +++ b/src-web/components/ImportDataDialog.tsx @@ -11,6 +11,7 @@ interface Props { export function ImportDataDialog({ importData }: Props) { const [isLoading, setIsLoading] = useState(false); const [filePath, setFilePath] = useLocalStorage('importFilePath', null); + return ( diff --git a/src-web/components/Settings/Settings.tsx b/src-web/components/Settings/Settings.tsx index 1629b68a..127ee197 100644 --- a/src-web/components/Settings/Settings.tsx +++ b/src-web/components/Settings/Settings.tsx @@ -66,8 +66,10 @@ export default function Settings({ hide }: Props) { )} ({ value, label: capitalize(value) }))} @@ -81,7 +83,7 @@ export default function Settings({ hide }: Props) { - + diff --git a/src-web/components/Settings/SettingsPlugins.tsx b/src-web/components/Settings/SettingsPlugins.tsx index 1447cd1c..8ffdcf3d 100644 --- a/src-web/components/Settings/SettingsPlugins.tsx +++ b/src-web/components/Settings/SettingsPlugins.tsx @@ -1,8 +1,11 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; import { openUrl } from '@tauri-apps/plugin-opener'; -import type { Plugin} from '@yaakapp-internal/models'; +import type { Plugin } from '@yaakapp-internal/models'; import { pluginsAtom } from '@yaakapp-internal/models'; +import { installPlugin, PluginVersion, searchPlugins } from '@yaakapp-internal/plugins'; import { useAtomValue } from 'jotai'; -import React from 'react'; +import React, { useState } from 'react'; +import { useDebouncedValue } from '../../hooks/useDebouncedValue'; import { useInstallPlugin } from '../../hooks/useInstallPlugin'; import { usePluginInfo } from '../../hooks/usePluginInfo'; import { useRefreshPlugins } from '../../hooks/usePlugins'; @@ -10,86 +13,86 @@ import { useUninstallPlugin } from '../../hooks/useUninstallPlugin'; import { Button } from '../core/Button'; import { IconButton } from '../core/IconButton'; import { InlineCode } from '../core/InlineCode'; +import { LoadingIcon } from '../core/LoadingIcon'; +import { PlainInput } from '../core/PlainInput'; import { HStack } from '../core/Stacks'; +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '../core/Table'; +import { TabContent, Tabs } from '../core/Tabs/Tabs'; import { EmptyStateText } from '../EmptyStateText'; import { SelectFile } from '../SelectFile'; export function SettingsPlugins() { const [directory, setDirectory] = React.useState(null); - const plugins = useAtomValue(pluginsAtom); const createPlugin = useInstallPlugin(); const refreshPlugins = useRefreshPlugins(); + const [tab, setTab] = useState(); return ( -
- {plugins.length === 0 ? ( -
- - Plugins extend the functionality of Yaak. -
- Add your first plugin to get started. -
-
- ) : ( - - - - - - - - - - {plugins.map((p) => ( - - ))} - -
PluginVersion
- )} -
{ - e.preventDefault(); - if (directory == null) return; - createPlugin.mutate(directory); - setDirectory(null); - }} +
+ -
- setDirectory(filePath)} - filePath={directory} - /> - - {directory && ( - - )} - refreshPlugins.mutate()} - /> - openUrl('https://feedback.yaak.app/help/articles/6911763-quick-start')} - /> - -
- + + + + + +
{ + e.preventDefault(); + if (directory == null) return; + createPlugin.mutate(directory); + setDirectory(null); + }} + > +
+ setDirectory(filePath)} + filePath={directory} + /> + + {directory && ( + + )} + refreshPlugins.mutate()} + /> + + openUrl('https://feedback.yaak.app/help/articles/6911763-quick-start') + } + /> + +
+
+
+
); } function PluginInfo({ plugin }: { plugin: Plugin }) { const pluginInfo = usePluginInfo(plugin.id); - const deletePlugin = useUninstallPlugin(plugin.id); + const deletePlugin = useUninstallPlugin(); return ( {pluginInfo.data?.name} @@ -101,9 +104,127 @@ function PluginInfo({ plugin }: { plugin: Plugin }) { size="sm" icon="trash" title="Uninstall plugin" - onClick={() => deletePlugin.mutate()} + onClick={() => deletePlugin.mutate(plugin.id)} /> ); } + +function PluginSearch() { + const [query, setQuery] = useState(''); + const debouncedQuery = useDebouncedValue(query); + const results = useQuery({ + queryKey: ['plugins', debouncedQuery], + queryFn: () => searchPlugins(query), + }); + + return ( +
+ + + +
+ {results.data == null ? ( + + + + ) : (results.data.results ?? []).length === 0 ? ( + No plugins found + ) : ( + + + + Name + Version + Description + + + + + {results.data.results.map((plugin) => { + return ( + + {plugin.displayName} + + {plugin.version} + + + {plugin.description ?? 'n/a'} + + + + + + ); + })} + +
+ )} +
+
+ ); +} + +function InstallPluginButton({ plugin }: { plugin: PluginVersion }) { + const plugins = useAtomValue(pluginsAtom); + const deletePlugin = useUninstallPlugin(); + const installed = plugins?.some((p) => p.id === plugin.id); + const installPluginMutation = useMutation({ + mutationKey: ['install_plugin', plugin.id], + mutationFn: installPlugin, + }); + + return ( + + ); +} + +function InstalledPlugins() { + const plugins = useAtomValue(pluginsAtom); + return plugins.length === 0 ? ( +
+ + Plugins extend the functionality of Yaak. +
+ Add your first plugin to get started. +
+
+ ) : ( + + + + + + + + + + {plugins.map((p) => ( + + ))} + +
PluginVersion
+ ); +} diff --git a/src-web/components/SettingsDropdown.tsx b/src-web/components/SettingsDropdown.tsx index 6710cebc..fc2c1453 100644 --- a/src-web/components/SettingsDropdown.tsx +++ b/src-web/components/SettingsDropdown.tsx @@ -2,12 +2,12 @@ import { openUrl } from '@tauri-apps/plugin-opener'; import { useLicense } from '@yaakapp-internal/license'; import { useRef } from 'react'; import { openSettings } from '../commands/openSettings'; -import { appInfo } from '../lib/appInfo'; import { useCheckForUpdates } from '../hooks/useCheckForUpdates'; import { useExportData } from '../hooks/useExportData'; -import { useImportData } from '../hooks/useImportData'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; +import { appInfo } from '../lib/appInfo'; import { showDialog } from '../lib/dialog'; +import { importData } from '../lib/importData'; import type { DropdownRef } from './core/Dropdown'; import { Dropdown } from './core/Dropdown'; import { Icon } from './core/Icon'; @@ -15,7 +15,6 @@ import { IconButton } from './core/IconButton'; import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog'; export function SettingsDropdown() { - const importData = useImportData(); const exportData = useExportData(); const dropdownRef = useRef(null); const checkForUpdates = useCheckForUpdates(); diff --git a/src-web/components/WebsocketRequestPane.tsx b/src-web/components/WebsocketRequestPane.tsx index 959a7cbb..b2feaada 100644 --- a/src-web/components/WebsocketRequestPane.tsx +++ b/src-web/components/WebsocketRequestPane.tsx @@ -234,7 +234,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque label="Request" onChangeValue={setActiveTab} tabs={tabs} - tabListClassName="mt-2 !mb-1.5" + tabListClassName="mt-1 !mb-1.5" > diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index 7e1a7b72..d01dae03 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -4,14 +4,19 @@ import { useAtomValue } from 'jotai'; import * as m from 'motion/react-m'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react'; -import { useEnsureActiveCookieJar, useSubscribeActiveCookieJarId } from '../hooks/useActiveCookieJar'; -import { activeEnvironmentAtom, useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment'; +import { + useEnsureActiveCookieJar, + useSubscribeActiveCookieJarId, +} from '../hooks/useActiveCookieJar'; +import { + activeEnvironmentAtom, + useSubscribeActiveEnvironmentId, +} from '../hooks/useActiveEnvironment'; import { activeRequestAtom } from '../hooks/useActiveRequest'; import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId'; import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace'; import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden'; import { useHotKey } from '../hooks/useHotKey'; -import { useImportData } from '../hooks/useImportData'; import { useSubscribeRecentCookieJars } from '../hooks/useRecentCookieJars'; import { useSubscribeRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useSubscribeRecentRequests } from '../hooks/useRecentRequests'; @@ -22,6 +27,7 @@ import { useSidebarWidth } from '../hooks/useSidebarWidth'; import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle'; import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette'; import { duplicateRequestAndNavigate } from '../lib/duplicateRequestAndNavigate'; +import { importData } from '../lib/importData'; import { jotaiStore } from '../lib/jotai'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; @@ -204,7 +210,6 @@ export function Workspace() { function WorkspaceBody() { const activeRequest = useAtomValue(activeRequestAtom); const activeWorkspace = useAtomValue(activeWorkspaceAtom); - const importData = useImportData(); if (activeWorkspace == null) { return ( diff --git a/src-web/components/core/Tabs/Tabs.tsx b/src-web/components/core/Tabs/Tabs.tsx index 001ea59d..274dcd1a 100644 --- a/src-web/components/core/Tabs/Tabs.tsx +++ b/src-web/components/core/Tabs/Tabs.tsx @@ -1,11 +1,9 @@ import classNames from 'classnames'; import type { ReactNode } from 'react'; import { memo, useEffect, useRef } from 'react'; -import { Icon } from '../Icon'; -import type { RadioDropdownProps } from '../RadioDropdown'; -import { RadioDropdown } from '../RadioDropdown'; -import { HStack } from '../Stacks'; import { ErrorBoundary } from '../../ErrorBoundary'; +import { Icon } from '../Icon'; +import { RadioDropdown, RadioDropdownProps } from '../RadioDropdown'; export type TabItem = | { @@ -28,6 +26,7 @@ interface Props { className?: string; children: ReactNode; addBorders?: boolean; + layout?: 'horizontal' | 'vertical'; } export function Tabs({ @@ -39,6 +38,7 @@ export function Tabs({ className, tabListClassName, addBorders, + layout = 'vertical', }: Props) { const ref = useRef(null); @@ -49,7 +49,10 @@ export function Tabs({ const tabs = ref.current?.querySelectorAll(`[data-tab]`); for (const tab of tabs ?? []) { const v = tab.getAttribute('data-tab'); - if (v === value) { + let parent = tab.closest('.tabs-container'); + if (parent !== ref.current) { + // Tab is part of a nested tab container, so ignore it + } else if (v === value) { tab.setAttribute('tabindex', '-1'); tab.setAttribute('data-state', 'active'); tab.setAttribute('aria-hidden', 'false'); @@ -67,29 +70,41 @@ export function Tabs({ ref={ref} className={classNames( className, + 'tabs-container', 'transform-gpu', - 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1', + 'h-full grid', + layout === 'horizontal' && 'grid-rows-1 grid-cols-[auto_minmax(0,1fr)]', + layout === 'vertical' && 'grid-rows-[auto_minmax(0,1fr)] grid-cols-1', )} >
- +
{tabs.map((t) => { const isActive = t.value === value; const btnClassName = classNames( - 'h-full flex items-center rounded', + 'h-sm flex items-center rounded', '!px-2 ml-[1px]', addBorders && 'border', isActive ? 'text-text' : 'text-text-subtle hover:text-text', - isActive && addBorders ? 'border-border-subtle' : 'border-transparent', + isActive && addBorders + ? 'border-border-subtle bg-surface-active' + : 'border-transparent', ); if ('options' in t) { @@ -135,7 +150,7 @@ export function Tabs({ ); } })} - +
{children}
diff --git a/src-web/hooks/useImportData.tsx b/src-web/hooks/useImportData.tsx deleted file mode 100644 index 3d10a326..00000000 --- a/src-web/hooks/useImportData.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import type { BatchUpsertResult } from '@yaakapp-internal/models'; -import { Button } from '../components/core/Button'; -import { FormattedError } from '../components/core/FormattedError'; -import { VStack } from '../components/core/Stacks'; -import { ImportDataDialog } from '../components/ImportDataDialog'; -import { showAlert } from '../lib/alert'; -import { showDialog } from '../lib/dialog'; -import { jotaiStore } from '../lib/jotai'; -import { pluralizeCount } from '../lib/pluralize'; -import { router } from '../lib/router'; -import { invokeCmd } from '../lib/tauri'; -import { activeWorkspaceAtom } from './useActiveWorkspace'; -import { useFastMutation } from './useFastMutation'; - -export function useImportData() { - const importData = async (filePath: string): Promise => { - const activeWorkspace = jotaiStore.get(activeWorkspaceAtom); - const imported = await invokeCmd('cmd_import_data', { - filePath, - workspaceId: activeWorkspace?.id, - }); - - const importedWorkspace = imported.workspaces[0]; - - showDialog({ - id: 'import-complete', - title: 'Import Complete', - size: 'sm', - hideX: true, - render: ({ hide }) => { - return ( - -
    -
  • {pluralizeCount('Workspace', imported.workspaces.length)}
  • - {imported.environments.length > 0 && ( -
  • {pluralizeCount('Environment', imported.environments.length)}
  • - )} - {imported.folders.length > 0 && ( -
  • {pluralizeCount('Folder', imported.folders.length)}
  • - )} - {imported.httpRequests.length > 0 && ( -
  • {pluralizeCount('HTTP Request', imported.httpRequests.length)}
  • - )} - {imported.grpcRequests.length > 0 && ( -
  • {pluralizeCount('GRPC Request', imported.grpcRequests.length)}
  • - )} - {imported.websocketRequests.length > 0 && ( -
  • {pluralizeCount('Websocket Request', imported.websocketRequests.length)}
  • - )} -
-
- -
-
- ); - }, - }); - - if (importedWorkspace != null) { - const environmentId = imported.environments[0]?.id ?? null; - await router.navigate({ - to: '/workspaces/$workspaceId', - params: { workspaceId: importedWorkspace.id }, - search: { environment_id: environmentId }, - }); - } - - return true; - }; - - return useFastMutation({ - mutationKey: ['import_data'], - onError: (err: string) => { - showAlert({ - id: 'import-failed', - title: 'Import Failed', - size: 'md', - body: {err}, - }); - }, - mutationFn: async () => { - return new Promise((resolve, reject) => { - showDialog({ - id: 'import', - title: 'Import Data', - size: 'sm', - render: ({ hide }) => { - const importAndHide = async (filePath: string) => { - try { - const didImport = await importData(filePath); - if (!didImport) { - return; - } - resolve(); - } catch (err) { - reject(err); - } finally { - hide(); - } - }; - return ; - }, - }); - }); - }, - }); -} diff --git a/src-web/hooks/usePluginInfo.ts b/src-web/hooks/usePluginInfo.ts index f09a601c..205e858c 100644 --- a/src-web/hooks/usePluginInfo.ts +++ b/src-web/hooks/usePluginInfo.ts @@ -10,10 +10,7 @@ function pluginInfoKey(id: string) { export function usePluginInfo(id: string) { return useQuery({ queryKey: pluginInfoKey(id), - queryFn: async () => { - const info = (await invokeCmd('cmd_plugin_info', { id })) as BootResponse; - return info; - }, + queryFn: () => invokeCmd('cmd_plugin_info', { id }), }); } diff --git a/src-web/hooks/useUninstallPlugin.ts b/src-web/hooks/useUninstallPlugin.ts index fc34fbb0..de738e89 100644 --- a/src-web/hooks/useUninstallPlugin.ts +++ b/src-web/hooks/useUninstallPlugin.ts @@ -1,11 +1,10 @@ -import { useFastMutation } from './useFastMutation'; -import type { Plugin } from '@yaakapp-internal/models'; import { invokeCmd } from '../lib/tauri'; +import { useFastMutation } from './useFastMutation'; -export function useUninstallPlugin(pluginId: string) { - return useFastMutation({ +export function useUninstallPlugin() { + return useFastMutation({ mutationKey: ['uninstall_plugin'], - mutationFn: async () => { + mutationFn: async (pluginId: string) => { return invokeCmd('cmd_uninstall_plugin', { pluginId }); }, }); diff --git a/src-web/lib/importData.tsx b/src-web/lib/importData.tsx new file mode 100644 index 00000000..eede8fc2 --- /dev/null +++ b/src-web/lib/importData.tsx @@ -0,0 +1,107 @@ +import type { BatchUpsertResult } from '@yaakapp-internal/models'; +import { Button } from '../components/core/Button'; +import { FormattedError } from '../components/core/FormattedError'; +import { VStack } from '../components/core/Stacks'; +import { ImportDataDialog } from '../components/ImportDataDialog'; +import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace'; +import { createFastMutation } from '../hooks/useFastMutation'; +import { showAlert } from './alert'; +import { showDialog } from './dialog'; +import { jotaiStore } from './jotai'; +import { pluralizeCount } from './pluralize'; +import { router } from './router'; +import { invokeCmd } from './tauri'; + +export const importData = createFastMutation({ + mutationKey: ['import_data'], + onError: (err: string) => { + showAlert({ + id: 'import-failed', + title: 'Import Failed', + size: 'md', + body: {err}, + }); + }, + mutationFn: async () => { + return new Promise((resolve, reject) => { + showDialog({ + id: 'import', + title: 'Import Data', + size: 'sm', + render: ({ hide }) => { + const importAndHide = async (filePath: string) => { + try { + const didImport = await performImport(filePath); + if (!didImport) { + return; + } + resolve(); + } catch (err) { + reject(err); + } finally { + hide(); + } + }; + return ; + }, + }); + }); + }, +}); + +async function performImport(filePath: string): Promise { + const activeWorkspace = jotaiStore.get(activeWorkspaceAtom); + const imported = await invokeCmd('cmd_import_data', { + filePath, + workspaceId: activeWorkspace?.id, + }); + + const importedWorkspace = imported.workspaces[0]; + + showDialog({ + id: 'import-complete', + title: 'Import Complete', + size: 'sm', + hideX: true, + render: ({ hide }) => { + return ( + +
    +
  • {pluralizeCount('Workspace', imported.workspaces.length)}
  • + {imported.environments.length > 0 && ( +
  • {pluralizeCount('Environment', imported.environments.length)}
  • + )} + {imported.folders.length > 0 && ( +
  • {pluralizeCount('Folder', imported.folders.length)}
  • + )} + {imported.httpRequests.length > 0 && ( +
  • {pluralizeCount('HTTP Request', imported.httpRequests.length)}
  • + )} + {imported.grpcRequests.length > 0 && ( +
  • {pluralizeCount('GRPC Request', imported.grpcRequests.length)}
  • + )} + {imported.websocketRequests.length > 0 && ( +
  • {pluralizeCount('Websocket Request', imported.websocketRequests.length)}
  • + )} +
+
+ +
+
+ ); + }, + }); + + if (importedWorkspace != null) { + const environmentId = imported.environments[0]?.id ?? null; + await router.navigate({ + to: '/workspaces/$workspaceId', + params: { workspaceId: importedWorkspace.id }, + search: { environment_id: environmentId }, + }); + } + + return true; +}