Import from Curl

This commit is contained in:
Gregory Schier
2024-05-09 07:31:52 -07:00
parent 1a5bf53b02
commit 25c4e4edaf
9 changed files with 131 additions and 38 deletions

View File

@@ -5,56 +5,57 @@ extern crate objc;
use std::collections::HashMap; use std::collections::HashMap;
use std::env::current_dir; 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::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
use ::http::uri::InvalidUri;
use ::http::Uri; use ::http::Uri;
use ::http::uri::InvalidUri;
use base64::Engine; use base64::Engine;
use fern::colors::ColoredLevelConfig; use fern::colors::ColoredLevelConfig;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use rand::random; use rand::random;
use serde_json::{json, Value}; use serde_json::{json, Value};
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteConnectOptions;
use sqlx::types::Json; use sqlx::types::Json;
use sqlx::{Pool, Sqlite, SqlitePool}; use tauri::{AppHandle, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Manager, WindowEvent};
use tauri::path::BaseDirectory; use tauri::path::BaseDirectory;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
use tauri::{AppHandle, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Manager, WindowEvent};
use tauri_plugin_log::{fern, Target, TargetKind}; use tauri_plugin_log::{fern, Target, TargetKind};
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::time::sleep; use tokio::time::sleep;
use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition};
use ::grpc::manager::{DynamicMessage, GrpcHandle}; use ::grpc::manager::{DynamicMessage, GrpcHandle};
use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use window_ext::TrafficLightWindowExt; use window_ext::TrafficLightWindowExt;
use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::analytics::{AnalyticsAction, AnalyticsResource};
use crate::grpc::metadata_to_map; use crate::grpc::metadata_to_map;
use crate::http::send_http_request; use crate::http::send_http_request;
use crate::models::{ use crate::models::{
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, cancel_pending_grpc_connections, cancel_pending_responses, CookieJar,
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request,
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request,
generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection, 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_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, get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent,
list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse,
list_http_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id, KeyValue, list_cookie_jars, list_environments, list_folders, list_grpc_connections,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, list_grpc_events, list_grpc_requests, list_http_requests, list_responses, list_workspaces,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, ModelType, set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar,
Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace,
GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, ModelType, Settings,
Workspace, WorkspaceExportResources, 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::render::render_request;
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
use crate::window_menu::app_menu; use crate::window_menu::app_menu;
@@ -736,8 +737,11 @@ async fn cmd_import_data(
"importer-postman", "importer-postman",
"importer-curl", "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 { 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 .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
if let Some(r) = v { if let Some(r) = v {
@@ -874,6 +878,32 @@ async fn cmd_request_to_curl(
Ok(run_plugin_export_curl(&app, &rendered)?) Ok(run_plugin_export_curl(&app, &rendered)?)
} }
#[tauri::command]
async fn cmd_curl_to_request(
app: AppHandle,
command: &str,
workspace_id: &str,
) -> Result<HttpRequest, String> {
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] #[tauri::command]
async fn cmd_export_data( async fn cmd_export_data(
window: WebviewWindow, window: WebviewWindow,
@@ -1585,6 +1615,7 @@ pub fn run() {
cmd_create_grpc_request, cmd_create_grpc_request,
cmd_create_http_request, cmd_create_http_request,
cmd_create_workspace, cmd_create_workspace,
cmd_curl_to_request,
cmd_delete_all_grpc_connections, cmd_delete_all_grpc_connections,
cmd_delete_all_http_responses, cmd_delete_all_http_responses,
cmd_delete_cookie_jar, cmd_delete_cookie_jar,

View File

@@ -1,17 +1,16 @@
use std::fs;
use std::rc::Rc; use std::rc::Rc;
use boa_engine::builtins::promise::PromiseState;
use boa_engine::{ use boa_engine::{
js_string, module::SimpleModuleLoader, property::Attribute, Context, JsNativeError, JsValue, Context, js_string, JsNativeError, JsValue, Module, module::SimpleModuleLoader,
Module, Source, property::Attribute, Source,
}; };
use boa_engine::builtins::promise::PromiseState;
use boa_runtime::Console; use boa_runtime::Console;
use log::{debug, error}; use log::{debug, error};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use tauri::path::BaseDirectory;
use crate::models::{HttpRequest, WorkspaceExportResources}; use crate::models::{HttpRequest, WorkspaceExportResources};
@@ -68,11 +67,8 @@ pub fn run_plugin_export_curl(
pub async fn run_plugin_import( pub async fn run_plugin_import(
app_handle: &AppHandle, app_handle: &AppHandle,
plugin_name: &str, plugin_name: &str,
file_path: &str, file_contents: &str,
) -> Result<Option<ImportResult>, String> { ) -> Result<Option<ImportResult>, 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( let result_json = run_plugin(
app_handle, app_handle,
plugin_name, plugin_name,

View File

@@ -38,6 +38,8 @@ import { GraphQLEditor } from './GraphQLEditor';
import { HeadersEditor } from './HeadersEditor'; import { HeadersEditor } from './HeadersEditor';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor'; import { UrlParametersEditor } from './UrlParameterEditor';
import { useCurlToRequest } from '../hooks/useCurlToRequest';
import { useConfirm } from '../hooks/useConfirm';
interface Props { interface Props {
style: CSSProperties; style: CSSProperties;
@@ -227,6 +229,9 @@ export const RequestPane = memo(function RequestPane({
[updateRequest], [updateRequest],
); );
const importCurl = useCurlToRequest();
const confirm = useConfirm();
const isLoading = useIsResponseLoading(activeRequestId ?? null); const isLoading = useIsResponseLoading(activeRequestId ?? null);
const { updateKey } = useRequestUpdateKey(activeRequestId ?? null); const { updateKey } = useRequestUpdateKey(activeRequestId ?? null);
@@ -241,6 +246,22 @@ export const RequestPane = memo(function RequestPane({
url={activeRequest.url} url={activeRequest.url}
method={activeRequest.method} method={activeRequest.method}
placeholder="https://example.com" 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} onSend={handleSend}
onCancel={handleCancel} onCancel={handleCancel}
onMethodChange={handleMethodChange} onMethodChange={handleMethodChange}

View File

@@ -14,6 +14,7 @@ type Props = Pick<HttpRequest, 'url'> & {
placeholder: string; placeholder: string;
onSend: () => void; onSend: () => void;
onUrlChange: (url: string) => void; onUrlChange: (url: string) => void;
onPaste?: (v: string) => void;
onCancel: () => void; onCancel: () => void;
submitIcon?: IconProps['icon'] | null; submitIcon?: IconProps['icon'] | null;
onMethodChange?: (method: string) => void; onMethodChange?: (method: string) => void;
@@ -31,6 +32,7 @@ export const UrlBar = memo(function UrlBar({
onSend, onSend,
onCancel, onCancel,
onMethodChange, onMethodChange,
onPaste,
submitIcon = 'sendHorizontal', submitIcon = 'sendHorizontal',
isLoading, isLoading,
}: Props) { }: Props) {
@@ -66,6 +68,7 @@ export const UrlBar = memo(function UrlBar({
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
onPaste={onPaste}
containerClassName="shadow shadow-gray-100 dark:shadow-gray-50" containerClassName="shadow shadow-gray-100 dark:shadow-gray-50"
onChange={onUrlChange} onChange={onUrlChange}
defaultValue={url} defaultValue={url}

View File

@@ -44,6 +44,7 @@ export interface EditorProps {
tooltipContainer?: HTMLElement; tooltipContainer?: HTMLElement;
useTemplating?: boolean; useTemplating?: boolean;
onChange?: (value: string) => void; onChange?: (value: string) => void;
onPaste?: (value: string) => void;
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void; onBlur?: () => void;
onKeyDown?: (e: KeyboardEvent) => void; onKeyDown?: (e: KeyboardEvent) => void;
@@ -69,6 +70,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
defaultValue, defaultValue,
forceUpdateKey, forceUpdateKey,
onChange, onChange,
onPaste,
onFocus, onFocus,
onBlur, onBlur,
onKeyDown, onKeyDown,
@@ -91,25 +93,31 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null); const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
useImperativeHandle(ref, () => cm.current?.view); 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<EditorProps['onChange']>(onChange); const handleChange = useRef<EditorProps['onChange']>(onChange);
useEffect(() => { useEffect(() => {
handleChange.current = onChange; handleChange.current = onChange;
}, [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<EditorProps['onPaste']>(onPaste);
useEffect(() => {
handlePaste.current = onPaste;
}, [onPaste]);
// Use ref so we can update the handler without re-initializing the editor
const handleFocus = useRef<EditorProps['onFocus']>(onFocus); const handleFocus = useRef<EditorProps['onFocus']>(onFocus);
useEffect(() => { useEffect(() => {
handleFocus.current = onFocus; handleFocus.current = onFocus;
}, [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<EditorProps['onBlur']>(onBlur); const handleBlur = useRef<EditorProps['onBlur']>(onBlur);
useEffect(() => { useEffect(() => {
handleBlur.current = onBlur; handleBlur.current = onBlur;
}, [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<EditorProps['onKeyDown']>(onKeyDown); const handleKeyDown = useRef<EditorProps['onKeyDown']>(onKeyDown);
useEffect(() => { useEffect(() => {
handleKeyDown.current = onKeyDown; handleKeyDown.current = onKeyDown;
@@ -187,6 +195,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
readOnly, readOnly,
singleLine, singleLine,
onChange: handleChange, onChange: handleChange,
onPaste: handlePaste,
onFocus: handleFocus, onFocus: handleFocus,
onBlur: handleBlur, onBlur: handleBlur,
onKeyDown: handleKeyDown, onKeyDown: handleKeyDown,
@@ -299,12 +308,14 @@ function getExtensions({
readOnly, readOnly,
singleLine, singleLine,
onChange, onChange,
onPaste,
onFocus, onFocus,
onBlur, onBlur,
onKeyDown, onKeyDown,
}: Pick<EditorProps, 'singleLine' | 'readOnly'> & { }: Pick<EditorProps, 'singleLine' | 'readOnly'> & {
container: HTMLDivElement | null; container: HTMLDivElement | null;
onChange: MutableRefObject<EditorProps['onChange']>; onChange: MutableRefObject<EditorProps['onChange']>;
onPaste: MutableRefObject<EditorProps['onPaste']>;
onFocus: MutableRefObject<EditorProps['onFocus']>; onFocus: MutableRefObject<EditorProps['onFocus']>;
onBlur: MutableRefObject<EditorProps['onBlur']>; onBlur: MutableRefObject<EditorProps['onBlur']>;
onKeyDown: MutableRefObject<EditorProps['onKeyDown']>; onKeyDown: MutableRefObject<EditorProps['onKeyDown']>;
@@ -321,6 +332,7 @@ function getExtensions({
focus: () => onFocus.current?.(), focus: () => onFocus.current?.(),
blur: () => onBlur.current?.(), blur: () => onBlur.current?.(),
keydown: (e) => onKeyDown.current?.(e), keydown: (e) => onKeyDown.current?.(e),
paste: (e) => onPaste.current?.(e.clipboardData?.getData('text/plain') ?? ''),
}), }),
tooltips({ parent }), tooltips({ parent }),
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap), keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),

View File

@@ -10,7 +10,7 @@ import { HStack } from './Stacks';
export type InputProps = Omit< export type InputProps = Omit<
HTMLAttributes<HTMLInputElement>, HTMLAttributes<HTMLInputElement>,
'onChange' | 'onFocus' | 'onKeyDown' 'onChange' | 'onFocus' | 'onKeyDown' | 'onPaste'
> & > &
Pick< Pick<
EditorProps, EditorProps,
@@ -33,6 +33,7 @@ export type InputProps = Omit<
onChange?: (value: string) => void; onChange?: (value: string) => void;
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void; onBlur?: () => void;
onPaste?: (value: string) => void;
defaultValue?: string; defaultValue?: string;
leftSlot?: ReactNode; leftSlot?: ReactNode;
rightSlot?: ReactNode; rightSlot?: ReactNode;
@@ -59,6 +60,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
onBlur, onBlur,
onChange, onChange,
onFocus, onFocus,
onPaste,
placeholder, placeholder,
require, require,
rightSlot, rightSlot,
@@ -172,6 +174,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
placeholder={placeholder} placeholder={placeholder}
onChange={handleChange} onChange={handleChange}
onPaste={onPaste}
className={editorClassName} className={editorClassName}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}

View File

@@ -6,6 +6,7 @@ export interface ConfirmProps {
onHide: () => void; onHide: () => void;
onResult: (result: boolean) => void; onResult: (result: boolean) => void;
variant?: 'delete' | 'confirm'; variant?: 'delete' | 'confirm';
confirmText?: string;
} }
const colors: Record<NonNullable<ConfirmProps['variant']>, ButtonProps['color']> = { const colors: Record<NonNullable<ConfirmProps['variant']>, ButtonProps['color']> = {
@@ -18,7 +19,7 @@ const confirmButtonTexts: Record<NonNullable<ConfirmProps['variant']>, string> =
confirm: 'Confirm', confirm: 'Confirm',
}; };
export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps) { export function Confirm({ onHide, onResult, confirmText, variant = 'confirm' }: ConfirmProps) {
const handleHide = () => { const handleHide = () => {
onResult(false); onResult(false);
onHide(); onHide();
@@ -32,7 +33,7 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps)
return ( return (
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse"> <HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button className="focus" color={colors[variant]} onClick={handleSuccess}> <Button className="focus" color={colors[variant]} onClick={handleSuccess}>
{confirmButtonTexts[variant]} {confirmText ?? confirmButtonTexts[variant]}
</Button> </Button>
<Button className="focus" color="gray" onClick={handleHide}> <Button className="focus" color="gray" onClick={handleHide}>
Cancel Cancel

View File

@@ -10,11 +10,13 @@ export function useConfirm() {
title, title,
description, description,
variant, variant,
confirmText,
}: { }: {
id: string; id: string;
title: DialogProps['title']; title: DialogProps['title'];
description?: DialogProps['description']; description?: DialogProps['description'];
variant: ConfirmProps['variant']; variant?: ConfirmProps['variant'];
confirmText?: ConfirmProps['confirmText'];
}) => }) =>
new Promise((onResult: ConfirmProps['onResult']) => { new Promise((onResult: ConfirmProps['onResult']) => {
dialog.show({ dialog.show({
@@ -23,7 +25,7 @@ export function useConfirm() {
description, description,
hideX: true, hideX: true,
size: 'sm', size: 'sm',
render: ({ hide }) => Confirm({ onHide: hide, variant, onResult }), render: ({ hide }) => Confirm({ onHide: hide, variant, onResult, confirmText }),
}); });
}); });
} }

View File

@@ -0,0 +1,24 @@
import { invoke } from '@tauri-apps/api/core';
import { useMutation } from '@tanstack/react-query';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useRequestUpdateKey } from './useRequestUpdateKey';
import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest';
export function useCurlToRequest() {
const workspaceId = useActiveWorkspaceId();
const updateRequest = useUpdateAnyHttpRequest();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
return useMutation({
mutationFn: async ({ requestId, command }: { requestId: string; command: string }) => {
const request: Record<string, unknown> = await invoke('cmd_curl_to_request', {
command,
workspaceId,
});
delete request.id;
await updateRequest.mutateAsync({ id: requestId, update: request });
wasUpdatedExternally(requestId);
console.log('FOO', request);
},
});
}