From 25c4e4edafe3c7ee688e83f7bde72bac2b32d6ae Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 9 May 2024 07:31:52 -0700 Subject: [PATCH] Import from Curl --- src-tauri/src/lib.rs | 71 ++++++++++++++++------- src-tauri/src/plugin.rs | 14 ++--- src-web/components/RequestPane.tsx | 21 +++++++ src-web/components/UrlBar.tsx | 3 + src-web/components/core/Editor/Editor.tsx | 20 +++++-- src-web/components/core/Input.tsx | 5 +- src-web/hooks/Confirm.tsx | 5 +- src-web/hooks/useConfirm.ts | 6 +- src-web/hooks/useCurlToRequest.ts | 24 ++++++++ 9 files changed, 131 insertions(+), 38 deletions(-) create mode 100644 src-web/hooks/useCurlToRequest.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a447b257..923c6b18 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,56 +5,57 @@ extern crate objc; use std::collections::HashMap; use std::env::current_dir; -use std::fs::{create_dir_all, read_to_string, File}; +use std::fs; +use std::fs::{create_dir_all, File, read_to_string}; use std::path::PathBuf; use std::process::exit; use std::str::FromStr; -use ::http::uri::InvalidUri; use ::http::Uri; +use ::http::uri::InvalidUri; use base64::Engine; use fern::colors::ColoredLevelConfig; use log::{debug, error, info, warn}; use rand::random; use serde_json::{json, Value}; +use sqlx::{Pool, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; use sqlx::sqlite::SqliteConnectOptions; use sqlx::types::Json; -use sqlx::{Pool, Sqlite, SqlitePool}; +use tauri::{AppHandle, RunEvent, State, WebviewUrl, WebviewWindow}; +use tauri::{Manager, WindowEvent}; use tauri::path::BaseDirectory; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; -use tauri::{AppHandle, RunEvent, State, WebviewUrl, WebviewWindow}; -use tauri::{Manager, WindowEvent}; use tauri_plugin_log::{fern, Target, TargetKind}; use tauri_plugin_shell::ShellExt; use tokio::sync::Mutex; use tokio::time::sleep; +use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition}; use ::grpc::manager::{DynamicMessage, GrpcHandle}; -use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition}; use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::grpc::metadata_to_map; use crate::http::send_http_request; use crate::models::{ - cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, - delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, - delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, - delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, - generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection, + cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, + create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, + delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, + delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, + duplicate_http_request, Environment, EnvironmentVariable, Folder, generate_model_id, + 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_workspace, get_workspace_export_resources, list_cookie_jars, - list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, - list_http_requests, list_responses, 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_workspace, CookieJar, - Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, - GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, ModelType, Settings, + get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent, + GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, + KeyValue, list_cookie_jars, list_environments, list_folders, list_grpc_connections, + list_grpc_events, list_grpc_requests, list_http_requests, list_responses, list_workspaces, + ModelType, set_key_value_raw, Settings, 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_workspace, Workspace, WorkspaceExportResources, }; -use crate::plugin::{run_plugin_export_curl, ImportResult}; +use crate::plugin::{ImportResult, run_plugin_export_curl, run_plugin_import}; use crate::render::render_request; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; use crate::window_menu::app_menu; @@ -736,8 +737,11 @@ async fn cmd_import_data( "importer-postman", "importer-curl", ]; + let file = fs::read_to_string(file_path) + .unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); + let file_contents = file.as_str(); for plugin_name in plugins { - let v = plugin::run_plugin_import(&w.app_handle(), plugin_name, file_path) + let v = plugin::run_plugin_import(&w.app_handle(), plugin_name, file_contents) .await .map_err(|e| e.to_string())?; if let Some(r) = v { @@ -874,6 +878,32 @@ async fn cmd_request_to_curl( Ok(run_plugin_export_curl(&app, &rendered)?) } +#[tauri::command] +async fn cmd_curl_to_request( + app: AppHandle, + command: &str, + workspace_id: &str, +) -> Result { + let v = run_plugin_import(&app, "importer-curl", command) + .await + .map_err(|e| e.to_string()); + match v { + Ok(Some(r)) => r + .resources + .http_requests + .get(0) + .ok_or("No curl command found".to_string()) + .map(|r| { + let mut request = r.clone(); + request.workspace_id = workspace_id.into(); + request.id = "".to_string(); + request + }), + Ok(None) => Err("Did not find curl request".to_string()), + Err(e) => Err(e), + } +} + #[tauri::command] async fn cmd_export_data( window: WebviewWindow, @@ -1585,6 +1615,7 @@ pub fn run() { cmd_create_grpc_request, cmd_create_http_request, cmd_create_workspace, + cmd_curl_to_request, cmd_delete_all_grpc_connections, cmd_delete_all_http_responses, cmd_delete_cookie_jar, diff --git a/src-tauri/src/plugin.rs b/src-tauri/src/plugin.rs index 4bc2ea75..c55ac836 100644 --- a/src-tauri/src/plugin.rs +++ b/src-tauri/src/plugin.rs @@ -1,17 +1,16 @@ -use std::fs; use std::rc::Rc; -use boa_engine::builtins::promise::PromiseState; use boa_engine::{ - js_string, module::SimpleModuleLoader, property::Attribute, Context, JsNativeError, JsValue, - Module, Source, + Context, js_string, JsNativeError, JsValue, Module, module::SimpleModuleLoader, + property::Attribute, Source, }; +use boa_engine::builtins::promise::PromiseState; use boa_runtime::Console; use log::{debug, error}; use serde::{Deserialize, Serialize}; use serde_json::json; -use tauri::path::BaseDirectory; use tauri::{AppHandle, Manager}; +use tauri::path::BaseDirectory; use crate::models::{HttpRequest, WorkspaceExportResources}; @@ -68,11 +67,8 @@ pub fn run_plugin_export_curl( pub async fn run_plugin_import( app_handle: &AppHandle, plugin_name: &str, - file_path: &str, + file_contents: &str, ) -> Result, String> { - let file = fs::read_to_string(file_path) - .unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); - let file_contents = file.as_str(); let result_json = run_plugin( app_handle, plugin_name, diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 889f75dc..0e858b19 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -38,6 +38,8 @@ import { GraphQLEditor } from './GraphQLEditor'; import { HeadersEditor } from './HeadersEditor'; import { UrlBar } from './UrlBar'; import { UrlParametersEditor } from './UrlParameterEditor'; +import { useCurlToRequest } from '../hooks/useCurlToRequest'; +import { useConfirm } from '../hooks/useConfirm'; interface Props { style: CSSProperties; @@ -227,6 +229,9 @@ export const RequestPane = memo(function RequestPane({ [updateRequest], ); + const importCurl = useCurlToRequest(); + const confirm = useConfirm(); + const isLoading = useIsResponseLoading(activeRequestId ?? null); const { updateKey } = useRequestUpdateKey(activeRequestId ?? null); @@ -241,6 +246,22 @@ export const RequestPane = memo(function RequestPane({ url={activeRequest.url} method={activeRequest.method} placeholder="https://example.com" + onPaste={async (command) => { + if (!command.startsWith('curl ')) { + return; + } + if ( + await confirm({ + id: 'paste-curl', + title: 'Import from Curl?', + description: + 'Do you want to overwrite the current request with the Curl command?', + confirmText: 'Overwrite', + }) + ) { + importCurl.mutate({ requestId: activeRequestId, command }); + } + }} onSend={handleSend} onCancel={handleCancel} onMethodChange={handleMethodChange} diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index 36e18265..a5323bde 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -14,6 +14,7 @@ type Props = Pick & { placeholder: string; onSend: () => void; onUrlChange: (url: string) => void; + onPaste?: (v: string) => void; onCancel: () => void; submitIcon?: IconProps['icon'] | null; onMethodChange?: (method: string) => void; @@ -31,6 +32,7 @@ export const UrlBar = memo(function UrlBar({ onSend, onCancel, onMethodChange, + onPaste, submitIcon = 'sendHorizontal', isLoading, }: Props) { @@ -66,6 +68,7 @@ export const UrlBar = memo(function UrlBar({ forceUpdateKey={forceUpdateKey} onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} + onPaste={onPaste} containerClassName="shadow shadow-gray-100 dark:shadow-gray-50" onChange={onUrlChange} defaultValue={url} diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 1b51fba7..284a3bd7 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -44,6 +44,7 @@ export interface EditorProps { tooltipContainer?: HTMLElement; useTemplating?: boolean; onChange?: (value: string) => void; + onPaste?: (value: string) => void; onFocus?: () => void; onBlur?: () => void; onKeyDown?: (e: KeyboardEvent) => void; @@ -69,6 +70,7 @@ export const Editor = forwardRef(function E defaultValue, forceUpdateKey, onChange, + onPaste, onFocus, onBlur, onKeyDown, @@ -91,25 +93,31 @@ export const Editor = forwardRef(function E const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null); useImperativeHandle(ref, () => cm.current?.view); - // Use ref so we can update the onChange handler without re-initializing the editor + // Use ref so we can update the handler without re-initializing the editor const handleChange = useRef(onChange); useEffect(() => { handleChange.current = onChange; }, [onChange]); - // Use ref so we can update the onChange handler without re-initializing the editor + // Use ref so we can update the handler without re-initializing the editor + const handlePaste = useRef(onPaste); + useEffect(() => { + handlePaste.current = onPaste; + }, [onPaste]); + + // Use ref so we can update the handler without re-initializing the editor const handleFocus = useRef(onFocus); useEffect(() => { handleFocus.current = onFocus; }, [onFocus]); - // Use ref so we can update the onChange handler without re-initializing the editor + // Use ref so we can update the handler without re-initializing the editor const handleBlur = useRef(onBlur); useEffect(() => { handleBlur.current = onBlur; }, [onBlur]); - // Use ref so we can update the onChange handler without re-initializing the editor + // Use ref so we can update the handler without re-initializing the editor const handleKeyDown = useRef(onKeyDown); useEffect(() => { handleKeyDown.current = onKeyDown; @@ -187,6 +195,7 @@ export const Editor = forwardRef(function E readOnly, singleLine, onChange: handleChange, + onPaste: handlePaste, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, @@ -299,12 +308,14 @@ function getExtensions({ readOnly, singleLine, onChange, + onPaste, onFocus, onBlur, onKeyDown, }: Pick & { container: HTMLDivElement | null; onChange: MutableRefObject; + onPaste: MutableRefObject; onFocus: MutableRefObject; onBlur: MutableRefObject; onKeyDown: MutableRefObject; @@ -321,6 +332,7 @@ function getExtensions({ focus: () => onFocus.current?.(), blur: () => onBlur.current?.(), keydown: (e) => onKeyDown.current?.(e), + paste: (e) => onPaste.current?.(e.clipboardData?.getData('text/plain') ?? ''), }), tooltips({ parent }), keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap), diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index b143ce57..adcc0720 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -10,7 +10,7 @@ import { HStack } from './Stacks'; export type InputProps = Omit< HTMLAttributes, - 'onChange' | 'onFocus' | 'onKeyDown' + 'onChange' | 'onFocus' | 'onKeyDown' | 'onPaste' > & Pick< EditorProps, @@ -33,6 +33,7 @@ export type InputProps = Omit< onChange?: (value: string) => void; onFocus?: () => void; onBlur?: () => void; + onPaste?: (value: string) => void; defaultValue?: string; leftSlot?: ReactNode; rightSlot?: ReactNode; @@ -59,6 +60,7 @@ export const Input = forwardRef(function Inp onBlur, onChange, onFocus, + onPaste, placeholder, require, rightSlot, @@ -172,6 +174,7 @@ export const Input = forwardRef(function Inp forceUpdateKey={forceUpdateKey} placeholder={placeholder} onChange={handleChange} + onPaste={onPaste} className={editorClassName} onFocus={handleFocus} onBlur={handleBlur} diff --git a/src-web/hooks/Confirm.tsx b/src-web/hooks/Confirm.tsx index fdf2c594..4b7a36e3 100644 --- a/src-web/hooks/Confirm.tsx +++ b/src-web/hooks/Confirm.tsx @@ -6,6 +6,7 @@ export interface ConfirmProps { onHide: () => void; onResult: (result: boolean) => void; variant?: 'delete' | 'confirm'; + confirmText?: string; } const colors: Record, ButtonProps['color']> = { @@ -18,7 +19,7 @@ const confirmButtonTexts: Record, string> = confirm: 'Confirm', }; -export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps) { +export function Confirm({ onHide, onResult, confirmText, variant = 'confirm' }: ConfirmProps) { const handleHide = () => { onResult(false); onHide(); @@ -32,7 +33,7 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps) return (