Download response, and some fixes

This commit is contained in:
Gregory Schier
2024-01-16 17:02:55 -08:00
parent 33374eefc7
commit ac1e646e68
12 changed files with 2650 additions and 4804 deletions

View File

@@ -24,7 +24,7 @@ openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installa
base64 = "0.21.0" base64 = "0.21.0"
boa_engine = { version = "0.17.3", features = ["annex-b"] } boa_engine = { version = "0.17.3", features = ["annex-b"] }
boa_runtime = { version = "0.17.3" } boa_runtime = { version = "0.17.3" }
chrono = { version = "0.4.23", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
futures = "0.3.26" futures = "0.3.26"
http = "0.2.8" http = "0.2.8"
rand = "0.8.5" rand = "0.8.5"

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
use std::fs; use std::fs;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::io::Write; use std::io::Write;
use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use base64::Engine; use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Method}; use http::{HeaderMap, HeaderName, HeaderValue, Method};
use log::warn; use http::header::{ACCEPT, USER_AGENT};
use log::{info, warn};
use reqwest::multipart; use reqwest::multipart;
use reqwest::redirect::Policy; use reqwest::redirect::Policy;
use sqlx::types::Json;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use sqlx::types::Json;
use tauri::{AppHandle, Wry}; use tauri::{AppHandle, Wry};
use crate::{emit_side_effect, models, render, response_err}; use crate::{emit_side_effect, models, render, response_err};
@@ -21,6 +22,7 @@ pub async fn send_http_request(
environment_id: &str, environment_id: &str,
app_handle: &AppHandle<Wry>, app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
download_path: Option<PathBuf>,
) -> Result<models::HttpResponse, String> { ) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let environment = models::get_environment(environment_id, pool).await.ok(); let environment = models::get_environment(environment_id, pool).await.ok();
@@ -299,6 +301,16 @@ pub async fn send_http_request(
if !request.id.is_empty() { if !request.id.is_empty() {
emit_side_effect(app_handle, "updated_model", &response); emit_side_effect(app_handle, "updated_model", &response);
} }
// Copy response to download path, if specified
match (download_path, response.body_path.clone()) {
(Some(dl_path), Some(body_path)) => {
info!("Downloading response body to {}", dl_path.display());
fs::copy(body_path, dl_path).expect("Failed to copy file for response download");
}
_ => {}
};
Ok(response) Ok(response)
} }
Err(e) => { Err(e) => {

View File

@@ -9,7 +9,7 @@ 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::{create_dir_all, File, read_to_string};
use std::process::exit; use std::process::exit;
use fern::colors::ColoredLevelConfig; use fern::colors::ColoredLevelConfig;
@@ -17,13 +17,13 @@ use log::{debug, info, warn};
use rand::random; use rand::random;
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::types::Json; use sqlx::types::Json;
use sqlx::{Pool, Sqlite, SqlitePool};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry}; use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
use tauri::{Manager, WindowEvent}; use tauri::{Manager, WindowEvent};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri_plugin_log::{fern, LogTarget}; use tauri_plugin_log::{fern, LogTarget};
use tauri_plugin_window_state::{StateFlags, WindowExt}; use tauri_plugin_window_state::{StateFlags, WindowExt};
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -84,7 +84,7 @@ async fn send_ephemeral_request(
let response = models::HttpResponse::new(); let response = models::HttpResponse::new();
let environment_id2 = environment_id.unwrap_or("n/a").to_string(); let environment_id2 = environment_id.unwrap_or("n/a").to_string();
request.id = "".to_string(); request.id = "".to_string();
send_http_request(request, &response, &environment_id2, &app_handle, pool).await send_http_request(request, &response, &environment_id2, &app_handle, pool, None).await
} }
#[tauri::command] #[tauri::command]
@@ -119,10 +119,9 @@ async fn filter_response(
}; };
let body = read_to_string(response.body_path.unwrap()).unwrap(); let body = read_to_string(response.body_path.unwrap()).unwrap();
let filter_result = let filter_result = plugin::run_plugin_filter(&window.app_handle(), plugin_name, filter, &body)
plugin::run_plugin_filter(&window.app_handle(), plugin_name, filter, &body) .await
.await .expect("Failed to run filter");
.expect("Failed to run filter");
Ok(filter_result.filtered) Ok(filter_result.filtered)
} }
@@ -220,6 +219,7 @@ async fn send_request(
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
request_id: &str, request_id: &str,
environment_id: Option<&str>, environment_id: Option<&str>,
download_dir: Option<&str>,
) -> Result<models::HttpResponse, String> { ) -> Result<models::HttpResponse, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
@@ -236,9 +236,15 @@ async fn send_request(
let app_handle2 = window.app_handle().clone(); let app_handle2 = window.app_handle().clone();
let pool2 = pool.clone(); let pool2 = pool.clone();
let download_path = if let Some(p) = download_dir {
Some(std::path::Path::new(p).to_path_buf())
} else {
None
};
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = if let Err(e) =
send_http_request(req, &response2, &environment_id2, &app_handle2, &pool2).await send_http_request(req, &response2, &environment_id2, &app_handle2, &pool2, download_path).await
{ {
response_err(&response2, e, &app_handle2, &pool2) response_err(&response2, e, &app_handle2, &pool2)
.await .await

View File

@@ -58,7 +58,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
? { ? {
key: 'edit', key: 'edit',
label: 'Manage Environments', label: 'Manage Environments',
hotkeyAction: 'environmentEditor.toggle', hotKeyAction: 'environmentEditor.toggle',
leftSlot: <Icon icon="box" />, leftSlot: <Icon icon="box" />,
onSelect: showEnvironmentDialog, onSelect: showEnvironmentDialog,
} }

View File

@@ -63,7 +63,7 @@ export function SettingsDropdown() {
{ {
key: 'hotkeys', key: 'hotkeys',
label: 'Keyboard shortcuts', label: 'Keyboard shortcuts',
hotkeyAction: 'hotkeys.showHelp', hotKeyAction: 'hotkeys.showHelp',
leftSlot: <Icon icon="keyboard" />, leftSlot: <Icon icon="keyboard" />,
onSelect: () => { onSelect: () => {
dialog.show({ dialog.show({
@@ -77,7 +77,7 @@ export function SettingsDropdown() {
{ {
key: 'settings', key: 'settings',
label: 'Settings', label: 'Settings',
hotkeyAction: 'settings.show', hotKeyAction: 'settings.show',
leftSlot: <Icon icon="settings" />, leftSlot: <Icon icon="settings" />,
onSelect: () => { onSelect: () => {
dialog.show({ dialog.show({

View File

@@ -23,6 +23,7 @@ import { useLatestResponse } from '../hooks/useLatestResponse';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useSendManyRequests } from '../hooks/useSendFolder'; import { useSendManyRequests } from '../hooks/useSendFolder';
import { useSendRequest } from '../hooks/useSendRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder'; import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest'; import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
@@ -61,6 +62,7 @@ export function Sidebar({ className }: Props) {
const folders = useFolders(); const folders = useFolders();
const deleteAnyRequest = useDeleteAnyRequest(); const deleteAnyRequest = useDeleteAnyRequest();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
const routes = useAppRoutes(); const routes = useAppRoutes();
const [hasFocus, setHasFocus] = useState<boolean>(false); const [hasFocus, setHasFocus] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -76,6 +78,10 @@ export function Sidebar({ className }: Props) {
namespace: NAMESPACE_NO_SYNC, namespace: NAMESPACE_NO_SYNC,
}); });
useHotKey('request.duplicate', () => {
duplicateRequest.mutate();
});
const isCollapsed = useCallback( const isCollapsed = useCallback(
(id: string) => collapsed.value?.[id] ?? false, (id: string) => collapsed.value?.[id] ?? false,
[collapsed.value], [collapsed.value],
@@ -517,18 +523,20 @@ const SidebarItem = forwardRef(function SidebarItem(
}: SidebarItemProps, }: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>, ref: ForwardedRef<HTMLLIElement>,
) { ) {
const activeRequest = useActiveRequest();
const createRequest = useCreateRequest(); const createRequest = useCreateRequest();
const createFolder = useCreateFolder(); const createFolder = useCreateFolder();
const deleteFolder = useDeleteFolder(itemId); const deleteFolder = useDeleteFolder(itemId);
const deleteRequest = useDeleteRequest(itemId); const deleteRequest = useDeleteRequest(itemId);
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true }); const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true });
const sendRequest = useSendRequest(itemId);
const sendAndDownloadRequest = useSendRequest(itemId, { download: true });
const sendManyRequests = useSendManyRequests(); const sendManyRequests = useSendManyRequests();
const latestResponse = useLatestResponse(itemId); const latestResponse = useLatestResponse(itemId);
const updateRequest = useUpdateRequest(itemId); const updateRequest = useUpdateRequest(itemId);
const updateAnyFolder = useUpdateAnyFolder(); const updateAnyFolder = useUpdateAnyFolder();
const prompt = usePrompt(); const prompt = usePrompt();
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const activeRequest = useActiveRequest();
const isActive = activeRequest?.id === itemId; const isActive = activeRequest?.id === itemId;
const handleSubmitNameEdit = useCallback( const handleSubmitNameEdit = useCallback(
@@ -599,7 +607,6 @@ const SidebarItem = forwardRef(function SidebarItem(
leftSlot: <Icon icon="sendHorizontal" />, leftSlot: <Icon icon="sendHorizontal" />,
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)), onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
}, },
{ type: 'separator', label: itemName },
{ {
key: 'rename', key: 'rename',
label: 'Rename', label: 'Rename',
@@ -641,15 +648,29 @@ const SidebarItem = forwardRef(function SidebarItem(
}, },
] ]
: [ : [
{
key: 'sendRequest',
label: 'Send',
hotKeyAction: 'request.send',
hotKeyLabelOnly: true, // Already bound in URL bar
leftSlot: <Icon icon="sendHorizontal" />,
onSelect: () => sendRequest.mutate(),
},
{
key: 'sendAndDownloadRequest',
label: 'Send and Download',
leftSlot: <Icon icon="download" />,
onSelect: () => sendAndDownloadRequest.mutate(),
},
{ type: 'separator' },
{ {
key: 'duplicateRequest', key: 'duplicateRequest',
label: 'Duplicate', label: 'Duplicate',
hotkeyAction: 'request.duplicate', hotKeyAction: 'request.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />, leftSlot: <Icon icon="copy" />,
onSelect: () => { onSelect: () => {
if (activeRequest?.id === itemId) { duplicateRequest.mutate();
duplicateRequest.mutate();
}
}, },
}, },
{ {

View File

@@ -26,7 +26,7 @@ export const SidebarActions = memo(function SidebarActions() {
{ {
key: 'create-request', key: 'create-request',
label: 'New Request', label: 'New Request',
hotkeyAction: 'request.create', hotKeyAction: 'request.create',
onSelect: () => createRequest.mutate({}), onSelect: () => createRequest.mutate({}),
}, },
{ {

View File

@@ -37,7 +37,8 @@ export type DropdownItemDefault = {
key: string; key: string;
type?: 'default'; type?: 'default';
label: ReactNode; label: ReactNode;
hotkeyAction?: HotkeyAction; hotKeyAction?: HotkeyAction;
hotKeyLabelOnly?: boolean;
variant?: 'danger'; variant?: 'danger';
disabled?: boolean; disabled?: boolean;
hidden?: boolean; hidden?: boolean;
@@ -338,12 +339,13 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
<> <>
{items.map( {items.map(
(item) => (item) =>
item.type !== 'separator' && ( item.type !== 'separator' &&
!item.hotKeyLabelOnly && (
<MenuItemHotKey <MenuItemHotKey
key={item.key} key={item.key}
onSelect={handleSelect} onSelect={handleSelect}
item={item} item={item}
action={item.hotkeyAction} action={item.hotKeyAction}
/> />
), ),
)} )}
@@ -440,7 +442,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
[focused], [focused],
); );
const rightSlot = item.rightSlot ?? <HotKey action={item.hotkeyAction ?? null} />; const rightSlot = item.rightSlot ?? <HotKey action={item.hotKeyAction ?? null} />;
return ( return (
<Button <Button

View File

@@ -121,6 +121,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
ref={wrapperRef} ref={wrapperRef}
className={classNames( className={classNames(
'w-full', 'w-full',
'pointer-events-auto', // Just in case we're placing in disabled parent
labelPosition === 'left' && 'flex items-center gap-2', labelPosition === 'left' && 'flex items-center gap-2',
labelPosition === 'top' && 'flex-row gap-0.5', labelPosition === 'top' && 'flex-row gap-0.5',
)} )}

View File

@@ -1,15 +1,40 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { save } from '@tauri-apps/api/dialog';
import slugify from 'slugify';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { getRequest } from '../lib/store';
import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useAlert } from './useAlert'; import { useAlert } from './useAlert';
export function useSendAnyRequest() { export function useSendAnyRequest(options: { download?: boolean } = {}) {
const environmentId = useActiveEnvironmentId(); const environmentId = useActiveEnvironmentId();
const alert = useAlert(); const alert = useAlert();
return useMutation<HttpResponse, string, string | null>({ return useMutation<HttpResponse | null, string, string | null>({
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }), mutationFn: async (id) => {
const request = await getRequest(id);
if (request == null) {
return null;
}
let downloadDir: string | null = null;
if (options.download) {
downloadDir = await save({
title: 'Select Download Destination',
defaultPath: slugify(request.name, { lower: true, trim: true, strict: true }),
});
if (downloadDir == null) {
return null;
}
}
return invoke('send_request', {
requestId: id,
environmentId,
downloadDir: downloadDir,
});
},
onSettled: () => trackEvent('HttpRequest', 'Send'), onSettled: () => trackEvent('HttpRequest', 'Send'),
onError: (err) => alert({ title: 'Export Failed', body: err }), onError: (err) => alert({ title: 'Export Failed', body: err }),
}); });

View File

@@ -2,9 +2,9 @@ import { useMutation } from '@tanstack/react-query';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { useSendAnyRequest } from './useSendAnyRequest'; import { useSendAnyRequest } from './useSendAnyRequest';
export function useSendRequest(id: string | null) { export function useSendRequest(id: string | null, options: { download?: boolean } = {}) {
const sendAnyRequest = useSendAnyRequest(); const sendAnyRequest = useSendAnyRequest(options);
return useMutation<HttpResponse, string>({ return useMutation<HttpResponse | null, string>({
mutationFn: () => sendAnyRequest.mutateAsync(id), mutationFn: () => sendAnyRequest.mutateAsync(id),
}); });
} }