This commit is contained in:
Gregory Schier
2025-01-26 13:13:45 -08:00
committed by GitHub
parent 82b1ad35ff
commit f678593903
99 changed files with 3492 additions and 1583 deletions

View File

@@ -70,7 +70,7 @@ pub async fn send_http_request<R: Runtime>(
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
debug!("Sending request to {url_string}");
debug!("Sending request to {} {url_string}", request.method);
let mut client_builder = reqwest::Client::builder()
.redirect(match workspace.setting_follow_redirects {
@@ -262,7 +262,7 @@ pub async fn send_http_request<R: Runtime>(
None => {}
Some(a) => {
for p in a {
let enabled = get_bool(p, "enabled");
let enabled = get_bool(p, "enabled", true);
let name = get_str(p, "name");
if !enabled || name.is_empty() {
continue;
@@ -296,7 +296,7 @@ pub async fn send_http_request<R: Runtime>(
None => {}
Some(fd) => {
for p in fd {
let enabled = get_bool(p, "enabled");
let enabled = get_bool(p, "enabled", true);
let name = get_str(p, "name").to_string();
if !enabled || name.is_empty() {
@@ -376,13 +376,11 @@ pub async fn send_http_request<R: Runtime>(
if let Some(auth_name) = request.authentication_type.to_owned() {
let req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&request.authentication)
.unwrap()
.as_object()
.unwrap()
.to_owned(),
method: sendable_req.method().to_string(),
context_id: format!("{:x}", md5::compute(request.id)),
values: serde_json::from_value(serde_json::to_value(&request.authentication).unwrap())
.unwrap(),
url: sendable_req.url().to_string(),
method: sendable_req.method().to_string(),
headers: sendable_req
.headers()
.iter()
@@ -604,10 +602,10 @@ fn ensure_proto(url_str: &str) -> String {
format!("http://{url_str}")
}
fn get_bool(v: &Value, key: &str) -> bool {
fn get_bool(v: &Value, key: &str, fallback: bool) -> bool {
match v.get(key) {
None => false,
Some(v) => v.as_bool().unwrap_or_default(),
None => fallback,
Some(v) => v.as_bool().unwrap_or(fallback),
}
}

View File

@@ -6,33 +6,25 @@ use crate::encoding::read_response_body;
use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request;
use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_http_request, render_json_value, render_template};
use crate::render::{render_grpc_request, render_template};
use crate::template_callback::PluginTemplateCallback;
use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use chrono::Utc;
use eventsource_client::{EventParser, SSE};
use log::{debug, error, info, warn};
use log::{debug, error, warn};
use rand::random;
use regex::Regex;
use serde::Serialize;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use std::fs::{create_dir_all, File};
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
use std::time::Duration;
use std::{fs, panic};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{AppHandle, Emitter, RunEvent, State, WebviewWindow};
use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_log::fern::colors::ColoredLevelConfig;
use tauri_plugin_log::{Builder, Target, TargetKind};
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use tokio::fs::read_to_string;
use tokio::sync::Mutex;
@@ -51,28 +43,24 @@ use yaak_models::queries::{
delete_all_http_responses_for_workspace, delete_cookie_jar, delete_environment, delete_folder,
delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response,
delete_plugin, delete_workspace, duplicate_folder, duplicate_grpc_request,
duplicate_http_request, ensure_base_environment, generate_id, generate_model_id,
get_base_environment, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_or_create_workspace_meta, get_plugin, get_workspace,
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders,
list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses_for_request, list_http_responses_for_workspace, list_key_values_raw,
list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings,
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
duplicate_http_request, ensure_base_environment, generate_model_id, get_base_environment,
get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request,
get_http_request, get_http_response, get_key_value_raw, get_or_create_settings,
get_or_create_workspace_meta, get_plugin, get_workspace, get_workspace_export_resources,
list_cookie_jars, list_environments, list_folders, list_grpc_connections_for_workspace,
list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses_for_workspace,
list_key_values_raw, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
upsert_workspace_meta, BatchUpsertResult, UpdateSource,
};
use yaak_plugins::events::{
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, Color,
FilterResponse, FindHttpResponsesResponse, GetHttpAuthenticationResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse,
HttpHeader, Icon, InternalEvent, InternalEventPayload, PromptTextResponse,
RenderHttpRequestResponse, RenderPurpose, SendHttpRequestResponse, ShowToastRequest,
TemplateRenderResponse, WindowContext,
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, FilterResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, HttpHeader, InternalEvent,
InternalEventPayload, JsonPrimitive, RenderPurpose, WindowContext,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
use yaak_sse::sse::ServerSentEvent;
use yaak_templates::format::format_json;
use yaak_templates::{Parser, Tokens};
@@ -82,11 +70,13 @@ mod encoding;
mod grpc;
mod http_request;
mod notifications;
mod plugin_events;
mod render;
#[cfg(target_os = "macos")]
mod tauri_plugin_mac_window;
mod template_callback;
mod updates;
mod window;
mod window_menu;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
@@ -242,7 +232,8 @@ async fn cmd_grpc_go<R: Runtime>(
if let Some(auth_name) = request.authentication_type.clone() {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&auth).unwrap().as_object().unwrap().to_owned(),
context_id: format!("{:x}", md5::compute(request_id.to_string())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.url.clone(),
headers: metadata
@@ -969,15 +960,31 @@ async fn cmd_template_functions<R: Runtime>(
}
#[tauri::command]
async fn cmd_get_http_authentication<R: Runtime>(
async fn cmd_get_http_authentication_summaries<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetHttpAuthenticationResponse>, String> {
let results =
plugin_manager.get_http_authentication(&window).await.map_err(|e| e.to_string())?;
) -> Result<Vec<GetHttpAuthenticationSummaryResponse>, String> {
let results = plugin_manager
.get_http_authentication_summaries(&window)
.await
.map_err(|e| e.to_string())?;
Ok(results.into_iter().map(|(_, a)| a).collect())
}
#[tauri::command]
async fn cmd_get_http_authentication_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<GetHttpAuthenticationConfigResponse, String> {
plugin_manager
.get_http_authentication_config(&window, auth_name, values, request_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_request_action<R: Runtime>(
window: WebviewWindow<R>,
@@ -987,6 +994,21 @@ async fn cmd_call_http_request_action<R: Runtime>(
plugin_manager.call_http_request_action(&window, req).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_authentication_action<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<(), String> {
plugin_manager
.call_http_authentication_action(&window, auth_name, action_index, values, request_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_curl_to_request<R: Runtime>(
window: WebviewWindow<R>,
@@ -1175,7 +1197,7 @@ async fn cmd_install_plugin<R: Runtime>(
window: WebviewWindow<R>,
) -> Result<Plugin, String> {
plugin_manager
.add_plugin_by_dir(WindowContext::from_window(&window), &directory, true)
.add_plugin_by_dir(&WindowContext::from_window(&window), &directory, true)
.await
.map_err(|e| e.to_string())?;
@@ -1205,7 +1227,7 @@ async fn cmd_uninstall_plugin<R: Runtime>(
.map_err(|e| e.to_string())?;
plugin_manager
.uninstall(WindowContext::from_window(&window), plugin.directory.as_str())
.uninstall(&WindowContext::from_window(&window), plugin.directory.as_str())
.await
.map_err(|e| e.to_string())?;
@@ -1458,7 +1480,7 @@ async fn cmd_reload_plugins<R: Runtime>(
plugin_manager: State<'_, PluginManager>,
) -> Result<(), String> {
plugin_manager
.initialize_all_plugins(window.app_handle(), WindowContext::from_window(&window))
.initialize_all_plugins(window.app_handle(), &WindowContext::from_window(&window))
.await
.map_err(|e| e.to_string())?;
Ok(())
@@ -1656,15 +1678,17 @@ async fn cmd_new_child_window(
current_pos.y + current_size.height / 2.0 - inner_size.1 / 2.0,
);
let config = CreateWindowConfig {
let config = window::CreateWindowConfig {
label: label.as_str(),
title,
url,
inner_size,
position,
inner_size: Some(inner_size),
position: Some(position),
navigation_tx: None,
hide_titlebar: true,
};
let child_window = create_window(&app_handle, config);
let child_window = window::create_window(&app_handle, config);
// NOTE: These listeners will remain active even when the windows close. Unfortunately,
// there's no way to unlisten to events for now, so we just have to be defensive.
@@ -1830,6 +1854,7 @@ pub fn run() {
Ok(())
})
.invoke_handler(tauri::generate_handler![
cmd_call_http_authentication_action,
cmd_call_http_request_action,
cmd_check_for_updates,
cmd_create_cookie_jar,
@@ -1859,7 +1884,8 @@ pub fn run() {
cmd_get_environment,
cmd_get_folder,
cmd_get_grpc_request,
cmd_get_http_authentication,
cmd_get_http_authentication_summaries,
cmd_get_http_authentication_config,
cmd_get_http_request,
cmd_get_key_value,
cmd_get_settings,
@@ -1998,113 +2024,21 @@ fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
}
.expect("Failed to generate label for new window");
let config = CreateWindowConfig {
let config = window::CreateWindowConfig {
url,
label: label.as_str(),
title: "Yaak",
inner_size: (DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT),
position: (
inner_size: Some((DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)),
position: Some((
// Offset by random amount so it's easier to differentiate
100.0 + random::<f64>() * 20.0,
100.0 + random::<f64>() * 20.0,
),
)),
navigation_tx: None,
hide_titlebar: true,
};
create_window(handle, config)
}
struct CreateWindowConfig<'s> {
url: &'s str,
label: &'s str,
title: &'s str,
inner_size: (f64, f64),
position: (f64, f64),
}
fn create_window(handle: &AppHandle, config: CreateWindowConfig) -> WebviewWindow {
#[allow(unused_variables)]
let menu = app_menu(handle).unwrap();
// This causes the window to not be clickable (in AppImage), so disable on Linux
#[cfg(not(target_os = "linux"))]
handle.set_menu(menu).expect("Failed to set app menu");
info!("Create new window label={}", config.label);
let mut win_builder =
tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into()))
.title(config.title)
.resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.inner_size(config.inner_size.0, config.inner_size.1)
.position(config.position.0, config.position.1)
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
// Add macOS-only things
#[cfg(target_os = "macos")]
{
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
}
// Add non-MacOS things
#[cfg(not(target_os = "macos"))]
{
// Doesn't seem to work from Rust, here, so we do it in main.tsx
win_builder = win_builder.decorations(false);
}
if let Some(w) = handle.webview_windows().get(config.label) {
info!("Webview with label {} already exists. Focusing existing", config.label);
w.set_focus().unwrap();
return w.to_owned();
}
let win = win_builder.build().unwrap();
let webview_window = win.clone();
win.on_menu_event(move |w, event| {
if !w.is_focused().unwrap() {
return;
}
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
"zoom_in" => w.emit("zoom_in", true).unwrap(),
"zoom_out" => w.emit("zoom_out", true).unwrap(),
"settings" => w.emit("settings", true).unwrap(),
"open_feedback" => {
if let Err(e) =
w.app_handle().opener().open_url("https://yaak.app/feedback", None::<&str>)
{
warn!("Failed to open feedback {e:?}")
}
}
// Commands for development
"dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(),
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap();
}
"dev.toggle_devtools" => {
if webview_window.is_devtools_open() {
webview_window.close_devtools();
} else {
webview_window.open_devtools();
}
}
_ => {}
}
});
win
window::create_window(handle, config)
}
async fn get_update_mode(h: &AppHandle) -> UpdateMode {
@@ -2140,36 +2074,24 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
// We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here
tauri::async_runtime::spawn(async move {
handle_plugin_event(&app_handle, &event, &plugin).await;
crate::plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
});
}
plugin_manager.unsubscribe(rx_id.as_str()).await;
});
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct FrontendCall<T: Serialize + Clone> {
args: T,
reply_id: String,
}
async fn call_frontend<T: Serialize + Clone, R: Runtime>(
async fn call_frontend<R: Runtime>(
window: WebviewWindow<R>,
event_name: &str,
args: T,
) -> PromptTextResponse {
let reply_id = format!("{event_name}_reply_{}", generate_id());
let payload = FrontendCall {
args,
reply_id: reply_id.clone(),
};
window.emit_to(window.label(), event_name, payload).unwrap();
let (tx, mut rx) = tokio::sync::watch::channel(PromptTextResponse::default());
event: &InternalEvent,
) -> Option<InternalEventPayload> {
window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
let (tx, mut rx) = tokio::sync::watch::channel(None);
let reply_id = event.id.clone();
let event_id = window.clone().listen(reply_id, move |ev| {
let resp: PromptTextResponse = serde_json::from_str(ev.payload()).unwrap();
if let Err(e) = tx.send(resp) {
let resp: InternalEvent = serde_json::from_str(ev.payload()).unwrap();
if let Err(e) = tx.send(Some(resp.payload)) {
warn!("Failed to prompt for text {e:?}");
}
});
@@ -2181,180 +2103,7 @@ async fn call_frontend<T: Serialize + Clone, R: Runtime>(
window.unlisten(event_id);
let v = rx.borrow();
v.clone()
}
async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// info!("Got event to app {}", event.id);
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
None
}
InternalEventPayload::ShowToastRequest(req) => {
match window_context {
WindowContext::Label { label } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
};
None
}
InternalEventPayload::PromptTextRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let resp = call_frontend(window, "show_prompt", req).await;
Some(InternalEventPayload::PromptTextResponse(resp))
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses_for_request(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.unwrap();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
}))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request = render_http_request(
&req.http_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await;
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
}
InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data =
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let toast_event = plugin_handle.build_event_to_send(
WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: resp.error,
color: Some(Color::Danger),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::ReloadResponse(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
}
let new_plugin = Plugin {
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin
};
upsert_plugin(&window, new_plugin, &UpdateSource::Plugin).await.unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let cookie_jar = cookie_jar_from_window(&window).await;
let environment = environment_from_window(&window).await;
let resp = create_default_http_response(
&window,
req.http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap();
let result = send_http_request(
&window,
&req.http_request,
&resp,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
http_response,
}))
}
_ => None,
};
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
v.to_owned()
}
fn get_window_from_window_context<R: Runtime>(

View File

@@ -0,0 +1,265 @@
use crate::http_request::send_http_request;
use crate::render::{render_http_request, render_json_value};
use crate::template_callback::PluginTemplateCallback;
use crate::window::{create_window, CreateWindowConfig};
use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context,
workspace_from_window,
};
use chrono::Utc;
use log::warn;
use tauri::{AppHandle, Emitter, Manager, Runtime, State};
use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::queries::{
create_default_http_response, delete_plugin_key_value, get_base_environment, get_http_request,
get_plugin_key_value, list_http_responses_for_request, list_plugins, set_plugin_key_value,
upsert_plugin, UpdateSource,
};
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest,
TemplateRenderResponse, WindowContext, WindowNavigateEvent,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
pub(crate) async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// info!("Got event to app {}", event.id);
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
Some(InternalEventPayload::CopyTextResponse(EmptyPayload {}))
}
InternalEventPayload::ShowToastRequest(req) => {
match window_context {
WindowContext::Label { label } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
};
Some(InternalEventPayload::ShowToastResponse(EmptyPayload {}))
}
InternalEventPayload::PromptTextRequest(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
call_frontend(window, event).await
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses_for_request(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.unwrap();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
}))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request = render_http_request(
&req.http_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await;
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
}
InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data =
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let toast_event = plugin_handle.build_event_to_send(
&WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!(
"Plugin error from {}: {}",
plugin_handle.name().await,
resp.error
),
color: Some(Color::Danger),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::ReloadResponse(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
}
let new_plugin = Plugin {
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin
};
upsert_plugin(&window, new_plugin, &UpdateSource::Plugin).await.unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
&WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let mut http_request = req.http_request;
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let cookie_jar = cookie_jar_from_window(&window).await;
let environment = environment_from_window(&window).await;
if http_request.workspace_id.is_empty() {
http_request.workspace_id = workspace.id;
}
let resp = if http_request.id.is_empty() {
HttpResponse::new()
} else {
create_default_http_response(
&window,
http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap()
};
let result = send_http_request(
&window,
&http_request,
&resp,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
http_response,
}))
}
InternalEventPayload::OpenWindowRequest(req) => {
let label = req.label;
let (tx, mut rx) = tokio::sync::mpsc::channel(128);
let win_config = CreateWindowConfig {
url: &req.url,
label: &label.clone(),
title: &req.title.unwrap_or_default(),
navigation_tx: Some(tx),
inner_size: req.size.map(|s| (s.width, s.height)),
position: None,
hide_titlebar: false,
};
create_window(app_handle, win_config);
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
tauri::async_runtime::spawn(async move {
while let Some(url) = rx.recv().await {
let label = label.clone();
let url = url.to_string();
let event_to_send = plugin_handle.build_event_to_send(
&WindowContext::Label { label },
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
Some(event_id.clone()),
);
plugin_handle.send(&event_to_send).await.unwrap();
}
});
None
}
InternalEventPayload::CloseWindowRequest(req) => {
if let Some(window) = app_handle.webview_windows().get(&req.label) {
window.close().expect("Failed to close window");
}
None
}
InternalEventPayload::SetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
set_plugin_key_value(app_handle, &name, &req.key, &req.value).await;
Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {}))
}
InternalEventPayload::GetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let value = get_plugin_key_value(app_handle, &name, &req.key).await.map(|v| v.value);
Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value }))
}
InternalEventPayload::DeleteKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let deleted = delete_plugin_key_value(app_handle, &name, &req.key).await;
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
}
_ => None,
};
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
}

View File

@@ -28,14 +28,13 @@ impl PluginTemplateCallback {
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
let window_context = self.window_context.to_owned();
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
let fn_name = if fn_name == "Response" { "response" } else { fn_name };
let function = self
.plugin_manager
.get_template_functions_with_context(window_context.to_owned())
.get_template_functions_with_context(&self.window_context)
.await
.map_err(|e| e.to_string())?
.iter()
@@ -54,6 +53,9 @@ impl TemplateCallback for PluginTemplateCallback {
FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base,
FormInput::Accordion(_) => continue,
FormInput::Banner(_) => continue,
FormInput::Markdown(_) => continue,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
@@ -63,7 +65,7 @@ impl TemplateCallback for PluginTemplateCallback {
let resp = self
.plugin_manager
.call_template_function(
window_context,
&self.window_context,
fn_name,
args_with_defaults,
self.render_purpose.to_owned(),

129
src-tauri/src/window.rs Normal file
View File

@@ -0,0 +1,129 @@
use crate::window_menu::app_menu;
use crate::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH};
use log::{info, warn};
use std::process::exit;
use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, TitleBarStyle, WebviewUrl, WebviewWindow,
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
#[derive(Default, Debug)]
pub(crate) struct CreateWindowConfig<'s> {
pub url: &'s str,
pub label: &'s str,
pub title: &'s str,
pub inner_size: Option<(f64, f64)>,
pub position: Option<(f64, f64)>,
pub navigation_tx: Option<mpsc::Sender<String>>,
pub hide_titlebar: bool,
}
pub(crate) fn create_window<R: Runtime>(
handle: &AppHandle<R>,
config: CreateWindowConfig,
) -> WebviewWindow<R> {
#[allow(unused_variables)]
let menu = app_menu(handle).unwrap();
// This causes the window to not be clickable (in AppImage), so disable on Linux
#[cfg(not(target_os = "linux"))]
handle.set_menu(menu).expect("Failed to set app menu");
info!("Create new window label={}", config.label);
let mut win_builder =
tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into()))
.title(config.title)
.resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
if let Some((w, h)) = config.inner_size {
win_builder = win_builder.inner_size(w, h);
} else {
win_builder = win_builder.inner_size(600.0, 600.0);
}
if let Some((x, y)) = config.position {
win_builder = win_builder.position(x, y);
} else {
win_builder = win_builder.center();
}
if let Some(tx) = config.navigation_tx {
win_builder = win_builder.on_navigation(move |url| {
let url = url.to_string();
let tx = tx.clone();
tauri::async_runtime::block_on(async move {
tx.send(url).await.unwrap();
});
true
});
}
if config.hide_titlebar {
#[cfg(target_os = "macos")]
{
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
}
#[cfg(not(target_os = "macos"))]
{
// Doesn't seem to work from Rust, here, so we do it in main.tsx
win_builder = win_builder.decorations(false);
}
}
if let Some(w) = handle.webview_windows().get(config.label) {
info!("Webview with label {} already exists. Focusing existing", config.label);
w.set_focus().unwrap();
return w.to_owned();
}
let win = win_builder.build().unwrap();
let webview_window = win.clone();
win.on_menu_event(move |w, event| {
if !w.is_focused().unwrap() {
return;
}
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
"zoom_in" => w.emit("zoom_in", true).unwrap(),
"zoom_out" => w.emit("zoom_out", true).unwrap(),
"settings" => w.emit("settings", true).unwrap(),
"open_feedback" => {
if let Err(e) =
w.app_handle().opener().open_url("https://yaak.app/feedback", None::<&str>)
{
warn!("Failed to open feedback {e:?}")
}
}
// Commands for development
"dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(),
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap();
}
"dev.toggle_devtools" => {
if webview_window.is_devtools_open() {
webview_window.close_devtools();
} else {
webview_window.open_devtools();
}
}
_ => {}
}
});
win
}

View File

@@ -3,9 +3,9 @@ use tauri::menu::{
WINDOW_SUBMENU_ID,
};
pub use tauri::AppHandle;
use tauri::Wry;
use tauri::Runtime;
pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let pkg_info = app_handle.package_info();
let config = app_handle.config();
let about_metadata = AboutMetadata {
@@ -37,7 +37,7 @@ pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
true,
&[
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata.clone()))?,
#[cfg(target_os = "macos")]
&MenuItemBuilder::with_id("open_feedback".to_string(), "Give Feedback")
.build(app_handle)?,